commit 55175acdf6f0ee254506d6d4d38554de3886f1ca Author: JogmansonReal <174800970+JogmansonReal@users.noreply.github.com> Date: Fri Oct 16 23:33:13 2026 -0700 Radicade Public Code diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72b693a --- /dev/null +++ b/.gitignore @@ -0,0 +1,95 @@ +/[Tt]emp/ +/[Oo]bj/ +*.log + +.vs/ + +.gradle/ + +ExportedObj/ +.consulo/ +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +/bin + +# Logging +/logs +*.log +appsettings.json + +# Local August client diagnostics and generated game-derived files +/artifacts/august-* +/artifacts/BepInEx.Unity.IL2CPP.dll + +# Generated test/publish output and scratch notes (never publish these) +/artifacts/ +_meta_scan.txt +**/bin/ +**/obj/ +Tests/**/TestResults/ +*.trx +*.coverage + +# Editor / tooling state +/.vs/ +/.freebuff/ +/.tmp-september-pack/ + +/RecNet-CDN +/Migrations +/RadicadeServer-Data +/.idea +/Server/bin +/Server/RadicadeServer-Data +/Server/.idea +/Server/obj +/Server/Discord/Webhooks/webhooks.json +/AntiCheat/bin +/AntiCheat/obj +/AntiCheat.Tests/bin +/AntiCheat.Tests/obj +/Server.Tests/bin +/Server.Tests/obj + +# Local reference checkouts +/.external/ +/.tmp-test-bin/ +/2023Patch/RadicadePatch/bin +2023Patch/EacProbe/obj/Debug/net6.0/apphost.exe +*.exe +/2023Patch/EacProbe +/2023Patch/EacProbe/bin +/2023Patch/EacProbe/bin/Debug +/2023Patch/EacProbe/bin/Debug/net6.0 +/2023Patch +!/2023Patch/ +/2023Patch/* +!/2023Patch/RadicadeAugustClient/ +/2023Patch/RadicadeAugustClient/bin/ +/2023Patch/RadicadeAugustClient/obj/ +!/2023Patch/RadicadePatch/ +/2023Patch/RadicadePatch/bin/ +/2023Patch/RadicadePatch/obj/ +!/2023Patch/RadicadeEacBypass/ +/2023Patch/RadicadeEacBypass/* +!/2023Patch/RadicadeEacBypass/HubSocketAuth.cs +!/2023Patch/RadicadeEacBypass/WebSocketRedirectPatches.cs +!/2023Patch/RadicadeEacBypass/RealtimeDiagnostics.cs +!/2023Patch/RadicadeEacBypass/RequestRouteLog.cs +!/2023Patch/RadicadeEacBypass/EmbeddedTlsPatches.cs +!/2023Patch/RadicadeEacBypass/NameserverConnectionPatches.cs +!/2023Patch/RadicadeEacBypass/NameserverRedirect.cs +!/2023Patch/RadicadeEacBypass/ImageSignatureRequest.cs +!/2023Patch/RadicadeEacBypass/Diag.cs +!/2023Patch/RadicadeEacBypass/ResponseRepair.cs +!/2023Patch/RadicadeEacBypass/HttpTolerancePatches.cs diff --git a/2023Patch/RadicadeAugustClient/CrashAddressMap.cs b/2023Patch/RadicadeAugustClient/CrashAddressMap.cs new file mode 100644 index 0000000..dd66beb --- /dev/null +++ b/2023Patch/RadicadeAugustClient/CrashAddressMap.cs @@ -0,0 +1,225 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + + + + + + + + + + + + + + + + + + + + + + + + + + + + +internal static class CrashAddressMap +{ + private const long DefaultTarget = 0x29a79ec; + + internal static bool Enabled => !_disabled; + + internal static long TargetRva => _target; + + private static readonly bool _disabled = IsDisabled(); + + private static readonly long _target = ReadTarget(); + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_ADDR_MAP"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static long ReadTarget() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_ADDR_TARGET"); + if (string.IsNullOrWhiteSpace(raw)) return DefaultTarget; + raw = raw.Trim(); + if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) raw = raw.Substring(2); + return long.TryParse(raw, System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, out var value) ? value : DefaultTarget; + } + catch { return DefaultTarget; } + } + + internal static void Start() + { + if (!Enabled) return; + var thread = new System.Threading.Thread(Run) { IsBackground = true, Name = "RadicadeAddrMap" }; + thread.Start(); + } + + private static void Run() + { + + + try { System.Threading.Thread.Sleep(5000); } catch { } + + var log = RadicadeEacBypassPlugin.LogSource; + var started = Environment.TickCount; + + IntPtr baseAddress = IntPtr.Zero; + var moduleSize = 0L; + try + { + using (var self = System.Diagnostics.Process.GetCurrentProcess()) + { + foreach (System.Diagnostics.ProcessModule module in self.Modules) + { + if (!string.Equals(module.ModuleName, "GameAssembly.dll", StringComparison.OrdinalIgnoreCase)) continue; + baseAddress = module.BaseAddress; + moduleSize = module.ModuleMemorySize; + break; + } + } + } + catch (Exception ex) + { + log.LogError("[AddrMap] could not locate GameAssembly.dll: " + ex.Message); + return; + } + + if (baseAddress == IntPtr.Zero || moduleSize <= 0) + { + log.LogError("[AddrMap] GameAssembly.dll is not loaded - nothing to map"); + return; + } + + var gameRoot = Path.GetDirectoryName(UnityEngine.Application.dataPath) ?? "."; + var path = Path.Combine(gameRoot, "BepInEx", "radicade-addr-map.txt"); + + var baseValue = baseAddress.ToInt64(); + var mapped = 0; + var images = 0; + var unmappable = 0; + var bestBelow = -1L; + string bestBelowName = null; + long bestAbove = -1; + string bestAboveName = null; + var failures = 0; + + try + { + using (var writer = new StreamWriter(path, false)) + { + writer.WriteLine("# il2cpp RVA -> method map, written by RadicadeAugustClient (run 24)"); + writer.WriteLine("# GameAssembly.dll base 0x" + baseValue.ToString("x") + ", mapped size 0x" + moduleSize.ToString("x")); + writer.WriteLine("# rvaimageNamespace.Class::Method - rva is relative to the base above"); + + unsafe + { + var domain = Il2CppInterop.Runtime.IL2CPP.il2cpp_domain_get(); + if (domain == IntPtr.Zero) + { + log.LogError("[AddrMap] no il2cpp domain; nothing to map"); + return; + } + + uint assemblyCount = 0; + var assemblies = Il2CppInterop.Runtime.IL2CPP.il2cpp_domain_get_assemblies(domain, ref assemblyCount); + for (uint a = 0; a < assemblyCount; a++) + { + var image = Il2CppInterop.Runtime.IL2CPP.il2cpp_assembly_get_image(assemblies[a]); + if (image == IntPtr.Zero) continue; + images++; + + string imageName; + try { imageName = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_name(image)) ?? "?"; } + catch { imageName = "?"; } + + var classCount = Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_class_count(image); + for (uint c = 0; c < classCount; c++) + { + var klass = Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_class(image, c); + if (klass == IntPtr.Zero) continue; + + string className; + try + { + var namespaze = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_namespace(klass)); + var name = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_name(klass)); + className = string.IsNullOrEmpty(namespaze) ? name : namespaze + "." + name; + } + catch { className = "?"; failures++; } + + var iterator = IntPtr.Zero; + IntPtr methodInfo; + while ((methodInfo = Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_methods(klass, ref iterator)) != IntPtr.Zero) + { + long entry; + try { entry = Marshal.ReadIntPtr(methodInfo).ToInt64(); } + catch { entry = 0; } + + if (entry <= baseValue || entry >= baseValue + moduleSize) + { + + + unmappable++; + continue; + } + + var rva = entry - baseValue; + string methodName; + try { methodName = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_method_get_name(methodInfo)) ?? "?"; } + catch { methodName = "?"; failures++; } + + writer.WriteLine("0x" + rva.ToString("x") + "\t" + imageName + "\t" + className + "::" + methodName); + mapped++; + + var full = className + "::" + methodName; + if (rva <= _target && rva > bestBelow) { bestBelow = rva; bestBelowName = full; } + if (rva > _target && (bestAbove < 0 || rva < bestAbove)) { bestAbove = rva; bestAboveName = full; } + } + } + } + } + } + } + catch (Exception ex) + { + log.LogError("[AddrMap] pass failed after " + mapped + " methods: " + ex); + return; + } + + var elapsed = Environment.TickCount - started; + log.LogInfo($"[AddrMap] mapped {mapped} methods from {images} images in {elapsed} ms -> {path} ({unmappable} without a mappable entry point)"); + + if (bestBelow < 0) + { + log.LogWarning("[AddrMap] no method starts below the target 0x" + _target.ToString("x") + + " - the crash is in code no managed method owns (il2cpp runtime or native plugin)." + + (bestAboveName != null ? " Nearest above: 0x" + bestAbove.ToString("x") + " " + bestAboveName : string.Empty)); + return; + } + + var span = bestAbove > bestBelow ? bestAbove - bestBelow : -1; + log.LogWarning("[AddrMap] crash target 0x" + _target.ToString("x") + " -> containing method 0x" + bestBelow.ToString("x") + + " " + bestBelowName + " (+0x" + (_target - bestBelow).ToString("x") + " into it)" + + (span > 0 ? "; next method 0x" + bestAbove.ToString("x") + " " + bestAboveName + " (span 0x" + span.ToString("x") + ")" : string.Empty) + + (failures > 0 ? "; " + failures + " name reads failed" : string.Empty)); + } +} diff --git a/2023Patch/RadicadeAugustClient/Plugin.cs b/2023Patch/RadicadeAugustClient/Plugin.cs new file mode 100644 index 0000000..a7fe268 --- /dev/null +++ b/2023Patch/RadicadeAugustClient/Plugin.cs @@ -0,0 +1,1430 @@ +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Logging; +using BepInEx.Unity.IL2CPP; +using HarmonyLib; +using RecRoom.AntiCheat; +using System.Linq; +using System.Runtime.InteropServices; + + + +[BepInPlugin("com.radicade.eacbypass", "Radicade August Client", "2.0.0")] +public sealed class RadicadeEacBypassPlugin : BasePlugin +{ + internal static ManualLogSource LogSource; + internal static ConfigEntry ServerUrl, TlsAllowedHosts; + internal static Il2CppSystem.Threading.SynchronizationContext MainThreadContext; + internal static string ImageKeyId; + + public override void Load() + { + LogSource = Log; + if (UnityEngine.Application.version != "20230825") + throw new NotSupportedException("This plugin requires Rec Room 20230825."); + MainThreadContext = Il2CppSystem.Threading.SynchronizationContext.Current; + Log.LogInfo($"[August] main-thread sync context: {(MainThreadContext != null ? MainThreadContext.GetType().Name : "NULL")}"); + ServerUrl = Config.Bind("Connection", "ServerUrl", "https://localhost:3030", "Private server URL."); + if (!Uri.TryCreate(ServerUrl.Value, UriKind.Absolute, out var server) || server.Scheme != "https") + throw new InvalidOperationException("The August client requires an HTTPS server URL."); + TlsAllowedHosts = Config.Bind("TLS", "AllowedHosts", server.Host, "Exact private server hosts allowed by the embedded TLS integration."); + if (!EmbeddedTlsPatches.IsHostAllowed(server.Host)) + throw new InvalidOperationException("TLS AllowedHosts must include the configured private server host."); + + var metadata = Path.Combine(UnityEngine.Application.dataPath, "il2cpp_data", "Metadata", "global-metadata.interop.dat"); + ImageKeyId = ImageSignatureRequest.DetectKeyId(File.ReadAllBytes(metadata)); + + + + + + + + + + + + + if (ModuleHidingPolicy.Enabled) + { + var hidden = PebModuleHider.HideModule("winhttp.dll"); + Log.LogInfo(hidden + ? "[ModuleHide] winhttp.dll unlinked from PEB module lists (scanner-invisible)" + : "[ModuleHide] WARNING: winhttp.dll not found in PEB lists - scanner will still see it"); + + + var wiped = PeHeaderWiper.WipeSignatures("winhttp.dll"); + Log.LogInfo(wiped + ? "[ModuleHide] winhttp.dll PE signatures wiped (MZ+PE) - region scans cannot classify it" + : "[ModuleHide] WARNING: PE signature wipe did not apply (signatures missing or VirtualProtect failed)"); + } + else + { + Log.LogInfo("[ModuleHide] disabled by default (RADICADE_MODULE_HIDE=1 re-arms runs 14/15): winhttp.dll keeps its loader-list entry and its PE headers, so a module scan has nothing to call obscured."); + } + + Log.LogInfo(ModuleHidingPolicy.DescribeModuleList()); + + + + + + + + + HileGate.Enabled = true; + Log.LogInfo("[HileGate] armed - anti-cheat report POSTs are " + (HileGate.DropEnabled + ? "dropped at the HTTP layer (RADICADE_HILE_GATE=drop)" + : "captured in full and then allowed through, so the report body is on the record (RADICADE_HILE_GATE=drop restores the drop)") + "."); + + var harmony = new Harmony("com.radicade.eacbypass"); + + EmbeddedTlsPatches.Apply(harmony); + harmony.PatchAll(typeof(NameserverConnectionPatches)); + WebSocketRedirectPatches.Apply(harmony); + harmony.PatchAll(typeof(AugustChallengePatches)); + harmony.PatchAll(typeof(AugustLaunchDiagnostics)); + harmony.PatchAll(typeof(AugustLaunchValidatorPatch)); + RsaVerifyNeutralizerPatches.Apply(harmony); + harmony.PatchAll(typeof(FileCheckEntryPatch)); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + harmony.PatchAll(typeof(ExitInitiatorTracerPatch)); + Log.LogInfo("[ExitInit] armed - managed exit initiators are blocked once a hile report has been dropped, and traced."); + + + + try + { + harmony.PatchAll(typeof(ExitStateVetoPatch)); + + + + + + Log.LogInfo("[ExitVeto] patches applied; blockExitState enabled=" + ExitStateVetoPatch.Enabled + + " (opt-in fallback only; a normal quit and server logouts work either way. RADICADE_EXIT_VETO=1 arms it)"); + } + catch (Exception vetoEx) + { + + + Log.LogError("[ExitVeto] failed to apply - continuing without it: " + vetoEx); + } + + + + + + + + + + try + { + harmony.PatchAll(typeof(ExitSequenceTracePatch)); + harmony.PatchAll(typeof(UnityQuitVetoPatch)); + + + harmony.PatchAll(typeof(EacNativeClientStubPatch)); + harmony.PatchAll(typeof(EacHealthQueryLiePatch)); + harmony.PatchAll(typeof(CheatManagerTracePatch)); + harmony.PatchAll(typeof(CheatScanSuppressPatch)); + Log.LogInfo("[CheatScan] verdict suppression enabled=" + CheatScanSuppressPatch.Enabled); + Log.LogInfo("[CheatType] cheat-detection suppression enabled=" + CheatDetectionSuppressPatch.Enabled + + " (RADICADE_CHEAT_TYPE=0 restores the detection and the dorm drop that follows it)"); + Log.LogInfo("[CheatDrop] dorm-drop guard enabled=" + CheatDormDropGuardPatch.Enabled + + " (RADICADE_CHEAT_DROP=0 lets every drop run)"); + + + + try + { + harmony.PatchAll(typeof(CheatDetectionSuppressPatch)); + Log.LogInfo("[CheatType] verdict handlers guarded=" + CheatDetectionSuppressPatch.Guarded + + (CheatDetectionSuppressPatch.Unguarded == null + ? " (all three)" + : " - UNGUARDED, not found on CheatManager: " + CheatDetectionSuppressPatch.Unguarded)); + } + catch (Exception cheatTypeEx) + { + Log.LogError("[CheatType] verdict suppression NOT armed: " + cheatTypeEx.Message); + } + + try + { + harmony.PatchAll(typeof(CheatDormDropGuardPatch)); + Log.LogInfo("[CheatDrop] dorm drop (EFLCHFDDPJP) held back for the anti-cheat reason; " + + "every other reason still drops the player."); + } + catch (Exception cheatDropEx) + { + Log.LogError("[CheatDrop] dorm-drop guard NOT armed: " + cheatDropEx.Message); + } + + Log.LogInfo("[EacStub] EasyAntiCheat Hydra runtime neutralisation enabled=" + EacGate.Enabled + + " (RADICADE_EAC_NEUTRALIZE=0 before launch leaves EAC alone)"); + Log.LogInfo("[ExitTrace] shutdown-request stack dump armed; [QuitVeto] wantsToQuit veto enabled=" + + UnityQuitVetoPatch.Enabled); + } + catch (Exception quitEx) + { + Log.LogError("[ExitTrace] failed to apply - continuing without them: " + quitEx); + } + + + + try + { + CrashAddressMap.Start(); + Log.LogInfo("[AddrMap] enabled=" + CrashAddressMap.Enabled + + " target=0x" + CrashAddressMap.TargetRva.ToString("x") + + " (RADICADE_ADDR_MAP=0 disables; RADICADE_ADDR_TARGET=0x... re-aims it)"); + } + catch (Exception mapEx) + { + + Log.LogError("[AddrMap] failed to start: " + mapEx); + } + + + + + try + { + harmony.PatchAll(typeof(ToolCollisionResetGuardPatch)); + Log.LogInfo("[ToolGuard] Tool.ResetToDefaultCollisionLayer skipped=" + ToolCollisionResetGuardPatch.Enabled + + " (RADICADE_TOOL_RESET=0 before launch restores the original reset)"); + } + catch (Exception toolEx) + { + Log.LogError("[ToolGuard] failed to apply - the room crash is NOT guarded: " + toolEx); + } + Log.LogInfo($"[August] Connection patches loaded for {ServerUrl.Value}; image key {ImageKeyId ?? "unknown"}."); + } +} + +[HarmonyPatch] +internal static class AugustLaunchDiagnostics +{ + [HarmonyPrefix, HarmonyPatch(typeof(NDIIPBKGGIG), "FIBOEONALKB")] + [HarmonyPatch(typeof(NDIIPBKGGIG), "IKHPLCCBENN")] + private static void Trace(JKBPKDAGEHN __0) + { + if (__0 == JKBPKDAGEHN.LAUNCH_VALIDATION_FAILED) + RadicadeEacBypassPlugin.LogSource.LogWarning("[August launch diagnostic] " + Il2CppSystem.Environment.StackTrace); + } +} + +[HarmonyPatch] +internal static class AugustChallengePatches +{ + [HarmonyPrefix, HarmonyPatch(typeof(EACManager), nameof(EACManager.GenerateChallengeResponse))] + private static bool Challenge(ref string __result) + { + __result = "radicade-eac-bypass"; + return false; + } + + + [HarmonyPrefix, HarmonyPatch(typeof(EACManager), "MGEAHKJIIMO")] + private static bool Ready(ref bool __result) + { + __result = true; + return false; + } +} + + + + + + + + + +[HarmonyPatch(typeof(ADJOODNBCKG.NFPDGJNDFOF), "GBAMNAKGEGD")] +internal static class AugustLaunchValidatorPatch +{ + static readonly System.Threading.CancellationTokenSource Pump = new(); + + [HarmonyPrefix] + internal static bool Bypass(ADJOODNBCKG.NFPDGJNDFOF __instance) + { + var promise = __instance.promise; + if (promise == null) + return true; + RadicadeEacBypassPlugin.LogSource.LogInfo("[August] Bypassing native launch validator (hash check cannot pass in a patched install)."); + + + + + + + + + + + var context = RadicadeEacBypassPlugin.MainThreadContext; + try + { + var before = Il2CppSystem.Threading.SynchronizationContext.Current; + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[August] validator thread context: {(before != null ? before.GetType().Name : "null")}; installing main-thread context"); + if (context != null) + Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(context); + try + { + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[August] resolving: done={SafeState(promise, "EPCPPDDJCMO")} handlers={SafeHandlerCount(promise)}"); + promise.OLKGFACPIPC(); + RadicadeEacBypassPlugin.LogSource.LogInfo("[August] Launch-validation promise resolved; boot continues."); + } + catch (Exception e) + { + RadicadeEacBypassPlugin.LogSource.LogError($"[August] Resolve failed: {e}"); + } + finally + { + Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(before); + } + } + catch (Exception outer) + { + RadicadeEacBypassPlugin.LogSource.LogError($"[August] Context install failed: {outer}"); + } + return false; + } + + static int SafeHandlerCount(PFDOLDLEIBA promise) + { + try { return promise.HFPMLNDOGLF?.Count ?? 0; } + catch { return -1; } + } + + static bool SafeState(PFDOLDLEIBA promise, string prop) + { + try + { + return prop switch + { + "EPCPPDDJCMO" => promise.EPCPPDDJCMO, + "MPJBGFFEEOP" => promise.MPJBGFFEEOP, + "NEIINCCHEPF" => promise.NEIINCCHEPF, + _ => false, + }; + } + catch { return false; } + } +} + + + + + + + + + + + + + + + + +internal static class RsaVerifyNeutralizerPatches +{ + static readonly byte[] P1KeyModulus = System.Convert.FromBase64String( + "X07yXkxaaLcZ1wVXfkWjgFkkqdoLhDFm0GPODsF+Q47pSUlbLvtXGqStnyEJEIrQmgDiicAvCdGRq4lovr2l5sIPMaoyizsbVHBdwLUrCsji0RvSBnmvN+8KqQ8STnB4DP4pAsPilfD35def4WuX/xMCXB5+hQUVhv27HPV8Dj9XzHuJAijIM9UwDZmvUcECyiO4wv+TaZi2+ELBtaLCQR8Gm1ZPeDEwP62Ch6MJy0jx5pkvvD0KdF9Wye+3/Wx31Zn/Trdo9HL4sGFWPDM9H9kQhZd5wkTHuxpwGIIhlzIwvY2/pBGdZKP6fi1D2jROEmVkBDyhmYY9nO+s3/bndQ=="); + + static int p1Calls, totalCalls; + + internal static void Apply(Harmony harmony) + { + + PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyHash", + new[] + { + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray), + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray), + typeof(Il2CppSystem.Security.Cryptography.HashAlgorithmName), + typeof(Il2CppSystem.Security.Cryptography.RSASignaturePadding) + }); + + + PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyHash", + new[] + { + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray), + typeof(int), + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray) + }); + + + PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyData", + new[] + { + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray), + typeof(Il2CppSystem.Object), + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray) + }); + } + + static void PatchVerify(Harmony harmony, Type target, string methodName, Type[] args) + { + try + { + var m = AccessTools.Method(target, methodName, args); + if (m == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[RsaVerify] {target.Name}.{methodName}({args.Length} args) not found - skipped"); + return; + } + harmony.Patch(m, prefix: new HarmonyMethod(typeof(RsaVerifyNeutralizerPatches).GetMethod(nameof(VerifyPrefix)))); + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[RsaVerify] patched {target.Name}.{methodName}({string.Join(", ", Array.ConvertAll(args, t => t.Name))})"); + } + catch (Exception e) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[RsaVerify] failed to patch {target.Name}.{methodName}: {e.GetType().Name}: {e.Message}"); + } + } public static bool VerifyPrefix( + Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider __instance, ref bool __result) + { + totalCalls++; + var isP1 = false; + try + { + var modulus = __instance.ExportParameters(false).Modulus; + isP1 = modulus != null && modulus.Length == P1KeyModulus.Length && modulus.SequenceEqual(P1KeyModulus); + if (isP1) + p1Calls++; + } + catch + { + + } + + + + + + + __result = isP1; + if (totalCalls <= 8) + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[ImageSig] RSA verify #{totalCalls} ({(isP1 ? "p1 key" : "game key")}) forced {(isP1 ? "true" : "false")}"); + return false; + } +} + + + + + + + + + + + + + + + + + + + + + +[HarmonyPatch] +internal static class FileCheckEntryPatch +{ + private static System.Collections.Generic.IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(KKHDHILFOGO), "GGIFLDPHCIP"); + yield return AccessTools.Method(typeof(KKHDHILFOGO), "GOCCLPJDJIC"); + } + + [HarmonyPrefix] + internal static bool Bypass(ref MCFFIFIPNKE __result) + { + var result = KKHDHILFOGO.BJGODLEIDGE.NOMPJGOIAEB("radicade: file check bypassed"); + __result = PFDOLDLEIBA.PLJINLOCHGM(result); + RadicadeEacBypassPlugin.LogSource.LogInfo("[FileCheck] entry bypassed - resolved success promise returned"); + return false; + } +} + + + + + + + + + + + +internal static unsafe class PebModuleHider +{ + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_BASIC_INFORMATION + { + public IntPtr Reserved1; + public IntPtr PebBaseAddress; + public IntPtr Reserved2_0; + public IntPtr Reserved2_1; + public IntPtr UniqueProcessId; + public IntPtr Reserved3; + } + + [DllImport("ntdll.dll")] + private static extern int NtQueryInformationProcess( + IntPtr hProcess, int processInfoClass, ref PROCESS_BASIC_INFORMATION pbi, + int cb, out int returnLength); + + private struct LIST_ENTRY { public IntPtr Flink, Blink; } + + + + + + private const int OffInMemoryOrder = 0x10; + private const int OffInInitOrder = 0x20; + private const int OffBaseDllName = 0x58; + + public static bool HideModule(string dllName) + { + var pbi = default(PROCESS_BASIC_INFORMATION); + int status = NtQueryInformationProcess(new IntPtr(-1), 0, ref pbi, + Marshal.SizeOf(), out _); + if (status != 0 || pbi.PebBaseAddress == IntPtr.Zero) + return false; + + byte* peb = (byte*)pbi.PebBaseAddress; + byte* ldr = *(byte**)(peb + 0x18); + if (ldr == null) + return false; + + LIST_ENTRY* head = (LIST_ENTRY*)(ldr + 0x10); + byte* target = null; + for (LIST_ENTRY* n = (LIST_ENTRY*)head->Flink; n != head && target == null; n = (LIST_ENTRY*)n->Flink) + { + byte* entry = (byte*)n; + ushort* us = (ushort*)(entry + OffBaseDllName); + ushort lenBytes = *us; + char* buf = *(char**)(entry + OffBaseDllName + 8); + if (buf == null || lenBytes == 0) + continue; + string name = new string(buf, 0, lenBytes / 2); + if (name.Equals(dllName, StringComparison.OrdinalIgnoreCase)) + target = entry; + } + + if (target == null) + return false; + + Unlink((LIST_ENTRY*)target); + Unlink((LIST_ENTRY*)(target + OffInMemoryOrder)); + Unlink((LIST_ENTRY*)(target + OffInInitOrder)); + return true; + } + + private static void Unlink(LIST_ENTRY* e) + { + var flink = (LIST_ENTRY*)e->Flink; + var blink = (LIST_ENTRY*)e->Blink; + if (flink == e || blink == e) + return; + blink->Flink = e->Flink; + flink->Blink = e->Blink; + e->Flink = (IntPtr)e; + e->Blink = (IntPtr)e; + } +} + + + + + + + + + + + + +internal static unsafe class PeHeaderWiper +{ + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr GetModuleHandleW(string moduleName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint newProtect, out uint oldProtect); + + public static bool WipeSignatures(string dllName) + { + IntPtr baseAddr = GetModuleHandleW(dllName); + if (baseAddr == IntPtr.Zero) + return false; + + byte* p = (byte*)baseAddr; + int eLfanew = *(int*)(p + 0x3C); + if (eLfanew <= 0 || eLfanew > 0x1000) + return false; + + if (!VirtualProtect(baseAddr, (UIntPtr)0x1000, 0x04 , out uint old)) + return false; + + bool wasMz = p[0] == (byte)'M' && p[1] == (byte)'Z'; + bool wasPe = *(uint*)(p + eLfanew) == 0x00004550; + + *(ushort*)p = 0; + *(uint*)(p + eLfanew) = 0; + + VirtualProtect(baseAddr, (UIntPtr)0x1000, old, out _); + return wasMz && wasPe; + } +} + + + + + + + + + + + + + + + + +[HarmonyPatch] +internal static class ExitInitiatorTracerPatch +{ + private static System.Collections.Generic.IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(CCKNJCHHBLL), "BEFDPLAELIH"); + yield return AccessTools.Method(typeof(CCKNJCHHBLL), "GIEEBIOOFML"); + yield return AccessTools.Method(typeof(CCKNJCHHBLL), "FNOEHNDLHHG"); + } + + [HarmonyPrefix] + internal static bool TraceAndBlock() + { + var stack = ""; + try { stack = Environment.StackTrace; } catch { } + + + + + + + + + var armed = HileGate.DroppedCount > 0; + RadicadeEacBypassPlugin.LogSource.LogWarning(armed + ? "[ExitInit] blocked managed exit initiator - caller trace:\n" + stack + : "[ExitInit] allowed managed exit initiator (no report filed yet) - caller trace:\n" + stack); + return !armed; + } +} + + + + + + + + + + + + + + + + + + + + +[HarmonyPatch] + + + + + + +[HarmonyPatch(typeof(BestHTTP.HTTPManager), "SendRequest", new[] { typeof(BestHTTP.HTTPRequest) })] +internal static class ExitSequenceTracePatch +{ + private static int _dumped; + + [HarmonyPriority(Priority.First)] + private static void Prefix(BestHTTP.HTTPRequest request) + { + try + { + var path = request?.Uri?.AbsolutePath; + if (path == null) return; + if (path.IndexOf("/matchmaking/player/logout", StringComparison.OrdinalIgnoreCase) < 0 && + path.IndexOf("/playersettings/playersettings", StringComparison.OrdinalIgnoreCase) < 0) + return; + if (++_dumped > 4) return; + + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[ExitTrace] shutdown request " + path + " - managed caller stack:" + ExitTraceHelper.SafeStack()); + } + catch (Exception traceEx) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[ExitTrace] dump failed: " + traceEx.Message); + } + } +} + + + + + + + + + + + + + + + + + + + + +[HarmonyPatch] +internal static class UnityQuitVetoPatch +{ + internal static bool Enabled => _enabled; + + private static readonly bool _enabled = IsEnabled(); + + private static int _blocks; + + private static bool IsEnabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_QUIT_VETO"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes"; + } + catch { return false; } + } + + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var noArg = AccessTools.Method(typeof(CCKNJCHHBLL), "GBGMBMGDGGD", Type.EmptyTypes); + if (noArg != null) yield return noArg; + + var flag = AccessTools.Method(typeof(CCKNJCHHBLL), "GBGMBMGDGGD", new[] { typeof(bool) }); + if (flag != null) yield return flag; + } + + [HarmonyPrefix] + private static bool Veto(ref bool __result) + { + if (!Enabled) return true; + + __result = false; + + var count = ++_blocks; + if (count <= 3 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[QuitVeto] wantsToQuit answered false (block #" + count + ") - Unity's quit is cancelled. " + + "This is the anti-cheat shutdown, not a user quit; the veto is armed by RADICADE_QUIT_VETO=1, set it to 0 to disable." + + ExitTraceHelper.SafeStack()); + return false; + } +} + + + + +internal static class ModuleHidingPolicy +{ + internal static bool Enabled { get; } = ReadFlag("RADICADE_MODULE_HIDE"); + + private static bool ReadFlag(string name) + { + try + { + var raw = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes"; + } + catch { return false; } + } + + internal static string DescribeModuleList() + { + try + { + using (var self = System.Diagnostics.Process.GetCurrentProcess()) + { + var seen = false; + var path = string.Empty; + + foreach (System.Diagnostics.ProcessModule module in self.Modules) + { + if (!string.Equals(module.ModuleName, "winhttp.dll", StringComparison.OrdinalIgnoreCase)) continue; + seen = true; + path = module.FileName ?? string.Empty; + break; + } + + return seen + ? "[ModuleList] winhttp.dll is enumerated by a Toolhelp module walk: " + path + : "[ModuleList] winhttp.dll is NOT enumerated by a Toolhelp module walk - a loaded image the loader lists do not know about is the most literal definition of obscured."; + } + } + catch (Exception ex) + { + return "[ModuleList] enumeration failed: " + ex.Message; + } + } +} + + + + + + + + + + + + + + + + + + + +[HarmonyPatch] +internal static class CheatScanSuppressPatch +{ + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _skips; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_SCAN"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static System.Reflection.MethodBase TargetMethod() + { + var type = AccessTools.TypeByName("CheatManager"); + if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved."); + + var method = AccessTools.Method(type, "MJLAGDJLOBC", System.Type.EmptyTypes); + if (method == null) throw new System.MissingMethodException("CheatManager.MJLAGDJLOBC() is gone - the scan moved or was renamed."); + + return method; + } + + [HarmonyPrefix] + private static bool Skip() + { + if (!Enabled) return true; + + var count = ++_skips; + if (count <= 3 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatScan] skipped CheatManager.MJLAGDJLOBC() (#" + count + + ") - this is the pass behind the Obscured/winhttp.dll verdict, so no report and no shutdown follow from it."); + return false; + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[HarmonyPatch] +internal static class CheatDetectionSuppressPatch +{ + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _skips; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_TYPE"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var type = AccessTools.TypeByName("CheatManager"); + if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved."); + + + + + + var found = new System.Collections.Generic.List(); + var missing = new System.Collections.Generic.List(); + foreach (var name in new[] { "OnObscuredTypeCheatDetected", "OnTimeCheatDetected", "OnAdvancedMovementCheatDetected" }) + { + var method = AccessTools.Method(type, name, System.Type.EmptyTypes); + if (method == null) + { + missing.Add(name); + continue; + } + + found.Add(name); + yield return method; + } + + _guarded = found.Count; + _unguarded = missing.Count == 0 ? null : string.Join(", ", missing); + } + + internal static int Guarded => _guarded; + + internal static string Unguarded => _unguarded; + + private static int _guarded; + + private static string _unguarded; + + [HarmonyPrefix] + private static bool Skip(System.Reflection.MethodBase __originalMethod) + { + if (!Enabled) return true; + + var count = ++_skips; + if (count <= 5 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatType] skipped CheatManager." + __originalMethod.Name + "() (#" + count + + ") - this is a verdict handler that ends in the dorm drop with EFLCHFDDPJP and the shutdown behind it, so the room survives the detection."); + return false; + } +} + + + + + + + + + + + + + + +[HarmonyPatch] +internal static class CheatDormDropGuardPatch +{ + private const string AntiCheatReason = "385120"; + + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _skips; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_DROP"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static System.Reflection.MethodBase TargetMethod() + { + var type = AccessTools.TypeByName("CheatManager"); + if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved."); + + var method = AccessTools.Method(type, "EFLCHFDDPJP", new[] { typeof(string) }); + if (method == null) + throw new System.MissingMethodException("CheatManager.EFLCHFDDPJP(string) is gone - the dorm drop moved or was renamed, so the guard is NOT armed."); + + return method; + } + + [HarmonyPrefix] + private static bool Skip(string JELLDPBCBLG) + { + if (!Enabled) return true; + + + if (string.IsNullOrEmpty(JELLDPBCBLG) || JELLDPBCBLG.IndexOf(AntiCheatReason, StringComparison.Ordinal) < 0) + return true; + + var count = ++_skips; + if (count <= 5 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatDrop] skipped CheatManager.EFLCHFDDPJP('" + JELLDPBCBLG + "') (#" + count + + ") - the anti-cheat's own drop, so the player stays where they are instead of being sent to the dormroom mid-session."); + return false; + } +} + + + + + + + + + + + + + +[HarmonyPatch] +internal static class CheatManagerTracePatch +{ + private const int PerMethodLimit = 4; + private const int LineLimit = 90; + + private static readonly System.Diagnostics.Stopwatch Clock = System.Diagnostics.Stopwatch.StartNew(); + private static readonly System.Collections.Generic.Dictionary Seen = new System.Collections.Generic.Dictionary(); + private static int _lines; + + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var outer = AccessTools.TypeByName("CheatManager"); + if (outer == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatTrace] CheatManager not resolvable - nothing traced."); + yield break; + } + + var types = new System.Collections.Generic.List { outer }; + foreach (var nested in outer.GetNestedTypes(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)) + types.Add(nested); + + foreach (var type in types) + { + foreach (var method in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly)) + { + if (method.IsAbstract || method.ContainsGenericParameters) continue; + yield return method; + } + } + } + + [HarmonyPrefix] + private static void Trace(System.Reflection.MethodBase __originalMethod) + { + try + { + if (_lines >= LineLimit) return; + + var key = __originalMethod.DeclaringType.Name + "." + __originalMethod.Name; + + Seen.TryGetValue(key, out var count); + if (count >= PerMethodLimit) return; + Seen[key] = count + 1; + _lines++; + + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatTrace] +" + Clock.ElapsedMilliseconds + "ms " + key); + } + catch { } + } +} + +internal static class ExitTraceHelper +{ + internal static string SafeStack() + { + try { return "\n" + Environment.StackTrace; } catch { return "\n"; } + } +} + + + + + + + + + + + + + + + + + + + + + + +internal static class EacGate +{ + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _logCount; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_EAC_NEUTRALIZE"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + internal static System.Type Find(string fullName) + { + try + { + var byName = System.Type.GetType(fullName + ", EasyAntiCheat.Client"); + if (byName != null) return byName; + } + catch { } + + + + + + + + try + { + var forced = typeof(EasyAntiCheat.Client.Hydra.Runtime).Assembly; + var direct = forced.GetType(fullName); + if (direct != null) return direct; + } + catch { } + + try { return AccessTools.TypeByName(fullName); } catch { return null; } + } + + private const int LogLimit = 8; + + internal static void Log(string message) + { + try + { + if (++_logCount > LogLimit) return; + RadicadeEacBypassPlugin.LogSource.LogWarning(message); + } + catch { } + } +} + + + + +[HarmonyPatch] +internal static class EacNativeClientStubPatch +{ + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var type = EacGate.Find("EasyAntiCheat.Client.Hydra.NativeClient"); + if (type == null) + { + EacGate.Log("[EacStub] Hydra.NativeClient not resolvable - EAC runtime left untouched."); + yield break; + } + + foreach (var method in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + if (method.Name == "Initialize") + yield return method; + } + + [HarmonyPrefix] + private static bool Skip(System.Reflection.MethodBase __originalMethod) + { + if (!EacGate.Enabled) return true; + + EacGate.Log("[EacStub] skipped " + __originalMethod.DeclaringType + "." + __originalMethod.Name + + " - EasyAntiCheat's native client never starts, so its module scanner never runs."); + return false; + } +} + + + + + +[HarmonyPatch] +internal static class EacHealthQueryLiePatch +{ + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var native = EacGate.Find("EasyAntiCheat.Client.Hydra.NativeClient"); + if (native != null) + foreach (var method in native.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + if (method.Name == "IsStartedWithEasyAntiCheat" && method.ReturnType == typeof(bool)) + yield return method; + + var runtime = EacGate.Find("EasyAntiCheat.Client.Hydra.Runtime"); + if (runtime != null) + foreach (var method in runtime.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + if ((method.Name == "IsActive" || method.Name == "get_Initialized") && method.ReturnType == typeof(bool)) + yield return method; + } + + [HarmonyPrefix] + private static bool Lie(ref bool __result, System.Reflection.MethodBase __originalMethod) + { + if (!EacGate.Enabled) return true; + + __result = true; + EacGate.Log("[EacStub] " + __originalMethod.DeclaringType + "." + __originalMethod.Name + " answered true."); + return false; + } +} + +[HarmonyPatch] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[HarmonyPatch] +internal static class ToolCollisionResetGuardPatch +{ + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _skips; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_TOOL_RESET"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static System.Reflection.MethodBase TargetMethod() + { + var method = AccessTools.Method(typeof(Tool), "ResetToDefaultCollisionLayer", Type.EmptyTypes); + if (method == null) + throw new MissingMethodException( + "Tool.ResetToDefaultCollisionLayer() not found - the room crash guard is NOT armed."); + return method; + } + + [HarmonyPrefix] + private static bool Skip(Tool __instance) + { + if (!Enabled) return true; + + var count = ++_skips; + + + if (count <= 5 || count % 50 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[ToolGuard] skipped Tool.ResetToDefaultCollisionLayer on " + Describe(__instance) + + " (#" + count + ") - this method is the frame directly above the access violation " + + "at GameAssembly.dll+0x470728d, which walks the tool's ECS collision-layer components. " + + "Tool resets still apply; only the collision layer is left as it was."); + return false; + } + + private static string Describe(Tool tool) + { + try + { + if (tool == null) return ""; + var owner = tool.gameObject; + return (owner == null) ? "" : "'" + owner.name + "'"; + } + catch (Exception ex) + { + return ""; + } + } +} + +internal static class ExitStateVetoPatch +{ + internal static bool Enabled => _enabled; + + private static readonly bool _enabled = IsEnabled(); + + private static int _blocks; + + private static bool IsEnabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_EXIT_VETO"); + if (string.IsNullOrWhiteSpace(raw)) return false; + raw = raw.Trim().ToLowerInvariant(); + return raw is "1" or "on" or "true" or "yes"; + } + catch { return false; } + } + + private static System.Collections.Generic.IEnumerable TargetMethods() + { + + var property = AccessTools.PropertySetter(typeof(CCKNJCHHBLL), "GLNIKJNLCIB"); + if (property != null) yield return property; + + + + var raw = AccessTools.Method(typeof(CCKNJCHHBLL), "KALEFEDAHCC", + new[] { typeof(CCKNJCHHBLL.CCFIKLBABNB) }); + if (raw != null) yield return raw; + } + + + + [HarmonyPrefix] + private static bool Block(CCKNJCHHBLL.CCFIKLBABNB __0) + { + if (!Enabled) return true; + if (__0 == CCKNJCHHBLL.CCFIKLBABNB.Running) return true; + + var count = ++_blocks; + + var caller = count == 1 ? ExitTraceHelper.SafeStack() : string.Empty; + if (count <= 3 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[ExitVeto] blocked AppExitState -> {__0} (block #{count}); the session keeps running. " + + "This is the anti-cheat-driven exit, not a user quit - the veto is armed by RADICADE_EXIT_VETO=1, set it to 0 to disable." + caller); + return false; + } +} + diff --git a/2023Patch/RadicadeAugustClient/README.md b/2023Patch/RadicadeAugustClient/README.md new file mode 100644 index 0000000..a6ec2f1 --- /dev/null +++ b/2023Patch/RadicadeAugustClient/README.md @@ -0,0 +1,34 @@ +# Radicade August Client + +Startup integration for Rec Room **20230825**, compiled against that build's +generated BepInEx interop assemblies. Requires the metadata-reader repair in +`tools/AugustInterop` first. It does not use the incompatible March UI patches. + +Build from the repository root: + +```powershell +dotnet build 2023Patch/RadicadeAugustClient/RadicadeAugustClient.csproj -c Release +``` + +Override `-p:BepInExRoot=/BepInEx` for a different installation. No automatic +deployment occurs. Install `bin/Release/net6.0/RadicadeAugustClient.dll` into +`BepInEx/plugins`, with the old RadicadeEacBypass.dll disabled/backed up. The two +plugins share a config identity and must not be installed together. + +Local config (`BepInEx/config/com.radicade.eacbypass.cfg`): + +```ini +[Connection] +ServerUrl = https://localhost:3030 +[TLS] +AllowedHosts = localhost +``` + +TLS exceptions are limited to the configured host allowlist. The plugin checks +the game version and requires HTTPS. It reads the normalized metadata copy only +to identify the image signature key. Native game files are not modified. + +Verified: build succeeds, plugin loads, TLS succeeds, and server configuration +downloads. **Not yet playable:** the next client error is launch validation; +login, Photon multiplayer, watch UI and room loading are not verified or fully +ported. See `docs/2023-AUGUST-BUILD.md` for the current status and backups. diff --git a/2023Patch/RadicadeAugustClient/RadicadeAugustClient.csproj b/2023Patch/RadicadeAugustClient/RadicadeAugustClient.csproj new file mode 100644 index 0000000..fa35556 --- /dev/null +++ b/2023Patch/RadicadeAugustClient/RadicadeAugustClient.csproj @@ -0,0 +1,22 @@ + + + net6.0 + enable + true + latest + C:\RecRoom\2023-08-29T06-53-33Z\BepInEx + + + + + + + + + + + + + + + diff --git a/2023Patch/RadicadeEacBypass/Diag.cs b/2023Patch/RadicadeEacBypass/Diag.cs new file mode 100644 index 0000000..fc12706 --- /dev/null +++ b/2023Patch/RadicadeEacBypass/Diag.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; + +/// +/// Single switch for the per-request chatter the patch used to print on every +/// login (the [Net], [Connection], [Auth], [HttpStatus] and per-connection [TLS] +/// lines). It is OFF by default: the log now carries patch status and real +/// problems only. Set RADICADE_DIAG=1 to get the full trace back when something +/// needs diagnosing. +/// +internal static class Diag +{ + internal static readonly bool Verbose = ReadFlag("RADICADE_DIAG"); + + private static readonly object Gate = new object(); + private static readonly HashSet Seen = new HashSet(StringComparer.Ordinal); + + internal static bool ReadFlag(string name) + { + try + { + var raw = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrWhiteSpace(raw)) return false; + var value = raw.Trim().ToLowerInvariant(); + return !(value == "0" || value == "off" || value == "false" || value == "no"); + } + catch + { + return false; + } + } + + /// Per-request tracing: printed only with RADICADE_DIAG=1. + internal static void Trace(string message) + { + if (!Verbose) return; + Info(message); + } + + internal static void Info(string message) + { + try { RadicadeEacBypassPlugin.LogSource.LogInfo(message); } catch { } + } + + internal static void Warn(string message) + { + try { RadicadeEacBypassPlugin.LogSource.LogWarning(message); } catch { } + } + + internal static void Error(string message) + { + try { RadicadeEacBypassPlugin.LogSource.LogError(message); } catch { } + } + + /// True the first time this key is seen in the session. + internal static bool FirstTime(string key) + { + try + { + lock (Gate) return Seen.Add(key); + } + catch + { + return false; + } + } +} diff --git a/2023Patch/RadicadeEacBypass/EmbeddedTlsPatches.cs b/2023Patch/RadicadeEacBypass/EmbeddedTlsPatches.cs new file mode 100644 index 0000000..4d5e057 --- /dev/null +++ b/2023Patch/RadicadeEacBypass/EmbeddedTlsPatches.cs @@ -0,0 +1,137 @@ +using System; +using System.Linq; +using HarmonyLib; +using TLS = Org.BouncyCastle.Crypto.Tls.LegacyTlsAuthentication; +using TimeValid = Org.BouncyCastle.Crypto.Tls.TimeValidVerifyer; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[HarmonyPatch] +internal static class EmbeddedTlsPatches +{ + internal static void Apply(Harmony harmony) + { + try + { + harmony.Patch(AccessTools.Method(typeof(TLS), "NotifyServerCertificate"), + prefix: new HarmonyMethod(typeof(EmbeddedTlsPatches), nameof(NotifyServerCertificatePrefix))); + RadicadeEacBypassPlugin.LogSource.LogInfo("[TLS] Patched LegacyTlsAuthentication.NotifyServerCertificate (host-scoped accept)"); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[TLS] NotifyServerCertificate patch failed: {ex.GetType().Name}: {ex.Message}"); + } + + try + { + harmony.Patch(AccessTools.Method(typeof(TimeValid), "IsValid"), + prefix: new HarmonyMethod(typeof(EmbeddedTlsPatches), nameof(IsValidPrefix))); + RadicadeEacBypassPlugin.LogSource.LogInfo("[TLS] Patched TimeValidVerifyer.IsValid (host-scoped accept)"); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[TLS] TimeValidVerifyer patch failed: {ex.GetType().Name}: {ex.Message}"); + } + } + + + + + + + + static bool NotifyServerCertificatePrefix(TLS __instance) + { + try + { + var uri = __instance.TargetUri; + var host = uri?.Host; + var verifyerType = __instance.verifyer != null ? __instance.verifyer.GetType().FullName : ""; + + Diag.Trace( + $"[TLS] NotifyServerCertificate: host={host ?? ""}, verifyer={verifyerType}"); + + if (host != null && IsHostAllowed(host)) + { + Diag.Trace( + $"[TLS] Accepting server certificate for allowed Radicade host '{host}' (embedded-stack bypass)"); + return false; + } + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[TLS] NotifyServerCertificate prefix error: {ex.GetType().Name}: {ex.Message}"); + } + return true; + } + + + + + + static bool IsValidPrefix(Il2CppSystem.Uri targetUri, ref bool __result) + { + try + { + var host = targetUri?.Host; + if (host != null && IsHostAllowed(host)) + { + Diag.Trace( + $"[TLS] TimeValidVerifyer.IsValid forced TRUE for allowed Radicade host '{host}'"); + __result = true; + return false; + } + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[TLS] IsValid prefix error: {ex.GetType().Name}: {ex.Message}"); + } + return true; + } + + + + + public static bool IsHostAllowed(string host) + { + if (string.IsNullOrWhiteSpace(host)) return false; + var raw = RadicadeEacBypassPlugin.TlsAllowedHosts?.Value; + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Split(',') + .Select(h => h.Trim().TrimEnd('.')) + .Where(h => h.Length > 0) + .Any(h => host.TrimEnd('.').Equals(h, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/2023Patch/RadicadeEacBypass/HttpTolerancePatches.cs b/2023Patch/RadicadeEacBypass/HttpTolerancePatches.cs new file mode 100644 index 0000000..ae51635 --- /dev/null +++ b/2023Patch/RadicadeEacBypass/HttpTolerancePatches.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; +using System.Text; +using System.Reflection; +using BestHTTP; +using HarmonyLib; + +/// +/// Fabric-level repairs for two things the game cannot survive as-is: +/// a chunked response the server truncated, and two responses whose shape the +/// client's reader rejects. +/// +internal static class HttpTolerancePatches +{ + private static long _healed; + private static int _lastHealReport; + + internal static void Apply(Harmony harmony) + { + try + { + var target = TargetMethod(); + if (target == null) + { + Diag.Warn("[Http] BestHTTP.HTTPResponse.ReadChunkLength(Stream) was not found: " + + "a truncated chunked body will keep failing its request."); + return; + } + + harmony.Patch(target, + finalizer: new HarmonyMethod(AccessTools.Method(typeof(HttpTolerancePatches), nameof(Finalizer)))); + Diag.Info("[Http] truncated chunked responses are completed instead of failing their request."); + } + catch (Exception ex) + { + Diag.Warn("[Http] could not arm the tolerant chunked reader (" + ex.GetType().Name + ": " + ex.Message + ")."); + } + } + + internal static MethodBase TargetMethod() => + AccessTools.Method(typeof(HTTPResponse), "ReadChunkLength", new[] { typeof(Stream) }); + + /// + /// "Can't parse '' as a hex number" is BestHTTP reading a chunk-size line from a + /// connection the server had already closed - the pooled keep-alive connection + /// it picked was dead. Ending the body there (chunk length 0) delivers the + /// response that did arrive instead of failing the request, which is what the + /// heartbeats need: their ack body is empty anyway. + /// + private static Exception Finalizer(Exception __exception, ref int __result) + { + try + { + if (__exception == null) return null; + if (!ResponseRepair.IsTruncatedChunkFailure(__exception.Message)) return __exception; + + ReportHealed(); + __result = 0; + __exception = null; + return null; + } + catch + { + return __exception; + } + } + + private static void ReportHealed() + { + var count = System.Threading.Interlocked.Increment(ref _healed); + var now = Environment.TickCount; + if (count != 1 && unchecked(now - _lastHealReport) < 60000) return; + _lastHealReport = now; + Diag.Warn("[Http] a chunked response ended early (dead pooled connection); the request was completed " + + "with the bytes that arrived instead of failing. Occurrences so far: " + count + "."); + } + + /// + /// Applies to a live response before the client's own + /// callback reads it. Called from the request funnel, always on the transport + /// thread that owns the response. + /// + internal static void RepairResponse(HTTPResponse response, string path, string query) + { + try + { + if (response == null || string.IsNullOrEmpty(path)) return; + if (response.StatusCode != 200) return; + + string body; + try { body = response.DataAsText; } + catch { return; } + + var repaired = ResponseRepair.RepairBody(path, query, body, out var note); + if (repaired == null) return; + + response.Data = Encoding.UTF8.GetBytes(repaired); + + if (Diag.FirstTime("repair:" + path)) + Diag.Warn("[Repair] " + path + ": " + note + "."); + } + catch (Exception ex) + { + Diag.Warn("[Repair] could not repair a " + path + " response: " + ex.GetType().Name + ": " + ex.Message); + } + } +} diff --git a/2023Patch/RadicadeEacBypass/HubSocketAuth.cs b/2023Patch/RadicadeEacBypass/HubSocketAuth.cs new file mode 100644 index 0000000..0d62c2d --- /dev/null +++ b/2023Patch/RadicadeEacBypass/HubSocketAuth.cs @@ -0,0 +1,124 @@ +using System; +using System.Text.Json; +using BestHTTP; +using Il2CppInterop.Runtime; + +internal static class HubSocketAuth +{ + internal static string Token { get; private set; } + + internal static string LastLoginRefusalCode { get; private set; } + + private static readonly string[] AuthorizationHeaders = { "Authorization", "authorization", "AUTHORIZATION" }; + + private static readonly System.Collections.Generic.HashSet Stated = new(); + + internal static bool IsServer(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var request) || + !Uri.TryCreate(RadicadeEacBypassPlugin.ServerUrl.Value, UriKind.Absolute, out var server)) return false; + return request.Host.Equals(server.Host, StringComparison.OrdinalIgnoreCase) && request.Port == server.Port && + (request.Scheme == server.Scheme || request.Scheme == (server.Scheme == "https" ? "wss" : "ws")); + } + internal static void Capture(HTTPRequest request) + { + if (request?.Uri == null || !IsServer(request.Uri.ToString())) return; + foreach (var name in AuthorizationHeaders) + { + var values = request.GetHeaderValues(name); + if (values == null || values.Count == 0) continue; + var value = values[0]; + if (value != null && value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + Token = value.Substring(7).Trim(); + } + } + internal static void CaptureFromLoginResponse(HTTPRequest request, string url) + { + if (!IsServer(url) || !new Uri(url).AbsolutePath.EndsWith("/connect/token", StringComparison.OrdinalIgnoreCase)) return; + Token = null; + var original = request.Callback; + request.Callback = DelegateSupport.ConvertDelegate( + new Action((req, response) => + { + try + { + if (response != null && response.StatusCode == 200) + { + using var document = JsonDocument.Parse(response.DataAsText); + + + + + + + + + + + if (document.RootElement.TryGetProperty("access_token", out var token) && + token.ValueKind == JsonValueKind.String && + !string.IsNullOrWhiteSpace(token.GetString())) + { + Token = token.GetString(); + LastLoginRefusalCode = null; + Diag.Trace("[Realtime] Login token captured (value omitted)"); + } + else + { + Token = null; + var error = document.RootElement.TryGetProperty("error", out var errorElement) ? errorElement.GetString() : null; + var description = document.RootElement.TryGetProperty("error_description", out var descriptionElement) ? descriptionElement.GetString() : null; + LastLoginRefusalCode = error; + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[Realtime] connect/token answered HTTP 200 without an access_token - that is a login REFUSAL, not a login. " + + "error='" + (error ?? "-") + "', error_description='" + (description ?? "-") + "'."); + } + } + } + catch (Exception ex) { RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] Token parse failed: " + ex.GetType().Name); } + finally { original?.Invoke(req, response); } + })); + } + internal static void AuthenticateHubRequest(HTTPRequest request, string url) + { + if (!IsServer(url) || !NameserverRedirect.IsHubUrl(url)) return; + if (!string.IsNullOrWhiteSpace(Token)) + { + request.Uri = new Il2CppSystem.Uri(NameserverRedirect.EnsureHubToken(url, Token)); + request.SetHeader("Authorization", "Bearer " + Token); + } + Diag.Trace("[Realtime] Hub request " + new Uri(url).AbsolutePath + + "; token=" + (NameserverRedirect.HasHubToken(request.Uri.ToString()) ? "present" : "missing")); + } + + internal static void AuthenticateServerRequest(HTTPRequest request, string url) + { + if (request == null || !IsServer(url) || string.IsNullOrWhiteSpace(Token)) return; + try + { + if (IsAnonymousPath(new Uri(url).AbsolutePath)) return; + + foreach (var name in AuthorizationHeaders) + { + var values = request.GetHeaderValues(name); + if (values != null && values.Count > 0) return; + } + + request.SetHeader("Authorization", "Bearer " + Token); + var path = new Uri(url).AbsolutePath; + if (Stated.Add(path)) + Diag.Trace("[Auth] stated the session token on " + path + " (the client sent none)"); + } + catch (Exception ex) + { + Diag.Warn("[Auth] could not state the token on " + url + ": " + ex.GetType().Name); + } + } + + private static bool IsAnonymousPath(string path) => + path.StartsWith("/auth/connect/token", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("/auth/cachedlogin", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("/auth/eac/", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("/images/", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("/cdn/", StringComparison.OrdinalIgnoreCase); +} diff --git a/2023Patch/RadicadeEacBypass/ImageSignatureRequest.cs b/2023Patch/RadicadeEacBypass/ImageSignatureRequest.cs new file mode 100644 index 0000000..aea32de --- /dev/null +++ b/2023Patch/RadicadeEacBypass/ImageSignatureRequest.cs @@ -0,0 +1,187 @@ +#nullable enable +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using System.Text; + + +public static class ImageSignatureRequest +{ + + + + const int HeaderScanLimit = 0x100; + + const int MinimumRecords = 32; + + + + + const int PrefixProbe = 8; + + const int PackingProbe = 1024; + + const int MaxLiteralLength = 1 << 20; + + static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true); + + + public static string? DetectKeyId(byte[]? metadata) + { + if (metadata == null || metadata.Length < 24 || + BinaryPrimitives.ReadUInt32LittleEndian(metadata) != 0xFAB11BAF || + BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(4)) != 27) + return null; + int table = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(8)); + int size = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(12)); + int data = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(16)); + int dataSize = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(20)); + if (table < 24 || size < 0 || size % 8 != 0 || (long)table + size > metadata.Length || + data < 24 || dataSize < 0 || (long)data + dataSize > metadata.Length) + return null; + + return ScanKeyTails(metadata, table, size / 8, data, dataSize); + } + + + + public static string? DetectKeyIdFromGameMetadata(byte[]? metadata) + { + if (metadata == null || metadata.Length < 24) return null; + if (BinaryPrimitives.ReadUInt32LittleEndian(metadata) == 0xFAB11BAF && + BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(4)) == 27) + return DetectKeyId(metadata); + return DetectKeyIdByHeaderScan(metadata); + } + + + + + static string? ScanKeyTails(byte[] metadata, int tableOffset, int recordCount, int dataOffset, int dataSize) + { + string? key = null; + for (int record = 0; record < recordCount; record++) + { + var (length, index) = Record(metadata, tableOffset, record); + if (length < 0 || index < 0 || (long)index + length > dataSize) return null; + if (length != 8 && length != 18) continue; + var literal = Encoding.UTF8.GetString(metadata, dataOffset + index, length); + if (literal != ".rec.net" && literal != ".radicade.internal") continue; + var candidate = "KEY:RSA:p1" + literal; + if (key != null && key != candidate) return null; + key = candidate; + } + return key; + } + + + + + static (int Length, int Index) Record(byte[] metadata, int tableOffset, int record) + { + int offset = tableOffset + 8 * record; + return (BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset)), + BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset + 4))); + } + + public static string? DetectKeyIdByHeaderScan(byte[]? metadata) + { + if (metadata == null || metadata.Length < HeaderScanLimit + 8) return null; + + var fields = new List(); + for (int offset = 0; offset + 4 <= HeaderScanLimit; offset += 4) + { + int value = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset)); + if (value > 0 && value <= metadata.Length && !fields.Contains(value)) fields.Add(value); + } + + var tables = new List<(int Offset, int Count)>(); + foreach (var tableOffset in fields) + foreach (var tableSize in fields) + { + if (tableSize % 8 != 0 || tableSize / 8 < MinimumRecords) continue; + if (tableOffset < HeaderScanLimit || (long)tableOffset + tableSize > metadata.Length) continue; + tables.Add((tableOffset, tableSize / 8)); + } + + var slices = new List<(int Offset, int Size)>(); + foreach (var dataOffset in fields) + foreach (var dataSize in fields) + { + if (dataSize <= 0 || dataOffset < HeaderScanLimit) continue; + if ((long)dataOffset + dataSize > metadata.Length) continue; + slices.Add((dataOffset, dataSize)); + } + + + + tables.Sort((a, b) => b.Count.CompareTo(a.Count)); + + foreach (var (tableOffset, count) in tables) + foreach (var (dataOffset, dataSize) in slices) + { + if (!LooksLikeLiteralTable(metadata, tableOffset, count, dataOffset, dataSize, PrefixProbe)) continue; + if (!SampleDecodesAsText(metadata, tableOffset, count, dataOffset, dataSize)) continue; + if (!LooksLikeLiteralTable(metadata, tableOffset, count, dataOffset, dataSize, PackingProbe)) continue; + var key = ScanKeyTails(metadata, tableOffset, count, dataOffset, dataSize); + if (key != null) return key; + } + return null; + } + + + + + static bool LooksLikeLiteralTable(byte[] metadata, int tableOffset, int count, int dataOffset, int dataSize, int probe) + { + int examined = Math.Min(count, probe); + int contiguous = 0; + long running = 0; + for (int record = 0; record < examined; record++) + { + var (length, index) = Record(metadata, tableOffset, record); + if (length < 0 || length > MaxLiteralLength || index < 0 || (long)index + length > dataSize) return false; + if (index == running) contiguous++; + running = index + length; + } + return contiguous >= examined - Math.Max(1, examined / 100); + } + + static bool SampleDecodesAsText(byte[] metadata, int tableOffset, int count, int dataOffset, int dataSize) + { + int step = Math.Max(1, count / 48); + for (int record = 0; record < count; record += step) + { + var (length, index) = Record(metadata, tableOffset, record); + if (length < 0 || index < 0 || (long)index + length > dataSize) return false; + if (length == 0) continue; + try + { + StrictUtf8.GetCharCount(metadata, dataOffset + index, length); + } + catch (ArgumentException) + { + return false; + } + } + return true; + } + + public static string AddKeyId(string url, string serverUrl, string? keyId) + { + if (string.IsNullOrEmpty(keyId) || + !Uri.TryCreate(url, UriKind.Absolute, out var request) || + !Uri.TryCreate(serverUrl, UriKind.Absolute, out var server) || + (server.Scheme != "http" && server.Scheme != "https") || + request.Scheme != server.Scheme || request.Authority != server.Authority || + !request.AbsolutePath.StartsWith(server.AbsolutePath.TrimEnd('/') + "/images/", StringComparison.OrdinalIgnoreCase)) + return url; + + + var parameters = request.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries) + .Where(p => !Uri.UnescapeDataString(p.Split('=')[0]).Equals("rcsig", StringComparison.OrdinalIgnoreCase)) + .Append("rcsig=" + Uri.EscapeDataString(keyId)); + return request.GetLeftPart(UriPartial.Path) + "?" + string.Join("&", parameters) + request.Fragment; + } +} diff --git a/2023Patch/RadicadeEacBypass/NameserverConnectionPatches.cs b/2023Patch/RadicadeEacBypass/NameserverConnectionPatches.cs new file mode 100644 index 0000000..830dadc --- /dev/null +++ b/2023Patch/RadicadeEacBypass/NameserverConnectionPatches.cs @@ -0,0 +1,510 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using BestHTTP; +using HarmonyLib; +using Il2CppInterop.Runtime; + + + + + + + + + + + + + + + + + + + + + + + + + +internal static class HileGate +{ + internal static bool Enabled; + + + + + + + + internal static int DroppedCount; + + + + + + + + public static bool DropEnabled { get; } = IsDropEnabled(); + + private static bool IsDropEnabled() + { + try + { + var raw = System.Environment.GetEnvironmentVariable("RADICADE_HILE_GATE"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() == "drop"; + } + catch { return false; } + } + + private static bool _dumped; + + public static bool ShouldDrop(HTTPRequest request) + { + if (!Enabled || request?.Uri == null) return false; + + var path = request.Uri.AbsolutePath; + if (path == null || + !path.EndsWith("/api/PlayerReporting/v1/hile", System.StringComparison.OrdinalIgnoreCase)) + return false; + + Dump(request); + return DropEnabled; + } + + + + + + public static void Dump(HTTPRequest request) + { + if (_dumped) return; + _dumped = true; + + try + { + var body = ""; + + try + { + var raw = request.RawData; + if (raw != null && raw.Length > 0) + { + body = System.Text.Encoding.UTF8.GetString(raw); + if (body.Length > 2000) body = body.Substring(0, 2000) + "...[truncated]"; + } + else + { + var entity = request.GetEntityBody(); + if (entity != null && entity.Length > 0) + { + var text = System.Text.Encoding.UTF8.GetString(entity); + body = text + " (" + entity.Length + " bytes in the entity body)"; + } + else + { + body = ""; + } + } + } + catch (System.Exception bodyEx) + { + body = ""; + } + + var caller = ""; + try { caller = System.Environment.StackTrace; } catch { } + + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[HileGate] anti-cheat report captured (full wire form):\n " + request.MethodType + + " " + request.Uri + "\n body: " + body + + "\n managed caller stack:" + caller); + } + catch (System.Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[HileGate] capture failed: " + ex.Message); + } + } +} + + + + + + + + + + + + + + + + + + +[HarmonyPatch(typeof(HTTPManager), "SendRequest", new[] { typeof(HTTPRequest) })] +internal static class NameserverConnectionPatches +{ + + [HarmonyPriority(Priority.Last)] + private static bool Prefix(HTTPRequest request, ref HTTPRequest __result) + { + if (RequestFunnel.Handle(request)) return true; + + __result = request; + return false; + } +} + +internal static class RequestFunnel +{ + private static readonly HashSet Seen = new(); + + private static bool IsSocketScheme(string url) => + url.StartsWith("ws:", StringComparison.OrdinalIgnoreCase) || + url.StartsWith("wss:", StringComparison.OrdinalIgnoreCase); + + internal static bool Handle(HTTPRequest request) + { + if (HileGate.ShouldDrop(request)) + { + HileGate.DroppedCount++; + RadicadeEacBypassPlugin.LogSource.LogWarning("[HileGate] dropped anti-cheat report POST (no response will arrive)."); + return false; + } + + try + { + var uri = request?.Uri?.ToString(); + if (!string.IsNullOrEmpty(uri)) + Diag.Trace("[Net] " + request.MethodType + " " + uri); + } + catch + { + + } + + if (request?.Uri == null) return true; + + if (!Seen.Add(request.Pointer.ToInt64())) return true; + + var original = request.Uri.ToString(); + + + + + + + + var rewritten = IsSocketScheme(original) + ? NameserverRedirect.RewriteWebSocket(original, RadicadeEacBypassPlugin.ServerUrl.Value) + : NameserverRedirect.Rewrite(original, RadicadeEacBypassPlugin.ServerUrl.Value); + var withKeyId = ImageSignatureRequest.AddKeyId(rewritten, RadicadeEacBypassPlugin.ServerUrl.Value, + RadicadeEacBypassPlugin.ImageKeyId); + if (withKeyId != rewritten) + { + + + Diag.Trace("[ImageSig] stated key id on " + withKeyId); + } + rewritten = withKeyId; + + if (rewritten != original) + { + request.Uri = new Il2CppSystem.Uri(rewritten); + AlignHostHeader(request, rewritten); + Diag.Trace("[Connection] Redirected nameserver request to the configured Radicade server."); + } + + if (RealtimeDiagnostics.Verbose && IsSocketScheme(original)) + { + try + { + var outgoing = new System.Text.StringBuilder("[Realtime] socket request: " + original); + var headers = request.Headers; + if (headers != null) + { + foreach (var header in headers) + { + outgoing.Append(" | ").Append(header.Key).Append("="); + var values = header.Value; + if (values != null) + foreach (var value in values) outgoing.Append(value).Append(","); + } + } + RadicadeEacBypassPlugin.LogSource.LogWarning(outgoing.ToString()); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] socket request dump failed: " + + ex.GetType().Name + ": " + ex.Message); + } + } + + HubSocketAuth.Capture(request); + HubSocketAuth.CaptureFromLoginResponse(request, request.Uri.ToString()); + + + + HubSocketAuth.AuthenticateHubRequest(request, request.Uri.ToString()); + + HubSocketAuth.AuthenticateServerRequest(request, request.Uri.ToString()); + + if (Diag.Verbose) AuthGrantTrace.Report(request); + HttpOutcomeTrace.Wrap(request); + + return true; + } + + private static void AlignHostHeader(HTTPRequest request, string url) + { + if (request == null || string.IsNullOrEmpty(url)) return; + + Uri target; + if (!Uri.TryCreate(url, UriKind.Absolute, out target) || string.IsNullOrEmpty(target.Host)) return; + + var authority = target.IsDefaultPort ? target.Host : target.Host + ":" + target.Port; + try + { + var stated = request.GetHeaderValues("Host"); + if (stated != null && stated.Count > 0) + { + if (stated.Count == 1 && stated[0] == authority) return; + + request.RemoveHeader("Host"); + } + + request.SetHeader("Host", authority); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[Connection] could not align the Host header on " + url + + ": " + ex.GetType().Name + ": " + ex.Message); + } + } +} + +internal static class AuthGrantTrace +{ + private static readonly HashSet Reported = new(); + + private static readonly string[] Fields = + { + "account_id", "username", "password", "birthday", "avatar", "avatarJson", + "email", "platform_auth", "device_id", "device_class", "client_id", "ver" + }; + + internal static void Report(HTTPRequest request) + { + try + { + if (request?.Uri == null) return; + if (!request.Uri.ToString().EndsWith("/auth/connect/token", StringComparison.OrdinalIgnoreCase)) return; + if (!Reported.Add(request.Pointer.ToInt64())) return; + + var body = ReadBody(request); + if (string.IsNullOrEmpty(body)) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[AuthGrant] connect/token body was unreadable; the grant is unknown for this request."); + return; + } + + string Value(string name) + { + foreach (var pair in body.Split('&')) + { + var separator = pair.IndexOf('='); + if (separator <= 0) continue; + if (!string.Equals(pair.Substring(0, separator), name, StringComparison.OrdinalIgnoreCase)) continue; + return pair.Substring(separator + 1); + } + return null; + } + + bool Present(string name) + { + var value = Value(name); + return !string.IsNullOrWhiteSpace(value); + } + + var grant = Value("grant_type") ?? ""; + var present = new List(); + foreach (var field in Fields) + if (Present(field)) present.Add(field); + + RadicadeEacBypassPlugin.LogSource.LogInfo("[AuthGrant] connect/token grant_type=" + grant + + "; fields present: " + (present.Count == 0 ? "" : string.Join(", ", present)) + + " (values omitted)"); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[AuthGrant] could not read the grant: " + ex.GetType().Name + ": " + ex.Message); + } + } + + private static string ReadBody(HTTPRequest request) + { + try + { + var raw = request.RawData; + if (raw != null && raw.Length > 0) return System.Text.Encoding.UTF8.GetString(raw); + } + catch { } + + try + { + var entity = request.GetEntityBody(); + if (entity != null && entity.Length > 0) return System.Text.Encoding.UTF8.GetString(entity); + } + catch { } + + return null; + } +} + +[HarmonyPatch] +internal static class SendRequestImplFunnelPatch +{ + private static IEnumerable TargetMethods() + { + var impl = AccessTools.Method(typeof(HTTPManager), "SendRequestImpl", new[] { typeof(HTTPRequest) }); + return impl == null ? Enumerable.Empty() : new MethodBase[] { impl }; + } + + private static void Prefix(object[] __args) + { + try + { + if (__args == null || __args.Length == 0) return; + RequestFunnel.Handle(__args[0] as HTTPRequest); + } + catch (Exception ex) + { + Diag.Warn("[Net] funnel trace failed: " + ex.GetType().Name + ": " + ex.Message); + } + } +} + +internal static class HttpOutcomeTrace +{ + private static readonly HashSet Wrapped = new(); + private static int _completed; + + internal static void Wrap(HTTPRequest request) + { + if (request == null) return; + + string url; + string method; + string path; + string query; + try + { + if (request.Uri == null) return; + url = Shorten(request.Uri.ToString()); + method = request.MethodType.ToString(); + path = request.Uri.AbsolutePath; + query = request.Uri.Query; + } + catch + { + return; + } + + try + { + if (!Wrapped.Add(request.Pointer.ToInt64())) return; + + var original = request.Callback; + request.Callback = DelegateSupport.ConvertDelegate( + new Action((req, response) => + { + try + { + HttpTolerancePatches.RepairResponse(response, path, query); + + var completed = System.Threading.Interlocked.Increment(ref _completed); + var status = -1; + var bytes = 0; + var reason = string.Empty; + if (response != null) + { + status = response.StatusCode; + try { var data = response.Data; if (data != null) bytes = data.Length; } catch { } + try { if (!response.IsSuccess && !string.IsNullOrEmpty(response.Message)) reason = " reason=" + response.Message; } catch { } + } + + Diag.Trace("[HttpStatus] #" + completed + " " + method + " " + url + " -> " + + (response == null ? "NO RESPONSE" : status.ToString()) + + " bytes=" + bytes + reason); + + if (RealtimeDiagnostics.Verbose && url.IndexOf("/notify/hub/v1", StringComparison.OrdinalIgnoreCase) >= 0) + { + try + { + var answer = new System.Text.StringBuilder(); + answer.Append("[Realtime] socket answer: status=").Append(response == null ? -1 : status) + .Append(" message=").Append(response == null ? "" : response.Message); + var answerHeaders = response?.Headers; + if (answerHeaders != null) + { + foreach (var header in answerHeaders) + { + answer.Append(" | ").Append(header.Key).Append("="); + var values = header.Value; + if (values != null) + foreach (var value in values) answer.Append(value).Append(","); + } + } + string body = null; + try { body = response?.DataAsText; } catch { } + if (!string.IsNullOrEmpty(body)) + answer.Append(" | body=").Append(body.Length > 200 ? body.Substring(0, 200) : body); + RadicadeEacBypassPlugin.LogSource.LogWarning(answer.ToString()); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] socket answer dump failed: " + + ex.GetType().Name + ": " + ex.Message); + } + } + } + catch + { + + } + finally + { + try { original?.Invoke(req, response); } catch { } + } + })); + } + catch (Exception ex) + { + Diag.Warn("[HttpStatus] could not instrument " + method + " " + url + ": " + + ex.GetType().Name + ": " + ex.Message); + } + } + + private static string Shorten(string url) + { + try + { + var uri = new Uri(url); + var path = uri.AbsolutePath; + if (path.StartsWith("/auth/cachedlogin/forplatformid/", StringComparison.OrdinalIgnoreCase)) + return "/auth/cachedlogin/forplatformid/{redacted}"; + return path + (string.IsNullOrEmpty(uri.Query) ? string.Empty : "?" + uri.Query.Length + " chars"); + } + catch + { + return ""; + } + } +} diff --git a/2023Patch/RadicadeEacBypass/NameserverRedirect.cs b/2023Patch/RadicadeEacBypass/NameserverRedirect.cs new file mode 100644 index 0000000..dad027b --- /dev/null +++ b/2023Patch/RadicadeEacBypass/NameserverRedirect.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; + + +public static class NameserverRedirect +{ + + + + + + + + + + + + + + private static readonly Dictionary ServicePrefixes = new(StringComparer.OrdinalIgnoreCase) + { + ["api.rec.net"] = "", + ["cdn.rec.net"] = "", + ["images.rec.net"] = "/images", + ["img.rec.net"] = "/images", + ["data.rec.net"] = "/data", + ["storage.rec.net"] = "/storage", + ["notify.rec.net"] = "/notify", + ["notifications.rec.net"] = "/notify", + ["notify.radicade.internal"] = "/notify", + ["notifications.radicade.internal"] = "/notify", + ["auth.rec.net"] = "/auth", + ["accounts.rec.net"] = "/accounts", + ["rooms.rec.net"] = "/rooms", + ["matchmaking.rec.net"] = "/matchmaking", + ["chat.rec.net"] = "/chat", + + ["api.radicade.app"] = "", + ["cdn.radicade.app"] = "/cdn", + ["ai.radicade.app"] = "/ai", + ["auth.radicade.app"] = "/auth", + ["www.radicade.app"] = "/www", + ["cards.radicade.app"] = "/cards", + ["notifications.radicade.app"] = "/notify", + ["images.radicade.app"] = "/images", + ["commerce.radicade.app"] = "/commerce", + ["data.radicade.app"] = "/data", + ["datacollection.radicade.app"] = "/datacollection", + ["discovery.radicade.app"] = "/discovery", + ["matchmaking.radicade.app"] = "/matchmaking", + ["storage.radicade.app"] = "/storage", + ["chat.radicade.app"] = "/chat", + ["geo.radicade.app"] = "/geo", + ["leaderboard.radicade.app"] = "/leaderboard", + ["accounts.radicade.app"] = "/accounts", + ["link.radicade.app"] = "/link", + ["locker.radicade.app"] = "/locker", + ["moderation.radicade.app"] = "/moderation", + ["roomcomments.radicade.app"] = "/roomcomments", + ["roomieintegrations.radicade.app"] = "/roomieintegrations", + ["clubs.radicade.app"] = "/clubs", + ["rooms.radicade.app"] = "/rooms", + ["econ.radicade.app"] = "/econ", + ["bugreporting.radicade.app"] = "/bugreporting", + ["cms.radicade.app"] = "/cms", + ["gamelogs.radicade.app"] = "/gamelogs", + ["platformnotifications.radicade.app"] = "/platformnotifications", + ["stringscdn.radicade.app"] = "/stringscdn", + ["strings.radicade.app"] = "/strings", + ["lists.radicade.app"] = "/lists", + ["playersettings.radicade.app"] = "/playersettings", + ["studio.radicade.app"] = "/studio", + ["studiocdn.radicade.app"] = "/studiocdn", + ["thorn.radicade.app"] = "/thorn", + ["videos.radicade.app"] = "/videos", + }; + + public static string Rewrite(string requestUrl, string serverUrl) + { + if (!Uri.TryCreate(requestUrl, UriKind.Absolute, out var request) || + (request.Scheme != "http" && request.Scheme != "https")) + return requestUrl; + + + bool nameserver = request.Host.Equals("ns.rec.net", StringComparison.OrdinalIgnoreCase) || + request.Host.Equals("ns.radicade.internal", StringComparison.OrdinalIgnoreCase); + bool patchedServer = request.Host.Equals("ns.radicade.app", StringComparison.OrdinalIgnoreCase); + + ServicePrefixes.TryGetValue(request.Host, out var servicePrefix); + + if (!nameserver && !patchedServer && servicePrefix == null) + return requestUrl; + + if (!Uri.TryCreate(serverUrl, UriKind.Absolute, out var server) || + (server.Scheme != "http" && server.Scheme != "https") || + !string.IsNullOrEmpty(server.UserInfo) || !string.IsNullOrEmpty(server.Query) || + !string.IsNullOrEmpty(server.Fragment)) + throw new ArgumentException("Connection.ServerUrl must be an HTTP(S) base URL without credentials, query or fragment.", nameof(serverUrl)); + + var path = request.AbsolutePath; + + if (nameserver) + { + + if (path == "/") path = "/ns"; + } + else if (servicePrefix is { Length: > 0 }) + { + + + if (servicePrefix == "/notify" && path.StartsWith("/notify/", StringComparison.OrdinalIgnoreCase)) + path = path.Substring("/notify".Length); + else if (path.StartsWith("/images/", StringComparison.OrdinalIgnoreCase)) + path = path.Substring("/images".Length); + else if (path.StartsWith("/img/", StringComparison.OrdinalIgnoreCase)) + path = path.Substring("/img".Length); + + path = servicePrefix + path; + } + + return server.AbsoluteUri.TrimEnd('/') + path + request.Query; + } + + public static string RewriteWebSocket(string requestUrl, string serverUrl) + { + if (!Uri.TryCreate(requestUrl, UriKind.Absolute, out var request)) + return requestUrl; + + var scheme = request.Scheme.ToLowerInvariant(); + var secure = scheme == "wss" || scheme == "https"; + if (scheme != "ws" && scheme != "wss" && scheme != "http" && scheme != "https") + return requestUrl; + + + + var asHttp = new UriBuilder(request) { Scheme = secure ? "https" : "http" }.Uri.AbsoluteUri; + var rewritten = Rewrite(asHttp, serverUrl); + if (rewritten == asHttp) return requestUrl; + + if (!Uri.TryCreate(rewritten, UriKind.Absolute, out var result)) return requestUrl; + return new UriBuilder(result) { Scheme = result.Scheme == "https" ? "wss" : "ws" }.Uri.AbsoluteUri; + } + + public static bool IsHubUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) return false; + + var lower = url.ToLowerInvariant(); + var query = lower.IndexOf('?'); + var path = query < 0 ? lower : lower.Substring(0, query); + + return path.Contains("/hub/v1") || path.EndsWith("/hub") + || path.Contains("/hub/") + || path.Contains("notifyhub") + || path.Contains("/notify/hub"); + } + + public static bool HasHubToken(string url) + { + if (string.IsNullOrWhiteSpace(url)) return false; + + var query = url.IndexOf('?'); + if (query < 0) return false; + + foreach (var pair in url.Substring(query + 1).Split('&')) + { + var equals = pair.IndexOf('='); + if (equals <= 0 || equals + 1 >= pair.Length) continue; + if (pair.Substring(0, equals).Equals("access_token", StringComparison.OrdinalIgnoreCase)) return true; + } + + return false; + } + + public static string EnsureHubToken(string url, string token) + { + if (!IsHubUrl(url)) return url; + if (HasHubToken(url)) return url; + if (string.IsNullOrWhiteSpace(token)) return url; + + var raw = token.Trim(); + if (raw.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) raw = raw.Substring(7).Trim(); + else if (raw.Equals("Bearer", StringComparison.OrdinalIgnoreCase)) raw = string.Empty; + if (raw.Length == 0) return url; + + + + var query = url.IndexOf('?'); + var basePart = query < 0 ? url : url.Substring(0, query); + var rest = query < 0 ? string.Empty : url.Substring(query + 1).TrimStart('&'); + var accessToken = "access_token=" + Uri.EscapeDataString(raw); + + return basePart + "?" + accessToken + (rest.Length == 0 ? string.Empty : "&" + rest); + } +} diff --git a/2023Patch/RadicadeEacBypass/RealtimeDiagnostics.cs b/2023Patch/RadicadeEacBypass/RealtimeDiagnostics.cs new file mode 100644 index 0000000..22fc523 --- /dev/null +++ b/2023Patch/RadicadeEacBypass/RealtimeDiagnostics.cs @@ -0,0 +1,903 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading; +using HarmonyLib; + +internal static class RealtimeDiagnostics +{ + private static readonly object Gate = new object(); + private static readonly Dictionary Targets = new Dictionary(); + private static readonly Dictionary NotificationIds = new Dictionary(); + private static readonly HashSet KnownNotifications = new HashSet(); + + private static readonly HashSet HighFrequencyIds = new HashSet(StringComparer.Ordinal) + { + "PresenceUpdate", + "AccountUpdate", + "SelfAccountUpdate", + "SubscriptionUpdatePresence", + "SubscriptionUpdateProfile", + "SubscriptionUpdateGameSession", + "SubscriptionUpdateRoom", + "SubscriptionUpdateRoomPlaylist" + }; + + private static int Frames; + private static int Failures; + + private const int VerboseFrames = 40; + + private const int PreviewChars = 400; + + internal static void StartHandlerKeyProbe(Harmony harmony) + { + if (Interlocked.Exchange(ref HandlerKeyProbeStarted, 1) != 0) return; + + var worker = new Thread(() => + { + Type dispatcher = null; + bool hooksInstalled = false; + bool tablesDumped = false; + + for (int attempt = 1; attempt <= HandlerKeyProbeAttempts; attempt++) + { + try + { + if (dispatcher == null) dispatcher = FindNotificationDispatcher(); + + if (!hooksInstalled && dispatcher != null) + hooksInstalled = InstallRegistrationHooks(harmony, dispatcher); + + if (!tablesDumped && attempt <= TableDumpAttempts) + tablesDumped = DumpDispatcherKeys(attempt, dispatcher); + + if (hooksInstalled && (tablesDumped || attempt >= TableDumpAttempts)) return; + } + catch (Exception ex) + { + if (attempt == 1) + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Realtime] handler-key probe failed ({ex.GetType().Name}: {ex.Message}); retrying."); + } + + Thread.Sleep(HandlerKeyProbeIntervalMs); + } + + if (!hooksInstalled) + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Realtime] no handler-registration method was located after {HandlerKeyProbeAttempts} attempts " + + "- notification ids cannot be checked against the client's own registrations in this build."); + }) + { IsBackground = true, Name = "RadicadeNotifyKeyProbe" }; + + worker.Start(); + } + + private const int HandlerKeyProbeAttempts = 60; + private const int HandlerKeyProbeIntervalMs = 2000; + private const int TableDumpAttempts = 3; + private static int HandlerKeyProbeStarted; + + private static readonly HashSet RegisteredHandlerKeys = new HashSet(StringComparer.Ordinal); + private static bool ChatKeyReported; + + private static bool InstallRegistrationHooks(Harmony harmony, Type dispatcher) + { + var hooked = 0; + var callbacks = 0; + var generics = 0; + + foreach (var method in AccessTools.GetDeclaredMethods(dispatcher)) + { + try + { + if (method.ReturnType != typeof(void)) continue; + + var parameters = method.GetParameters(); + if (parameters.Length != 1 && parameters.Length != 2) continue; + + var keyType = parameters[0].ParameterType; + if (keyType != typeof(string) && !keyType.IsEnum) continue; + + if (parameters.Length == 2) + { + if (!IsCallback(parameters[1].ParameterType)) continue; + callbacks++; + + + + + if (method.IsGenericMethodDefinition) { generics++; continue; } + } + + harmony.Patch(method, + prefix: new HarmonyMethod(AccessTools.Method(typeof(RealtimeDiagnostics), nameof(RegistrationPrefix)))); + hooked++; + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Realtime] could not hook {dispatcher.Name}.{method.Name} for key logging ({ex.GetType().Name}: {ex.Message})."); + } + } + + if (hooked == 0) return false; + + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[Realtime] watching {hooked} dispatcher method(s) on {dispatcher.FullName} " + + $"({callbacks} registration, {generics} generic overload(s) skipped but funnel through the core, rest channels): " + + "every notification id this client accepts will be printed as it registers."); + + return true; + } + + private static bool IsCallback(Type type) + { + try + { + if (typeof(Delegate).IsAssignableFrom(type)) return true; + + for (var current = type; current != null; current = current.BaseType) + { + var name = current.Name; + if (name == "Delegate" || name == "MulticastDelegate") return true; + } + } + catch { } + + return false; + } + + private static void RegistrationPrefix(object[] __args, MethodBase __originalMethod) + { + try + { + if (__args == null || __args.Length == 0) return; + + var key = DescribeKey(__args[0]); + if (key == null) return; + + if (__args.Length == 1) + { + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[Realtime] the client called {__originalMethod?.Name}({key}) - " + + "a realtime channel/lookup name it expects the server to publish on."); + return; + } + + bool first; + lock (Gate) first = RegisteredHandlerKeys.Add(key); + if (!first) return; + + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[Realtime] the client registered a notification handler under {key} " + + $"({__originalMethod?.DeclaringType?.Name}.{__originalMethod?.Name}) - " + + $"a notification whose Id EQUALS {key} is handled, anything else is dropped."); + + + + + + + + if (key.IndexOf("Progression", StringComparison.Ordinal) >= 0 + || key.IndexOf("AppVersion", StringComparison.Ordinal) >= 0 + || key.IndexOf("Announcement", StringComparison.Ordinal) >= 0) + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[Realtime] {key} binds a payload of type {DescribeCallback(__args.Length > 1 ? __args[1] : null)}"); + + ReportChatKeyMatch(); + } + catch { } + } + + private static string DescribeCallback(object? callback) + { + if (callback == null) return "(no callback)"; + + try + { + var type = callback.GetType(); + var candidates = new List(); + + if (type.IsGenericType) candidates.AddRange(type.GetGenericArguments()); + + for (var current = type; current != null; current = current.BaseType) + { + MethodInfo? invoke = null; + try { invoke = current.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance); } + catch { } + if (invoke == null) continue; + + foreach (var parameter in invoke.GetParameters()) candidates.Add(parameter.ParameterType); + } + + var payloads = candidates + .Where(t => t != null && t != typeof(void)) + .Distinct() + .ToList(); + + if (payloads.Count == 0) return $"{type.FullName} (no payload readable)"; + + return string.Join(" | ", payloads.Select(DescribePayloadType)); + } + catch (Exception ex) + { + return $"(unreadable: {ex.GetType().Name})"; + } + } + + private static string DescribePayloadType(Type type) + { + try + { + if (type.IsPrimitive || type == typeof(string)) return type.Name; + + var members = new List(); + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + members.Add(field.Name); + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + members.Add(property.Name); + + members = members.Distinct().OrderBy(m => m, StringComparer.Ordinal).ToList(); + if (members.Count == 0) return type.FullName ?? type.Name; + + return $"{type.FullName} {{{string.Join(", ", members)}}}"; + } + catch + { + return type.FullName ?? type.Name; + } + } + + private static string DescribeKey(object value) + { + if (value is string text) return Quote(text); + if (value == null) return null; + + try + { + var type = value.GetType(); + return type.IsEnum ? Quote($"{type.Name}.{value}") : null; + } + catch + { + return null; + } + } + + private static void ReportChatKeyMatch() + { + lock (Gate) + { + if (ChatKeyReported) return; + + var numeric = RegisteredHandlerKeys.Contains("\"90\""); + var named = RegisteredHandlerKeys.FirstOrDefault( + key => key.IndexOf("ChatMessage", StringComparison.Ordinal) >= 0); + + if (!numeric && named == null) return; + ChatKeyReported = true; + + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[Realtime] chat push key check: this client registered " + + $"{(numeric ? "a handler for the chat enum's number (\"90\")" : "NO handler for the chat enum's number (\"90\")")}" + + $"{(named == null ? string.Empty : $", and {named} by name")}. " + + "Only an exact match is delivered, so the server's notification Id has to be the " + + "spelling that was registered - for chat that is the name."); + } + } + + private const int MaxTables = 25; + private const int MaxKeysPerTable = 60; + + private static bool DumpDispatcherKeys(int attempt, Type dispatcher) + { + var tables = new List(); + if (dispatcher != null) CollectStringKeyedTables(dispatcher, tables); + + if (tables.Count == 0) + { + + + + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var name = assembly.GetName().Name ?? string.Empty; + if (!name.StartsWith("RecNet", StringComparison.Ordinal)) continue; + + Type[] types; + try { types = assembly.GetTypes(); } + catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t != null).ToArray(); } + catch { continue; } + + foreach (var type in types) + { + try { CollectStringKeyedTables(type, tables); } catch { } + if (tables.Count >= MaxTables) break; + } + + if (tables.Count >= MaxTables) break; + } + } + + if (tables.Count == 0) return false; + + RadicadeEacBypassPlugin.LogSource.LogInfo( + dispatcher != null + ? $"[Realtime] the client's notification dispatcher ({dispatcher.FullName}) holds these string keys (attempt {attempt}). " + + "A notification Id must EQUAL one of them to be handled - this is the contract:" + : $"[Realtime] could not pin the notification dispatcher, so every string-keyed static table in RecNet.* is listed " + + $"instead (attempt {attempt}). The handler keys are among them; a notification Id must EQUAL one to be handled:"); + + foreach (var table in tables) + RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {table}"); + + + + + + var flat = " " + string.Join(" ", tables) + " "; + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[Realtime] chat push key check: the client registered {Registered(flat, "\"90\"")} for \"90\" and " + + $"{Registered(flat, "ChatMessageReceived")} for \"ChatMessageReceived\". " + + "Whichever is registered is the string the server's chat notification Id must equal."); + + return true; + } + + private static string Registered(string flatKeys, string key) + => flatKeys.Contains(" " + key, StringComparison.Ordinal) ? "a handler" : "NO handler"; + + private static void CollectStringKeyedTables(Type type, List tables) + { + foreach (var field in type.GetFields( + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) + { + if (tables.Count >= MaxTables) return; + + Type fieldType; + try { fieldType = field.FieldType; } catch { continue; } + if (!IsStringKeyed(fieldType)) continue; + + object value; + try { value = field.GetValue(null); } + catch { continue; } + + AddTable(tables, $"{type.Name}.{field.Name}", value); + } + + + + + + } + + private static bool IsStringKeyed(Type type) + { + try + { + return type.IsGenericType && type.GetGenericArguments()[0] == typeof(string); + } + catch + { + return false; + } + } + + private static void AddTable(List tables, string label, object value) + { + if (value == null) return; + + var keys = new List(); + try + { + if (value is System.Collections.IDictionary map) + { + foreach (var key in map.Keys) keys.Add(Quote(key?.ToString())); + } + else if (value is System.Collections.IEnumerable items) + { + foreach (var item in items) keys.Add(KeyOf(item)); + } + else return; + } + catch + { + return; + } + + if (keys.Count == 0) return; + keys.Sort(StringComparer.Ordinal); + + var shown = keys.Count <= MaxKeysPerTable + ? string.Join(", ", keys) + : string.Join(", ", keys.Take(MaxKeysPerTable)) + $", … (+{keys.Count - MaxKeysPerTable} more)"; + + tables.Add($"{label}: {shown}"); + } + + private static string KeyOf(object item) + { + if (item == null) return "(null)"; + + try + { + var type = item.GetType(); + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) + { + var key = type.GetProperty("Key"); + if (key != null) return Quote(key.GetValue(item)?.ToString()); + } + } + catch { } + + return Quote(item.ToString()); + } + + private static string Quote(string text) => text == null ? "(null)" : $"\"{text}\""; + + private static Type FindNotificationDispatcher() + { + + + var byName = FindType("LMBGGGGNBOB"); + if (byName != null && DeclaresNotificationEnum(byName)) return byName; + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + try { types = assembly.GetTypes(); } + catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t != null).ToArray(); } + catch { continue; } + + foreach (var type in types) + { + try + { + if (!type.IsEnum || !IsNotificationEnum(type)) continue; + + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[Realtime] notification enum located: {type.FullName} " + + $"(message = {Convert.ToInt64(Enum.Parse(type, "MessageReceived"), System.Globalization.CultureInfo.InvariantCulture)}, " + + $"kick = {Convert.ToInt64(Enum.Parse(type, "ModerationKick"), System.Globalization.CultureInfo.InvariantCulture)})"); + + + + + if (type.DeclaringType != null) return type.DeclaringType; + continue; + } + catch { } + } + } + + return null; + } + + private static bool DeclaresNotificationEnum(Type type) + { + try + { + foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) + if (nested.IsEnum && IsNotificationEnum(nested)) return true; + } + catch { } + + return false; + } + + private static bool IsNotificationEnum(Type type) + { + var members = Enum.GetNames(type); + return Array.IndexOf(members, "MessageReceived") >= 0 + && Array.IndexOf(members, "ModerationKick") >= 0 + && Array.IndexOf(members, "PresenceHeartbeatResponse") >= 0; + } + + internal static System.DateTime? ServerRequestedExitUtc; + + private const int ExitRequestWindowSeconds = 120; + + internal static bool ServerRequestedExitRecently() + => ServerRequestedExitUtc.HasValue + && (System.DateTime.UtcNow - ServerRequestedExitUtc.Value).TotalSeconds < ExitRequestWindowSeconds; + + private static void NoteServerRequestedExit(string id) + { + if (id != "20" && id != "6") return; + if (ServerRequestedExitRecently()) return; + + ServerRequestedExitUtc = System.DateTime.UtcNow; + RadicadeEacBypassPlugin.LogSource.LogInfo( + $"[Realtime] Id={id} arrived - the server asked this client to leave, so the AppExit guard will let the exit through."); + } + + internal static void ApplyExitWatch(Harmony harmony) + { + try + { + var transport = FindType("BestHTTP.SignalRCore.Transports.WebSocketTransport"); + var target = transport == null ? null : AccessTools.DeclaredMethod(transport, "OnMessage"); + if (target == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[Realtime] could not watch for the server's quit/logout frames - a moderator's Quit Game may be suppressed as a room-load exit."); + return; + } + + harmony.Patch(target, prefix: new HarmonyMethod(typeof(RealtimeDiagnostics), nameof(ExitWatchPrefix))); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] exit watch failed to install: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static void ExitWatchPrefix(string __1) + { + var text = __1; + if (string.IsNullOrEmpty(text)) return; + + + + + if (text.IndexOf("\"Notification\"", StringComparison.Ordinal) < 0) return; + if (text.IndexOf("\"Id\":\"20\"", StringComparison.Ordinal) >= 0 || text.IndexOf("\"Id\":20", StringComparison.Ordinal) >= 0) + { + NoteServerRequestedExit("20"); + return; + } + + if (text.IndexOf("\"Id\":\"6\"", StringComparison.Ordinal) >= 0 || text.IndexOf("\"Id\":6", StringComparison.Ordinal) >= 0) + NoteServerRequestedExit("6"); + } + + internal static bool Verbose = true; + + internal static void Apply(Harmony harmony, bool verbose) + { + Verbose = verbose; + var transport = FindType("BestHTTP.SignalRCore.Transports.WebSocketTransport"); + if (transport == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] BestHTTP.SignalRCore.Transports.WebSocketTransport not found - notify frame logging stays inactive."); + } + else + { + + + + Hook(harmony, transport, "OnOpen", nameof(OnOpenPrefix), "socket open"); + Hook(harmony, transport, "HandleHandshakeResponse", nameof(HandshakePrefix), "SignalR handshake answer"); + Hook(harmony, transport, "OnMessage", nameof(OnMessagePrefix), "inbound frames"); + Hook(harmony, transport, "OnError", nameof(OnErrorPrefix), "transport errors"); + Hook(harmony, transport, "OnClosed", nameof(OnClosedPrefix), "socket close with code/reason"); + } + + var hub = FindType("BestHTTP.SignalRCore.HubConnection"); + if (hub == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] BestHTTP.SignalRCore.HubConnection not found - hub-pump guard stays inactive."); + } + else + { + HookFinalizer(harmony, hub, "OnMessages", nameof(OnMessagesFinalizer)); + } + + RadicadeEacBypassPlugin.LogSource.LogInfo(Verbose + ? "[Realtime] notify-hub tap armed: every inbound frame is read and logged." + : "[Realtime] notify-hub tap armed for moderation only - a voice chat restriction is applied from the frame it arrives in; frames are not logged (Diagnostics=false)."); + + + + + if (Verbose) StartHandlerKeyProbe(harmony); + } + + private static Type FindType(string name) + { + + + + var direct = AccessTools.TypeByName(name); + if (direct != null) return direct; + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + var found = assembly.GetType(name, false); + if (found != null) return found; + } + catch { } + } + + return null; + } + + private static void Hook(Harmony harmony, Type type, string method, string prefix, string what) + { + try + { + var target = AccessTools.DeclaredMethod(type, method); + if (target == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] {type.Name}.{method} not found - {what} will not be logged."); + return; + } + + harmony.Patch(target, prefix: new HarmonyMethod(typeof(RealtimeDiagnostics), prefix)); + if (Verbose) + RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] hooked {type.Name}.{method} ({what})"); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] could not hook {type.Name}.{method}: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static void HookFinalizer(Harmony harmony, Type type, string method, string finalizer) + { + try + { + var target = AccessTools.DeclaredMethod(type, method); + if (target == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] {type.Name}.{method} not found - the hub-pump guard stays inactive."); + return; + } + + harmony.Patch(target, finalizer: new HarmonyMethod(typeof(RealtimeDiagnostics), finalizer)); + if (Verbose) + RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] guarded {type.Name}.{method} (a frame the client cannot parse is logged, not thrown)"); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] could not guard {type.Name}.{method}: {ex.GetType().Name}: {ex.Message}"); + } + } + + + + + + + private static void OnOpenPrefix() + { + if (Verbose) + RadicadeEacBypassPlugin.LogSource.LogInfo("[Realtime] socket open - the notify hub WebSocket upgraded and is ready."); + } + + private static void HandshakePrefix(string __0) + { + + + var text = __0 ?? string.Empty; + if (text.IndexOf("error", StringComparison.OrdinalIgnoreCase) >= 0) + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] hub handshake answered with an error: {Trim(text, 300)}"); + else if (Verbose) + RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] hub handshake accepted: {Trim(text, 120)}"); + } + + private static void OnMessagePrefix(string __1) => ObserveFrame(__1); + + private static void OnErrorPrefix(string __1) => + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] transport error: {Trim(__1, 300)}"); + + private static void OnClosedPrefix(ushort __1, string __2) => + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] socket closed code={__1} reason={Trim(__2, 200)}"); + + private static Exception OnMessagesFinalizer(Exception __exception) + { + if (__exception == null) return null; + + lock (Gate) Failures++; + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Realtime] the client failed to handle a hub frame ({__exception.GetType().Name}: {__exception.Message}) - " + + $"frame swallowed so the notify socket and the rest of the batch survive (failures={Failures}). " + + "A parse error here means a notification payload the client's own parser rejects; the IdToken/summary lines " + + "above name which notification it was."); + + return null; + } + + + + + + private static void ObserveFrame(string data) + { + if (string.IsNullOrEmpty(data)) + { + if (Verbose) + RadicadeEacBypassPlugin.LogSource.LogInfo("[Realtime] empty frame received (keep-alive)."); + return; + } + + + foreach (var record in data.Split('\u001e')) + { + if (record.Length == 0) continue; + + + + + if (!Verbose && record.IndexOf(VoiceChatRestrictionPatches.NotificationId, StringComparison.Ordinal) < 0) + continue; + + int index; + lock (Gate) index = ++Frames; + + DescribeRecord(index, record); + } + } + + private static void DescribeRecord(int index, string record) + { + try + { + using var document = JsonDocument.Parse(record); + var root = document.RootElement; + + var type = root.TryGetProperty("type", out var typeElement) ? typeElement.GetRawText() : "?"; + var target = root.TryGetProperty("target", out var targetElement) ? targetElement.GetString() : null; + + if (string.Equals(target, "Notification", StringComparison.Ordinal)) + { + DescribeNotification(index, root, record); + return; + } + + Count(Targets, target ?? ("type " + type)); + + + + if (!Verbose) return; + if (index <= VerboseFrames) + RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] frame #{index} target={target ?? "-"} type={type} len={record.Length}: {Trim(record, PreviewChars)}"); + else + Summarize(index); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] frame #{index} is not readable as a hub record ({ex.GetType().Name}: {ex.Message}): {Trim(record, PreviewChars)}"); + } + } + + private static void DescribeNotification(int index, JsonElement root, string record) + { + string envelope = null; + if (root.TryGetProperty("arguments", out var arguments) + && arguments.ValueKind == JsonValueKind.Array + && arguments.GetArrayLength() > 0) + { + var first = arguments[0]; + envelope = first.ValueKind == JsonValueKind.String ? first.GetString() : first.GetRawText(); + } + + string id = null; + string idToken = "(no Id)"; + string msg = "(no Msg)"; + + if (!string.IsNullOrEmpty(envelope)) + { + try + { + using var inner = JsonDocument.Parse(envelope); + var payload = inner.RootElement; + + + + + if (TryGetProperty(payload, "Id", out var idElement)) + { + idToken = idElement.ValueKind.ToString(); + id = idElement.ValueKind == JsonValueKind.String ? idElement.GetString() : idElement.GetRawText(); + } + + if (TryGetProperty(payload, "Msg", out var msgElement)) + msg = Trim(msgElement.GetRawText(), 200); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] Notification #{index} envelope did not parse at all ({ex.GetType().Name}: {ex.Message}): {Trim(envelope, PreviewChars)}"); + envelope = null; + } + } + + + + if (!string.IsNullOrEmpty(id)) NoteServerRequestedExit(id); + + + + + if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(envelope)) + VoiceChatRestrictionPatches.Observe(id, envelope); + + Count(Targets, "Notification"); + Count(NotificationIds, id ?? "(no Id)"); + + bool firstOfItsKind = false; + if (!string.IsNullOrEmpty(id)) + lock (Gate) firstOfItsKind = KnownNotifications.Add(id); + + + if (!Verbose) return; + + + + + + + bool worthFullDetail = string.IsNullOrEmpty(id) || !HighFrequencyIds.Contains(id); + + if (worthFullDetail || firstOfItsKind || index <= VerboseFrames) + RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] Notification #{index} Id={id ?? "(none)"} IdToken={idToken} Msg={msg}"); + else + Summarize(index); + + if (worthFullDetail || firstOfItsKind) + RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {id ?? "envelope"} envelope: {Trim(envelope ?? record, PreviewChars)}"); + } + + private static void Summarize(int index) + { + if (index % 25 != 0) return; + + string targets; + string ids; + int failures; + lock (Gate) + { + targets = Join(Targets); + ids = Join(NotificationIds); + failures = Failures; + } RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {index} frames so far - targets: {targets}; notifications: {ids}; handler failures: {failures}"); + } + + private static bool TryGetProperty(JsonElement element, string name, out JsonElement value) + { + if (element.ValueKind == JsonValueKind.Object) + { + if (element.TryGetProperty(name, out value)) return true; + + + + var lower = char.ToLowerInvariant(name[0]) + name.Substring(1); + if (element.TryGetProperty(lower, out value)) return true; + } + + value = default; + return false; + } + + private static void Count(Dictionary counts, string key) + { + lock (Gate) + { + counts.TryGetValue(key, out var current); + counts[key] = current + 1; + } + } + + private static string Join(Dictionary counts) + { + var parts = new List(); + foreach (var pair in counts) parts.Add($"{pair.Key} x{pair.Value}"); + return parts.Count == 0 ? "none" : string.Join(", ", parts); + } + + private static string Trim(string text, int limit) + { + if (string.IsNullOrEmpty(text)) return string.Empty; + var flat = text.Replace("\r", " ").Replace("\n", " "); + return flat.Length <= limit ? flat : flat.Substring(0, limit) + "..."; + } +} diff --git a/2023Patch/RadicadeEacBypass/RequestRouteLog.cs b/2023Patch/RadicadeEacBypass/RequestRouteLog.cs new file mode 100644 index 0000000..20978ce --- /dev/null +++ b/2023Patch/RadicadeEacBypass/RequestRouteLog.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using BestHTTP; +using HarmonyLib; + +internal static class RequestRouteLog +{ + private static readonly object Gate = new(); + private static readonly HashSet Seen = new(); + + private const int Limit = 300; + + internal static void Apply(Harmony harmony) + { + try + { + var response = AccessTools.TypeByName("BestHTTP.HTTPResponse"); + if (response == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[HttpLog] BestHTTP.HTTPResponse not found - REST routes stay unlogged."); + return; + } + + + + + System.Reflection.ConstructorInfo constructor = null; + foreach (var candidate in AccessTools.GetDeclaredConstructors(response)) + { + var parameters = candidate.GetParameters(); + if (parameters.Length == 4 + && parameters[0].ParameterType.Name.Contains("HTTPRequest") + && parameters[2].ParameterType.Name == "Boolean" + && parameters[3].ParameterType.Name == "Boolean") + { + constructor = candidate; + break; + } + } + + if (constructor == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[HttpLog] HTTPResponse ctor not found - REST routes stay unlogged."); + return; + } + + harmony.Patch(constructor, postfix: new HarmonyMethod(typeof(RequestRouteLog), nameof(ResponsePostfix))); + RadicadeEacBypassPlugin.LogSource.LogInfo("[HttpLog] hooked HTTPResponse(HTTPRequest, Stream) - every REST route and status is logged once"); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[HttpLog] could not hook HTTPResponse: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static void ResponsePostfix(HTTPResponse __instance, HTTPRequest __0) + { + try + { + var uri = __0?.Uri?.ToString(); + if (string.IsNullOrEmpty(uri)) return; + + if (!HubSocketAuth.IsServer(uri)) return; + + var path = new Uri(uri).AbsolutePath; + + + + if (NameserverRedirect.IsHubUrl(uri)) return; + if (path.Contains("/images/", StringComparison.OrdinalIgnoreCase)) return; + if (path.Contains("/cdn/", StringComparison.OrdinalIgnoreCase)) return; + + int status; + try { status = __instance.StatusCode; } + catch { return; } + + var method = "REQ"; + try { method = __0.MethodType.ToString().ToUpperInvariant(); } + catch { } + + var line = $"{status} {method} {path}"; + + lock (Gate) + { + if (Seen.Count >= Limit || !Seen.Add(line)) return; + } + + RadicadeEacBypassPlugin.LogSource.LogInfo($"[HttpLog] {line}"); + } + catch + { + + } + } +} diff --git a/2023Patch/RadicadeEacBypass/ResponseRepair.cs b/2023Patch/RadicadeEacBypass/ResponseRepair.cs new file mode 100644 index 0000000..da14141 --- /dev/null +++ b/2023Patch/RadicadeEacBypass/ResponseRepair.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +/// +/// Pure string surgery for two responses the client cannot cope with as the +/// server sends them. No engine or BestHTTP types live here so the rules can be +/// exercised outside the game. +/// +internal static class ResponseRepair +{ + /// Loopback probe target: any transport fails (or answers) instantly, so a probe phase can never stall. + internal const string LoopbackProbeAddress = "127.0.0.1:50000"; + + private const string QosPathSuffix = "/matchmaking/player/qos"; + private const string UserSegmentPathSuffix = "/data/commonquery/usersegment"; + private const string AddressKey = "\"address\""; + + /// + /// BestHTTP's chunked reader throws exactly this when the chunk-size line is + /// empty, which is what a keep-alive connection the server had already closed + /// looks like. It is not a bad request, so it must not fail one. + /// + internal static bool IsTruncatedChunkFailure(string message) + { + if (string.IsNullOrEmpty(message)) return false; + return message.IndexOf("as a hex number", StringComparison.OrdinalIgnoreCase) >= 0; + } + + /// + /// Rewrites a response body when the client would otherwise mis-handle it. + /// Returns null when the body is left alone; explains + /// what changed when it did. + /// + internal static string RepairBody(string path, string query, string body, out string note) + { + note = null; + if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(body)) return null; + + if (EndsWith(path, QosPathSuffix)) return RepairProbeList(body, ref note); + if (EndsWith(path, UserSegmentPathSuffix)) return RepairUserSegment(query, body, ref note); + return null; + } + + /// + /// The probe list points at retired GCP addresses: five of its six entries are + /// unreachable, so the client's region-probe phase waits out a ping timeout on + /// each boot and login sits there. Pinning every entry at loopback keeps the + /// list's shape but makes the probes answer (or refuse) immediately. + /// + private static string RepairProbeList(string body, ref string note) + { + var repaired = new StringBuilder(body.Length); + var index = 0; + var replaced = 0; + + while (true) + { + var key = body.IndexOf(AddressKey, index, StringComparison.Ordinal); + if (key < 0) + { + repaired.Append(body, index, body.Length - index); + break; + } + + repaired.Append(body, index, key - index + AddressKey.Length); + + var cursor = key + AddressKey.Length; + while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) cursor++; + if (cursor >= body.Length || body[cursor] != ':') + { + repaired.Append(body, key + AddressKey.Length, cursor - key - AddressKey.Length); + index = cursor; + continue; + } + + repaired.Append(body, key + AddressKey.Length, cursor - key - AddressKey.Length); + repaired.Append(':'); + cursor++; + while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) cursor++; + if (cursor >= body.Length || body[cursor] != '"') + { + index = cursor; + continue; + } + + var close = body.IndexOf('"', cursor + 1); + if (close < 0) + { + repaired.Append(body, cursor, body.Length - cursor); + break; + } + + repaired.Append('"').Append(LoopbackProbeAddress).Append('"'); + replaced++; + index = close + 1; + } + + if (replaced == 0) return null; + + note = replaced.ToString(CultureInfo.InvariantCulture) + " region probe address(es) pinned to " + + LoopbackProbeAddress + " (the server's list points at retired hosts)"; + return repaired.ToString(); + } + + /// + /// The client's model for this route is a name -> row map + /// (Dictionary<string, ...>), so the flat member object the server answers + /// with makes its reader demand '{' where a string sits and throw + /// "expected:'{', actual:'\"none\"'" (a crash report per login). Wrapping the + /// flat object under the row key(s) the request named gives it the map it wants. + /// + private static string RepairUserSegment(string query, string body, ref string note) + { + var trimmed = body.TrimStart(); + if (trimmed.Length == 0 || trimmed[0] != '{') return null; + + var keys = new List(); + foreach (var value in QueryValues(query)) + { + AddKey(keys, value); + AddKey(keys, "user_segment:" + value); + } + if (keys.Count == 0) AddKey(keys, "user_segment"); + + var repaired = new StringBuilder(body.Length * (keys.Count + 1) + 16); + repaired.Append('{'); + for (var i = 0; i < keys.Count; i++) + { + if (i > 0) repaired.Append(','); + repaired.Append('"').Append(keys[i]).Append("\":").Append(body); + } + repaired.Append('}'); + + note = "userSegment answered as a name -> row map (" + string.Join(", ", keys) + ")"; + return repaired.ToString(); + } + + private static void AddKey(List keys, string key) + { + if (string.IsNullOrEmpty(key)) return; + foreach (var existing in keys) + if (existing == key) return; + keys.Add(key); + } + + internal static IEnumerable QueryValues(string query) + { + if (string.IsNullOrEmpty(query)) yield break; + foreach (var pair in query.TrimStart('?').Split('&')) + { + if (pair.Length == 0) continue; + var separator = pair.IndexOf('='); + var value = separator < 0 ? pair : pair.Substring(separator + 1); + value = Uri.UnescapeDataString(value).Trim(); + if (value.Length == 0) continue; + yield return value; + } + } + + private static bool EndsWith(string value, string suffix) => + value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase); +} diff --git a/2023Patch/RadicadeEacBypass/WebSocketRedirectPatches.cs b/2023Patch/RadicadeEacBypass/WebSocketRedirectPatches.cs new file mode 100644 index 0000000..2e1ed76 --- /dev/null +++ b/2023Patch/RadicadeEacBypass/WebSocketRedirectPatches.cs @@ -0,0 +1,32 @@ +using System; +using HarmonyLib; + +internal static class WebSocketRedirectPatches +{ + internal static void Apply(Harmony harmony) + { + var type = AccessTools.TypeByName("BestHTTP.WebSocket.WebSocket"); + int count = 0; + if (type != null) + foreach (var constructor in AccessTools.GetDeclaredConstructors(type)) + { + var parameters = constructor.GetParameters(); + if (parameters.Length == 0 || parameters[0].ParameterType != typeof(Il2CppSystem.Uri)) continue; + harmony.Patch(constructor, prefix: new HarmonyMethod(typeof(WebSocketRedirectPatches), nameof(Prefix))); + count++; + } + RadicadeEacBypassPlugin.LogSource.LogInfo("[Realtime] WebSocket URI hooks installed: " + count); + } + private static void Prefix(ref Il2CppSystem.Uri __0) + { + if (__0 == null) return; + var url = NameserverRedirect.RewriteWebSocket(__0.ToString(), RadicadeEacBypassPlugin.ServerUrl.Value); + if (HubSocketAuth.IsServer(url) && NameserverRedirect.IsHubUrl(url)) + { + url = NameserverRedirect.EnsureHubToken(url, HubSocketAuth.Token); + Diag.Trace("[Realtime] Hub socket constructed; token=" + + (NameserverRedirect.HasHubToken(url) ? "present" : "missing")); + } + __0 = new Il2CppSystem.Uri(url); + } +} diff --git a/2023Patch/RadicadePatch/ClientTextRewrites.cs b/2023Patch/RadicadePatch/ClientTextRewrites.cs new file mode 100644 index 0000000..03e1945 --- /dev/null +++ b/2023Patch/RadicadePatch/ClientTextRewrites.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using Il2CppInterop.Runtime; +using Il2CppInterop.Runtime.InteropTypes; +using TMPro; +using UnityEngine.UI; + +internal static class ClientTextRewrites +{ + private static readonly (string From, string To)[] Rules = + { + ("https://recroom.zendesk.com/home", "https://support.radicade.app/"), + ("recroom.zendesk.com", "support.radicade.app"), + ("https://www.recroom.com/privacy-policy/", "https://radicade.app/privacy-policy/"), + ("https://recroom.com/terms-of-service", "https://radicade.app/terms-of-service/"), + ("https://www.recroom.com/community/", "https://radicade.app/community/"), + }; + + private static readonly (string From, string To)[] ExactRules = + { + ("This account has been locked for security reasons. Contact us at recroom.zendesk.com.", + LockedAccountSentence), + ("This account has been locked for security reasons. Contact us at support.radicade.app.", + LockedAccountSentence), + }; + + private const string GenericLoginFailure = "Login failed. Please try again."; + private const string LockedAccountErrorCode = "ACCOUNT_PERMALOCKED"; + + internal const string LockedAccountSentence = + "This account has been locked for security reasons. Contact us at https://support.radicade.app."; + + private static bool _rewriting; + + internal static void Apply(Harmony harmony) + { + var mode = AsciiLower((Environment.GetEnvironmentVariable("RADICADE_CLIENT_TEXT") ?? string.Empty).Trim()); + if (mode == "0" || mode == "off") + { + return; + } + + bool onEnable = mode.Length == 0 || mode == "1" || mode == "all" || mode == "onenable"; + bool onSet = mode.Length == 0 || mode == "1" || mode == "all" || mode == "settext"; + + var seen = new HashSet(); + var names = new List(); + + if (onEnable) + { + Hook(harmony, seen, names, AccessTools.Method(typeof(TextMeshProUGUI), "OnEnable"), nameof(GraphicEvent), "TextMeshProUGUI.OnEnable"); + Hook(harmony, seen, names, AccessTools.Method(typeof(Text), "OnEnable"), nameof(GraphicEvent), "UI.Text.OnEnable"); + } + if (onSet) + { + Hook(harmony, seen, names, AccessTools.Method(typeof(TMP_Text), "set_text"), nameof(GraphicEvent), "TMP_Text.set_text"); + Hook(harmony, seen, names, AccessTools.Method(typeof(TMP_Text), "SetTextInternal"), nameof(GraphicEvent), "TMP_Text.SetTextInternal"); + Hook(harmony, seen, names, AccessTools.Method(typeof(Text), "set_text"), nameof(GraphicEvent), "UI.Text.set_text"); + } + + if (names.Count == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning("[ClientText] no text hooks could be installed (mode=" + (mode.Length == 0 ? "all" : mode) + "); the watch UI keeps its original text."); + } + + private static void Hook(Harmony harmony, HashSet seen, List names, + MethodBase target, string handler, string label) + { + if (target == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[ClientText] hook target missing: " + label); + return; + } + if (!seen.Add(target)) return; + + try + { + var method = new HarmonyMethod(typeof(ClientTextRewrites) + .GetMethod(handler, BindingFlags.Static | BindingFlags.NonPublic)); + harmony.Patch(target, postfix: method); + names.Add(label); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[ClientText] could not hook " + label + ": " + + ex.GetType().Name + ": " + ex.Message); + } + } + + private static void GraphicEvent(object __instance) + { + try + { + if (__instance is not Il2CppObjectBase obj) return; + if (obj.TryCast() is { } tmp) { Fix(tmp); return; } + if (obj.TryCast() is { } txt) { Fix(txt); } + } + catch { } + } + + private static void Fix(TMP_Text instance) + { + try + { + if (instance == null || _rewriting) return; + var current = instance.text; + var rewritten = Rewrite(current); + if (ReferenceEquals(current, rewritten) || string.Equals(rewritten, current, StringComparison.Ordinal)) return; + _rewriting = true; + try { instance.text = rewritten; } finally { _rewriting = false; } + } + catch { } + } + + private static void Fix(Text instance) + { + try + { + if (instance == null || _rewriting) return; + var current = instance.text; + var rewritten = Rewrite(current); + if (ReferenceEquals(current, rewritten) || string.Equals(rewritten, current, StringComparison.Ordinal)) return; + _rewriting = true; + try { instance.text = rewritten; } finally { _rewriting = false; } + } + catch { } + } + + private static string Rewrite(string value) + { + if (string.IsNullOrEmpty(value)) return value; + + if (HubSocketAuth.LastLoginRefusalCode == LockedAccountErrorCode && + IndexOfAsciiIgnoreCase(value, GenericLoginFailure, 0) >= 0) + { + var locked = Replace(value, GenericLoginFailure, LockedAccountSentence); + return locked; + } + + if (IndexOfAsciiIgnoreCase(value, "recroom", 0) < 0 && + IndexOfAsciiIgnoreCase(value, "radicade", 0) < 0) return value; + + var result = value; + foreach (var (from, to) in ExactRules) + if (IndexOfAsciiIgnoreCase(result, from, 0) >= 0) + result = Replace(result, from, to); + + foreach (var (from, to) in Rules) + if (IndexOfAsciiIgnoreCase(result, from, 0) >= 0) + result = Replace(result, from, to); + + return result; + } + + private static string Replace(string source, string from, string to) + { + var sb = new System.Text.StringBuilder(source.Length); + var i = 0; + while (i < source.Length) + { + var hit = IndexOfAsciiIgnoreCase(source, from, i); + if (hit < 0) { sb.Append(source, i, source.Length - i); break; } + sb.Append(source, i, hit - i).Append(to); + i = hit + from.Length; + } + return sb.ToString(); + } + + private static int IndexOfAsciiIgnoreCase(string haystack, string needle, int start) + { + if (haystack == null || needle == null) return -1; + var n = needle.Length; + var h = haystack.Length; + if (n == 0) return start <= h ? start : -1; + if (n > h) return -1; + var last = h - n; + for (var i = (start < 0 ? 0 : start); i <= last; i++) + { + var j = 0; + while (j < n && LowerAscii(haystack[i + j]) == LowerAscii(needle[j])) j++; + if (j == n) return i; + } + return -1; + } + + private static char LowerAscii(char c) => c >= 'A' && c <= 'Z' ? (char)(c + 32) : c; + + private static string AsciiLower(string s) + { + for (var i = 0; i < s.Length; i++) + if (s[i] >= 'A' && s[i] <= 'Z') + return string.Create(s.Length, s, (span, state) => + { + for (var k = 0; k < state.Length; k++) span[k] = LowerAscii(state[k]); + }); + return s; + } + +} diff --git a/2023Patch/RadicadePatch/ColliderCastGuardPatch.cs b/2023Patch/RadicadePatch/ColliderCastGuardPatch.cs new file mode 100644 index 0000000..c0d2c60 --- /dev/null +++ b/2023Patch/RadicadePatch/ColliderCastGuardPatch.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; +using HarmonyLib; +using UnityEngine; + +internal static class ColliderCastGuardPatch +{ + internal static void Apply(Harmony harmony) + { + if (!Enabled) return; + int installed = 0; + foreach (var method in TargetMethods()) + { + try + { + Install(harmony, method); + installed++; + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogError("[ColliderGuard] failed on " + Describe(method) + ": " + ex); + } + } + } + + internal static void Install(Harmony harmony, MethodBase method) + { + var prefix = IsColliderFrame(method) + ? new HarmonyMethod(typeof(ColliderCastGuardPatch), nameof(NoteCollider)) : null; + harmony.Patch(method, prefix: prefix, + finalizer: new HarmonyMethod(typeof(ColliderCastGuardPatch), nameof(Swallow))); + } + private static readonly bool _disabled = IsDisabled(); + + private static int _swallows; + + private static readonly Dictionary LastCollider = new Dictionary(); + + + private static readonly HashSet ColliderFrames = new HashSet(); + + private static readonly string[] ColliderParameterTypes = new string[2] { "AOCLEIAJHKP", "AKCGGCOFMHJ" }; + + private static long _inspections; + + private static readonly string[] CastTypes = new string[3] { "BPJMFIFDPJO", "CBKENBPBBPK", "AOCLEIAJHKP" }; + + private const string RbexAssembly = "RecRoom.Rbex.Runtime"; + + private const string GameAssembly = "Assembly-CSharp"; + + private static readonly (string Assembly, string Type, string Method, int Args, string Role)[] Specs = new(string, string, string, int, string)[5] + { + ("RecRoom.Rbex.Runtime", "NFMLBBHIOHJ", "JAMGDJKMPGM", 2, "locomotion: attach/detach a collider child"), + ("RecRoom.Rbex.Runtime", "NPDJILHGCBI", "JAMGDJKMPGM", 2, "locomotion: collider-child update (twin implementation)"), + ("RecRoom.Rbex.Runtime", "MOBAMIJAHPJ", "JDAFCHGGCBG", 1, "teardown: unregister a collider from its body"), + ("RecRoom.Rbex.Runtime", "CBKENBPBBPK", "JDIBOGNGEFK", 0, "teardown: body Dispose -> collider walk"), + ("Assembly-CSharp", "RecRoom.Core.Locomotion.PlayerMovement", "AuthorityUpdateMovement", 2, "locomotion: authority tick (backstop for every caller of the cast)") + }; + + internal static bool Enabled => !_disabled; + + [HarmonyPrefix] + private static void NoteCollider(object __0, MethodBase __originalMethod) + { + try + { + if (__0 == null || __originalMethod == null || !ColliderFrames.Contains(__originalMethod)) + { + return; + } + long num = ++_inspections; + if (num > 200 && num % 50 != 0L) + { + return; + } + string text = DescribeCollider(__0); + if (text != null) + { + if (__originalMethod != null) + { + LastCollider[__originalMethod] = text; + } + } + } + catch + { + } + } + + private static string DescribeCollider(object collider) + { + string text = null; + try + { + CBKENBPBBPK val = (CBKENBPBBPK)((collider is CBKENBPBBPK) ? collider : null); + if (val != null) + { + text = NameOf(val.JPBNIBGFKJO); + } + else + { + BPJMFIFDPJO val2 = (BPJMFIFDPJO)((collider is BPJMFIFDPJO) ? collider : null); + if (val2 != null) + { + text = NameOf(val2.JPBNIBGFKJO); + } + } + } + catch + { + } + string name; + try + { + name = collider.GetType().Name; + } + catch + { + return null; + } + if (text != null) + { + return name + " on '" + text + "'"; + } + return name; + } + + private static string NameOf(GameObject gameObject) + { + try + { + return gameObject == null ? null : gameObject.name; + } + catch + { + return null; + } + } + + private static bool IsDisabled() + { + try + { + string environmentVariable = Environment.GetEnvironmentVariable("RADICADE_COLLIDER_GUARD"); + if (string.IsNullOrWhiteSpace(environmentVariable)) + { + return false; + } + bool result; + switch (environmentVariable.Trim().ToLowerInvariant()) + { + case "0": + case "off": + case "false": + case "no": + result = true; + break; + default: + result = false; + break; + } + return result; + } + catch + { + return false; + } + } + + private static IEnumerable TargetMethods() + { + List list = new List(); + (string, string, string, int, string)[] specs = Specs; + for (int i = 0; i < specs.Length; i++) + { + (string, string, string, int, string) tuple = specs[i]; + foreach (MethodBase item in Resolve(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4)) + { + list.Add(item); + if (IsColliderFrame(item)) + { + ColliderFrames.Add(item); + } + } + } + if (list.Count == 0) + { + RadicadeEacBypassPlugin.LogSource.LogWarning((object)("[ColliderGuard] none of the " + Specs.Length + " known collider frames resolved - the physics cast failure is NOT guarded.")); + } + return list; + } + + private static bool IsColliderFrame(MethodBase method) + { + try + { + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length == 0) + { + return false; + } + string name = parameters[0].ParameterType.Name; + string[] colliderParameterTypes = ColliderParameterTypes; + foreach (string b in colliderParameterTypes) + { + if (string.Equals(name, b, StringComparison.Ordinal)) + { + return true; + } + } + } + catch + { + } + return false; + } + + private static Type FindType(string assemblyName, string typeName) + { + try + { + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (Assembly assembly in assemblies) + { + try + { + if (string.Equals(assembly.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)) + { + Type type = assembly.GetType(typeName, throwOnError: false); + if (type != null) + { + return type; + } + } + } + catch + { + } + } + } + catch + { + } + try + { + return Type.GetType(typeName + ", " + assemblyName, throwOnError: false); + } + catch + { + } + try + { + return Assembly.Load(new AssemblyName(assemblyName)).GetType(typeName, throwOnError: false); + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning((object)("[ColliderGuard] " + assemblyName + " could not be loaded: " + ex.Message)); + return null; + } + } + + private static IEnumerable Resolve(string assemblyName, string typeName, string methodName, int args) + { + var type = FindType(assemblyName, typeName); + if (type == null) yield break; + foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + if (method.Name == methodName && method.GetParameters().Length == args) + yield return method; + } + [HarmonyFinalizer] + private static Exception Swallow(Exception __exception, MethodBase __originalMethod) + { + if (__exception == null || !Enabled) + { + return __exception; + } + if (!IsPhysicsCastFailure(__exception)) + { + return __exception; + } + int num = ++_swallows; + if (num <= 5 || num % 100 == 0) + { + RadicadeEacBypassPlugin.LogSource.LogWarning((object)("[ColliderGuard] caught a collider cast failure in " + Describe(__originalMethod) + " (#" + num + ")" + DescribeArgument(__originalMethod) + ": " + __exception.GetType().Name + ": " + __exception.Message + " - the failed operation was aborted; remaining work in that method did not run.")); + } + return null; + } + + private static string DescribeArgument(MethodBase method) + { + try + { + if (method != null && LastCollider.TryGetValue(method, out var value)) + { + return " collider=" + value; + } + } + catch + { + } + return string.Empty; + } + + private static string Describe(MethodBase method) + { + try + { + return (method == null) ? "" : (method.DeclaringType?.ToString() + "." + method.Name); + } + catch + { + return ""; + } + } + + private static bool IsPhysicsCastFailure(Exception exception) + { + string text; + try + { + text = exception.Message ?? string.Empty; + } + catch + { + return false; + } + if (text.IndexOf("Unable to cast object of type", StringComparison.OrdinalIgnoreCase) < 0) + { + return false; + } + string[] castTypes = CastTypes; + foreach (string value in castTypes) + { + if (text.IndexOf(value, StringComparison.Ordinal) >= 0) + { + return true; + } + } + return false; + } +} + + diff --git a/2023Patch/RadicadePatch/ConnectionGate.cs b/2023Patch/RadicadePatch/ConnectionGate.cs new file mode 100644 index 0000000..7930854 --- /dev/null +++ b/2023Patch/RadicadePatch/ConnectionGate.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; +using HarmonyLib; + +internal static class ConnectionGate +{ + private const int WaitMilliseconds = 15000; + + private static readonly ManualResetEventSlim Opened = new ManualResetEventSlim(false); + + private static int _holds; + + internal static void Install(Harmony harmony) + { + harmony.PatchAll(typeof(ConnectionGatePatch)); + } + + internal static void Open() + { + Opened.Set(); + } + + internal static void Hold() + { + if (Opened.IsSet) return; + + var count = Interlocked.Increment(ref _holds); + RadicadeEacBypassPlugin.LogSource.LogWarning("[Patch] a server request arrived before the patch list finished; holding it until it does (hold #" + + count + ")."); + + if (Opened.Wait(WaitMilliseconds)) return; + + RadicadeEacBypassPlugin.LogSource.LogError("[Patch] the patch list did not finish within " + + (WaitMilliseconds / 1000) + "s; releasing this request and every later one so the client is not held forever."); + Opened.Set(); + } +} + +[HarmonyPatch(typeof(BestHTTP.HTTPManager), "SendRequest", new[] { typeof(BestHTTP.HTTPRequest) })] +internal static class ConnectionGatePatch +{ + [HarmonyPriority(Priority.First)] + internal static void Prefix() + { + ConnectionGate.Hold(); + } +} diff --git a/2023Patch/RadicadePatch/CrashAddressMap.cs b/2023Patch/RadicadePatch/CrashAddressMap.cs new file mode 100644 index 0000000..c221276 --- /dev/null +++ b/2023Patch/RadicadePatch/CrashAddressMap.cs @@ -0,0 +1,191 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +internal static class CrashAddressMap +{ + private const long DefaultTarget = 0x29a79ec; + + internal static bool Enabled => !_disabled; + + internal static long TargetRva => _target; + + private static readonly bool _disabled = IsDisabled(); + + private static readonly long _target = ReadTarget(); + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_ADDR_MAP"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static long ReadTarget() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_ADDR_TARGET"); + if (string.IsNullOrWhiteSpace(raw)) return DefaultTarget; + raw = raw.Trim(); + if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) raw = raw.Substring(2); + return long.TryParse(raw, System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, out var value) ? value : DefaultTarget; + } + catch { return DefaultTarget; } + } + + internal static void Start() + { + if (!Enabled) return; + var thread = new System.Threading.Thread(Run) { IsBackground = true, Name = "RadicadeAddrMap" }; + thread.Start(); + } + + private static void Run() + { + try { System.Threading.Thread.Sleep(5000); } catch { } + + var log = RadicadeEacBypassPlugin.LogSource; + + IntPtr baseAddress = IntPtr.Zero; + var moduleSize = 0L; + try + { + using (var self = System.Diagnostics.Process.GetCurrentProcess()) + { + foreach (System.Diagnostics.ProcessModule module in self.Modules) + { + if (!string.Equals(module.ModuleName, "GameAssembly.dll", StringComparison.OrdinalIgnoreCase)) continue; + baseAddress = module.BaseAddress; + moduleSize = module.ModuleMemorySize; + break; + } + } + } + catch (Exception ex) + { + log.LogError("[AddrMap] could not locate GameAssembly.dll: " + ex.Message); + return; + } + + if (baseAddress == IntPtr.Zero || moduleSize <= 0) + { + log.LogError("[AddrMap] GameAssembly.dll is not loaded - nothing to map"); + return; + } + + var gameRoot = Path.GetDirectoryName(UnityEngine.Application.dataPath) ?? "."; + var path = Path.Combine(gameRoot, "BepInEx", "radicade-addr-map.txt"); + + var baseValue = baseAddress.ToInt64(); + var mapped = 0; + var images = 0; + var unmappable = 0; + var bestBelow = -1L; + string bestBelowName = null; + long bestAbove = -1; + string bestAboveName = null; + var failures = 0; + + try + { + using (var writer = new StreamWriter(path, false)) + { + writer.WriteLine("# il2cpp RVA -> method map, written by RadicadeAugustClient (run 24)"); + writer.WriteLine("# GameAssembly.dll base 0x" + baseValue.ToString("x") + ", mapped size 0x" + moduleSize.ToString("x")); + writer.WriteLine("# rvaimageNamespace.Class::Method - rva is relative to the base above"); + + unsafe + { + var domain = Il2CppInterop.Runtime.IL2CPP.il2cpp_domain_get(); + if (domain == IntPtr.Zero) + { + log.LogError("[AddrMap] no il2cpp domain; nothing to map"); + return; + } + + uint assemblyCount = 0; + var assemblies = Il2CppInterop.Runtime.IL2CPP.il2cpp_domain_get_assemblies(domain, ref assemblyCount); + for (uint a = 0; a < assemblyCount; a++) + { + var image = Il2CppInterop.Runtime.IL2CPP.il2cpp_assembly_get_image(assemblies[a]); + if (image == IntPtr.Zero) continue; + images++; + + string imageName; + try { imageName = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_name(image)) ?? "?"; } + catch { imageName = "?"; } + + var classCount = Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_class_count(image); + for (uint c = 0; c < classCount; c++) + { + var klass = Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_class(image, c); + if (klass == IntPtr.Zero) continue; + + string className; + try + { + var namespaze = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_namespace(klass)); + var name = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_name(klass)); + className = string.IsNullOrEmpty(namespaze) ? name : namespaze + "." + name; + } + catch { className = "?"; failures++; } + + var iterator = IntPtr.Zero; + IntPtr methodInfo; + while ((methodInfo = Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_methods(klass, ref iterator)) != IntPtr.Zero) + { + long entry; + try { entry = Marshal.ReadIntPtr(methodInfo).ToInt64(); } + catch { entry = 0; } + + if (entry <= baseValue || entry >= baseValue + moduleSize) + { + unmappable++; + continue; + } + + var rva = entry - baseValue; + string methodName; + try { methodName = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_method_get_name(methodInfo)) ?? "?"; } + catch { methodName = "?"; failures++; } + + writer.WriteLine("0x" + rva.ToString("x") + "\t" + imageName + "\t" + className + "::" + methodName); + mapped++; + + var full = className + "::" + methodName; + if (rva <= _target && rva > bestBelow) { bestBelow = rva; bestBelowName = full; } + if (rva > _target && (bestAbove < 0 || rva < bestAbove)) { bestAbove = rva; bestAboveName = full; } + } + } + } + } + } + } + catch (Exception ex) + { + log.LogError("[AddrMap] pass failed after " + mapped + " methods: " + ex); + return; + } + + + if (bestBelow < 0) + { + log.LogWarning("[AddrMap] no method starts below the target 0x" + _target.ToString("x") + + " - the crash is in code no managed method owns (il2cpp runtime or native plugin)." + + (bestAboveName != null ? " Nearest above: 0x" + bestAbove.ToString("x") + " " + bestAboveName : string.Empty)); + return; + } + + var span = bestAbove > bestBelow ? bestAbove - bestBelow : -1; + log.LogWarning("[AddrMap] crash target 0x" + _target.ToString("x") + " -> containing method 0x" + bestBelow.ToString("x") + + " " + bestBelowName + " (+0x" + (_target - bestBelow).ToString("x") + " into it)" + + (span > 0 ? "; next method 0x" + bestAbove.ToString("x") + " " + bestAboveName + " (span 0x" + span.ToString("x") + ")" : string.Empty) + + (failures > 0 ? "; " + failures + " name reads failed" : string.Empty)); + } +} diff --git a/2023Patch/RadicadePatch/DynamicUiPrefabRepair.cs b/2023Patch/RadicadePatch/DynamicUiPrefabRepair.cs new file mode 100644 index 0000000..6f692d3 --- /dev/null +++ b/2023Patch/RadicadePatch/DynamicUiPrefabRepair.cs @@ -0,0 +1,53 @@ +using HarmonyLib; +using Il2CppInterop.Runtime; +using RRUI; +using RRUI.Data; +using UnityEngine; + +internal static class DynamicUiPrefabRepair +{ + private static bool reportedMissing; + + internal static void Apply(Harmony harmony) + { + harmony.Patch(AccessTools.PropertyGetter(typeof(DynamicUIConfig), "Instance"), + postfix: new HarmonyMethod(typeof(DynamicUiPrefabRepair), nameof(Repair))); + } + + private static void Repair(DynamicUIConfig __result) + { + if (__result == null) return; + var standard = __result.DynamicFloatPrefab_MultiSelectConfig; + var pro = __result.DynamicProUXFloatPrefab_MultiSelectConfig; + var fallback = Compatible(standard) ? standard : Compatible(pro) ? pro : null; + if (fallback == null) + { + if (!reportedMissing) + { + reportedMissing = true; + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[DynamicUI] No compatible multi-select float prefab is available; leaving the missing mapping visible."); + } + return; + } + + _ = __result.LandscapePrefabsByType; + _ = __result.PortraitPrefabsByType; + _ = __result.ProUXPrefabsByType; + RepairMap(__result.landscapePrefabsByType, fallback, "landscape"); + RepairMap(__result.portraitPrefabsByType, fallback, "portrait"); + RepairMap(__result.proUXPrefabsByType, fallback, "ProUX"); + } + + private static bool Compatible(GameObject prefab) => prefab != null && + prefab.GetComponentInChildren(true) != null; + + private static void RepairMap(Il2CppSystem.Collections.Generic.Dictionary map, + GameObject fallback, string layout) + { + if (map == null) return; + var key = Il2CppType.Of(); + if (map.ContainsKey(key) && map[key] != null) return; + map[key] = fallback; + } +} diff --git a/2023Patch/RadicadePatch/InviteOnlineFriendsPatch.cs b/2023Patch/RadicadePatch/InviteOnlineFriendsPatch.cs new file mode 100644 index 0000000..48c4f30 --- /dev/null +++ b/2023Patch/RadicadePatch/InviteOnlineFriendsPatch.cs @@ -0,0 +1,42 @@ +using HarmonyLib; + +internal static class InviteOnlineFriendsPatch +{ + internal static void Apply(Harmony harmony) + { + harmony.Patch(AccessTools.PropertyGetter(typeof(ECFHJOKNJIJ), "OHKPFEMEPAE"), + prefix: new HarmonyMethod(typeof(InviteOnlineFriendsPatch), nameof(EnableDialog))); + harmony.Patch(AccessTools.PropertyGetter(typeof(ECFHJOKNJIJ), "EBCNNMAJGBN"), + prefix: new HarmonyMethod(typeof(InviteOnlineFriendsPatch), nameof(IncomingDialog))); + } + + private static bool EnableDialog(ECFHJOKNJIJ __instance, ref bool __result) + { + return ReadServerOverride(__instance, "Radicade.Invites.ShowOnlineFriendsBeforeJoining", ref __result); + } + + private static bool IncomingDialog(ECFHJOKNJIJ __instance, ref bool __result) + { + return ReadServerOverride(__instance, "Radicade.Invites.UseRRUIDialog", ref __result); + } + + private static readonly System.Collections.Generic.HashSet logged = new(); + + private static bool ReadServerOverride(ECFHJOKNJIJ instance, string key, ref bool result) + { + try + { + var configs = instance.CCNKOPKDIJI; + if (configs == null || !configs.FNBNNDMHFCK) return true; + if (!bool.TryParse(configs.KFNCGHMLMAI(key), out var value)) return true; + result = value; + return false; + } + catch (System.Exception ex) + { + if (logged.Add(key + ":error")) + RadicadeEacBypassPlugin.LogSource.LogWarning("[InviteUI] config lookup failed: " + ex.Message); + return true; + } + } +} diff --git a/2023Patch/RadicadePatch/KeepsakeToolPatch.cs b/2023Patch/RadicadePatch/KeepsakeToolPatch.cs new file mode 100644 index 0000000..cea39e3 --- /dev/null +++ b/2023Patch/RadicadePatch/KeepsakeToolPatch.cs @@ -0,0 +1,461 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using HarmonyLib; + +internal static class KeepsakeToolPatch +{ + private static readonly Type Palette = + typeof(RecRoom.AGUI.ToolUIs.MakerPenPaletteMainScreen); + + private static readonly Type Registry = typeof(PMJEHEPKBJL); + + private static readonly Type ToolTypeEnum = typeof(AOCJJLANBMH); + + private static readonly Type ToolConfig = typeof(RuntimeSpawnableToolConfig); + + + private static readonly Dictionary RepeatedLines = new Dictionary(); + + private static string Mode + => (Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_TOOL") + ?? (RadicadeEacBypassPlugin.KeepsakeToolEnabled?.Value == true ? "on" : "skip")).Trim(); + + private static bool Requested => Mode.ToLowerInvariant() switch + { + "on" or "true" or "1" or "yes" or "enable" or "enabled" => true, + "off" or "false" or "0" => true, + _ => false, + }; + + internal static bool Forcing => Mode.ToLowerInvariant() switch + { + "on" or "true" or "1" or "yes" or "enable" or "enabled" => true, + _ => false, + }; + + internal static void Apply(Harmony harmony) + { + if (!Requested) + { + return; + } + + if (Palette == null || Registry == null || ToolTypeEnum == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[Keepsake] palette/registry/tool-type type not found (" + + $"palette={Palette?.FullName ?? "null"}, registry={Registry?.FullName ?? "null"}, " + + $"toolType={ToolTypeEnum?.FullName ?? "null"}) - keepsake patch skipped"); + return; + } + + + Patch(harmony, Registry, "JBAINDGLMBO", new[] { ToolTypeEnum }, null, nameof(ConfigPostfix)); + + PatchByName(harmony, Registry, "LPBDCDIMBEN", nameof(PredicatePostfix)); + + + PatchByNameOnFamily(harmony, Palette, "LKGMIHJOIID", nameof(PredicatePostfix)); + PatchByNameOnFamily(harmony, Palette, "OLKPEGJNJGG", nameof(ConfigFilterPostfix)); + PatchByNameOnFamily(harmony, Palette, "IBHJBFJMBFB", nameof(ConfigFilterPostfix)); + + Patch(harmony, Palette, "OnUsedPaletteItem", null, nameof(UsedPalettePrefix), null); + + Audit(harmony); + } + + private static void Audit(Harmony harmony) + { + var service = AccessTools.TypeByName("AMAOGLDGNLL"); + foreach (var name in new[] + { + "MJDEEABFHPJ", "NCAKOLMAAEA", "CAFCLGFGKAO", "KHPJGNADGEE", "NGJMABOMHHB", + "FCOMKMPDEIO", "AEHHFFIKLG" + }) + PatchByName(harmony, service, name, nameof(CallPostfix), 2); + + if (!string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_AUDIT"), "1", + StringComparison.Ordinal)) return; + + Log("[Keepsake] RADICADE_KEEPSAKE_AUDIT=1: instrumenting the room/progression models too"); + foreach (var typeName in new[] { "RRUI.Data.RoomModel", "RRUI.Data.RoomListModel", "RRUI.Data.ProgressionEventsModel" }) + { + var model = AccessTools.TypeByName(typeName); + foreach (var name in new[] + { + "IsKeepsakeRoom", "GetCachedAreAllKeepsakesCollected", + "GetTotalKeepsakeCountPerCategoryForRoom", "GetUncollectedKeepsakeCountPerCategoryForRoom", + "GetTotalKeepsakeCountForRoom", "GetUncollectedKeepsakeCountForRoom", + "IsKeepsakeRoomListUpsellItemOwned", "AHACLFABDEO", "CJPFGOBHGBB" + }) + PatchByName(harmony, model, name, nameof(ModelPostfix), 4); + } + + } + + internal static object KeepsakeMember() + { + try + { + return ToolTypeEnum == null ? null : Enum.Parse(ToolTypeEnum, "KEEPSAKE"); + } + catch (Exception e) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[Keepsake] KEEPSAKE enum member missing: " + e.Message); + return null; + } + } + + internal static bool IsKeepsake(object toolType) + => toolType != null && KeepsakeMember()?.Equals(toolType) == true; + + internal static void Log(string message) + => RadicadeEacBypassPlugin.LogSource.LogInfo(message); + + private static void LogOnce(string message, int limit = 3) + { + var seen = RepeatedLines.TryGetValue(message, out var count) ? count + 1 : 1; + RepeatedLines[message] = seen; + if (seen <= limit) Log(message); + else if (seen == limit + 1) Log($"[Keepsake] (suppressing further copies of: {message})"); + } + + + private static void ConfigPostfix(object[] __args, RuntimeSpawnableToolConfig __result) + { + var toolType = __args.Length > 0 ? __args[0] : null; + var keepsake = IsKeepsake(toolType); + if (!keepsake && !IsRestricted(__result)) return; + + + if (!keepsake || !Forcing) return; + + var allowed = Field(__result, "spawnableToolTypesAllowedInRooms2"); + if (Contains(allowed, KeepsakeMember())) return; + AddToCollection(allowed, KeepsakeMember()); + } + + private static void PredicatePostfix(MethodBase __originalMethod, object[] __args, ref bool __result) + { + var toolType = FirstToolType(__args); + var keepsake = IsKeepsake(toolType); + + if (keepsake && Forcing && !__result) + { + __result = true; + } + } + + private static void CallPostfix(MethodBase __originalMethod, object[] __args, object __result) + { + } + + private static void ModelPostfix(MethodBase __originalMethod, object[] __args, object __result) + { + } + + private static string DescribeResult(object result) + { + if (result == null) return "(null)"; + var type = result.GetType(); + try + { + if (type == typeof(bool) || type == typeof(int) || type == typeof(long)) + return result.ToString(); + + var status = AccessTools.Property(type, "IsCompleted")?.GetValue(result) as bool?; + if (status == true) + { + var value = AccessTools.Property(type, "Result")?.GetValue(result); + return $"{type.Name}(completed) = {value}"; + } + + if (status == false) return $"{type.Name}(pending)"; + } + catch + { + } + + return type.Name; + } + + private static void ConfigFilterPostfix(MethodBase __originalMethod, object[] __args, ref bool __result) + { + var config = __args.Length > 0 ? __args[0] : null; + var name = Field(config, "UniqueName")?.ToString() ?? "(unnamed tool)"; + var keepsake = string.Equals(name, "KEEPSAKE", StringComparison.OrdinalIgnoreCase); + if (!keepsake) return; + + if (!Forcing || __result) return; + + __result = true; + } + + private static void UsedPalettePrefix(object[] __args) + { + } + + + private static object FirstToolType(object[] args) + { + if (args == null) return null; + foreach (var arg in args) + { + if (arg == null) continue; + if (ToolTypeEnum.IsInstanceOfType(arg) || arg.GetType() == ToolTypeEnum) return arg; + } + + return null; + } + + private static bool IsRestricted(object config) + => Field(config, "AvailableInMakerPen") is bool makerPen && !makerPen; + + private static string Frames(int take = 10) + { + try + { + var frames = new StackTrace(true).GetFrames(); + var sb = new StringBuilder(); + var taken = 0; + foreach (var frame in frames) + { + var method = frame.GetMethod(); + var owner = method?.DeclaringType?.Name ?? "?"; + if (owner.Contains("KeepsakeToolPatch") || owner.Contains("Harmony")) continue; + sb.Append(owner).Append('.').Append(method?.Name ?? "?").Append(" <- "); + if (++taken >= take) break; + } + return sb.Length == 0 ? "(no frames)" : sb.ToString().TrimEnd(' ', '<', '-'); + } + catch (Exception e) + { + return "(stack failed: " + e.GetType().Name + ")"; + } + } + + private static string DescribeList(object collection) + { + if (collection == null) return "(null)"; + var type = collection.GetType(); + + var count = 0; + var keepsake = false; + var names = new StringBuilder(); + try + { + var countValue = AccessTools.Property(type, "Count")?.GetValue(collection); + count = countValue is int n ? n : 0; + if (count == 0) return "0 item(s) []"; + + var indexer = type.GetProperty("Item"); + for (var i = 0; i < count && i < 12; i++) + { + var item = indexer?.GetValue(collection, new object[] { i }); + if (IsKeepsake(item)) keepsake = true; + names.Append(item).Append(','); + } + } + catch (Exception e) + { + return $"(list read failed: {e.GetType().Name}: {e.Message})"; + } + + return $"{count} item(s) [{names.ToString().TrimEnd(',')}]" + (keepsake ? " KEEPSAKE present" : ""); + } + + private static bool Contains(object collection, object item) + { + if (collection == null || item == null) return false; + try + { + var contains = AccessTools.Method(collection.GetType(), "Contains"); + return contains?.Invoke(collection, new[] { item }) is true; + } + catch + { + return false; + } + } + + private static bool AddToCollection(object collection, object item) + { + if (collection == null || item == null) return false; + try + { + var type = collection.GetType(); + MethodInfo add = null; + foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) + { + if (candidate.Name != "Add" || candidate.GetParameters().Length != 1) continue; + add = candidate; + break; + } + + if (add == null) return false; + add.Invoke(collection, new[] { item }); + return true; + } + catch (Exception e) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Keepsake] could not add to {collection.GetType().Name}: {e.GetType().Name}: {e.Message}"); + return false; + } + } + + private static string Describe(object config) + { + if (config == null) return "(null config)"; + + var sb = new StringBuilder(config.GetType().Name).Append('{'); + foreach (var name in new[] + { + "UniqueName", "AvailableInMakerPen", "AvailableToDevsOnly", "IsBetaItem", + "AvailableInSpawner", "CanSaveIntoInventions", "LimitInventionSaveForJuniors" + }) + { + var value = Field(config, name); + if (value != null) sb.Append(name).Append('=').Append(value).Append(' '); + } + + var allowed = Field(config, "spawnableToolTypesAllowedInRooms2"); + if (allowed != null) + sb.Append("allowedInRooms=").Append(DescribeList(allowed)).Append(' '); + + return sb.Append('}').ToString(); + } + + private static object Field(object instance, string name) + { + if (instance == null) return null; + try + { + var type = instance.GetType(); + var property = AccessTools.Property(type, name); + if (property != null) return property.GetValue(instance); + return AccessTools.Field(type, name)?.GetValue(instance); + } + catch + { + return null; + } + } + + + private static void PatchByNameOnFamily(Harmony harmony, Type root, string method, string postfix) + { + var patched = 0; + foreach (var type in Family(root)) + { + foreach (var found in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.Static | BindingFlags.Instance)) + { + if (found.Name != method) continue; + if (Bind(harmony, found, null, postfix)) patched++; + } + } + + if (patched == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Keepsake] no method named {method} on {root.Name} or its nested types - not instrumented"); + } + + private static void PatchByName(Harmony harmony, Type target, string method, string postfix, + int maxArguments = 1) + { + if (target == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Keepsake] type for {method} not found - not instrumented"); + return; + } + + var patched = 0; + foreach (var found in target.GetMethods(BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.Static | BindingFlags.Instance)) + { + if (found.Name != method) continue; + var parameters = found.GetParameters(); + if (parameters.Length > maxArguments) continue; + if (parameters.Any(p => p.ParameterType.IsByRef || p.IsOut || p.ParameterType.IsPointer)) continue; + if (maxArguments == 1 && (parameters.Length != 1 || + (!ToolTypeEnum.IsAssignableFrom(parameters[0].ParameterType) + && parameters[0].ParameterType != ToolTypeEnum))) continue; + if (Bind(harmony, found, null, postfix)) patched++; + } + + } + + private static IEnumerable Family(Type root) + { + yield return root; + Type[] nested; + try + { + nested = root.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); + } + catch + { + yield break; + } + + foreach (var type in nested) + foreach (var deeper in Family(type)) + yield return deeper; + } + + private static void Patch(Harmony harmony, Type target, string method, Type[] args, + string prefix, string postfix) + { + if (target == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning($"[Keepsake] type for {method} not found - not instrumented"); + return; + } + + try + { + var found = args == null + ? AccessTools.Method(target, method) + : AccessTools.Method(target, method, args); + if (found == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Keepsake] {target.Name}.{method} not found - not instrumented"); + return; + } + + Bind(harmony, found, prefix, postfix); + } + catch (Exception e) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Keepsake] FAILED to instrument {target.Name}.{method}: {e.GetType().Name}: {e.Message}"); + } + } + + private static bool Bind(Harmony harmony, MethodBase found, string prefix, string postfix) + { + try + { + var self = typeof(KeepsakeToolPatch); + const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic; + harmony.Patch(found, + prefix: prefix == null ? null : new HarmonyMethod(self.GetMethod(prefix, flags)), + postfix: postfix == null ? null : new HarmonyMethod(self.GetMethod(postfix, flags))); + return true; + } + catch (Exception e) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[Keepsake] FAILED to instrument {found.DeclaringType?.Name}.{found.Name}: " + + $"{e.GetType().Name}: {e.Message}"); + return false; + } + } +} diff --git a/2023Patch/RadicadePatch/LogFloodGate.cs b/2023Patch/RadicadePatch/LogFloodGate.cs new file mode 100644 index 0000000..6363b07 --- /dev/null +++ b/2023Patch/RadicadePatch/LogFloodGate.cs @@ -0,0 +1,205 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; + +internal static class LogFloodGate +{ + internal const double WindowSeconds = 30.0; + + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + private static readonly Dictionary Seen = new Dictionary(); + private static long _totalSuppressed; + + private static bool _selfTestPending; + private static int _selfTestFired; + + private sealed class Entry + { + public double WindowStart; + public int Suppressed; + public int ReportedAt; + } + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_LOG_GATE"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + internal static void Apply(Harmony harmony) + { + try + { + var target = AccessTools.Method(typeof(ODGGOALOLEG), "LogException", + new[] { typeof(Il2CppSystem.Exception), typeof(UnityEngine.Object) }); + if (target == null) + { + Warn("[LogGate] ODGGOALOLEG.LogException(Il2CppSystem.Exception, UnityEngine.Object) not found - " + + "not armed; the per-frame exception flood stays as-is."); + return; + } + + var prefix = AccessTools.Method(typeof(LogFloodGate), nameof(Prefix)); + if (prefix == null) + { + Warn("[LogGate] patch method Prefix not found - not armed."); + return; + } + + harmony.Patch(target, prefix: new HarmonyMethod(prefix)); + } + catch (Exception e) + { + Warn($"[LogGate] failed to arm ({e.GetType().Name}: {e.Message}); the exception flood stays as-is."); + } + } + + [HarmonyPrefix] + internal static bool Prefix(Il2CppSystem.Exception __0) + { + if (!Enabled) return true; + try + { + if (_selfTestPending && System.Threading.Interlocked.Exchange(ref _selfTestFired, 1) == 0) + { + _selfTestPending = false; + RunSelfTest(); + } + + var signature = Signature(__0); + if (signature == null) return true; + + var now = Time.realtimeSinceStartup; + lock (Seen) + { + if (Seen.Count > 512) Evict(now); + + if (!Seen.TryGetValue(signature, out var entry)) + { + Seen[signature] = new Entry { WindowStart = now }; + return true; + } + + if (now - entry.WindowStart < WindowSeconds) + { + entry.Suppressed++; + _totalSuppressed++; + if (entry.Suppressed == 1) + Warn($"[LogGate] repeats of this exception are now suppressed for {WindowSeconds:0}s at a time: {signature}"); + else if (entry.Suppressed - entry.ReportedAt >= 500) + { + entry.ReportedAt = entry.Suppressed; + Warn($"[LogGate] {entry.Suppressed} identical repeats of this exception suppressed so far " + + $"(session total {_totalSuppressed}): {signature}"); + } + return false; + } + + var dropped = entry.Suppressed; + entry.WindowStart = now; + entry.Suppressed = 0; + entry.ReportedAt = 0; + if (dropped > 0) + Warn($"[LogGate] {dropped} identical repeats were collapsed in the previous {WindowSeconds:0}s window " + + $"(session total {_totalSuppressed}); logging this occurrence in full: {signature}"); + return true; + } + } + catch + { + return true; + } + } + + private static void Evict(double now) + { + var stale = new List(); + foreach (var pair in Seen) + if (now - pair.Value.WindowStart > 600.0) + stale.Add(pair.Key); + foreach (var key in stale) + Seen.Remove(key); + } + + private static string Signature(Il2CppSystem.Exception exception) + { + string type, message, stack; + try { type = exception.GetType().Name; } catch { type = "Exception"; } + try { message = exception.Message; } catch { message = ""; } + try { stack = exception.StackTrace; } catch { stack = null; } + if (string.IsNullOrEmpty(message) && string.IsNullOrEmpty(stack)) return null; + return Trim(type + ": " + message, 150) + " @ " + FirstFrame(stack); + } + + private static string FirstFrame(string stack) + { + if (string.IsNullOrEmpty(stack)) return ""; + var start = stack.IndexOf(" at ", StringComparison.Ordinal); + if (start < 0) + { + var newline = stack.IndexOf('\n'); + return Trim(newline < 0 ? stack : stack.Substring(0, newline), 120); + } + var end = stack.IndexOf('\n', start); + return Trim(end < 0 ? stack.Substring(start) : stack.Substring(start, end - start), 120); + } + + private static string Trim(string value, int max) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + value = value.Replace('\r', ' ').Replace('\n', ' ').Trim(); + return value.Length <= max ? value : value.Substring(0, max) + "..."; + } + + private static void Warn(string message) + { + try { RadicadeEacBypassPlugin.LogSource.LogWarning(message); } catch { } + } + + internal static void RequestSelfTestIfAsked() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_LOG_GATE_SELFTEST"); + if (string.IsNullOrWhiteSpace(raw)) return; + if (raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no") return; + + _selfTestPending = true; + RadicadeEacBypassPlugin.LogSource.LogInfo( + "[LogGate] self-test armed: the next exception through the game handler fires five identical ones to prove the gate (RADICADE_LOG_GATE_SELFTEST)."); + } + catch (Exception e) + { + Warn($"[LogGate] self-test could not be armed: {e.GetType().Name}: {e.Message}"); + } + } + + private static void RunSelfTest() + { + try + { + var before = _totalSuppressed; + var exception = new Il2CppSystem.Exception("radicade log gate self-test"); + const int shots = 5; + for (var i = 0; i < shots; i++) + UnityEngine.Debug.LogException(exception); + + var collapsed = _totalSuppressed - before; + Warn($"[LogGate] self-test: fired {shots} identical exceptions through the live game handler; {collapsed} were collapsed, " + + $"{shots - collapsed} reached the log " + + $"({(collapsed == shots - 1 ? "PASS" : "FAIL")} - the first of a burst always logs in full, the rest are suppressed)."); + } + catch (Exception e) + { + Warn($"[LogGate] self-test failed to run: {e.GetType().Name}: {e.Message}"); + } + } +} diff --git a/2023Patch/RadicadePatch/LogNoiseFilter.cs b/2023Patch/RadicadePatch/LogNoiseFilter.cs new file mode 100644 index 0000000..d5d5171 --- /dev/null +++ b/2023Patch/RadicadePatch/LogNoiseFilter.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using UnityEngine; + +/// +/// Drops known-noise log lines inside Unity's own logger, so they never reach the +/// console or BepInEx's LogOutput.log. The game funnels both Debug.LogError and +/// Debug.LogWarning through Engine.Logger.Log(LogType, object) - that is the +/// single place where a message can be refused before anyone formats it. +/// +internal static class LogNoiseFilter +{ + private const string SelfTestProbe = + "The character used for Underline is not available in font asset [radicade-filter-self-test]."; + + internal static void Apply(Harmony harmony) + { + var prefix = AccessTools.Method(typeof(LogNoiseFilter), nameof(Prefix)); + if (prefix == null) + { + Diag.Warn("[LogFilter] the filter method was not found; known-noise log spam stays as-is."); + return; + } + + var patched = 0; + foreach (var target in Targets()) + { + try + { + harmony.Patch(target, prefix: new HarmonyMethod(prefix)); + patched++; + } + catch (Exception ex) + { + Diag.Warn("[LogFilter] could not patch " + target.DeclaringType?.Name + "." + target.Name + + " (" + ex.GetType().Name + ": " + ex.Message + ")."); + } + } + + if (patched == 0) + { + Diag.Warn("[LogFilter] no Unity log funnel could be patched; the RigidbodyEx and font-underline spam stays as-is."); + return; + } + + Diag.Info("[LogFilter] armed on " + patched + " Unity log funnel(s): " + LogNoiseRules.PatternCount + + " known-noise pattern(s) are dropped before they are printed."); + SelfTest(); + } + + private static IEnumerable Targets() + { + var log = AccessTools.Method(typeof(Logger), "Log", + new[] { typeof(LogType), typeof(Il2CppSystem.Object) }); + if (log != null) yield return log; + + var logWithContext = AccessTools.Method(typeof(Logger), "Log", + new[] { typeof(LogType), typeof(Il2CppSystem.Object), typeof(UnityEngine.Object) }); + if (logWithContext != null) yield return logWithContext; + } + + private static bool Prefix(object[] __args) + { + try + { + if (__args == null || __args.Length < 2) return true; + var message = __args[1]; + if (message == null) return true; + return !LogNoiseRules.ShouldDrop(message.ToString()); + } + catch + { + return true; + } + } + + /// + /// Fires one probe line through Debug.LogWarning and checks that the rules saw + /// it. If they did not, the filter is not wired to this build's logger and the + /// log says so instead of pretending the spam is gone. + /// + private static void SelfTest() + { + try + { + var before = LogNoiseRules.DroppedCount; + UnityEngine.Debug.LogWarning(SelfTestProbe); + if (LogNoiseRules.DroppedCount > before) + Diag.Info("[LogFilter] self-test PASS: a probe log line was intercepted inside Unity's logger."); + else + Diag.Warn("[LogFilter] self-test FAIL: a probe log line reached the log, so this build's spam is not being filtered."); + } + catch (Exception ex) + { + Diag.Warn("[LogFilter] self-test could not run (" + ex.GetType().Name + ": " + ex.Message + ")."); + } + } +} diff --git a/2023Patch/RadicadePatch/LogNoiseRules.cs b/2023Patch/RadicadePatch/LogNoiseRules.cs new file mode 100644 index 0000000..5a13dbc --- /dev/null +++ b/2023Patch/RadicadePatch/LogNoiseRules.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; + +/// +/// Log lines the client prints on every room load that say nothing a player can +/// act on and bury the lines that matter. Pure rules only, so they can be +/// exercised outside the game. +/// +internal static class LogNoiseRules +{ + private static readonly string[] Noise = + { + // Every chair and couch the dorm/Rec Center spawns complains that its + // RigidbodyEx has no entity component - dozens of lines per room load, + // all identical in substance. + "has an RigidbodyEx but no", + + // TMP asks for an underline glyph the legacy Diggity font does not carry; + // the text renders fine without it. + "The character used for Underline is not available in font asset", + }; + + private static int _dropped; + + internal static int PatternCount => Noise.Length; + + internal static int DroppedCount => Volatile.Read(ref _dropped); + + internal static bool ShouldDrop(string message) + { + if (string.IsNullOrEmpty(message)) return false; + + foreach (var pattern in Noise) + { + if (message.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) < 0) continue; + Interlocked.Increment(ref _dropped); + return true; + } + + return false; + } +} diff --git a/2023Patch/RadicadePatch/Plugin.cs b/2023Patch/RadicadePatch/Plugin.cs new file mode 100644 index 0000000..a93bed6 --- /dev/null +++ b/2023Patch/RadicadePatch/Plugin.cs @@ -0,0 +1,1038 @@ +using BepInEx; +using BepInEx.Logging; +using BepInEx.Unity.IL2CPP; +using HarmonyLib; +using RecRoom.AntiCheat; +using System.Linq; +using System.Runtime.InteropServices; + +internal sealed class FixedSetting +{ + internal T Value { get; } + + internal FixedSetting(T value) => Value = value; +} + +[BepInPlugin("com.radicade.patch", "RadicadePatch", "3.0.0")] +public sealed class RadicadeEacBypassPlugin : BasePlugin +{ + internal static ManualLogSource LogSource; + + internal const string ServerAddress = "https://ns.radicade.app/"; + internal const string ServerHost = "ns.radicade.app"; + + internal static FixedSetting ServerUrl = new(ServerAddress); + internal static FixedSetting TlsAllowedHosts = new(ServerHost); + internal static FixedSetting RealtimeLog = new(false); + internal static FixedSetting KeepsakeToolEnabled = new(true); + internal static Il2CppSystem.Threading.SynchronizationContext MainThreadContext; + internal static string ImageKeyId; + + static string ReadMetadataKey(string path) + { + try + { + return File.Exists(path) + ? ImageSignatureRequest.DetectKeyIdFromGameMetadata(File.ReadAllBytes(path)) + : null; + } + catch (Exception e) + { + LogSource.LogWarning($"[ImageSig] key id detection failed for {Path.GetFileName(path)}: {e.GetType().Name}: {e.Message}"); + return null; + } + } + + void LogFixedConnection() + { + Log.LogInfo($"[TLS] fixed server {ServerUrl.Value}; AllowedHosts = '{TlsAllowedHosts.Value}'"); + if (!EmbeddedTlsPatches.IsHostAllowed(ServerHost)) + Log.LogError($"[TLS] built-in AllowedHosts '{TlsAllowedHosts.Value}' does not cover '{ServerHost}' - the embedded TLS stack will reject the server certificate."); + } + + void WarnIfMetadataPatchedForAnotherServer(Uri server) + { + try + { + var marker = Path.Combine(UnityEngine.Application.dataPath, "radicade-patch.json"); + if (!File.Exists(marker)) + { + Log.LogWarning($"[Meta] no {marker}: this install was never metadata-patched, so the client still resolves the official hosts."); + return; + } + + var text = File.ReadAllText(marker); + var key = text.IndexOf("\"serverUrl\"", StringComparison.Ordinal); + var colon = key < 0 ? -1 : text.IndexOf(':', key); + var open = colon < 0 ? -1 : text.IndexOf('"', colon + 1); + var close = open < 0 ? -1 : text.IndexOf('"', open + 1); + if (close < 0) + { + Log.LogWarning($"[Meta] {marker} has no readable serverUrl field; cannot confirm the patched server matches {server}."); + return; + } + + var patched = text[(open + 1)..close].TrimEnd('/'); + var wanted = server.GetLeftPart(UriPartial.Authority).TrimEnd('/'); + if (patched.Equals(wanted, StringComparison.OrdinalIgnoreCase)) + { + Log.LogInfo($"[Meta] metadata patched for {patched}; matches Connection:ServerUrl."); + return; + } + + Log.LogWarning($"[Meta] this install's metadata was patched for '{patched}' but Connection:ServerUrl is '{wanted}'. " + + "The client's own literals still point at the patched address, so requests will not reach the configured server. " + + $"Re-patch from the pristine copy: python tools/patch-0831-client-metadata.py --server {wanted} --apply"); + } + catch (Exception e) + { + Log.LogWarning($"[Meta] metadata marker check failed: {e.GetType().Name}: {e.Message}"); + } + } + + public override void Load() + { + LogSource = Log; + if (UnityEngine.Application.version != "20230831") + throw new NotSupportedException("This plugin requires Rec Room 20230831."); + + MainThreadContext = Il2CppSystem.Threading.SynchronizationContext.Current; + if (MainThreadContext == null) + Log.LogWarning("[Patch] no main-thread synchronization context was captured; promise resolution may stall."); + + if (!Uri.TryCreate(ServerUrl.Value, UriKind.Absolute, out var server) || server.Scheme != "https") + throw new InvalidOperationException("The RadicadePatch client requires an HTTPS server URL."); + + LogFixedConnection(); + WarnIfMetadataPatchedForAnotherServer(server); + + var metadataRoot = Path.Combine(UnityEngine.Application.dataPath, "il2cpp_data", "Metadata"); + var gameKeyId = ReadMetadataKey(Path.Combine(metadataRoot, "global-metadata.dat")); + var interopKeyId = gameKeyId == null ? ReadMetadataKey(Path.Combine(metadataRoot, "global-metadata.interop.dat")) : null; + ImageKeyId = gameKeyId ?? interopKeyId; + if (ImageKeyId == null) + Log.LogWarning("[ImageSig] no image key id was found in the game metadata; rewritten URLs will not state one."); + + var harmony = new Harmony("com.radicade.patch"); + + var steps = new (string Label, Action Apply)[] + { + ("module hiding", ApplyModuleHiding), + ("hile gate", ArmHileGate), + ("connection gate", () => ConnectionGate.Install(harmony)), + ("august challenge", () => harmony.PatchAll(typeof(AugustChallengePatches))), + ("august launch diagnostics", () => harmony.PatchAll(typeof(AugustLaunchDiagnostics))), + ("august launch validator", () => harmony.PatchAll(typeof(AugustLaunchValidatorPatch))), + ("rsa verify neutraliser", () => RsaVerifyNeutralizerPatches.Apply(harmony)), + ("file check", () => harmony.PatchAll(typeof(FileCheckEntryPatch))), + ("eac native client stub", () => harmony.PatchAll(typeof(EacNativeClientStubPatch))), + ("eac health query", () => harmony.PatchAll(typeof(EacHealthQueryLiePatch))), + ("cheat scan suppression", () => harmony.PatchAll(typeof(CheatScanSuppressPatch))), + ("cheat detection suppression", () => harmony.PatchAll(typeof(CheatDetectionSuppressPatch))), + ("cheat dorm drop guard", () => harmony.PatchAll(typeof(CheatDormDropGuardPatch))), + ("cheat manager trace", () => harmony.PatchAll(typeof(CheatManagerTracePatch))), + ("exit initiator tracer", () => harmony.PatchAll(typeof(ExitInitiatorTracerPatch))), + ("exit sequence trace", () => harmony.PatchAll(typeof(ExitSequenceTracePatch))), + ("unity quit veto", () => harmony.PatchAll(typeof(UnityQuitVetoPatch))), + ("exit state veto", () => harmony.PatchAll(typeof(ExitStateVetoPatch))), + ("crash address map", CrashAddressMap.Start), + ("tool collision reset guard", () => harmony.PatchAll(typeof(ToolCollisionResetGuardPatch))), + ("collider cast guard", () => ColliderCastGuardPatch.Apply(harmony)), + ("unbound prop freeze", () => harmony.PatchAll(typeof(UnboundPropFreezePatch))), + ("keepsake tool", () => KeepsakeToolPatch.Apply(harmony)), + ("dynamic ui prefab repair", () => DynamicUiPrefabRepair.Apply(harmony)), + ("client text rewrites", () => ClientTextRewrites.Apply(harmony)), + ("voice chat restriction capture", () => VoiceChatRestrictionPatches.ArmInstanceCapture(harmony)), + ("voice chat ban flags", () => VoiceChatBanFlagPatches.Apply(harmony)), + ("invite online friends", () => InviteOnlineFriendsPatch.Apply(harmony)), + ("log flood gate", () => ApplyLogFloodGate(harmony)), + ("realtime exit watch", () => RealtimeDiagnostics.ApplyExitWatch(harmony)), + ("realtime diagnostics", () => RealtimeDiagnostics.Apply(harmony, RealtimeLog?.Value ?? true)), + ("embedded tls", () => EmbeddedTlsPatches.Apply(harmony)), + ("http response tolerance", () => HttpTolerancePatches.Apply(harmony)), + ("log noise filter", () => LogNoiseFilter.Apply(harmony)), + ("nameserver connection", () => harmony.PatchAll(typeof(NameserverConnectionPatches))), + ("websocket redirect", () => WebSocketRedirectPatches.Apply(harmony)), + }; + + var failed = 0; + for (var i = 0; i < steps.Length; i++) + { + try + { + steps[i].Apply(); + Log.LogInfo($"[Patch] {i + 1}/{steps.Length} {steps[i].Label}"); + } + catch (Exception stepEx) + { + failed++; + Log.LogError($"[Patch] {i + 1}/{steps.Length} {steps[i].Label} failed: {stepEx}"); + } + } + + Log.LogInfo("PATCH LIST END"); + if (failed > 0) + Log.LogWarning($"[Patch] {failed} of {steps.Length} steps failed, so the client may not behave as configured."); + + ConnectionGate.Open(); + } + + void ApplyModuleHiding() + { + if (!ModuleHidingPolicy.Enabled) + return; + + var hidden = PebModuleHider.HideModule("winhttp.dll"); + if (!hidden) + Log.LogWarning("[ModuleHide] winhttp.dll was not found in the PEB module lists; a module scan will still see it."); + + var wiped = PeHeaderWiper.WipeSignatures("winhttp.dll"); + if (!wiped) + Log.LogWarning("[ModuleHide] the winhttp.dll PE signature wipe did not apply (signatures missing or VirtualProtect failed)."); + + if (hidden && wiped) + Log.LogInfo("[ModuleHide] winhttp.dll is unlinked from the PEB module lists and its PE signatures are wiped."); + } + + void ArmHileGate() + { + HileGate.Enabled = true; + Log.LogInfo(HileGate.DropEnabled + ? "[HileGate] armed: anti-cheat report POSTs are dropped at the HTTP layer." + : "[HileGate] armed: anti-cheat report POSTs are captured in full and then allowed through (RADICADE_HILE_GATE=drop drops them instead)."); + } + + void ApplyLogFloodGate(Harmony harmony) + { + LogFloodGate.Apply(harmony); + LogFloodGate.RequestSelfTestIfAsked(); + } +} + +[HarmonyPatch] +internal static class AugustLaunchDiagnostics +{ + [HarmonyPrefix, HarmonyPatch(typeof(MILEHJBNIKI), "LMMMGKJCPOF")] + [HarmonyPatch(typeof(MILEHJBNIKI), "MBNHJLNEIKL")] + private static void Trace(FDJFJOBADEI __0) + { + if (__0 == FDJFJOBADEI.LAUNCH_VALIDATION_FAILED) + RadicadeEacBypassPlugin.LogSource.LogWarning("[August launch diagnostic] " + Il2CppSystem.Environment.StackTrace); + } +} + +[HarmonyPatch] +internal static class AugustChallengePatches +{ + [HarmonyPrefix, HarmonyPatch(typeof(EACManager), nameof(EACManager.GenerateChallengeResponse))] + private static bool Challenge(ref string __result) + { + __result = "radicade-eac-bypass"; + return false; + } + + [HarmonyPrefix, HarmonyPatch(typeof(EACManager), "JGFKOKIDHIN")] + private static bool Ready(ref bool __result) + { + __result = true; + return false; + } +} + +[HarmonyPatch(typeof(IDLGLOLKLEB.MKGADIPJDLK), "IDDOELEGIFB")] +internal static class AugustLaunchValidatorPatch +{ + static readonly System.Threading.CancellationTokenSource Pump = new(); + + [HarmonyPrefix] + internal static bool Bypass(IDLGLOLKLEB.MKGADIPJDLK __instance) + { + var promise = __instance.promise; + if (promise == null) + return true; + + var context = RadicadeEacBypassPlugin.MainThreadContext; + try + { + var before = Il2CppSystem.Threading.SynchronizationContext.Current; + if (context != null) + Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(context); + try + { + promise.BEJNCICKNOM(); + } + catch (Exception e) + { + RadicadeEacBypassPlugin.LogSource.LogError($"[August] Resolve failed: {e}"); + } + finally + { + Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(before); + } + } + catch (Exception outer) + { + RadicadeEacBypassPlugin.LogSource.LogError($"[August] Context install failed: {outer}"); + } + return false; + } + +} + +internal static class RsaVerifyNeutralizerPatches +{ + static readonly byte[] P1KeyModulus = System.Convert.FromBase64String( + "X07yXkxaaLcZ1wVXfkWjgFkkqdoLhDFm0GPODsF+Q47pSUlbLvtXGqStnyEJEIrQmgDiicAvCdGRq4lovr2l5sIPMaoyizsbVHBdwLUrCsji0RvSBnmvN+8KqQ8STnB4DP4pAsPilfD35def4WuX/xMCXB5+hQUVhv27HPV8Dj9XzHuJAijIM9UwDZmvUcECyiO4wv+TaZi2+ELBtaLCQR8Gm1ZPeDEwP62Ch6MJy0jx5pkvvD0KdF9Wye+3/Wx31Zn/Trdo9HL4sGFWPDM9H9kQhZd5wkTHuxpwGIIhlzIwvY2/pBGdZKP6fi1D2jROEmVkBDyhmYY9nO+s3/bndQ=="); + + static int p1Calls, totalCalls; + + internal static void Apply(Harmony harmony) + { + PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyHash", + new[] + { + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray), + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray), + typeof(Il2CppSystem.Security.Cryptography.HashAlgorithmName), + typeof(Il2CppSystem.Security.Cryptography.RSASignaturePadding) + }); + + PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyHash", + new[] + { + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray), + typeof(int), + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray) + }); + + PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyData", + new[] + { + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray), + typeof(Il2CppSystem.Object), + typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray) + }); + } + + static void PatchVerify(Harmony harmony, Type target, string methodName, Type[] args) + { + try + { + var m = AccessTools.Method(target, methodName, args); + if (m == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[RsaVerify] {target.Name}.{methodName}({args.Length} args) not found - skipped"); + return; + } + harmony.Patch(m, prefix: new HarmonyMethod(typeof(RsaVerifyNeutralizerPatches).GetMethod(nameof(VerifyPrefix)))); + } + catch (Exception e) + { + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[RsaVerify] failed to patch {target.Name}.{methodName}: {e.GetType().Name}: {e.Message}"); + } + } public static bool VerifyPrefix( + Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider __instance, ref bool __result) + { + totalCalls++; + var isP1 = false; + try + { + var modulus = __instance.ExportParameters(false).Modulus; + isP1 = modulus != null && modulus.Length == P1KeyModulus.Length && modulus.SequenceEqual(P1KeyModulus); + if (isP1) + p1Calls++; + } + catch + { + } + + __result = isP1; + if (totalCalls <= 8) + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[ImageSig] RSA verify #{totalCalls} ({(isP1 ? "p1 key" : "game key")}) forced {(isP1 ? "true" : "false")}"); + return false; + } +} + +[HarmonyPatch] +internal static class FileCheckEntryPatch +{ + private static System.Collections.Generic.IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(GBOAMDFCNJA), "PEEADMOKNLI"); + yield return AccessTools.Method(typeof(GBOAMDFCNJA), "IBPPCNEAGDP"); + } + + [HarmonyPrefix] + internal static bool Bypass(ref MFBKDKPGMNP __result) + { + var result = GBOAMDFCNJA.CMIGBOACMAG.NDGAGMGBOMD("radicade: file check bypassed"); + __result = NOKBHGDKGNF.BGFCCOEKHOE(result); + return false; + } +} + +internal static unsafe class PebModuleHider +{ + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_BASIC_INFORMATION + { + public IntPtr Reserved1; + public IntPtr PebBaseAddress; + public IntPtr Reserved2_0; + public IntPtr Reserved2_1; + public IntPtr UniqueProcessId; + public IntPtr Reserved3; + } + + [DllImport("ntdll.dll")] + private static extern int NtQueryInformationProcess( + IntPtr hProcess, int processInfoClass, ref PROCESS_BASIC_INFORMATION pbi, + int cb, out int returnLength); + + private struct LIST_ENTRY { public IntPtr Flink, Blink; } + + private const int OffInMemoryOrder = 0x10; + private const int OffInInitOrder = 0x20; + private const int OffBaseDllName = 0x58; + + public static bool HideModule(string dllName) + { + var pbi = default(PROCESS_BASIC_INFORMATION); + int status = NtQueryInformationProcess(new IntPtr(-1), 0, ref pbi, + Marshal.SizeOf(), out _); + if (status != 0 || pbi.PebBaseAddress == IntPtr.Zero) + return false; + + byte* peb = (byte*)pbi.PebBaseAddress; + byte* ldr = *(byte**)(peb + 0x18); + if (ldr == null) + return false; + + LIST_ENTRY* head = (LIST_ENTRY*)(ldr + 0x10); + byte* target = null; + for (LIST_ENTRY* n = (LIST_ENTRY*)head->Flink; n != head && target == null; n = (LIST_ENTRY*)n->Flink) + { + byte* entry = (byte*)n; + ushort* us = (ushort*)(entry + OffBaseDllName); + ushort lenBytes = *us; + char* buf = *(char**)(entry + OffBaseDllName + 8); + if (buf == null || lenBytes == 0) + continue; + string name = new string(buf, 0, lenBytes / 2); + if (name.Equals(dllName, StringComparison.OrdinalIgnoreCase)) + target = entry; + } + + if (target == null) + return false; + + Unlink((LIST_ENTRY*)target); + Unlink((LIST_ENTRY*)(target + OffInMemoryOrder)); + Unlink((LIST_ENTRY*)(target + OffInInitOrder)); + return true; + } + + private static void Unlink(LIST_ENTRY* e) + { + var flink = (LIST_ENTRY*)e->Flink; + var blink = (LIST_ENTRY*)e->Blink; + if (flink == e || blink == e) + return; + blink->Flink = e->Flink; + flink->Blink = e->Blink; + e->Flink = (IntPtr)e; + e->Blink = (IntPtr)e; + } +} + +internal static unsafe class PeHeaderWiper +{ + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr GetModuleHandleW(string moduleName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint newProtect, out uint oldProtect); + + public static bool WipeSignatures(string dllName) + { + IntPtr baseAddr = GetModuleHandleW(dllName); + if (baseAddr == IntPtr.Zero) + return false; + + byte* p = (byte*)baseAddr; + int eLfanew = *(int*)(p + 0x3C); + if (eLfanew <= 0 || eLfanew > 0x1000) + return false; + + if (!VirtualProtect(baseAddr, (UIntPtr)0x1000, 0x04 , out uint old)) + return false; + + bool wasMz = p[0] == (byte)'M' && p[1] == (byte)'Z'; + bool wasPe = *(uint*)(p + eLfanew) == 0x00004550; + + *(ushort*)p = 0; + *(uint*)(p + eLfanew) = 0; + + VirtualProtect(baseAddr, (UIntPtr)0x1000, old, out _); + return wasMz && wasPe; + } +} + +[HarmonyPatch] +internal static class ExitInitiatorTracerPatch +{ + private static System.Collections.Generic.IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(KJAMNFCDBIO), "GMAKAKECEDK"); + yield return AccessTools.Method(typeof(KJAMNFCDBIO), "FLMPEJAIKFO"); + yield return AccessTools.Method(typeof(KJAMNFCDBIO), "LHNNEFHHPBG"); + } + + [HarmonyPrefix] + internal static bool TraceAndBlock() + { + var armed = HileGate.DroppedCount > 0; + if (armed) + { + var stack = ""; + try { stack = Environment.StackTrace; } catch { } + RadicadeEacBypassPlugin.LogSource.LogWarning("[ExitInit] blocked a managed exit initiator - caller trace:\n" + stack); + } + return !armed; + } +} + +[HarmonyPatch] +[HarmonyPatch(typeof(BestHTTP.HTTPManager), "SendRequest", new[] { typeof(BestHTTP.HTTPRequest) })] +internal static class ExitSequenceTracePatch +{ + private static int _dumped; + + [HarmonyPriority(Priority.First)] + private static void Prefix(BestHTTP.HTTPRequest request) + { + try + { + var path = request?.Uri?.AbsolutePath; + if (path == null) return; + if (path.IndexOf("/matchmaking/player/logout", StringComparison.OrdinalIgnoreCase) < 0 && + path.IndexOf("/playersettings/playersettings", StringComparison.OrdinalIgnoreCase) < 0) + return; + if (++_dumped > 4) return; + + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[ExitTrace] shutdown request " + path + " - managed caller stack:" + ExitTraceHelper.SafeStack()); + } + catch (Exception traceEx) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[ExitTrace] dump failed: " + traceEx.Message); + } + } +} + +[HarmonyPatch] +internal static class UnityQuitVetoPatch +{ + internal static bool Enabled => _enabled; + + private static readonly bool _enabled = IsEnabled(); + + private static int _blocks; + + private static bool IsEnabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_QUIT_VETO"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes"; + } + catch { return false; } + } + + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var noArg = AccessTools.Method(typeof(KJAMNFCDBIO), "OFBGCPAHADA", Type.EmptyTypes); + if (noArg != null) yield return noArg; + + var flag = AccessTools.Method(typeof(KJAMNFCDBIO), "OFBGCPAHADA", new[] { typeof(bool) }); + if (flag != null) yield return flag; + } + + [HarmonyPrefix] + private static bool Veto(ref bool __result) + { + if (!Enabled) return true; + + __result = false; + + var count = ++_blocks; + if (count <= 3 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning( + "[QuitVeto] wantsToQuit answered false (block #" + count + ") - Unity's quit is cancelled. " + + "This is the anti-cheat shutdown, not a user quit; the veto is armed by RADICADE_QUIT_VETO=1, set it to 0 to disable." + + ExitTraceHelper.SafeStack()); + return false; + } +} + +internal static class ModuleHidingPolicy +{ + internal static bool Enabled { get; } = ReadFlag("RADICADE_MODULE_HIDE"); + + private static bool ReadFlag(string name) + { + try + { + var raw = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes"; + } + catch { return false; } + } + +} + +[HarmonyPatch] +internal static class CheatScanSuppressPatch +{ + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _skips; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_SCAN"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static System.Reflection.MethodBase TargetMethod() + { + var type = AccessTools.TypeByName("CheatManager"); + if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved."); + + var method = AccessTools.Method(type, "EKGOMPDGNEK", System.Type.EmptyTypes); + if (method == null) throw new System.MissingMethodException("CheatManager.EKGOMPDGNEK() is gone - the scan moved or was renamed."); + + return method; + } + + [HarmonyPrefix] + private static bool Skip() + { + if (!Enabled) return true; + + var count = ++_skips; + if (count <= 3 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatScan] skipped CheatManager.EKGOMPDGNEK() (#" + count + + ") - this is the pass behind the Obscured/winhttp.dll verdict, so no report and no shutdown follow from it."); + return false; + } +} + +[HarmonyPatch] +internal static class CheatDetectionSuppressPatch +{ + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _skips; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_TYPE"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var type = AccessTools.TypeByName("CheatManager"); + if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved."); + + var found = new System.Collections.Generic.List(); + var missing = new System.Collections.Generic.List(); + foreach (var name in new[] { "OnObscuredTypeCheatDetected", "OnTimeCheatDetected", "OnAdvancedMovementCheatDetected" }) + { + var method = AccessTools.Method(type, name, System.Type.EmptyTypes); + if (method == null) + { + missing.Add(name); + continue; + } + + found.Add(name); + yield return method; + } + + _guarded = found.Count; + _unguarded = missing.Count == 0 ? null : string.Join(", ", missing); + } + + internal static int Guarded => _guarded; + + internal static string Unguarded => _unguarded; + + private static int _guarded; + + private static string _unguarded; + + [HarmonyPrefix] + private static bool Skip(System.Reflection.MethodBase __originalMethod) + { + if (!Enabled) return true; + + var count = ++_skips; + if (count <= 5 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatType] skipped CheatManager." + __originalMethod.Name + "() (#" + count + + ") - this is a verdict handler that ends in the dorm drop with FOKOIBODNAD and the shutdown behind it, so the room survives the detection."); + return false; + } +} + +[HarmonyPatch] +internal static class CheatDormDropGuardPatch +{ + private const string AntiCheatReason = "385120"; + + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _skips; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_DROP"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static System.Reflection.MethodBase TargetMethod() + { + var type = AccessTools.TypeByName("CheatManager"); + if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved."); + + var method = AccessTools.Method(type, "FOKOIBODNAD", new[] { typeof(string) }); + if (method == null) + throw new System.MissingMethodException("CheatManager.FOKOIBODNAD(string) is gone - the dorm drop moved or was renamed, so the guard is NOT armed."); + + return method; + } + + [HarmonyPrefix] + private static bool Skip(string __0) + { + if (!Enabled) return true; + + if (string.IsNullOrEmpty(__0) || __0.IndexOf(AntiCheatReason, StringComparison.Ordinal) < 0) + return true; + + var count = ++_skips; + if (count <= 5 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatDrop] skipped CheatManager.FOKOIBODNAD('" + __0 + "') (#" + count + + ") - the anti-cheat's own drop, so the player stays where they are instead of being sent to the dormroom mid-session."); + return false; + } +} + +[HarmonyPatch] +internal static class CheatManagerTracePatch +{ + private const int PerMethodLimit = 4; + private const int LineLimit = 90; + + private static readonly System.Diagnostics.Stopwatch Clock = System.Diagnostics.Stopwatch.StartNew(); + private static readonly System.Collections.Generic.Dictionary Seen = new System.Collections.Generic.Dictionary(); + private static int _lines; + + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var outer = AccessTools.TypeByName("CheatManager"); + if (outer == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatTrace] CheatManager not resolvable - nothing traced."); + yield break; + } + + var types = new System.Collections.Generic.List { outer }; + foreach (var nested in outer.GetNestedTypes(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)) + types.Add(nested); + + foreach (var type in types) + { + foreach (var method in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly)) + { + if (method.IsAbstract || method.ContainsGenericParameters) continue; + yield return method; + } + } + } + + private static readonly bool Enabled = IsEnabled(); + + private static bool IsEnabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_TRACE"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes"; + } + catch { return false; } + } + + [HarmonyPrefix] + private static void Trace(System.Reflection.MethodBase __originalMethod) + { + if (!Enabled) return; + try + { + if (_lines >= LineLimit) return; + + var key = __originalMethod.DeclaringType.Name + "." + __originalMethod.Name; + + Seen.TryGetValue(key, out var count); + if (count >= PerMethodLimit) return; + Seen[key] = count + 1; + _lines++; + + RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatTrace] +" + Clock.ElapsedMilliseconds + "ms " + key); + } + catch { } + } +} + +internal static class ExitTraceHelper +{ + internal static string SafeStack() + { + try { return "\n" + Environment.StackTrace; } catch { return "\n"; } + } +} + +internal static class EacGate +{ + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static int _logCount; + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_EAC_NEUTRALIZE"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + internal static System.Type Find(string fullName) + { + try + { + var byName = System.Type.GetType(fullName + ", EasyAntiCheat.Client"); + if (byName != null) return byName; + } + catch { } + + try + { + var forced = typeof(EasyAntiCheat.Client.Hydra.Runtime).Assembly; + var direct = forced.GetType(fullName); + if (direct != null) return direct; + } + catch { } + + try { return AccessTools.TypeByName(fullName); } catch { return null; } + } + + private const int LogLimit = 8; + + internal static void Log(string message) + { + try + { + if (++_logCount > LogLimit) return; + RadicadeEacBypassPlugin.LogSource.LogWarning(message); + } + catch { } + } +} + +[HarmonyPatch] +internal static class EacNativeClientStubPatch +{ + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var type = EacGate.Find("EasyAntiCheat.Client.Hydra.NativeClient"); + if (type == null) + { + EacGate.Log("[EacStub] Hydra.NativeClient not resolvable - EAC runtime left untouched."); + yield break; + } + + foreach (var method in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + if (method.Name == "Initialize") + yield return method; + } + + [HarmonyPrefix] + private static bool Skip(System.Reflection.MethodBase __originalMethod) + { + if (!EacGate.Enabled) return true; + + EacGate.Log("[EacStub] skipped " + __originalMethod.DeclaringType + "." + __originalMethod.Name + + " - EasyAntiCheat's native client never starts, so its module scanner never runs."); + return false; + } +} + +[HarmonyPatch] +internal static class EacHealthQueryLiePatch +{ + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var native = EacGate.Find("EasyAntiCheat.Client.Hydra.NativeClient"); + if (native != null) + foreach (var method in native.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + if (method.Name == "IsStartedWithEasyAntiCheat" && method.ReturnType == typeof(bool)) + yield return method; + + var runtime = EacGate.Find("EasyAntiCheat.Client.Hydra.Runtime"); + if (runtime != null) + foreach (var method in runtime.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + if ((method.Name == "IsActive" || method.Name == "get_Initialized") && method.ReturnType == typeof(bool)) + yield return method; + } + + [HarmonyPrefix] + private static bool Lie(ref bool __result, System.Reflection.MethodBase __originalMethod) + { + if (!EacGate.Enabled) return true; + + __result = true; + EacGate.Log("[EacStub] " + __originalMethod.DeclaringType + "." + __originalMethod.Name + " answered true."); + return false; + } +} + +[HarmonyPatch] +[HarmonyPatch] +internal static class ToolCollisionResetGuardPatch +{ + internal static bool Enabled => !_disabled; + + private static readonly bool _disabled = IsDisabled(); + + private static bool IsDisabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_TOOL_RESET"); + if (string.IsNullOrWhiteSpace(raw)) return false; + return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; + } + catch { return false; } + } + + private static System.Reflection.MethodBase TargetMethod() + { + var method = AccessTools.Method(typeof(Tool), "ResetToDefaultCollisionLayer", Type.EmptyTypes); + if (method == null) + throw new MissingMethodException( + "Tool.ResetToDefaultCollisionLayer() not found - the room crash guard is NOT armed."); + return method; + } + + [HarmonyPrefix] + private static bool Skip(Tool __instance) + { + if (!Enabled) return true; + ToolCollisionLayerRestore.Restore(__instance); + return false; + } + + private static string Describe(Tool tool) + { + try + { + if (tool == null) return ""; + var owner = tool.gameObject; + return (owner == null) ? "" : "'" + owner.name + "'"; + } + catch (Exception ex) + { + return ""; + } + } +} + +internal static class ExitStateVetoPatch +{ + internal static bool Enabled => _enabled; + + private static readonly bool _enabled = IsEnabled(); + + private static int _blocks; + + private static bool IsEnabled() + { + try + { + var raw = Environment.GetEnvironmentVariable("RADICADE_EXIT_VETO"); + if (string.IsNullOrWhiteSpace(raw)) return false; + raw = raw.Trim().ToLowerInvariant(); + return raw is "1" or "on" or "true" or "yes"; + } + catch { return false; } + } + + private static System.Collections.Generic.IEnumerable TargetMethods() + { + var property = AccessTools.PropertySetter(typeof(KJAMNFCDBIO), "JGKJEDKNFHN"); + if (property != null) yield return property; + + var raw = AccessTools.Method(typeof(KJAMNFCDBIO), "EHPDJDJLPKM", + new[] { typeof(KJAMNFCDBIO.CNNHHNMPLJE) }); + if (raw != null) yield return raw; + } + + [HarmonyPrefix] + private static bool Block(KJAMNFCDBIO.CNNHHNMPLJE __0) + { + if (!Enabled) return true; + if (__0 == KJAMNFCDBIO.CNNHHNMPLJE.Running) return true; + + var count = ++_blocks; + var caller = count == 1 ? ExitTraceHelper.SafeStack() : string.Empty; + if (count <= 3 || count % 25 == 0) + RadicadeEacBypassPlugin.LogSource.LogWarning( + $"[ExitVeto] blocked AppExitState -> {__0} (block #{count}); the session keeps running. " + + "This is the anti-cheat-driven exit, not a user quit - the veto is armed by RADICADE_EXIT_VETO=1, set it to 0 to disable." + caller); + return false; + } +} + diff --git a/2023Patch/RadicadePatch/RadicadePatch.csproj b/2023Patch/RadicadePatch/RadicadePatch.csproj new file mode 100644 index 0000000..376e060 --- /dev/null +++ b/2023Patch/RadicadePatch/RadicadePatch.csproj @@ -0,0 +1,29 @@ + + + net6.0 + enable + true + latest + RadicadePatch + RadicadePatch + + C:\RadicadeBeta\BepInEx + + + + + + + + + + + + + + + + + + + diff --git a/2023Patch/RadicadePatch/ToolCollisionLayerRestore.cs b/2023Patch/RadicadePatch/ToolCollisionLayerRestore.cs new file mode 100644 index 0000000..4b0306b --- /dev/null +++ b/2023Patch/RadicadePatch/ToolCollisionLayerRestore.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +internal static class ToolCollisionLayerRestore +{ + private static int _failures; + + internal static void Restore(Tool tool) + { + if (tool == null) return; + try + { + if (tool.MEDCLKFANNC) return; + int layer = (int)tool.EHNBBIAIJIK; + if (layer < 0 || layer > 31) + throw new InvalidOperationException("Invalid default collision layer " + layer); + + foreach (var collider in tool.GetComponentsInChildren(true)) + { + if (collider == null || collider.isTrigger) continue; + if (collider.GetComponentInParent() != tool) continue; + var owner = collider.gameObject; + if (!ShouldRestore(owner.layer, (int)tool.HeldCollisionLayer, + (int)tool.EquippedCollisionLayer, (int)tool.AttachedCollisionLayer)) continue; + if (owner.layer == layer) continue; + owner.layer = layer; + } + } + catch (Exception ex) + { + if (++_failures <= 5) + RadicadeEacBypassPlugin.LogSource.LogWarning("[ToolGuard] collision-layer restore failed: " + ex); + } + } + + private static bool ShouldRestore(int layer, int held, int equipped, int attached) + { + return layer == held || layer == equipped || layer == attached + || layer is 0 or 10 or 11 or 13 or 17 or 18 or 23 or 24 or 27; + } +} diff --git a/2023Patch/RadicadePatch/UnboundPropFreezePatch.cs b/2023Patch/RadicadePatch/UnboundPropFreezePatch.cs new file mode 100644 index 0000000..4bbdabc --- /dev/null +++ b/2023Patch/RadicadePatch/UnboundPropFreezePatch.cs @@ -0,0 +1,147 @@ +using System; +using HarmonyLib; +using Il2CppInterop.Runtime; +using RecRoom; +using RecRoom.ObjectModel; +using UnityEngine; + +[HarmonyPatch] +internal static class UnboundPropFreezePatch +{ + private static readonly bool _disabled = IsDisabled(); + + + private static int _alreadyInert; + + private static bool _loggedNoRigidbody; + + private static bool _loggedFailure; + + private static readonly Type[] Companions = new Type[3] + { + typeof(EmbodiedObject), + typeof(RbexEntity), + typeof(TransformEntity) + }; + + internal static bool Enabled => !_disabled; + + private static bool IsDisabled() + { + try + { + string environmentVariable = Environment.GetEnvironmentVariable("RADICADE_UNBOUND_PROP_FREEZE"); + if (string.IsNullOrWhiteSpace(environmentVariable)) + { + return false; + } + bool result; + switch (environmentVariable.Trim().ToLowerInvariant()) + { + case "0": + case "off": + case "false": + case "no": + result = true; + break; + default: + result = false; + break; + } + return result; + } + catch + { + return false; + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(RigidbodyEx), "Awake")] + private static void Postfix(RigidbodyEx __instance) + { + if (!Enabled || __instance == null) + { + return; + } + try + { + if (!IsBound(__instance)) + { + Freeze(__instance); + } + } + catch (Exception ex) + { + if (!_loggedFailure) + { + _loggedFailure = true; + RadicadeEacBypassPlugin.LogSource.LogWarning((object)("[PropGuard] could not freeze an unbound prop (" + ex.GetType().Name + ": " + ex.Message + "); the prop is left exactly as the game built it.")); + } + } + } + + private static bool IsBound(RigidbodyEx rigidbodyEx) + { + Type[] companions = Companions; + foreach (Type type in companions) + { + try + { + if (rigidbodyEx.GetComponent(Il2CppType.From(type)) != null) + { + return true; + } + } + catch + { + return true; + } + } + return false; + } + + private static void Freeze(RigidbodyEx rigidbodyEx) + { + Rigidbody unityRigidbody = rigidbodyEx.unityRigidbody; + if (unityRigidbody == null) + { + if (!_loggedNoRigidbody) + { + _loggedNoRigidbody = true; + } + try + { + rigidbodyEx.ClearUnityRigidbodyVelocity(); + return; + } + catch + { + return; + } + } + if (unityRigidbody.isKinematic) + { + _alreadyInert++; + return; + } + unityRigidbody.isKinematic = true; + unityRigidbody.velocity = Vector3.zero; + unityRigidbody.angularVelocity = Vector3.zero; + + } + + private static string Describe(RigidbodyEx rigidbodyEx) + { + try + { + GameObject gameObject = ((Component)rigidbodyEx).gameObject; + return (gameObject == null) ? "" : ("'" + gameObject.name + "'"); + } + catch + { + return ""; + } + } + + } diff --git a/2023Patch/RadicadePatch/VoiceChatBanFlagPatches.cs b/2023Patch/RadicadePatch/VoiceChatBanFlagPatches.cs new file mode 100644 index 0000000..69f46ff --- /dev/null +++ b/2023Patch/RadicadePatch/VoiceChatBanFlagPatches.cs @@ -0,0 +1,66 @@ +using System; +using System.Reflection; +using HarmonyLib; + +internal static class VoiceChatBanFlagPatches +{ + private static bool _armed; + + internal static void Apply(Harmony harmony) + { + if (_armed) return; + _armed = true; + + Patch(harmony, "IsVoiceChatBanned", nameof(BannedPostfix)); + Patch(harmony, "AllowedToVoiceChat", nameof(AllowedPostfix)); + } + + private static void Patch(Harmony harmony, string propertyName, string handler) + { + try + { + + + var getter = AccessTools.Method(typeof(SettingsModel), "get_" + propertyName) + ?? AccessTools.Property(typeof(SettingsModel), propertyName)?.GetGetMethod(true); + if (getter == null) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[VoiceBanFlag] SettingsModel." + + propertyName + " has no getter in this build, so the Audio page keeps its own value."); + return; + } + + harmony.Patch(getter, postfix: new HarmonyMethod(typeof(VoiceChatBanFlagPatches) + .GetMethod(handler, BindingFlags.Static | BindingFlags.NonPublic))); + + } + catch (Exception ex) + { + RadicadeEacBypassPlugin.LogSource.LogWarning("[VoiceBanFlag] could not patch SettingsModel." + + propertyName + ": " + ex.GetType().Name + ": " + ex.Message); + } + } + + private static void BannedPostfix(ref bool __result) + { + try + { + if (!VoiceChatRestrictionPatches.RestrictionInForce) return; + + __result = true; + } + catch { } + } + + private static void AllowedPostfix(ref bool __result) + { + try + { + if (!VoiceChatRestrictionPatches.RestrictionInForce) return; + + __result = false; + } + catch { } + } + +} diff --git a/2023Patch/RadicadePatch/obfuscar.xml b/2023Patch/RadicadePatch/obfuscar.xml new file mode 100644 index 0000000..0c28596 --- /dev/null +++ b/2023Patch/RadicadePatch/obfuscar.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..00c1312 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,4 @@ +ns.radicade.app { + encode gzip + reverse_proxy 127.0.0.1:3030 +} \ No newline at end of file diff --git a/RadNet.sln b/RadNet.sln new file mode 100644 index 0000000..1486ad0 --- /dev/null +++ b/RadNet.sln @@ -0,0 +1,70 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{9DDA5EBF-9DCD-45C3-83ED-2AC383109529}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Tests", "Server.Tests\Server.Tests.csproj", "{C108E570-E61B-49CF-8590-CF8B983ED6B0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2023Patch", "2023Patch", "{05A810AF-0524-D221-1DA2-A901B183F7A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RadicadePatch", "2023Patch\RadicadePatch\RadicadePatch.csproj", "{A6E9037F-1069-44A3-BE64-4D52084AEF0A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Debug|x64.ActiveCfg = Debug|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Debug|x64.Build.0 = Debug|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Debug|x86.ActiveCfg = Debug|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Debug|x86.Build.0 = Debug|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Release|Any CPU.Build.0 = Release|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Release|x64.ActiveCfg = Release|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Release|x64.Build.0 = Release|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Release|x86.ActiveCfg = Release|Any CPU + {9DDA5EBF-9DCD-45C3-83ED-2AC383109529}.Release|x86.Build.0 = Release|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Debug|x64.Build.0 = Debug|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Debug|x86.Build.0 = Debug|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Release|Any CPU.Build.0 = Release|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Release|x64.ActiveCfg = Release|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Release|x64.Build.0 = Release|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Release|x86.ActiveCfg = Release|Any CPU + {C108E570-E61B-49CF-8590-CF8B983ED6B0}.Release|x86.Build.0 = Release|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Debug|x64.Build.0 = Debug|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Debug|x86.Build.0 = Debug|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Release|Any CPU.Build.0 = Release|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Release|x64.ActiveCfg = Release|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Release|x64.Build.0 = Release|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Release|x86.ActiveCfg = Release|Any CPU + {A6E9037F-1069-44A3-BE64-4D52084AEF0A}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A6E9037F-1069-44A3-BE64-4D52084AEF0A} = {05A810AF-0524-D221-1DA2-A901B183F7A6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DCFE80C5-655C-43CB-872D-BE9F89D58EDE} + EndGlobalSection +EndGlobal diff --git a/RoomImports/Showdown/Showdown.room b/RoomImports/Showdown/Showdown.room new file mode 100644 index 0000000..1f49253 Binary files /dev/null and b/RoomImports/Showdown/Showdown.room differ diff --git a/RoomImports/Showdown/Showdown.room.before-run33 b/RoomImports/Showdown/Showdown.room.before-run33 new file mode 100644 index 0000000..da9240d Binary files /dev/null and b/RoomImports/Showdown/Showdown.room.before-run33 differ diff --git a/RoomImports/Showdown/Showdown_0_1.assetbundle b/RoomImports/Showdown/Showdown_0_1.assetbundle new file mode 100644 index 0000000..b87bae0 Binary files /dev/null and b/RoomImports/Showdown/Showdown_0_1.assetbundle differ diff --git a/Server/AccountCreation/AccountCreationData.cs b/Server/AccountCreation/AccountCreationData.cs new file mode 100644 index 0000000..078241b --- /dev/null +++ b/Server/AccountCreation/AccountCreationData.cs @@ -0,0 +1,29 @@ +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.RecNet.Accounts +{ + public class AccountCreationData + { + public Platforms platform { get; set; } + public string platformId { get; set; } = null!; + public string deviceId { get; set; } = null!; + public string? password { get; set; } + + + public DeviceClass deviceClass { get; set; } + + + + public string? username { get; set; } + + public DateTime? birthday { get; set; } + + public DateTime? birthdayUTC { get; set; } + + public string? email { get; set; } + + public string? avatar { get; set; } + + public string? avatarJson { get; set; } + } +} diff --git a/Server/AccountCreation/RegistrationGuard.cs b/Server/AccountCreation/RegistrationGuard.cs new file mode 100644 index 0000000..29ae72e --- /dev/null +++ b/Server/AccountCreation/RegistrationGuard.cs @@ -0,0 +1,131 @@ +using System.Collections.Concurrent; + +namespace RadicadeServer.Utils +{ + public static class RegistrationGuard + { + public static readonly TimeSpan Window = TimeSpan.FromMinutes(10); + + + + + + private static int maxCreationsPerIp = 20; + private static int maxCreationsPerDevice = 10; + private static TimeSpan minimumDeviceInterval = TimeSpan.FromSeconds(10); + + public static int MaxCreationsPerIp => maxCreationsPerIp; + + public static int MaxCreationsPerDevice => maxCreationsPerDevice; + + public static TimeSpan MinimumDeviceInterval => minimumDeviceInterval; + + public static void Configure(int maxCreationsPerIp, int maxCreationsPerDevice, TimeSpan minimumDeviceInterval) + { + RegistrationGuard.maxCreationsPerIp = maxCreationsPerIp; + RegistrationGuard.maxCreationsPerDevice = maxCreationsPerDevice; + RegistrationGuard.minimumDeviceInterval = minimumDeviceInterval; + Reset(); + } + + private sealed class Counter + { + public readonly List Events = []; + public DateTime LastCreation = DateTime.MinValue; + } + + private static readonly ConcurrentDictionary ByIp = new(StringComparer.Ordinal); + private static readonly ConcurrentDictionary ByDevice = new(StringComparer.Ordinal); + private static readonly ConcurrentDictionary IdentityGates = new(StringComparer.Ordinal); + + public static string? Check(string? ip, string? deviceId) + { + var now = DateTime.UtcNow; + + if (!string.IsNullOrWhiteSpace(deviceId)) + { + var device = ByDevice.GetOrAdd(deviceId, _ => new Counter()); + lock (device) + { + device.Events.RemoveAll(evt => now - evt > Window); + if (now - device.LastCreation < MinimumDeviceInterval) + return "An account was just created on this device. Please wait a moment and try again."; + if (device.Events.Count >= MaxCreationsPerDevice) + return "Too many accounts have been created on this device. Please try again later."; + } + } + + if (!string.IsNullOrWhiteSpace(ip)) + { + var source = ByIp.GetOrAdd(ip, _ => new Counter()); + lock (source) + { + source.Events.RemoveAll(evt => now - evt > Window); + if (source.Events.Count >= MaxCreationsPerIp) + return "Too many accounts have been created from this connection. Please try again later."; + } + } + + return null; + } + + public static void Record(string? ip, string? deviceId) + { + var now = DateTime.UtcNow; + + if (!string.IsNullOrWhiteSpace(deviceId)) + { + var device = ByDevice.GetOrAdd(deviceId, _ => new Counter()); + lock (device) + { + device.Events.RemoveAll(evt => now - evt > Window); + device.Events.Add(now); + device.LastCreation = now; + } + } + + if (!string.IsNullOrWhiteSpace(ip)) + { + var source = ByIp.GetOrAdd(ip, _ => new Counter()); + lock (source) + { + source.Events.RemoveAll(evt => now - evt > Window); + source.Events.Add(now); + } + } + } + + public static string IdentityKey(string platform, string? platformId, string? deviceId) => + platform + "|" + (platformId?.Trim() ?? string.Empty) + "|" + (deviceId?.Trim() ?? string.Empty); + + public static Task LockNameAsync(string name) => + LockIdentityAsync("name|" + UsernameRules.ComparisonKey(name)); + + public static async Task LockIdentityAsync(string key) + { + var gate = IdentityGates.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); + await gate.WaitAsync(); + return new Releaser(gate); + } + + private sealed class Releaser(SemaphoreSlim gate) : IDisposable + { + private int released; + + public void Dispose() + { + if (Interlocked.Exchange(ref released, 1) == 0) gate.Release(); + } + } + + public static void Reset() + { + ByIp.Clear(); + ByDevice.Clear(); + IdentityGates.Clear(); + } + + internal static string Describe(string? ip, string? deviceId) => + "ip=" + (ip ?? "") + " device=" + (deviceId ?? ""); + } +} diff --git a/Server/Activities/CharadesV1.cs b/Server/Activities/CharadesV1.cs new file mode 100644 index 0000000..ebe8bbb --- /dev/null +++ b/Server/Activities/CharadesV1.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Activities.Charades.V1 +{ + [Route("api/activities/charades/v1")] + public class CharadesV1(RecDBContext db) : Controller + { + [HttpGet("words/{cardType}")] + public async Task>> GetCardWords(string cardType) + { + switch (cardType) + { + case "Charades": + var charades = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "Charades"); + return Ok(charades?.Value.ParseCardBoxWords()); + case "CharadesAprilFoolsDay": + var charadesAprilFools = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "CharadesAprilFools"); + return Ok(charadesAprilFools?.Value.ParseCardBoxWords()); + case "Icebreakers": + var icebreakers = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "Icebreakers"); + return Ok(icebreakers?.Value.ParseCardBoxWords()); + } + return Ok(); + } + } +} diff --git a/Server/Admin/AdminAuthAPI.cs b/Server/Admin/AdminAuthAPI.cs new file mode 100644 index 0000000..8c429b2 --- /dev/null +++ b/Server/Admin/AdminAuthAPI.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Auth; +using RadicadeServer.DB; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin; + +[Route("admin/auth")] +[EnableCors("AdminPanel")] +public class AdminAuthAPI(RecDBContext db) : Controller +{ + private CookieOptions SessionCookieOptions => new() + { + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Strict, + MaxAge = AdminSession.Lifetime, + Path = "/" + }; + + public record LoginRequest(string? Username, string? Password); + + [HttpPost("login")] + public async Task Login([FromBody] LoginRequest request) + { + var username = request.Username?.Trim() ?? ""; + var ip = Request.GetClientIp(); + + if (AdminLoginThrottle.IsLocked(username, ip, out var remaining)) + return LockoutResponse(remaining); + + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password)) + return Unauthorized(new { error = "invalid_credentials", message = "Invalid username or password." }); + + var user = await db.AdminUsers.FirstOrDefaultAsync(x => x.Username == username); + + if (user == null || !AdminSecurity.VerifyPassword(user.PasswordHash, request.Password)) + { + await Task.Delay(350); + AdminLoginThrottle.RecordFailure(username, ip); + if (AdminLoginThrottle.IsLocked(username, ip, out remaining)) + return LockoutResponse(remaining); + return Unauthorized(new { error = "invalid_credentials", message = "Invalid username or password." }); + } + + if (AdminSecurity.NeedsRehash(user.PasswordHash)) + { + user.PasswordHash = AdminSecurity.HashPassword(request.Password); + await db.SaveChangesAsync(); + } + + AdminLoginThrottle.Clear(username, ip); + await AdminSession.PruneAsync(db); + return await CompleteLoginAsync(user); + } + + private async Task CompleteLoginAsync(AdminUser user) + { + var token = await AdminSession.CreateAsync(db, user.Id); + user.LastLoginAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + + Response.Cookies.Append(AdminSession.CookieName, token, SessionCookieOptions); + return Ok(new { ok = true, user = Identity(user) }); + } + + [HttpGet("session")] + public async Task Session() + { + var user = await RequireSessionAsync(); + if (user == null) return Unauthorized(new { error = "not_authenticated" }); + + var (rotated, didRotate) = AdminSession.TryRotate(db, CurrentToken()); + if (didRotate && !string.IsNullOrEmpty(rotated)) + Response.Cookies.Append(AdminSession.CookieName, rotated, SessionCookieOptions); + + return Ok(new { user = Identity(user) }); + } + + [HttpGet("me")] + public async Task Me() + { + var user = await RequireSessionAsync(); + if (user == null) return Unauthorized(new { error = "not_authenticated" }); + return Ok(new { user = Identity(user) }); + } + + [HttpPost("logout")] + public async Task Logout() + { + await AdminSession.RevokeAsync(db, CurrentToken()); + Response.Cookies.Delete(AdminSession.CookieName, new CookieOptions { Path = "/" }); + return Ok(new { ok = true }); + } + + private string? CurrentToken() => + Request.Cookies.TryGetValue(AdminSession.CookieName, out var token) ? token : null; + + private async Task RequireSessionAsync() + { + var (ok, userId) = await AdminSession.ValidateAsync(db, CurrentToken()); + if (!ok) return null; + + return await db.AdminUsers.FirstOrDefaultAsync(x => x.Id == userId); + } + + private static object Identity(AdminUser user) => new + { + id = user.Id, + username = user.Username + }; + + private static string Display(string ip) => string.IsNullOrWhiteSpace(ip) ? "an unknown address" : ip; + + private IActionResult LockoutResponse(TimeSpan remaining) + { + var seconds = (int)Math.Ceiling(remaining.TotalSeconds); + Response.Headers["Retry-After"] = seconds.ToString(); + + var message = seconds > 60 + ? $"Too many failed sign-in attempts. Try again in {(int)Math.Ceiling(remaining.TotalMinutes)} minutes." + : $"Too many failed sign-in attempts. Try again in {seconds} seconds."; + return StatusCode(429, new { error = "locked_out", message, retry_after_seconds = seconds }); + } +} diff --git a/Server/Admin/AdminBoardAPI.cs b/Server/Admin/AdminBoardAPI.cs new file mode 100644 index 0000000..da9b25b --- /dev/null +++ b/Server/Admin/AdminBoardAPI.cs @@ -0,0 +1,6 @@ +namespace RadicadeServer.Admin +{ + public class AdminBoardAPI + { + } +} diff --git a/Server/Admin/AdminClubAPI.cs b/Server/Admin/AdminClubAPI.cs new file mode 100644 index 0000000..f5c17a3 --- /dev/null +++ b/Server/Admin/AdminClubAPI.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Clubs; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin +{ + [Route("admin/clubs")] + [EnableCors("AdminPanel")] + public class AdminClubAPI(RecDBContext db) : Controller + { + private Task RejectInvalidKey(string key) + { + return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); + } + + [HttpPost("upload/image")] + public async Task UploadImage([FromQuery] string key, [FromQuery] long clubId, [FromForm] IFormFile file) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var clubs = await ClubStore.LoadAsync(db); + var club = clubs.FirstOrDefault(x => x.clubId == clubId); + if (club == null) return NotFound("Club does not exist."); + + if (file == null || file.Length == 0) return BadRequest("No file provided."); + + var allowed = new[] { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" }; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (string.IsNullOrEmpty(ext) || !allowed.Contains(ext)) + return BadRequest($"Unsupported image format '{(string.IsNullOrEmpty(ext) ? "no extension" : ext)}'. Use png/jpg/jpeg/gif/webp."); + + Directory.CreateDirectory(DBConfig.CDNImages); + var name = Guid.NewGuid().ToString() + ext; + var path = Path.Join(DBConfig.CDNImages, name); + + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + club.imageName = name; + + await ClubStore.SaveAsync(db, clubs); + + return Ok($"Uploaded {name}!\nURL: {DBConfig.ServerUrl}/images/img/{name}"); + } + + [HttpGet("set/image")] + public async Task SetImage([FromQuery] string key, [FromQuery] long clubId, [FromQuery] string imgName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var clubs = await ClubStore.LoadAsync(db); + var club = clubs.FirstOrDefault(x => x.clubId == clubId); + if (club == null) return NotFound("Club does not exist."); + + imgName = Path.GetFileName(imgName ?? "").Trim(); + + if (string.IsNullOrEmpty(imgName)) imgName = ClubStore.DefaultImageName; + + club.imageName = imgName; + + await ClubStore.SaveAsync(db, clubs); + + return Ok($"Set to {imgName}!\nURL: {DBConfig.ServerUrl}/images/img/{imgName}"); + } + } +} diff --git a/Server/Admin/AdminConfigAPI.cs b/Server/Admin/AdminConfigAPI.cs new file mode 100644 index 0000000..096ca62 --- /dev/null +++ b/Server/Admin/AdminConfigAPI.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Cors; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin +{ + [Route("admin/config")] + [EnableCors("AdminPanel")] + public class AdminConfigAPI(RecDBContext db) : Controller + { + private Task RejectInvalidKey(string key) + { + return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); + } + + [HttpGet("forcerefresh")] + public async Task ForceRefresh([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + MainConfig? config = await db.Config.FirstOrDefaultAsync(); + if (config == null) return StatusCode(500); + + bool didChanges = false; + + if (config.ExpectingReload) + { + didChanges = true; + config.SetAsActive(); + config.ExpectingReload = false; + } + + if (config.RefreshConfig(true)) + didChanges = true; + + if (didChanges) + await db.SaveChangesAsync(); + + return Ok(config?.ToActualConfig()); + } + + [HttpGet("full")] + public async Task GetFullConfig([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + MainConfig? config = await db.Config.FirstOrDefaultAsync(); + return Ok(config); + } + } +} diff --git a/Server/Admin/AdminModels.cs b/Server/Admin/AdminModels.cs new file mode 100644 index 0000000..783a903 --- /dev/null +++ b/Server/Admin/AdminModels.cs @@ -0,0 +1,67 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.Admin; + + + + + +public class AdminUser +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Required, MaxLength(64)] + public string Username { get; set; } = ""; + + + + + + [Required] + public string PasswordHash { get; set; } = ""; + + + [MaxLength(64)] + public string? TotpSecret { get; set; } + + public bool TotpEnabled { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime? LastLoginAt { get; set; } +} + + + + + + + + +public class AdminSessionRecord +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long AdminUserId { get; set; } + + + [Required, MaxLength(64)] + public string TokenHash { get; set; } = ""; + + + + [MaxLength(64)] + public string? PreviousTokenHash { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime LastSeenAt { get; set; } = DateTime.UtcNow; + + + public DateTime ExpiresAt { get; set; } + + public DateTime? RevokedAt { get; set; } +} diff --git a/Server/Admin/AdminPanelAPI.cs b/Server/Admin/AdminPanelAPI.cs new file mode 100644 index 0000000..0be1190 --- /dev/null +++ b/Server/Admin/AdminPanelAPI.cs @@ -0,0 +1,7032 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Identity; +using System.Globalization; +using RadicadeServer.Auth; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.Radicade.Mail; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Board; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.VersionCheck; +using RadicadeServer.RecNet.DB; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Maintenance; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Relationships; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Store; +using RadicadeServer.RecNet.Storefront; +using RadicadeServer.API.Moderation; +using RadicadeServer.Clubs; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin; + +[Route("admin/panel")] +[EnableCors("AdminPanel")] +public class AdminPanelAPI(RecDBContext db) : Controller +{ + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + private static readonly object StateLock = new(); + + + + private static readonly PasswordHasher PasswordHasher = new(); + + private const long RadicadeTeamAccountId = 1; + private static string StatePath => Path.Join(DBConfig.PersistentLogs, "admin-panel-state.json"); + + private static readonly SemaphoreSlim WeeklySaveGate = new(1); + + + + private static List WeeklyRewards() + + { + + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + + var equipment = JsonSerializer.Deserialize>(DBConfig.DefaultEquipmentItems, options) ?? []; + + var avatars = JsonSerializer.Deserialize>(DBConfig.DefaultAvatarCatalog, options) ?? []; + + return equipment.Where(x => !string.IsNullOrWhiteSpace(x.PrefabName) && !string.IsNullOrWhiteSpace(x.ModificationGuid)) + + .Select(x => new Gift { EquipmentPrefabName = x.PrefabName, EquipmentModificationGuid = x.ModificationGuid, + + FriendlyName = x.FriendlyName, Tooltip = x.Tooltip, Rarity = x.Rarity, Context = GiftContext.All_Weekly_Challenge_Complete }) + + .Concat(avatars.Where(x => !string.IsNullOrWhiteSpace(x.AvatarItemDesc)).Select(x => new Gift { + + AvatarItemDesc = x.AvatarItemDesc, AvatarItemType = (AvatarItemType)x.AvatarItemType, + + FriendlyName = x.FriendlyName, Tooltip = x.Tooltip, Rarity = (Rarity)x.Rarity, Context = GiftContext.All_Weekly_Challenge_Complete })) + + .DistinctBy(WeeklyRewardKey).ToList(); + + } + + + + private static string WeeklyRewardKey(Gift gift) => !string.IsNullOrWhiteSpace(gift.AvatarItemDesc) + + ? "avatar:" + gift.AvatarItemDesc : "equipment:" + gift.EquipmentPrefabName + ":" + gift.EquipmentModificationGuid; + + + + [HttpGet("weekly-challenges/rewards")] + + public async Task GetWeeklyRewards([FromQuery] string key) + + { + + var rejection = await RejectInvalidKey(key); + + if (rejection != null) return rejection; + + return Ok(WeeklyRewards().Select(g => new { key = WeeklyRewardKey(g), + + kind = string.IsNullOrWhiteSpace(g.AvatarItemDesc) ? "equipment" : "avatar", + + name = string.IsNullOrWhiteSpace(g.FriendlyName) ? WeeklyRewardKey(g) : g.FriendlyName, + + detail = string.IsNullOrWhiteSpace(g.AvatarItemDesc) ? g.EquipmentPrefabName + " · " + g.EquipmentModificationGuid : g.AvatarItemDesc })); + + } + + + + [HttpGet("weekly-challenges")] + + public async Task GetWeeklySchedule([FromQuery] string key) + + { + + var rejection = await RejectInvalidKey(key); + + if (rejection != null) return rejection; + + var weeks = new List(); + + var now = DateTime.UtcNow; + + for (var i = 0; i < 4; i++) weeks.Add(await WeeklyChallenges.GetDefinitionAsync(db, now.AddDays(i * 7))); + + return Ok(new { weeks, current = weeks[0], next = weeks[1], required = WeeklyChallenges.Required }); + + } + + + + [HttpPost("weekly-challenges")] + + public async Task SaveWeeklySchedule([FromQuery] string key, [FromBody] WeeklyScheduleRequest? request) + + { + + var rejection = await RejectInvalidKey(key); + + if (rejection != null) return rejection; + + var current = WeeklyChallenges.CreateMap(DateTime.UtcNow); + + if (request == null) return BadRequest("Choose a weekly schedule."); + + var offset = request.ChallengeMapId - current.ChallengeMapId; + + if (offset < 0 || offset > 3) return Conflict("The weekly schedule changed. Reload the page."); + + var map = WeeklyChallenges.CreateMap(current.StartAt.AddDays(offset * 7)); + + if (request.ChallengeIds == null || request.ChallengeIds.Count is < 3 or > 5 + + || request.ChallengeIds.Distinct().Count() != request.ChallengeIds.Count) + + return BadRequest("Choose three to five different challenges."); + + if (string.IsNullOrWhiteSpace(request.Title) || request.Title.Length > 100 || request.FallbackTokens is < 1 or > 10000) + + return BadRequest("Enter a title (up to 100 characters) and 1–10,000 fallback tokens."); + + var reward = WeeklyRewards().FirstOrDefault(x => WeeklyRewardKey(x) == request.RewardKey); + + if (reward == null) return BadRequest("Choose an equipment skin or avatar item from the catalog."); + + var library = LoadState().Challenges; + + foreach (var id in request.ChallengeIds) + + { + + var row = library.FirstOrDefault(x => x["id"]?.ToString() == id.ToString(CultureInfo.InvariantCulture)); + + var config = row?["config"]?.ToString() ?? ""; + + if (row == null || id <= 0 || id > int.MaxValue || string.IsNullOrWhiteSpace(row["name"]?.ToString()) + + || !WeeklyChallenges.IsSupportedDefinition(config)) + + return BadRequest($"Challenge {id} is missing or unsupported. Edit it using a gameplay preset."); + + map.Challenges.Add(new() { ChallengeId = (int)id, Name = row["name"]!.ToString(), + + Description = row["description"]?.ToString() ?? "", Tooltip = row["tooltip"]?.ToString() ?? "", Config = config }); + + } + + map.Enabled = request.Enabled; + + map.ChallengeThemeString = request.Title.Trim(); + + map.Gift = reward.ForClient(); + + map.FallbackTokens = request.FallbackTokens; + + map.FallbackGiftName = $"{request.FallbackTokens} Tokens"; + + map.Revision = Guid.NewGuid().ToString("N"); + + await WeeklySaveGate.WaitAsync(); + + try + + { + + return await db.Database.CreateExecutionStrategy().ExecuteAsync(async () => + + { + + db.ChangeTracker.Clear(); + + await using var tx = db.Database.IsSqlServer() + + ? await db.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable) : null; + + var scheduleKey = WeeklyChallenges.ScheduleKey(map.ChallengeMapId); + + var query = db.Database.IsSqlServer() + + ? db.ServerConfig.FromSqlInterpolated($"SELECT * FROM [ServerConfig] WITH (UPDLOCK, HOLDLOCK) WHERE [Key] = {scheduleKey}") + + : db.ServerConfig.Where(x => x.Key == scheduleKey); + + var record = await query.SingleOrDefaultAsync(); + + var revision = record == null ? "" : JsonSerializer.Deserialize(record.Value)!.Revision; + + if (revision != request.Revision) return Conflict("This weekly was edited elsewhere. Reload before saving."); + + if (record == null) db.ServerConfig.Add(new ServerConfigVal(scheduleKey, JsonSerializer.Serialize(map))); + + else record.Value = JsonSerializer.Serialize(map); + + await db.SaveChangesAsync(); + + if (tx != null) await tx.CommitAsync(); + + return Ok(map); + + }); + + } + + finally { WeeklySaveGate.Release(); } + + } + + + + private static AdminPanelState LoadState() + { + lock (StateLock) + { + if (!System.IO.File.Exists(StatePath)) + return new AdminPanelState(); + + try + { + var json = System.IO.File.ReadAllText(StatePath); + return JsonSerializer.Deserialize(json, JsonOptions) ?? new AdminPanelState(); + } + catch + { + return new AdminPanelState(); + } + } + } + + private static void SaveState(AdminPanelState state) + { + lock (StateLock) + { + Directory.CreateDirectory(DBConfig.PersistentLogs); + System.IO.File.WriteAllText(StatePath, JsonSerializer.Serialize(state, JsonOptions)); + } + } + + + + + + + + + + + + + + public static void AddModerationReport(long reporterId, long reportedId, string reportMsg, string details, ReportCategory category, long roomId, + string? reportType = null, long? targetId = null, string? targetName = null) + { + lock (StateLock) + { + var state = LoadState(); + + long nextId = 1; + foreach (var existing in state.ModerationReports) + { + var rawId = existing["id"]?.ToString(); + if (rawId != null && long.TryParse(rawId, out var parsedId) && parsedId >= nextId) + nextId = parsedId + 1; + } + + var report = new JsonObject + { + ["id"] = nextId, + ["report_type"] = reportType ?? "Moderator", + ["reporter_id"] = reporterId, + ["reported_id"] = reportedId, + ["reason"] = reportMsg, + ["details"] = details, + ["category"] = (int)category, + ["room_id"] = roomId, + ["status"] = "Pending", + ["resolution_notes"] = "", + ["created_at"] = DateTime.UtcNow.ToString("O") + }; + + if (targetId.HasValue) + report["target_id"] = targetId.Value; + if (!string.IsNullOrWhiteSpace(targetName)) + report["target_name"] = targetName; + + state.ModerationReports.Add(report); + + SaveState(state); + } + } + + private Task RejectInvalidKey(string key) + { + return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); + } + + /// The first of these that carries something: an item's own name beats nothing, and a + /// catalogue's name beats a raw descriptor used as one. + private static string NonEmpty(params string?[] values) + { + foreach (var value in values) + { + if (!string.IsNullOrWhiteSpace(value)) return value.Trim(); + } + return ""; + } + + private static string RarityLabel(int rarity) => ((Rarity)rarity).ToString(); + + private static string ImageUrl(string? name) + { + if (string.IsNullOrWhiteSpace(name) || + name.Equals("default", StringComparison.OrdinalIgnoreCase) || + name.Equals("default.png", StringComparison.OrdinalIgnoreCase) || + name.Equals(DBConfig.DefaultProfileImage, StringComparison.OrdinalIgnoreCase) || + name.Equals(DBConfig.DefaultRoomImage, StringComparison.OrdinalIgnoreCase)) + return $"{DBConfig.ServerUrl}/images/default"; + return $"{DBConfig.ServerUrl}/images/img/{name}"; + } + + + + + + + private async Task> GetAdminUsers(string? eqCol, string? eqVal) + { + var query = db.AdminUsers.AsNoTracking(); + if (eqCol == "user_id" && !string.IsNullOrWhiteSpace(eqVal)) + query = query.Where(x => x.Username == eqVal); + + var rows = await query.ToListAsync(); + return rows.Select(u => new + { + id = u.Id, + user_id = u.Username, + username = u.Username, + created_at = u.CreatedAt, + last_login_at = u.LastLoginAt + }).Cast().ToList(); + } + + [HttpGet("session")] + public async Task Session([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + + + if (Request.Cookies.TryGetValue(AdminSession.CookieName, out var token)) + { + var (ok, userId) = await AdminSession.ValidateAsync(db, token); + if (ok) + { + var user = await db.AdminUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId); + if (user != null) + return Ok(new { user = new { id = user.Id, username = user.Username } }); + } + } + + return Ok(new { user = (object?)null }); + } + + + + + + + + [HttpGet("appeals")] + public async Task ListAppeals([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var appeals = await db.Moderation + .AsNoTracking() + .Where(x => x.AppealCode != null && x.AppealText != null) + .OrderByDescending(x => x.AppealSubmittedAt) + .Select(x => new + { + id = x.Id, + player_id = x.PlayerId, + player_name = db.Profiles.Where(p => p.accountId == x.PlayerId).Select(p => p.username).FirstOrDefault() ?? "unknown", + code = x.AppealCode, + reason = x.Message, + category = x.Category.ToString(), + permanent = x.ExpireDuration >= DateTime.MaxValue.AddYears(-10), + expires_at = x.ExpireDuration, + appeal_text = x.AppealText, + appeal_contact = x.AppealContact, + + email = db.Players.Where(pl => pl.AccountId == x.PlayerId).Select(pl => pl.Email).FirstOrDefault() ?? "", + submitted_at = x.AppealSubmittedAt, + resolution = x.AppealResolution, + resolved_at = x.AppealResolvedAt, + ban_active = x.IsBan && x.ExpireDuration > DateTime.UtcNow + }) + .ToListAsync(); + + return Ok(appeals); + } + + + + + + + + + + [HttpPost("appeals/{appealId:long}/accept")] + public async Task AcceptAppeal(long appealId, [FromQuery] string key, [FromQuery(Name = "send_email")] string? sendEmail = null, [FromQuery(Name = "custom_email")] string? customEmail = null) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + var wantsEmail = sendEmail == "1" || string.Equals(sendEmail, "true", StringComparison.OrdinalIgnoreCase); + + var ban = await db.Moderation.FirstOrDefaultAsync(x => x.Id == appealId && x.AppealCode != null && x.AppealText != null); + if (ban == null) return NotFound("Appeal not found"); + + ban.IsBan = false; + ban.AppealResolution = "Accepted"; + ban.AppealResolvedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + + + + + await NotifyLoginRefresh(ban.PlayerId); + + if (wantsEmail) + await SendAppealEmailAsync(ban, accepted: true, customText: customEmail); + + return Ok(new OkResponse { Success = true, Message = "Appeal accepted - ban lifted and player notified." }); + } + + + + + + + + [HttpPost("appeals/{appealId:long}/reject")] + public async Task RejectAppeal(long appealId, [FromQuery] string key, [FromQuery(Name = "send_email")] string? sendEmail = null, [FromQuery(Name = "custom_email")] string? customEmail = null) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + var wantsEmail = sendEmail == "1" || string.Equals(sendEmail, "true", StringComparison.OrdinalIgnoreCase); + + var ban = await db.Moderation.FirstOrDefaultAsync(x => x.Id == appealId && x.AppealCode != null && x.AppealText != null); + if (ban == null) return NotFound("Appeal not found"); + + ban.AppealResolution = "Rejected"; + ban.AppealResolvedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + + if (wantsEmail) + await SendAppealEmailAsync(ban, accepted: false, customText: customEmail); + + return Ok(new OkResponse { Success = true, Message = "Appeal rejected - ban remains active." }); + } + + + + + + + + + [HttpPost("appeals/{appealId:long}/reduce")] + public async Task ReduceAppealBanTime(long appealId, [FromQuery] string key, + [FromQuery(Name = "send_email")] string? sendEmail = null, [FromQuery(Name = "custom_email")] string? customEmail = null, [FromQuery] int minutes = 0) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + if (minutes <= 0) + return BadRequest(new OkResponse { Success = false, Message = "Enter how many minutes to reduce the ban by." }); + + var ban = await db.Moderation.FirstOrDefaultAsync(x => x.Id == appealId && x.AppealCode != null && x.AppealText != null); + if (ban == null) return NotFound("Appeal not found"); + if (!ban.IsBan) return BadRequest(new OkResponse { Success = false, Message = "This ban is no longer active." }); + + var now = DateTime.UtcNow; + var reduction = TimeSpan.FromMinutes(minutes); + if (ban.ExpireDuration >= DateTime.MaxValue.AddYears(-10)) + { + + ban.ExpireDuration = now.Add(reduction); + } + else + { + var target = ban.ExpireDuration.Subtract(reduction); + ban.ExpireDuration = target < now.AddMinutes(1) ? now.AddMinutes(1) : target; + } + + + ban.AppealResolution = "Reduced"; + ban.AppealResolvedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + + await NotifyLoginRefresh(ban.PlayerId); + + var wantsEmail = sendEmail == "1" || string.Equals(sendEmail, "true", StringComparison.OrdinalIgnoreCase); + if (wantsEmail) + await SendAppealTimeChangeEmailAsync(ban, reduced: true, customText: customEmail); + + return Ok(new OkResponse { Success = true, Message = "Ban time reduced - player notified." }); + } + + + + + + + + [HttpPost("appeals/{appealId:long}/extend")] + public async Task ExtendAppealBanTime(long appealId, [FromQuery] string key, + [FromQuery(Name = "send_email")] string? sendEmail = null, [FromQuery(Name = "custom_email")] string? customEmail = null, [FromQuery] int minutes = 0) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + if (minutes <= 0) + return BadRequest(new OkResponse { Success = false, Message = "Enter how many minutes to extend the ban by." }); + + var ban = await db.Moderation.FirstOrDefaultAsync(x => x.Id == appealId && x.AppealCode != null && x.AppealText != null); + if (ban == null) return NotFound("Appeal not found"); + if (!ban.IsBan) return BadRequest(new OkResponse { Success = false, Message = "This ban is no longer active." }); + if (ban.ExpireDuration >= DateTime.MaxValue.AddYears(-10)) + return BadRequest(new OkResponse { Success = false, Message = "This ban is permanent and cannot be extended." }); + + ban.ExpireDuration = ban.ExpireDuration.AddMinutes(minutes); + + + ban.AppealResolution = "Extended"; + ban.AppealResolvedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + + await NotifyLoginRefresh(ban.PlayerId); + + var wantsEmail = sendEmail == "1" || string.Equals(sendEmail, "true", StringComparison.OrdinalIgnoreCase); + if (wantsEmail) + await SendAppealTimeChangeEmailAsync(ban, reduced: false, customText: customEmail); + + return Ok(new OkResponse { Success = true, Message = "Ban time extended - player notified." }); + } + + + + + + + private async Task NotifyLoginRefresh(long playerId) + { + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile != null) + { + await Notify.SendNotify(playerId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile }); + } + } + + + + + + + private async Task SendAppealEmailAsync(RecNet.Moderation.ModerationDB ban, bool accepted, string? customText = null) + { + if (string.IsNullOrWhiteSpace(DBConfig.MailApiKey)) + { + return; + } + + var player = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == ban.PlayerId); + var email = player?.Email; + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) return; + + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == ban.PlayerId); + var name = string.IsNullOrWhiteSpace(profile?.username) ? "there" : profile.username; + var reason = string.IsNullOrWhiteSpace(ban.Message) ? "the listed reason" : ban.Message; + + if (!string.IsNullOrWhiteSpace(customText)) + { + var subject = accepted ? "Your Radicade ban appeal was approved" : "Your Radicade ban appeal was rejected"; + await RadicadeMail.SendAsync(email, subject, BuildCustomEmailHtml(subject, customText)); + return; + } + + if (accepted) + { + var html = + $"
" + + $"\"Radicade\"" + + $"

Your Radicade ban appeal was approved

" + + $"

Hi {HtmlEncode(name)},

" + + $"

Your appeal against the ban for {HtmlEncode(reason)} has been accepted. The ban has been lifted and you can log back into Radicade.

" + + $"

Thanks for reaching out - enjoy the game!

" + + $"

- Radicade Moderation

"; + + await RadicadeMail.SendAsync(email, "Your Radicade ban appeal was approved", html); + } + else + { + var html = + $"
" + + $"\"Radicade\"" + + $"

Your Radicade ban appeal was rejected

" + + $"

Hi {HtmlEncode(name)},

" + + $"

We reviewed your appeal against the ban for {HtmlEncode(reason)} and it was rejected. The ban remains active.

" + + $"

If you believe this decision was a mistake, you may submit another appeal once the cooldown period has passed.

" + + "

Take this time to review our Code Of Conduct:

" + + "

Code Of Conduct

" + + $"

- Radicade Moderation

"; + + await RadicadeMail.SendAsync(email, "Your Radicade ban appeal was rejected", html); + } + } + + + + + + + + private async Task SendAppealTimeChangeEmailAsync(RecNet.Moderation.ModerationDB ban, bool reduced, string? customText = null) + { + if (string.IsNullOrWhiteSpace(DBConfig.MailApiKey)) + { + return; + } + + var player = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == ban.PlayerId); + var email = player?.Email; + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) return; + + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == ban.PlayerId); + var name = string.IsNullOrWhiteSpace(profile?.username) ? "there" : profile.username; + var reason = string.IsNullOrWhiteSpace(ban.Message) ? "the listed reason" : ban.Message; + + var action = reduced ? "reduced" : "extended"; + + if (!string.IsNullOrWhiteSpace(customText)) + { + var subject = $"Your Radicade ban time was {action}"; + await RadicadeMail.SendAsync(email, subject, BuildCustomEmailHtml(subject, customText)); + return; + } + var permanent = ban.ExpireDuration >= DateTime.MaxValue.AddYears(-10); + var when = permanent + ? "permanent" + : DateTime.SpecifyKind(ban.ExpireDuration, DateTimeKind.Utc).ToString("MMM d, yyyy 'at' h:mm tt 'UTC'"); + + + var cocCta = reduced + ? "" + : "

Take this time to review our Code Of Conduct:

" + + "

Code Of Conduct

"; + + var html = + $"
" + + $"\"Radicade\"" + + $"

Your Radicade ban time was {action}

" + + $"

Hi {HtmlEncode(name)},

" + + $"

As part of your appeal, the ban for {HtmlEncode(reason)} has been {action}. It now ends {when}.

" + + cocCta + + $"

- Radicade Moderation

"; + + await RadicadeMail.SendAsync(email, $"Your Radicade ban time was {action}", html); + } + + private static string BuildCustomEmailHtml(string title, string customText) + { + var body = HtmlEncode(customText).Replace("\r", "").Replace("\n", "
"); + return + $"
" + + $"\"Radicade\"" + + $"

{HtmlEncode(title)}

" + + $"

{body}

" + + $"

- Radicade Moderation

"; + } + + private static string HtmlEncode(string value) + { + return System.Net.WebUtility.HtmlEncode(value); + } + + + + + + + private static readonly Dictionary RoomRoleNames = new(StringComparer.OrdinalIgnoreCase) + { + ["none"] = RoleType.None, + ["banned"] = RoleType.Banned, + ["host"] = RoleType.Host, + ["moderator"] = RoleType.Moderator, + ["co-owner"] = RoleType.CoOwner, + ["coowner"] = RoleType.CoOwner, + ["temporarycoowner"] = RoleType.TemporaryCoOwner, + ["creator"] = RoleType.Creator + }; + + private static string RoomRoleKey(RoleType role) => role switch + { + RoleType.Creator => "creator", + RoleType.CoOwner => "co-owner", + RoleType.TemporaryCoOwner => "co-owner", + RoleType.Moderator => "moderator", + RoleType.Host => "host", + _ => "none" + }; + + private static bool TryParseRoomRole(string? raw, out RoleType role) + { + role = RoleType.None; + if (string.IsNullOrWhiteSpace(raw)) return false; + if (RoomRoleNames.TryGetValue(raw.Trim(), out role)) return true; + if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric) && Enum.IsDefined(typeof(RoleType), numeric)) + { + role = (RoleType)numeric; + return true; + } + return Enum.TryParse(raw, true, out role); + } + + private static bool TryParseRoomAccessibility(string? raw, out Accessibility value) + { + value = Accessibility.Public; + if (string.IsNullOrWhiteSpace(raw)) return false; + if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric)) + { + if (!Enum.IsDefined(typeof(Accessibility), numeric)) return false; + value = (Accessibility)numeric; + return true; + } + return Enum.TryParse(raw, true, out value) && Enum.IsDefined(typeof(Accessibility), value); + } + + private static Accessibility ParseRoomAccessibility(string? raw) + => TryParseRoomAccessibility(raw, out var parsed) ? parsed : Accessibility.Public; + + [HttpGet("rooms/{roomId:long}")] + public async Task RoomPanelDetails(long roomId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + var stats = await db.RoomStats.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); + + var roleRows = await db.RoomRoles.AsNoTracking().Where(x => x.RoomId == roomId).ToListAsync(); + var roleAccountIds = roleRows.Select(r => r.AccountId).Append(room.CreatorAccountId).Distinct().ToList(); + var roleProfiles = await db.Profiles.AsNoTracking().Where(x => roleAccountIds.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId); + + object RoleRow(long accountId, RoleType role) + { + roleProfiles.TryGetValue(accountId, out var member); + var handle = member?.username ?? $"account_{accountId}"; + return new + { + account_id = accountId, + username = handle, + display_name = string.IsNullOrWhiteSpace(member?.displayName) ? handle : member!.displayName, + profile_image = ImageUrl(member?.profileImage), + role = RoomRoleKey(role), + role_key = role.ToString(), + role_value = (int)role, + is_creator = accountId == room.CreatorAccountId + }; + } + + var members = new List(); + var seenAccounts = new HashSet(); + + members.Add(RoleRow(room.CreatorAccountId, RoleType.Creator)); + seenAccounts.Add(room.CreatorAccountId); + + foreach (var row in roleRows.OrderBy(r => (int)r.Role).ThenBy(r => r.AccountId)) + { + if (!seenAccounts.Add(row.AccountId)) continue; + members.Add(RoleRow(row.AccountId, row.Role)); + } + + var subRows = await db.SubRooms.AsNoTracking().Where(x => x.RoomId == roomId).OrderBy(x => x.SubRoomId).ToListAsync(); + var subrooms = subRows.Select((sub, index) => (object)new + { + id = sub.SubRoomId, + name = sub.Name, + list = index + 1, + maxPlayers = sub.MaxPlayers, + max_players = sub.MaxPlayers, + accessibility = sub.Accessibility.ToString().ToLowerInvariant(), + sandbox = sub.IsSandbox, + save = sub.DataBlob ?? "", + unity_scene_id = sub.UnitySceneId, + created_at = DateTime.SpecifyKind(sub.CreatedAt, DateTimeKind.Utc) + }).ToList(); + + var saves = new List(); + try + { + if (Directory.Exists(DBConfig.CDNStorage)) + { + saves = Directory.GetFiles(DBConfig.CDNStorage, "*.room") + .OrderByDescending(f => System.IO.File.GetLastWriteTimeUtc(f)) + .Select(f => (object)new { id = Path.GetFileName(f), label = Path.GetFileName(f) }) + .ToList(); + } + } + catch + { + saves = new List(); + } + + return Ok(new + { + room = new + { + id = room.RoomId, + name = room.Name, + description = room.Description, + image_name = room.ImageName, + custom_warning = room.CustomWarning, + creator_account_id = room.CreatorAccountId, + publish_state = room.State.ToString(), + accessibility = room.Accessibility.ToString(), + tags = room.Tags, + is_dorm = room.IsDorm, + is_rro = room.IsRRO, + cloning_allowed = room.CloningAllowed, + disable_mic_auto_mute = room.DisableMicAutoMute, + disable_room_comments = room.DisableRoomComments, + encrypt_voice_chat = room.EncryptVoiceChat, + supports_screens = room.SupportsScreens, + supports_walk_vr = room.SupportsWalkVR, + supports_teleport_vr = room.SupportsTeleportVR, + supports_quest2 = room.SupportsQuest2, + supports_mobile = room.SupportsMobile, + supports_juniors = room.SupportsJuniors, + min_level = room.MinLevel, + max_players = room.MaxPlayers, + max_player_calculation_mode = (int)room.MaxPlayerCalculationMode, + created_at = DateTime.SpecifyKind(room.CreatedAt, DateTimeKind.Utc), + published_at = room.PublishedAt == default ? (DateTime?)null : DateTime.SpecifyKind(room.PublishedAt, DateTimeKind.Utc), + featured = SafeRoomTags(room.Tags).Any(t => string.Equals(t, "featured", StringComparison.OrdinalIgnoreCase)), + cheer_count = stats?.CheerCount ?? 0, + favorite_count = stats?.FavoriteCount ?? 0, + visitor_count = stats?.VisitorCount ?? 0, + visit_count = stats?.VisitCount ?? 0, + join_gift_enabled = room.JoinGiftEnabled, + join_gift_once_per_player = room.JoinGiftOncePerPlayer, + join_gift_version = room.JoinGiftVersion, + join_gift_title = room.JoinGiftTitle, + join_gift_message = room.JoinGiftMessage, + join_gift_avatar_item = room.JoinGiftAvatarItem, + join_gift_equipment_item = room.JoinGiftEquipmentItem, + join_gift_consumable_item = room.JoinGiftConsumableItem, + join_gift_currency_amount = room.JoinGiftCurrencyAmount, + join_gift_currency_type = room.JoinGiftCurrencyType.ToString(), + join_gift_xp = room.JoinGiftXp, + join_gift_context = room.JoinGiftContext.ToString(), + join_gift_rarity = room.JoinGiftRarity.ToString(), + join_gift_avatar_item_type = room.JoinGiftAvatarItemType.ToString(), + join_gift_equipment_modification_guid = room.JoinGiftEquipmentModificationGuid, + // Digits in a string: an account id is eighteen digits and a JSON number would + // arrive in the panel rounded, so a saved id could name the wrong account. + join_gift_from_player_id = Math.Max(0, room.JoinGiftFromPlayerId).ToString(CultureInfo.InvariantCulture), + join_gifts = JoinGiftRows(room, await RoomJoinGiftUtils.ConfiguredGiftsAsync(db, room.RoomId)) + }, + roles = members, + subrooms, + saves + }); + } + + [HttpPost("rooms/{roomId:long}/name")] + public async Task RenameRoom(long roomId, [FromQuery] string key, [FromQuery] string? name) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var next = (name ?? "").Trim(); + if (string.IsNullOrWhiteSpace(next)) return BadRequest("A room has to have a name."); + if (next.Length > 64) return BadRequest("Room names are limited to 64 characters."); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + var previous = room.Name; + room.Name = next; + await db.SaveChangesAsync(); + + AdminActionDiscord.LogAction("Rename Room", await GetPanelAdminUsernameAsync(), $"Renamed room {roomId} from \"{previous}\" to \"{next}\"."); + + return Ok(new { success = true, room_id = roomId, name = room.Name }); + } + + [HttpPost("rooms/{roomId:long}/roles")] + public async Task SetRoomRole(long roomId, [FromQuery] string key, [FromQuery] long accountId, [FromQuery] string? role) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + if (!TryParseRoomRole(role, out var nextRole)) return BadRequest("Unknown room role."); + if (accountId == room.CreatorAccountId && nextRole != RoleType.Creator) + return BadRequest("The room's creator keeps the Creator role."); + + var existing = await db.RoomRoles.FirstOrDefaultAsync(x => x.RoomId == roomId && x.AccountId == accountId); + if (existing == null) + { + db.RoomRoles.Add(new RoomRole + { + RoomId = roomId, + AccountId = accountId, + Role = nextRole, + InvitedRole = RoleType.None + }); + } + else + { + if (existing.Role == nextRole) return Ok(new { success = true, room_id = roomId, account_id = accountId, role = RoomRoleKey(nextRole) }); + existing.Role = nextRole; + } + + await db.SaveChangesAsync(); + + try + { + var updatedRoom = await RoomUtils.GetRoom(roomId); + if (updatedRoom != null) + await Notify.SendNotify(accountId, new Notification { Id = "RoomUpdate", Msg = updatedRoom }); + } + catch (Exception) { } + + AdminActionDiscord.LogAction("Room Role", await GetPanelAdminUsernameAsync(), + $"Set account {accountId} to {nextRole} on room {roomId} ({room.Name})."); + + return Ok(new { success = true, room_id = roomId, account_id = accountId, role = RoomRoleKey(nextRole) }); + } + + [HttpPost("rooms/{roomId:long}/subrooms/add")] + public async Task AddSubroom(long roomId, [FromQuery] string key, [FromQuery] string? name, + [FromQuery] int maxPlayers = 0, [FromQuery] string? accessibility = null, [FromQuery] string? sandbox = null, [FromQuery] string? saveId = null) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + var subName = (name ?? "").Trim(); + if (string.IsNullOrWhiteSpace(subName)) return BadRequest("A new subroom needs a name."); + + var sub = new SubRoom + { + RoomId = roomId, + Name = subName, + MaxPlayers = maxPlayers > 0 ? maxPlayers : 20, + Accessibility = ParseRoomAccessibility(accessibility), + IsSandbox = string.Equals(sandbox, "true", StringComparison.OrdinalIgnoreCase) || sandbox == "1", + DataBlob = saveId ?? "", + SavedByAccountId = room.CreatorAccountId, + CreatedAt = DateTime.UtcNow + }; + + db.SubRooms.Add(sub); + await db.SaveChangesAsync(); + + var index = await db.SubRooms.CountAsync(x => x.RoomId == roomId && x.SubRoomId <= sub.SubRoomId); + + AdminActionDiscord.LogAction("Add Subroom", await GetPanelAdminUsernameAsync(), + $"Added subroom \"{sub.Name}\" to room {roomId} ({room.Name})."); + + return Ok(new { success = true, subroomId = sub.SubRoomId, subroomList = index, maxPlayers = sub.MaxPlayers }); + } + + [HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/name")] + public async Task RenameSubroom(long roomId, long subRoomId, [FromQuery] string key, [FromQuery] string? name) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var next = (name ?? "").Trim(); + if (string.IsNullOrWhiteSpace(next)) return BadRequest("A subroom has to have a name."); + + var sub = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId && x.RoomId == roomId); + if (sub == null) return NotFound("Subroom does not exist."); + + sub.Name = next; + await db.SaveChangesAsync(); + + AdminActionDiscord.LogAction("Rename Subroom", await GetPanelAdminUsernameAsync(), $"Renamed subroom {subRoomId} on room {roomId} to \"{next}\"."); + + return Ok(new { success = true, subroomId = sub.SubRoomId, name = sub.Name }); + } + + [HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/delete")] + public async Task DeleteSubroom(long roomId, long subRoomId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var sub = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId && x.RoomId == roomId); + if (sub == null) return NotFound("Subroom does not exist."); + + var permissions = await db.SubRoomPermissions.Where(x => x.subRoomId == subRoomId).ToListAsync(); + if (permissions.Count > 0) db.SubRoomPermissions.RemoveRange(permissions); + + db.SubRooms.Remove(sub); + await db.SaveChangesAsync(); + + AdminActionDiscord.LogAction("Delete Subroom", await GetPanelAdminUsernameAsync(), $"Deleted subroom \"{sub.Name}\" ({subRoomId}) from room {roomId}."); + + return Ok(new { success = true, subroomId = subRoomId }); + } + + [HttpPost("rooms/{roomId:long}/saves")] + public async Task UploadRoomSave(long roomId, [FromQuery] string key, [FromForm] List file) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + var chosen = file?.FirstOrDefault(f => f.Length > 0 && Path.GetExtension(f.FileName).Equals(".room", StringComparison.OrdinalIgnoreCase)) + ?? file?.FirstOrDefault(f => f.Length > 0); + if (chosen == null) return BadRequest("No .room save was uploaded."); + + Directory.CreateDirectory(DBConfig.CDNStorage); + var stored = Guid.NewGuid().ToString("N") + Path.GetExtension(chosen.FileName).ToLowerInvariant(); + using (var stream = new FileStream(Path.Join(DBConfig.CDNStorage, stored), FileMode.Create)) + { + await chosen.CopyToAsync(stream); + } + + AdminActionDiscord.LogAction("Upload Room Save", await GetPanelAdminUsernameAsync(), + $"Uploaded save \"{chosen.FileName}\" ({stored}) for room {roomId} ({room.Name})."); + + return Ok(new { success = true, saveId = stored, fileName = chosen.FileName }); + } + + [HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/save")] + public async Task SetSubroomSave(long roomId, long subRoomId, [FromQuery] string key, + [FromQuery] string? saveId, [FromForm] List? file) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var sub = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId && x.RoomId == roomId); + if (sub == null) return NotFound("Subroom does not exist."); + + var chosen = file?.FirstOrDefault(f => f.Length > 0 && Path.GetExtension(f.FileName).Equals(".room", StringComparison.OrdinalIgnoreCase)) + ?? file?.FirstOrDefault(f => f.Length > 0); + + string stored; + if (chosen != null) + { + Directory.CreateDirectory(DBConfig.CDNStorage); + stored = Guid.NewGuid().ToString("N") + Path.GetExtension(chosen.FileName).ToLowerInvariant(); + using var stream = new FileStream(Path.Join(DBConfig.CDNStorage, stored), FileMode.Create); + await chosen.CopyToAsync(stream); + } + else + { + stored = (saveId ?? "").Trim(); + stored = Path.GetFileName(stored); + if (!string.IsNullOrEmpty(stored) && !System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, stored))) + return BadRequest($"No uploaded save named '{stored}'."); + } + + sub.DataBlob = stored; + await db.SaveChangesAsync(); + + AdminActionDiscord.LogAction("Set Subroom Save", await GetPanelAdminUsernameAsync(), + $"Pointed subroom \"{sub.Name}\" ({subRoomId}) on room {roomId} at save \"{(string.IsNullOrEmpty(stored) ? "none" : stored)}\"."); + + return Ok(new { success = true, subroomId = sub.SubRoomId, saveId = stored }); + } + + [HttpPost("players/{playerId:long}/{kind}")] + public Task PlayerPanelAction(long playerId, string kind, [FromQuery] string key) + => ApplyPlayerPanelAction(playerId, kind, key); + + [HttpPost("players/{playerId:long}/{kind}/{sub}")] + public Task PlayerPanelSubAction(long playerId, string kind, string sub, [FromQuery] string key) + => ApplyPlayerPanelAction(playerId, $"{kind}/{sub}", key); + + /// Take one saved photo off an account's gallery: the row that indexes it, the cheers + /// that point at it, and the file itself. A player can do this to their own gallery in + /// game (ImagesAPI's deletesaved), so the panel does it the same way rather than + /// leaving the file behind. + [HttpDelete("players/{playerId:long}/photos/{imageId:long}")] + public async Task DeletePlayerPhoto(long playerId, long imageId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var image = await db.Images.FirstOrDefaultAsync(x => x.Id == imageId && x.OwnerId == playerId); + if (image == null) return NotFound("That photo is not on this account's gallery."); + + var cheers = db.ImageInteractions.Where(x => x.SavedImageId == image.Id); + db.ImageInteractions.RemoveRange(cheers); + db.Images.Remove(image); + await db.SaveChangesAsync(); + + var fullPath = Path.Join(DBConfig.CDNImages, image.Name); + if (System.IO.File.Exists(fullPath)) + System.IO.File.Delete(fullPath); + + AdminActionDiscord.LogAction("Delete Photo", await GetPanelAdminUsernameAsync(), + $"Deleted photo \"{image.Name}\" ({imageId}) from player {playerId}'s gallery."); + return Ok(new { success = true, player_id = playerId, image_id = imageId, name = image.Name }); + } + + /// Take one invention off the account that created it, along with the versions it was + /// saved in and every player's personal details about it. Requests and other accounts' + /// downloads of it stop resolving on the next read. + [HttpDelete("players/{playerId:long}/inventions/{inventionId:long}")] + public async Task DeletePlayerInvention(long playerId, long inventionId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.InventionId == inventionId && x.CreatorPlayerId == playerId); + if (invention == null) return NotFound("That invention does not belong to this account."); + + var versions = await db.InventionVersions.Where(x => x.InventionId == inventionId).ToListAsync(); + var details = await db.InventionPersonalDetails.Where(x => x.InventionId == inventionId).ToListAsync(); + db.InventionVersions.RemoveRange(versions); + db.InventionPersonalDetails.RemoveRange(details); + db.Inventions.Remove(invention); + await db.SaveChangesAsync(); + + AdminActionDiscord.LogAction("Delete Invention", await GetPanelAdminUsernameAsync(), + $"Deleted invention \"{invention.Name}\" ({inventionId}) created by player {playerId}, with {versions.Count} saved version(s)."); + return Ok(new { success = true, player_id = playerId, invention_id = inventionId, name = invention.Name }); + } + + private async Task ApplyPlayerPanelAction(long playerId, string kind, string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound("Player does not exist."); + var display = string.IsNullOrWhiteSpace(profile.displayName) ? profile.username : profile.displayName; + + string? Q(string name) + { + var raw = Request.Query[name].ToString(); + return string.IsNullOrEmpty(raw) ? null : raw; + } + bool QBool(string name, bool fallback = false) + { + var raw = Q(name); + if (raw == null) return fallback; + return raw.Equals("true", StringComparison.OrdinalIgnoreCase) || raw == "1"; + } + int QInt(string name, int fallback = 0) => int.TryParse(Q(name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback; + + var payload = new Dictionary(); + var audit = ""; + + switch (kind) + { + case "ban": + { + var reason = (Q("reason") ?? "").Trim(); + var seconds = QInt("durationSeconds", -1); + payload["banned"] = true; + payload["ban_reason"] = reason; + payload["ban_method"] = Q("banMethod") ?? "Admin Panel"; + var moderatorNote = (Q("moderatorNote") ?? "").Trim(); + if (!string.IsNullOrWhiteSpace(moderatorNote)) payload["moderator_note"] = moderatorNote; + payload["ban_until"] = seconds <= 0 + ? "permanent" + : DateTime.UtcNow.AddSeconds(seconds).ToString("O", CultureInfo.InvariantCulture); + audit = seconds <= 0 + ? $"Banned player {playerId} ({display}) permanently - {reason}" + : $"Banned player {playerId} ({display}) for {seconds} second(s) - {reason}"; + break; + } + case "unban": + payload["banned"] = false; + audit = $"Lifted player {playerId} ({display})'s ban"; + break; + case "coach": + { + var message = (Q("message") ?? "").Trim(); + if (string.IsNullOrWhiteSpace(message)) return BadRequest("A coach message cannot be empty."); + + await Notify.SendMessage(playerId, new Message + { + Type = MessageType.CoachMessage, + Data = message, + FromPlayerId = 1 + }); + + AdminActionDiscord.LogAction("Coach Message", await GetPanelAdminUsernameAsync(), $"Sent a coach message to player {playerId} ({display}): {message}"); + return Ok(new { success = true, player_id = playerId, message }); + } + case "roles/add": + { + var added = (Q("roles") ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (added.Length == 0) return BadRequest("Pick at least one role to add."); + + var merged = new List(SplitRoles(profile.role)); + foreach (var role in added) + { + if (!merged.Contains(role, StringComparer.OrdinalIgnoreCase)) merged.Add(role); + } + payload["roles"] = string.Join(",", merged); + audit = $"Added role(s) {string.Join(",", added)} to player {playerId} ({display})"; + break; + } + case "roles/clear": + payload["roles"] = ""; + audit = $"Cleared player {playerId} ({display})'s in-game roles"; + break; + case "lockname": + payload["name_locked"] = QBool("locked"); + audit = $"{(QBool("locked") ? "Locked" : "Unlocked")} player {playerId} ({display})'s name"; + break; + case "lockbio": + payload["bio_locked"] = QBool("locked"); + audit = $"{(QBool("locked") ? "Locked" : "Unlocked")} player {playerId} ({display})'s bio"; + break; + case "junior": + payload["is_junior"] = QBool("junior"); + audit = $"Marked player {playerId} ({display}) as {(QBool("junior") ? "a Junior account" : "no longer Junior")}"; + break; + case "resetpassword": + { + var password = Q("password") ?? ""; + if (password.Length < 8) return BadRequest("A reset password needs at least 8 characters."); + payload["password"] = password; + audit = $"Reset player {playerId} ({display})'s password"; + break; + } + case "username": + { + var username = (Q("username") ?? "").Trim(); + if (string.IsNullOrWhiteSpace(username)) return BadRequest("Enter the new username."); + if (username.Length < 3 || username.Length > 32) return BadRequest("Usernames are 3-32 characters."); + if (await db.Profiles.AnyAsync(x => x.accountId != playerId && x.username == username)) + return BadRequest($"The username '{username}' is already taken."); + payload["username"] = username; + audit = $"Renamed player {playerId} ({display}) to {username}"; + break; + } + case "reputation": + payload["reputation"] = QInt("reputation"); + payload["noteriety"] = QInt("notoriety"); + audit = $"Saved player {playerId} ({display})'s reputation"; + break; + case "cheers": + payload["cheer_general"] = QInt("general"); + payload["cheer_helpful"] = QInt("helpful"); + payload["cheer_great_host"] = QInt("greatHost"); + payload["cheer_sportsman"] = QInt("sportsman"); + payload["cheer_creative"] = QInt("creative"); + payload["cheer_credit"] = QInt("credit"); + audit = $"Saved player {playerId} ({display})'s cheers"; + break; + case "progression": + payload["level"] = QInt("level", 1); + payload["xp"] = QInt("xp"); + audit = $"Set player {playerId} ({display}) to level {QInt("level", 1)} / {QInt("xp")} XP"; + break; + case "notes": + { + var text = (Q("note") ?? "").Trim(); + if (string.IsNullOrWhiteSpace(text)) return BadRequest("A note cannot be empty."); + + var state = LoadState(); + long nextId = 1; + foreach (var existing in state.Notes) + { + if (long.TryParse(existing["id"]?.ToString(), out var parsedId) && parsedId >= nextId) + nextId = parsedId + 1; + } + + var note = new JsonObject + { + ["id"] = nextId, + ["player_id"] = playerId, + ["text"] = text, + ["author"] = await GetPanelAdminUsernameAsync(), + ["created_at"] = DateTime.UtcNow.ToString("O") + }; + state.Notes.Add(note); + SaveState(state); + + return Ok(new { success = true, player_id = playerId, note = note }); + } + case "settings": + { + var settingKey = (Q("setting_key") ?? "").Trim(); + if (string.IsNullOrWhiteSpace(settingKey)) return BadRequest("A setting needs a key."); + return await ApplyPlayerSetting(playerId, new Dictionary { [settingKey] = Q("value") ?? "" }); + } + case "settings/delete": + { + var settingKey = (Q("setting_key") ?? "").Trim(); + if (string.IsNullOrWhiteSpace(settingKey)) return BadRequest("A setting needs a key."); + return await ApplyPlayerSetting(playerId, new Dictionary { [settingKey] = null }); + } + case "settings/reset": + await ApplyPlayerSetting(playerId, null); + AdminActionDiscord.LogAction("Reset Settings", await GetPanelAdminUsernameAsync(), $"Reset every in-game setting for player {playerId} ({display})."); + return Ok(new { success = true, player_id = playerId }); + case "settings/birthday": + return await ApplyPlayerSetting(playerId, new Dictionary { ["birthday"] = (Q("birthday") ?? "").Trim() }); + case "settings/profilepicture": + { + var owner = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (owner == null) return NotFound("Player does not exist."); + owner.profileImage = DBConfig.DefaultProfileImage; + await db.SaveChangesAsync(); + AdminActionDiscord.LogAction("Reset Profile Picture", await GetPanelAdminUsernameAsync(), $"Reset player {playerId} ({display})'s profile picture."); + return Ok(new { success = true, player_id = playerId }); + } + case "items/revoke": + { + var itemKind = (Q("itemKind") ?? "").Trim().ToLowerInvariant(); + var itemId = (Q("itemId") ?? "").Trim(); + var modGuid = (Q("modGuid") ?? "").Trim(); + if (itemId.Length == 0) return BadRequest("An item to revoke is required."); + + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (unlocks == null) return NotFound("This account has no locker to take an item from."); + + int removed; + string removedName; + switch (itemKind) + { + case "avatar": + { + var items = unlocks.GetAvatarItems(); + removedName = items.FirstOrDefault(i => i != null && string.Equals((i.AvatarItemDesc ?? "").Trim(), itemId, StringComparison.OrdinalIgnoreCase))?.FriendlyName ?? itemId; + removed = items.RemoveAll(i => i != null && string.Equals((i.AvatarItemDesc ?? "").Trim(), itemId, StringComparison.OrdinalIgnoreCase)); + unlocks.AvatarItems = items.ToJson(); + break; + } + case "equipment": + { + var items = unlocks.GetEquipment(); + bool Matches(Equipment i) => i != null + && string.Equals((i.PrefabName ?? "").Trim(), itemId, StringComparison.OrdinalIgnoreCase) + && (modGuid.Length == 0 || string.Equals((i.ModificationGuid ?? "").Trim(), modGuid, StringComparison.OrdinalIgnoreCase)); + removedName = items.FirstOrDefault(Matches)?.FriendlyName ?? itemId; + removed = items.RemoveAll(Matches); + unlocks.Equipment = items.ToJson(); + break; + } + case "consumable": + { + var items = unlocks.GetConsumables(); + removedName = items.FirstOrDefault(i => i != null && string.Equals((i.ConsumableItemDesc ?? "").Trim(), itemId, StringComparison.OrdinalIgnoreCase))?.ConsumableItemDesc ?? itemId; + removed = items.RemoveAll(i => i != null && string.Equals((i.ConsumableItemDesc ?? "").Trim(), itemId, StringComparison.OrdinalIgnoreCase)); + unlocks.SetConsumables(items); + break; + } + default: + return BadRequest("The item kind must be avatar, equipment or consumable."); + } + + if (removed == 0) return NotFound("That item is not in this account's locker."); + await db.SaveChangesAsync(); + AdminActionDiscord.LogAction("Revoke Item", await GetPanelAdminUsernameAsync(), + $"Took {removed} {itemKind} item(s) named \"{removedName}\" out of player {playerId} ({display})'s locker."); + return Ok(new { success = true, player_id = playerId, item_kind = itemKind, item_id = itemId, removed }); + } + case "locker/reset": + { + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (unlocks == null) return NotFound("This account has no locker to reset."); + unlocks.AvatarItems = DBConfig.DefaultAvatarUnlocks; + unlocks.Consumables = DBConfig.DefaultConsumableUnlocks; + unlocks.Equipment = DBConfig.DefaultEquipmentUnlocks; + await db.SaveChangesAsync(); + AdminActionDiscord.LogAction("Reset Locker", await GetPanelAdminUsernameAsync(), + $"Reset player {playerId} ({display})'s locker to the default inventory."); + return Ok(new { success = true, player_id = playerId }); + } + case "lock": + { + var caseRef = (Q("case") ?? Q("case_ref") ?? "").Trim(); + var result = await AccountLock.ApplyAsync(db, playerId, await GetPanelAdminUsernameAsync(), + string.IsNullOrWhiteSpace(caseRef) ? null : caseRef); + if (!result.Success) + return StatusCode(500, new { success = false, message = result.Error ?? "The account lock could not be applied." }); + return Ok(new + { + success = true, + player_id = playerId, + locked = true, + ban_method = AccountLock.BanMethod, + ban_category = ReportCategory.Moderator.ToString(), + permanent = true, + changed = result.Changed + }); + } + case "unlock": + { + var caseRef = (Q("case") ?? Q("case_ref") ?? "").Trim(); + var result = await AccountLock.RemoveAsync(db, playerId, await GetPanelAdminUsernameAsync(), + string.IsNullOrWhiteSpace(caseRef) ? null : caseRef); + if (!result.Success) + return Conflict(new { success = false, message = result.Error ?? "The account lock could not be removed." }); + return Ok(new { success = true, player_id = playerId, locked = false }); + } + case "quit": + return await QuitPlayerGame(playerId, key); + case "forcelogout": + return await ForceLogoutPlayer(playerId, key); + default: + return NotFound($"Unknown player action '{kind}'."); + } + + var applied = await UpdatePlayer(playerId, JsonSerializer.SerializeToElement(payload)); + if (applied is IActionResult failure) return failure; + + if (!string.IsNullOrEmpty(audit)) + AdminActionDiscord.LogAction(kind, await GetPanelAdminUsernameAsync(), audit); + + return Ok(new { success = true, player_id = playerId }); + } + + private async Task ApplyPlayerSetting(long playerId, Dictionary? changes) + { + var row = await db.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == playerId); + var settings = row?.DeserializeSettings() ?? new List(); + + if (changes != null) + { + foreach (var change in changes) + { + settings.RemoveAll(s => string.Equals(s.Key, change.Key, StringComparison.OrdinalIgnoreCase)); + if (change.Value != null) settings.Add(new Setting { Key = change.Key, Value = change.Value }); + } + } + else + { + settings = new List(); + } + + var payload = new Dictionary + { + ["settings"] = settings.ToDictionary(s => s.Key, s => s.Value) + }; + + var applied = await UpdatePlayer(playerId, JsonSerializer.SerializeToElement(payload)); + if (applied is IActionResult failure) return failure; + + return Ok(new { success = true, player_id = playerId, settings = settings.Count }); + } + + [HttpGet("players/{playerId:long}/details")] + public async Task PlayerDetails(long playerId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var rooms = await db.Rooms + .Where(r => r.CreatorAccountId == playerId) + .Select(r => new + { + id = r.RoomId, + name = r.Name, + description = r.Description, + image_name = r.ImageName, + state = r.State.ToString(), + accessibility = r.Accessibility.ToString(), + created_at = r.CreatedAt, + max_players = r.MaxPlayers, + supports_juniors = r.SupportsJuniors, + cloning_allowed = r.CloningAllowed + }) + .ToListAsync(); + + var inventions = await db.Inventions + .Where(i => i.CreatorPlayerId == playerId) + .Select(i => new + { + id = i.InventionId, + name = i.Name, + description = i.Description, + image_name = i.ImageName, + is_published = i.IsPublished, + num_downloads = i.NumDownloads, + cheer_count = i.CheerCount, + created_at = i.CreatedAt + }) + .ToListAsync(); + + var events = await db.PlayerEvents + .Where(e => e.CreatorPlayerId == playerId) + .Select(e => new + { + id = e.PlayerEventId, + name = e.Name, + description = e.Description, + image_name = e.ImageName, + room_id = e.RoomId, + start_time = e.StartTime, + end_time = e.EndTime, + attendee_count = e.AttendeeCount, + accessibility = e.Accessibility.ToString() + }) + .ToListAsync(); + + var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == playerId); + var balance = new + { + rec_tokens = currency?.RecTokens ?? 0, + laser_tag_tickets = currency?.LaserTagTickets ?? 0, + gold = currency?.Gold ?? 0, + dracula_silver = currency?.DraculaSilver ?? 0 + }; + + // What the account actually owns: the unlocks row the game reads when it dresses an + // avatar. The panel shows this as the player's locker, so each entry is resolved + // against the server's own catalogues here - the saved entry often carries no name + // or picture of its own, and the catalogue is the only place that has them. + var unlocksRow = await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == playerId); + var lockerAvatar = unlocksRow?.GetAvatarItems() ?? new List(); + var lockerEquipment = unlocksRow?.GetEquipment() ?? new List(); + var lockerConsumables = unlocksRow?.GetConsumables() ?? new List(); + var avatarCatalog = (DBConfig.DefaultAvatarCatalog.FromJson>() ?? new List()) + .Where(x => x != null && !string.IsNullOrWhiteSpace(x.AvatarItemDesc)) + .GroupBy(x => x.AvatarItemDesc.Trim(), StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); + var equipmentCatalog = (DBConfig.DefaultEquipmentItems.FromJson>(CatalogJsonOptions) ?? new List()) + .Where(x => x != null && !string.IsNullOrWhiteSpace(x.PrefabName)) + .GroupBy(x => x.PrefabName.Trim(), StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); + var consumableCatalog = (DBConfig.DefaultConsumableItems.FromJson>(CatalogJsonOptions) ?? new List()) + .Where(x => x != null && !string.IsNullOrWhiteSpace(x.ConsumableItemDesc)) + .GroupBy(x => x.ConsumableItemDesc.Trim(), StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); + var locker = new + { + avatar = lockerAvatar + .Where(i => i != null && !string.IsNullOrWhiteSpace(i.AvatarItemDesc)) + .Select(i => + { + avatarCatalog.TryGetValue(i.AvatarItemDesc.Trim(), out var meta); + return new + { + desc = i.AvatarItemDesc.Trim(), + name = NonEmpty(i.FriendlyName, meta?.FriendlyName, i.AvatarItemDesc), + rarity = RarityLabel(meta?.Rarity ?? i.Rarity), + thumbnail_image = NonEmpty(i.ThumbnailImage, meta?.ThumbnailImage, "") + }; + }) + .Cast().ToList(), + equipment = lockerEquipment + .Where(i => i != null && !string.IsNullOrWhiteSpace(i.PrefabName)) + .Select(i => + { + equipmentCatalog.TryGetValue(i.PrefabName.Trim(), out var meta); + return new + { + prefab = i.PrefabName.Trim(), + guid = (i.ModificationGuid ?? "").Trim(), + name = NonEmpty(i.FriendlyName, meta?.FriendlyName, i.PrefabName), + rarity = (meta?.Rarity ?? i.Rarity).ToString() + }; + }) + .Cast().ToList(), + consumables = lockerConsumables + .Where(i => i != null && !string.IsNullOrWhiteSpace(i.ConsumableItemDesc)) + .Select(i => + { + consumableCatalog.TryGetValue(i.ConsumableItemDesc.Trim(), out var meta); + return new + { + desc = i.ConsumableItemDesc.Trim(), + count = i.Count, + name = NonEmpty(meta?.FriendlyName, i.ConsumableItemDesc), + rarity = meta == null ? "None" : ((Rarity)meta.Rarity).ToString() + }; + }) + .Cast().ToList() + }; + + + + var photos = await db.Images + .Where(i => i.OwnerId == playerId) + .OrderByDescending(i => i.CreatedAt) + .Select(i => new + { + id = i.Id, + name = i.Name, + image_url = $"{DBConfig.ServerUrl}/images/img/{i.Name}", + type = i.ImageType.ToString(), + accessibility = i.Accessibility.ToString(), + room_id = i.RoomId, + created_at = DateTime.SpecifyKind(i.CreatedAt, DateTimeKind.Utc) + }) + .ToListAsync(); + + + + + var roomIds = rooms.Select(r => r.id).ToList(); + var roomReports = LoadState().ModerationReports + .Where(r => string.Equals(r["report_type"]?.ToString(), "Room", StringComparison.OrdinalIgnoreCase) + && (string.Equals(r["reported_id"]?.ToString(), playerId.ToString(), StringComparison.Ordinal) + || (r["target_id"]?.ToString() is string targetRaw + && long.TryParse(targetRaw, out var targetId) && roomIds.Contains(targetId)))) + .OrderByDescending(r => r["created_at"]?.ToString()) + .ToList(); + + + + var moderation = (await db.Moderation + .Where(m => m.PlayerId == playerId) + .ToListAsync()) + .Select(m => new + { + id = m.Id, + category = m.Category.ToString(), + message = m.Message, + moderator_note = m.ModeratorNote, + ban_method = m.BanMethod, + created_at = DateTime.SpecifyKind(m.CreatedAt, DateTimeKind.Utc), + is_ban = m.IsBan, + + + + expire_duration = DateTime.SpecifyKind(m.ExpireDuration, DateTimeKind.Utc) + }) + .ToList(); + + + + var voteKicks = await db.VoteKicks + .Where(v => v.PlayerId == playerId) + .OrderByDescending(v => v.CreatedAt) + .Select(v => new + { + id = v.Id, + reason = v.Reason, + category = v.Category.ToString(), + is_force = v.IsForce, + created_at = DateTime.SpecifyKind(v.CreatedAt, DateTimeKind.Utc), + expires_at = DateTime.SpecifyKind(v.ExpiresAt, DateTimeKind.Utc) + }) + .ToListAsync(); + + var friendRows = await db.Relationships + .Where(r => r.AccountId == playerId && r.RelationshipType == RelationshipType.Friend) + .ToListAsync(); + var friendIds = friendRows.Select(r => r.PlayerId).Distinct().ToList(); + var friendProfiles = friendIds.Count > 0 + ? await db.Profiles.Where(p => friendIds.Contains(p.accountId)).ToDictionaryAsync(p => p.accountId) + : new Dictionary(); + var friendProgression = friendIds.Count > 0 + ? await db.Progression.Where(p => friendIds.Contains(p.PlayerId)).ToDictionaryAsync(p => p.PlayerId) + : new Dictionary(); + var friends = friendRows.Select(r => + { + friendProfiles.TryGetValue(r.PlayerId, out var fp); + friendProgression.TryGetValue(r.PlayerId, out var fprog); + return new + { + player_id = r.PlayerId, + username = fp?.username ?? "", + display_name = fp?.displayName ?? $"Player {r.PlayerId}", + profile_image = fp != null ? ImageUrl(fp.profileImage) : ImageUrl(null), + level = fprog?.Level ?? 0, + relationship_type = r.RelationshipType.ToString() + }; + }).ToList(); + + var taggedImages = await db.Images.ToListAsync(); + var photosOfMe = taggedImages + .Where(i => + { + try + { + var ids = i.PlayerIds.FromJson>() ?? new List(); + return ids.Contains(playerId); + } + catch + { + return false; + } + }) + .OrderByDescending(i => i.CreatedAt) + .Select(i => new + { + id = i.Id, + name = i.Name, + image_url = $"{DBConfig.ServerUrl}/images/img/{i.Name}", + owner_id = i.OwnerId, + room_id = i.RoomId, + created_at = DateTime.SpecifyKind(i.CreatedAt, DateTimeKind.Utc) + }) + .ToList(); + + var notes = LoadState().Notes + .Where(n => string.Equals(n["player_id"]?.ToString(), playerId.ToString(), StringComparison.Ordinal)) + .OrderByDescending(n => n["created_at"]?.ToString()) + .Cast() + .ToList(); + + var clubRecords = await ClubStore.LoadAsync(db); + var clubs = clubRecords + .Where(c => c.ownerPlayerId == playerId) + .OrderByDescending(c => c.createdAt) + .Select(c => new + { + id = c.clubId, + name = c.name, + description = c.description, + primary_tag = c.primaryTag, + created_at = DateTime.SpecifyKind(c.createdAt, DateTimeKind.Utc) + }) + .Cast() + .ToList(); + + var progress = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + var progressJson = progress?.GetProgressionJson(); + var progression = new + { + level = progress?.Level ?? 0, + xp = progress?.XP ?? 0, + last_xp_earned_at = progressJson?.LastXpEarnedAt, + xp_earned_today = progressJson?.XpEarnedToday ?? 0 + }; + + var accountLocked = moderation.Any(m => m.is_ban + && string.Equals(m.ban_method, "Account Lock", StringComparison.OrdinalIgnoreCase) + && m.expire_duration > DateTime.UtcNow); + + var warningRows = await db.PlayerWarnings + .AsNoTracking() + .Where(w => w.WarnedPlayerId == playerId) + .OrderByDescending(w => w.Id) + .ToListAsync(); + var warningCount = warningRows.Count; + var pendingWarningCount = warningRows.Count(w => w.AcknowledgedAt == null); + var warnings = warningRows.Take(25).Select(w => new + { + id = w.Id, + category = w.ReportCategory.ToString(), + report_category = (int)w.ReportCategory, + reason = w.DisplayReason ?? string.Empty, + moderator_id = w.ModeratorPlayerId, + note = w.ModeratorNote ?? string.Empty, + created_at = DateTime.SpecifyKind(w.CreatedAt, DateTimeKind.Utc), + acknowledged_at = w.AcknowledgedAt.HasValue + ? DateTime.SpecifyKind(w.AcknowledgedAt.Value, DateTimeKind.Utc) + : (DateTime?)null + }).Cast().ToList(); + + var voiceRestriction = await VoiceChatRestrictions.ActiveAsync(db, playerId); + + return Ok(new + { + rooms, + inventions, + events, + balance, + locker, + moderation, + voice_restricted = voiceRestriction != null, + voice_restriction = voiceRestriction == null + ? null + : new + { + reason = voiceRestriction.Reason, + category = voiceRestriction.Category.ToString(), + report_category = (int)voiceRestriction.Category, + issued_by = voiceRestriction.IssuedBy, + issued_by_username = voiceRestriction.IssuedByUsername, + note = voiceRestriction.ModeratorNote, + started_at = DateTime.SpecifyKind(voiceRestriction.StartedAt, DateTimeKind.Utc), + expires_at = DateTime.SpecifyKind(voiceRestriction.ExpiresAt, DateTimeKind.Utc), + minutes_remaining = (int)Math.Max(0, (voiceRestriction.ExpiresAt - DateTime.UtcNow).TotalMinutes) + }, + vote_kicks = voteKicks, + photos, + photos_of_me = photosOfMe, + friends, + clubs, + notes, + progression, + room_reports = roomReports, + warn_count = warningCount, + pending_warn_count = pendingWarningCount, + warnings, + account_locked = accountLocked + }); + } + + [HttpGet("notify/status")] + public async Task NotifyStatus([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var accounts = Notify.ConnectedAccountIds(); + return Ok(new + { + hub_context_ready = Notify.Context != null, + connected_connections = Notify.ConnectedConnectionCount, + connected_account_count = accounts.Count, + connected_accounts = accounts + }); + } + + [HttpGet("{table}")] + public async Task List(string table, [FromQuery] string key, [FromQuery(Name = "eq_col")] string? eqCol, [FromQuery(Name = "eq_val")] string? eqVal, [FromQuery] string? order, [FromQuery] bool ascending = true) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + return table switch + { + "admin_users" => Ok(await GetAdminUsers(eqCol, eqVal)), + "players" => Ok(await GetPlayers(order, ascending)), + "gift_drops" => Ok(await GetGiftDrops(order, ascending)), + "storefront_items" => Ok(await GetStorefrontItems(eqCol, eqVal, order, ascending)), + "storefronts" => Ok(GetStorefronts(order, ascending)), + "challenges" => Ok(GetChallenges(order, ascending)), + "moderation_log" => Ok(FilterByPlayer(LoadState().ModerationLog, eqCol, eqVal, order, ascending)), + "gift_log" => Ok(await ProcessGiftLog(eqCol, eqVal, order, ascending)), + "notes" => Ok(FilterByPlayer(LoadState().Notes, eqCol, eqVal, order, ascending)), + "audit_log" => Ok(LoadState().AuditLog), + "rooms" => Ok(await GetRooms(order, ascending)), + "room_comments" => Ok(await GetRoomComments(order, ascending)), + "community_board" => Ok(GetCommunityBoard()), + "moderation_reports" => Ok(GetModerationReports(order, ascending)), + "player_events" => Ok(await GetPlayerEvents(order, ascending)), + "player_event_responses" => Ok(await GetPlayerEventResponses(eqCol, eqVal, order, ascending)), + "event_invitations" => Ok(await GetEventInvitations(eqCol, eqVal, order, ascending)), + "avatar_items" => Ok(GetAvatarItems()), + "equipment_items" => Ok(GetEquipmentItems()), + "consumable_items" => Ok(GetConsumableItems()), + "announcements" => Ok(GetAnnouncements()), + "ip_bans" => Ok(await GetIpBans(order, ascending)), + "hardware_bans" => Ok(await GetHardwareBans(order, ascending)), + "account_creation_blocks" => Ok(await GetAccountCreationBlocks(order, ascending)), + "leaderboard_scores" => Ok(await GetLeaderboardScores(eqCol, eqVal, order, ascending)), + "game_blocks" => Ok(await GetGameBlocks(order, ascending)), + "gameconfigs" => Ok(await db.GameConfig.ToListAsync()), + "serverconfigs" => Ok(await db.ServerConfig.ToListAsync()), + "clubs" => Ok(await GetClubs()), + "featured_rooms" => Ok(await GetFeaturedRooms()), + "room_recommendations" => Ok(await GetRoomRecommendations()), + "item_sets" => Ok(await GetItemSets()), + "playlists" => Ok(await GetPlaylists()), + "split_tests" => Ok(await GetSplitTests()), + "cohort_nux_buttons" => Ok(await GetCohortNuxButtons()), + "challenge_maps" => Ok(await GetChallengeMaps()), + "tags" => Ok(await GetTags()), + _ => GenericTableList(table) + }; + } + + private IActionResult GenericTableList(string table) + { + var prop = db.GetType().GetProperties() + .FirstOrDefault(p => p.PropertyType.IsGenericType + && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>) + && string.Equals(p.Name, table, StringComparison.OrdinalIgnoreCase)); + if (prop == null) return NotFound($"Unknown admin panel table '{table}'."); + + var set = prop.GetValue(db) as System.Collections.IEnumerable; + var rows = set?.Cast().ToList() ?? new List(); + return Ok(rows); + } + + private static List SafeRoomTags(string? tags) + { + if (string.IsNullOrWhiteSpace(tags)) return new List(); + try { return StringTools.ReadTags(tags).Select(t => t.Tag).ToList(); } + catch { return new List(); } + } + + private async Task> GetClubs() + { + var config = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "Clubs"); + if (config == null || string.IsNullOrWhiteSpace(config.Value)) return new List(); + + try + { + var clubs = config.Value.FromJson>() ?? new List(); + var clubReports = LoadState().ModerationReports + .Where(r => string.Equals(r["report_type"]?.ToString(), "Club", StringComparison.OrdinalIgnoreCase)) + .Select(r => r["target_id"]?.ToString()) + .Where(v => !string.IsNullOrWhiteSpace(v)) + .GroupBy(v => v!, StringComparer.Ordinal) + .ToDictionary(g => g.Key, g => g.Count(), StringComparer.Ordinal); + + return clubs.Select(c => + { + var categories = new List(); + if (!string.IsNullOrWhiteSpace(c.primaryTag)) categories.Add(c.primaryTag); + if (c.customTags != null) categories.AddRange(c.customTags.Where(t => !string.IsNullOrWhiteSpace(t))); + + return new + { + club_id = c.clubId, + name = c.name, + description = c.description, + primary_tag = c.primaryTag, + categories = categories, + image_name = c.imageName, + banner_image_name = c.bannerImageName, + owner_player_id = c.ownerPlayerId, + created_at = c.createdAt, + privacy = c.visibility, + privacy_label = c.visibility == 0 ? "Private" : "Public", + member_access = c.joinability, + member_access_label = c.joinability switch { 1 => "Invite Only", 2 => "Ask to Join", _ => "Open" }, + member_count = ClubStore.GetMemberCount(c), + report_count = clubReports.TryGetValue(c.clubId.ToString(), out var rc) ? rc : 0 + }; + }).Cast().ToList(); + } + catch + { + return new List(); + } + } + + private async Task> GetFeaturedRooms() + { + var rooms = await db.Rooms.AsNoTracking().ToListAsync(); + return rooms + .Where(r => SafeRoomTags(r.Tags).Any(t => string.Equals(t, "featured", StringComparison.OrdinalIgnoreCase))) + .Select(r => new + { + room_id = r.RoomId, + name = r.Name, + creator_player_id = r.CreatorAccountId, + tags = r.Tags, + accessibility = r.Accessibility.ToString(), + is_rro = r.IsRRO, + is_dorm = r.IsDorm, + created_at = r.CreatedAt + }).Cast().ToList(); + } + + private async Task> GetRoomRecommendations() + { + var top = await db.Rooms + .AsNoTracking() + .Where(r => r.Accessibility == Accessibility.Public && !r.IsDorm) + .Select(r => new + { + r.RoomId, + r.Name, + r.CreatorAccountId, + r.Tags, + r.IsRRO, + r.CreatedAt, + Engagement = db.RoomInteractions + .Where(i => i.RoomId == r.RoomId) + .Sum(i => (i.Favorited ? 1 : 0) + (i.Cheered ? 1 : 0)) + }) + .OrderByDescending(x => x.Engagement) + .Take(10) + .ToListAsync(); + + var rank = 1; + return top.Select(x => new + { + rank = rank++, + room_id = x.RoomId, + name = x.Name, + creator_player_id = x.CreatorAccountId, + tags = x.Tags, + is_rro = x.IsRRO, + engagement = x.Engagement, + created_at = x.CreatedAt + }).Cast().ToList(); + } + + private async Task> GetItemSets() + { + var curated = await db.ItemSets.AsNoTracking().OrderBy(x => x.ItemSetId).ToListAsync(); + var derived = await db.GiftItems + .AsNoTracking() + .Where(g => g.ItemSetId > 0) + .GroupBy(g => new { g.ItemSetId, g.ItemSetFriendlyName }) + .Select(g => new { item_set_id = g.Key.ItemSetId, item_set_friendly_name = g.Key.ItemSetFriendlyName, item_count = g.Count() }) + .OrderBy(x => x.item_set_id) + .ToListAsync(); + + var result = new List(); + var curatedIds = curated.Select(c => c.ItemSetId).ToHashSet(); + foreach (var set in curated) + { + var count = derived.Where(d => d.item_set_id == set.ItemSetId).Sum(d => d.item_count); + result.Add(new { item_set_id = set.ItemSetId, item_set_friendly_name = set.FriendlyName, item_count = count, source = "curated" }); + } + foreach (var set in derived.Where(d => !curatedIds.Contains(d.item_set_id))) + { + result.Add(new { item_set_id = set.item_set_id, item_set_friendly_name = set.item_set_friendly_name, item_count = set.item_count, source = "derived" }); + } + return result; + } + + private async Task> GetPlaylists() + { + var playlists = await db.Playlists.AsNoTracking().OrderBy(x => x.PlaylistId).ToListAsync(); + return playlists.Select(p => new + { + playlist_id = p.PlaylistId, + keywords = p.Keywords + }).Cast().ToList(); + } + + private async Task CreatePlaylist(string keywords) + { + var playlist = new PlaylistDB { Keywords = keywords }; + db.Playlists.Add(playlist); + await db.SaveChangesAsync(); + return new { playlist_id = playlist.PlaylistId, keywords = playlist.Keywords }; + } + + private async Task CreateItemSet(string friendlyName) + { + var set = new ItemSetDB { FriendlyName = friendlyName }; + db.ItemSets.Add(set); + await db.SaveChangesAsync(); + return new { item_set_id = set.ItemSetId, item_set_friendly_name = set.FriendlyName, item_count = 0, source = "curated" }; + } + + private async Task CreateTag(string tag, string description) + { + var row = new TagDB { Tag = tag, Description = description }; + db.Tags.Add(row); + await db.SaveChangesAsync(); + return new { tag_id = (long?)row.TagId, tag = row.Tag, description = row.Description, room_count = 0, source = "curated" }; + } + + private async Task UpdatePlaylist(long id, JsonElement payload) + { + var playlist = await db.Playlists.FirstOrDefaultAsync(x => x.PlaylistId == id); + if (playlist == null) return NotFound("Playlist not found."); + var keywords = GetJsonString(payload, "keywords"); + if (keywords != null) playlist.Keywords = keywords; + await db.SaveChangesAsync(); + return Ok(new { playlist_id = playlist.PlaylistId, keywords = playlist.Keywords }); + } + + private async Task UpdateItemSet(long id, JsonElement payload) + { + var set = await db.ItemSets.FirstOrDefaultAsync(x => x.ItemSetId == id); + if (set == null) return NotFound("Item set not found."); + var friendlyName = GetJsonString(payload, "item_set_friendly_name") ?? GetJsonString(payload, "friendly_name"); + if (!string.IsNullOrWhiteSpace(friendlyName)) set.FriendlyName = friendlyName; + await db.SaveChangesAsync(); + return Ok(new { item_set_id = set.ItemSetId, item_set_friendly_name = set.FriendlyName, source = "curated" }); + } + + private async Task UpdateTag(long id, JsonElement payload) + { + var tag = await db.Tags.FirstOrDefaultAsync(x => x.TagId == id); + if (tag == null) return NotFound("Tag not found."); + var tagValue = GetJsonString(payload, "tag"); + if (!string.IsNullOrWhiteSpace(tagValue)) tag.Tag = tagValue; + var description = GetJsonString(payload, "description"); + if (description != null) tag.Description = description; + await db.SaveChangesAsync(); + return Ok(new { tag_id = (long?)tag.TagId, tag = tag.Tag, description = tag.Description, source = "curated" }); + } + + private async Task> GetSplitTests() + { + var configs = await db.GameConfig.AsNoTracking().ToListAsync(); + var available = configs.FirstOrDefault(c => c.Key == "splitTestsAvailable")?.Value ?? ""; + var hard = configs.FirstOrDefault(c => c.Key == "splitTestHardOverrides")?.Value ?? ""; + var probs = configs.FirstOrDefault(c => c.Key == "splitTestSegmentProbabilities")?.Value ?? ""; + + var hardMap = new Dictionary(); + foreach (var part in hard.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var eq = part.IndexOf('='); + if (eq <= 0) continue; + hardMap[part[..eq].Trim()] = part[(eq + 1)..].Trim(); + } + + JsonNode? probNode = null; + try { probNode = JsonNode.Parse(probs); } catch { } + + var rows = new List(); + foreach (var part in available.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var eq = part.IndexOf('='); + if (eq <= 0) continue; + var name = part[..eq].Trim(); + var segments = part[(eq + 1)..].Trim(); + string? probabilities = null; + if (probNode is JsonObject obj && obj.TryGetPropertyValue(name, out var segNode) && segNode != null) + probabilities = segNode.ToJsonString(); + rows.Add(new + { + test_name = name, + available_segments = segments, + hard_override = hardMap.TryGetValue(name, out var h) ? h : "", + segment_probabilities = probabilities ?? "" + }); + } + return rows; + } + + private async Task> GetCohortNuxButtons() + { + var rows = new List(); + var game = await db.GameConfig.AsNoTracking().ToListAsync(); + var server = await db.ServerConfig.AsNoTracking().ToListAsync(); + + void AddFromConfig(string key, string value, string source) + { + if (MatchesNux(key)) + { + rows.Add(new { config_key = key, value, source }); + return; + } + foreach (var entry in value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (!MatchesNux(entry)) continue; + var eq = entry.IndexOf('='); + rows.Add(new + { + config_key = eq > 0 ? entry[..eq].Trim() : entry, + value = eq > 0 ? entry[(eq + 1)..].Trim() : "", + source + }); + } + } + + foreach (var c in game) AddFromConfig(c.Key, c.Value, "GameConfig"); + foreach (var c in server) AddFromConfig(c.Key, c.Value, "ServerConfig"); + return rows; + + static bool MatchesNux(string s) => + s.Contains("NUX", StringComparison.OrdinalIgnoreCase) || s.Contains("Cohort", StringComparison.OrdinalIgnoreCase); + } + + private async Task> GetChallengeMaps() + { + var rooms = await db.Rooms.AsNoTracking().ToListAsync(); + return rooms + .Where(r => SafeRoomTags(r.Tags).Any(t => + string.Equals(t, "challenge", StringComparison.OrdinalIgnoreCase) || + string.Equals(t, "quest", StringComparison.OrdinalIgnoreCase))) + .Select(r => new + { + room_id = r.RoomId, + name = r.Name, + creator_player_id = r.CreatorAccountId, + tags = r.Tags, + accessibility = r.Accessibility.ToString(), + is_rro = r.IsRRO, + created_at = r.CreatedAt + }).Cast().ToList(); + } + + private async Task> GetTags() + { + var curated = await db.Tags.AsNoTracking().OrderBy(x => x.Tag).ToListAsync(); + var rooms = await db.Rooms.AsNoTracking().ToListAsync(); + var derived = rooms + .SelectMany(r => SafeRoomTags(r.Tags).Select(t => new { Tag = t, RoomId = r.RoomId })) + .Where(t => !string.IsNullOrWhiteSpace(t.Tag)) + .GroupBy(t => t.Tag, StringComparer.OrdinalIgnoreCase) + .Select(g => new { tag = g.Key, room_count = g.Select(x => x.RoomId).Distinct().Count() }) + .OrderByDescending(x => x.room_count) + .ThenBy(x => x.tag) + .ToList(); + + var result = new List(); + var curatedTags = curated.Select(c => c.Tag).ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var tag in curated) + { + var count = derived.FirstOrDefault(d => string.Equals(d.tag, tag.Tag, StringComparison.OrdinalIgnoreCase))?.room_count ?? 0; + result.Add(new { tag_id = (long?)tag.TagId, tag = tag.Tag, description = tag.Description, room_count = count, source = "curated" }); + } + foreach (var tag in derived.Where(d => !curatedTags.Contains(d.tag))) + { + result.Add(new { tag_id = (long?)null, tag = tag.tag, description = "", room_count = tag.room_count, source = "derived" }); + } + return result; + } + + + + + + private static string? SaveDataUrlFile(string? dataUrl, string directory) + { + if (string.IsNullOrWhiteSpace(dataUrl) || !dataUrl.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + return null; + + var comma = dataUrl.IndexOf(','); + if (comma < 0) return null; + + var header = dataUrl.Substring(0, comma); + string ext = header.Contains("png", StringComparison.OrdinalIgnoreCase) ? ".png" + : header.Contains("jpeg", StringComparison.OrdinalIgnoreCase) ? ".jpg" + : header.Contains("jpg", StringComparison.OrdinalIgnoreCase) ? ".jpg" + : header.Contains("gif", StringComparison.OrdinalIgnoreCase) ? ".gif" + : header.Contains("webp", StringComparison.OrdinalIgnoreCase) ? ".webp" + : ".png"; + + try + { + var bytes = Convert.FromBase64String(dataUrl.Substring(comma + 1)); + Directory.CreateDirectory(directory); + var name = Guid.NewGuid().ToString("N") + ext; + System.IO.File.WriteAllBytes(Path.Join(directory, name), bytes); + return name; + } + catch + { + return null; + } + } + + private static string FormatBanTime(TimeSpan timeRemaining) + { + if (timeRemaining.TotalDays > 365 * 10) + return "This ban is permanent."; + + var parts = new List(); + if (timeRemaining.Days > 0) + parts.Add($"{timeRemaining.Days} Days"); + if (timeRemaining.Hours > 0) + parts.Add($"{timeRemaining.Hours} Hours"); + if (timeRemaining.Minutes > 0) + parts.Add($"{timeRemaining.Minutes} Minutes"); + if (timeRemaining.Seconds > 0) + parts.Add($"{timeRemaining.Seconds} Seconds"); + + return parts.Count > 0 ? string.Join(", ", parts) : "0 Seconds"; + } + + [HttpPost("upload/video")] + [RequestSizeLimit(AdminUtils.MaxUploadBytes)] + public async Task UploadVideo([FromQuery] string key, [FromForm] IFormFile file) + { + + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (file == null || file.Length == 0) return BadRequest("No file provided."); + + + var allowed = new[] { ".mp4", ".webm", ".mov", ".m4v", ".ogv" }; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (!allowed.Contains(ext)) return BadRequest($"Unsupported video format '{ext}'. Use mp4/webm/mov."); + + Directory.CreateDirectory(DBConfig.CDNVideos); + var name = Guid.NewGuid().ToString("N") + ext; + var path = Path.Join(DBConfig.CDNVideos, name); + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Ok(new { name, url = $"{DBConfig.ServerUrl}/cdn/video/{name}" }); + } + + [HttpPost("upload/image")] + [RequestSizeLimit(AdminUtils.MaxUploadBytes)] + public async Task UploadImage([FromQuery] string key, [FromForm] IFormFile file) + { + + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (file == null || file.Length == 0) return BadRequest("No file provided."); + + + + var allowed = new[] { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" }; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (!allowed.Contains(ext)) return BadRequest($"Unsupported image format '{ext}'. Use png/jpg/jpeg/gif/webp."); + + Directory.CreateDirectory(DBConfig.CDNImages); + var name = Guid.NewGuid().ToString("N") + ext; + var path = Path.Join(DBConfig.CDNImages, name); + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Ok(new { name, url = $"{DBConfig.ServerUrl}/images/img/{name}" }); + } + + [HttpPost("upload/room")] + [RequestSizeLimit(AdminUtils.MaxUploadBytes)] + public async Task UploadRoom([FromQuery] string key, [FromForm] IFormFile file, [FromQuery] string? name) + { + + // An oversized or truncated body fails form binding, so it is reported as the bad + // request it is rather than as whatever a half-read form left behind. + if (!ModelState.IsValid) return BadRequest($"Invalid upload request. Use a multipart form no larger than {AdminUtils.MaxUploadLabel}."); + + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (file == null || file.Length == 0) return BadRequest("No file provided."); + + + + + + // A subroom is imported as three kinds of file, and each keeps the role its + // extension names: the .room is the save the subroom loads, the .assetbundle + // is the Unity scene its unity_assets metadata points at, and the .htr files + // are the audio/data blobs that save references by name. They are imported one + // file at a time, so every one of them lands in the CDN under the name it was + // uploaded as - the 2023 client asks for exactly that name (GET /data/), + // which is why a file this route renamed to a GUID could never be found. + // An explicit ?name= wins; otherwise the uploaded file's own name is the one to + // store, and it is held to the same bare-name rule either way. + var candidate = !string.IsNullOrWhiteSpace(name) ? name : Path.GetFileName(file.FileName); + if (!AdminUtils.TryResolveUploadName(candidate, out var finalName, out var nameError)) + return BadRequest(nameError); + if (string.IsNullOrEmpty(finalName)) + return BadRequest("The uploaded file has no name to store it under."); + + // The stored name decides which of the three a file is, so a renamed upload is + // classified by what it is being stored as. + var kind = RoomFileKind(finalName); + if (kind == null) + { + var ext = Path.GetExtension(finalName).ToLowerInvariant(); + return BadRequest($"Unsupported room file format '{(string.IsNullOrEmpty(ext) ? "no extension" : ext)}'. Only .room, .htr and .assetbundle room files are allowed."); + } + + Directory.CreateDirectory(DBConfig.CDNStorage); + var path = Path.Join(DBConfig.CDNStorage, finalName); + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Ok(new { name = finalName, kind, url = $"{DBConfig.ServerUrl}/cdn/data/{finalName}" }); + } + + /// The stored name decides which of the three a room file is: the extension is the role. + private static string? RoomFileKind(string fileName) => Path.GetExtension(fileName).ToLowerInvariant() switch + { + ".room" => "room", + ".htr" => "htr", + ".assetbundle" => "assetbundle", + _ => null + }; + + /// + /// One part of a file too big to send in a single request. + /// + /// A whole room file in one request does not survive the trip to this server: the edge + /// proxy in front of it refuses a request body over 100 MB and holds a streamed body in + /// memory, so importing a large .assetbundle died between the panel and here and surfaced + /// to the admin as a broken connection rather than as a size refusal. The panel therefore + /// posts anything large in parts, and this route appends each one to + /// "<name>.uploading" and promotes it to "<name>" when the last part lands, so + /// a half-imported file is never served, listed or bound. + /// + /// The offset is checked against what is already stored, so a part retried after its + /// answer was lost either continues where it left off or is told exactly where to continue + /// (409, with the stored byte count) instead of corrupting the file. + /// + [HttpPost("upload/part")] + [RequestSizeLimit(AdminUtils.MaxUploadBytes)] + public async Task UploadPart([FromQuery] string key, [FromQuery] string name, [FromQuery] long offset, [FromQuery] long total) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (!AdminUtils.TryResolveUploadName(name, out var finalName, out var nameError)) + return BadRequest(nameError); + if (string.IsNullOrEmpty(finalName)) + return BadRequest("The uploaded file has no name to store it under."); + if (AdminUtils.IsUploadPart(finalName)) + return BadRequest($"'{finalName}' is a partial upload name; import the file under its final name."); + if (total <= 0 || total > AdminUtils.MaxUploadBytes) + return BadRequest($"A file of that size cannot be imported this way; the limit is {AdminUtils.MaxUploadLabel}."); + if (offset < 0 || offset > total) + return BadRequest("The part's offset is outside the file it belongs to."); + + Directory.CreateDirectory(DBConfig.CDNStorage); + var partPath = Path.Join(DBConfig.CDNStorage, finalName + AdminUtils.PartSuffix); + var finalPath = Path.Join(DBConfig.CDNStorage, finalName); + + // A part starting at zero begins the file over, whether or not an abandoned attempt left + // bytes behind; any other offset has to line up with what is stored or it is refused. + var stored = System.IO.File.Exists(partPath) ? new FileInfo(partPath).Length : 0; + if (offset == 0) + { + if (System.IO.File.Exists(partPath)) System.IO.File.Delete(partPath); + stored = 0; + } + if (stored != offset) + return Conflict(new + { + error = "part_out_of_order", + message = $"This import already holds {stored} of {total} bytes, not {offset}.", + stored, + total + }); + + long received = 0; + using (var stream = new FileStream(partPath, offset == 0 ? FileMode.Create : FileMode.Append, FileAccess.Write, FileShare.None)) + { + var buffer = new byte[81920]; + int read; + while ((read = await Request.Body.ReadAsync(buffer, HttpContext.RequestAborted)) > 0) + { + received += read; + if (offset + received > total) + { + stream.Dispose(); + System.IO.File.Delete(partPath); + return BadRequest($"This part carried more than the {total} bytes it was sent for; the import was discarded."); + } + await stream.WriteAsync(buffer.AsMemory(0, read), HttpContext.RequestAborted); + } + } + + var written = offset + received; + if (written < total) + return Ok(new { name = finalName, kind = RoomFileKind(finalName), received = written, total, complete = false }); + + // The last part: the file becomes the upload it was always meant to be. + if (System.IO.File.Exists(finalPath)) System.IO.File.Delete(finalPath); + System.IO.File.Move(partPath, finalPath); + return Ok(new { name = finalName, kind = RoomFileKind(finalName), url = $"{DBConfig.ServerUrl}/cdn/data/{finalName}", received = written, total, complete = true }); + } + + /// A part left behind by an import that never finished is not an upload and must not + /// accumulate: one older than a day was abandoned, so it goes. + private static void SweepAbandonedParts() + { + if (!Directory.Exists(DBConfig.CDNStorage)) return; + foreach (var file in Directory.GetFiles(DBConfig.CDNStorage, "*" + AdminUtils.PartSuffix)) + { + try + { + if (System.IO.File.GetLastWriteTimeUtc(file) < DateTime.UtcNow.AddHours(-24)) System.IO.File.Delete(file); + } + catch (IOException) { } + } + } + + [HttpPost("upload/datablob")] + [RequestSizeLimit(AdminUtils.MaxUploadBytes)] + public async Task UploadDatablob([FromQuery] string key, [FromForm] IFormFile file, [FromQuery] string? name) + { + + if (!ModelState.IsValid) return BadRequest($"Invalid upload request. Use a multipart form no larger than {AdminUtils.MaxUploadLabel}."); + + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (file == null || file.Length == 0) return BadRequest("No file provided."); + + // Same rule as a room file: the blob keeps the name it arrived under unless + // the caller names it, because the client can only ask for a name it knows. + var candidate = !string.IsNullOrWhiteSpace(name) ? name : Path.GetFileName(file.FileName); + if (!AdminUtils.TryResolveUploadName(candidate, out var requestedName, out var nameError)) + return BadRequest(nameError); + if (string.IsNullOrEmpty(requestedName)) + return BadRequest("The uploaded file has no name to store it under."); + + Directory.CreateDirectory(DBConfig.CDNStorage); + var finalName = requestedName; + var path = Path.Join(DBConfig.CDNStorage, finalName); + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Ok(new { name = finalName, url = $"{DBConfig.ServerUrl}/cdn/data/{finalName}" }); + } + + [HttpGet("uploads")] + public async Task ListUploads([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + + + + string[] Scan(string dir) + { + if (!Directory.Exists(dir)) return Array.Empty(); + SweepAbandonedParts(); + return Directory.GetFiles(dir) + .Select(f => Path.GetFileName(f)!) + // A part of a file still being uploaded is not an upload: it is invisible until + // its last part lands and it becomes the file it was sent as. + .Where(n => !AdminUtils.IsUploadPart(n)) + .OrderByDescending(f => System.IO.File.GetLastWriteTimeUtc(Path.Join(dir, f))) + .ToArray(); + } + + var images = Scan(DBConfig.CDNImages); + var videos = Scan(DBConfig.CDNVideos); + var storage = Scan(DBConfig.CDNStorage); + var bundles = storage.Where(n => n.EndsWith(".assetbundle", StringComparison.OrdinalIgnoreCase)).ToArray(); + + + + long SizeOf(string dir, string name) + { + try { return new FileInfo(Path.Join(dir, name)).Length; } + catch { return 0; } + } + + return Ok(new + { + images = images.Select(n => new { name = n, url = $"{DBConfig.ServerUrl}/images/img/{n}", size = SizeOf(DBConfig.CDNImages, n) }).Cast().ToList(), + videos = videos.Select(n => new { name = n, url = $"{DBConfig.ServerUrl}/cdn/video/{n}", size = SizeOf(DBConfig.CDNVideos, n) }).Cast().ToList(), + rooms = storage.Select(n => new { name = n, url = $"{DBConfig.ServerUrl}/cdn/room/{n}", size = SizeOf(DBConfig.CDNStorage, n) }).Cast().ToList(), + assetbundles = bundles.Select(n => new { name = n, url = $"{DBConfig.ServerUrl}/cdn/data/{n}", size = SizeOf(DBConfig.CDNStorage, n) }).Cast().ToList(), + datablobs = storage.Select(n => new { name = n, url = $"{DBConfig.ServerUrl}/cdn/data/{n}", size = SizeOf(DBConfig.CDNStorage, n) }).Cast().ToList() + }); + } + + [HttpDelete("uploads")] + public async Task DeleteUpload([FromQuery] string key, [FromQuery] string type, [FromQuery] string name) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (string.IsNullOrWhiteSpace(name)) return BadRequest("No file name provided."); + + + + name = Path.GetFileName(name); + string dir = type switch + { + "image" => DBConfig.CDNImages, + "video" => DBConfig.CDNVideos, + _ => DBConfig.CDNStorage + }; + + var path = Path.Join(dir, name); + if (!System.IO.File.Exists(path)) return NotFound("File not found."); + + System.IO.File.Delete(path); + return Ok(new { deleted = name }); + } + + [HttpPost("{table}")] + public async Task Insert(string table, [FromQuery] string key, [FromBody] JsonElement payload) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (table == "gift_drops" || table == "storefront_items") + return Ok(await CreateGiftDrop(payload)); + + if (table == "ip_bans") + return Ok(await AddIpBan(payload)); + + if (table == "hardware_bans") + return Ok(await AddHardwareBan(payload)); + + if (table == "account_creation_blocks") + return Ok(await AddAccountCreationBlock(payload)); + + if (table == "game_blocks") + { + + + var addGameBlockResult = await AddGameBlock(payload); + return addGameBlockResult is IActionResult actionResult ? actionResult : Ok(addGameBlockResult); + } + + if (table == "gameconfigs" || table == "serverconfigs") + return Ok(await UpsertConfig(table, null, payload)); + + if (table == "playlists") + { + var keywords = GetJsonString(payload, "keywords") ?? ""; + if (string.IsNullOrWhiteSpace(keywords)) return BadRequest("Keywords are required."); + return Ok(await CreatePlaylist(keywords)); + } + + if (table == "item_sets") + { + var friendlyName = GetJsonString(payload, "item_set_friendly_name") ?? GetJsonString(payload, "friendly_name") ?? ""; + if (string.IsNullOrWhiteSpace(friendlyName)) return BadRequest("A friendly name is required."); + return Ok(await CreateItemSet(friendlyName)); + } + + if (table == "tags") + { + var tag = GetJsonString(payload, "tag") ?? ""; + if (string.IsNullOrWhiteSpace(tag)) return BadRequest("A tag is required."); + var description = GetJsonString(payload, "description") ?? ""; + return Ok(await CreateTag(tag, description)); + } + + var state = LoadState(); + return table switch + { + "storefronts" => Ok(AddStorefront(state, payload)), + "challenges" => Ok(AddChallenge(state, payload)), + "moderation_log" => InsertLoggedRows(state, state.ModerationLog, payload, "moderation_log"), + "gift_log" => await InsertGiftLogRows(state, payload), + "notes" => Ok(RowsResponse(payload, AddRows(state, state.Notes, payload))), + "audit_log" => InsertLoggedRows(state, state.AuditLog, payload, "audit_log"), + "community_board" => Ok(await UpdateCommunityBoard(state, payload)), + "announcements" => await UpdateAnnouncements(payload), + _ => NotFound($"Unknown admin panel table '{table}'.") + }; + } + + public async Task Update(string table, long id, [FromQuery] string key, [FromBody] JsonElement payload) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (table == "players") + { + + + var updatePlayerResult = await UpdatePlayer(id, payload); + return updatePlayerResult is IActionResult actionResult ? actionResult : Ok(updatePlayerResult); + } + + if (table == "rooms") + { + var updateRoomResult = await UpdateRoom(id, payload); + return updateRoomResult is IActionResult roomActionResult ? roomActionResult : Ok(updateRoomResult); + } + + if (table == "player_events") + return Ok(await UpdatePlayerEvent(id, payload)); + + if (table == "gift_drops" || table == "storefront_items") + return Ok(await UpdateGiftDrop(id, payload)); + + if (table == "playlists") + return await UpdatePlaylist(id, payload); + + if (table == "item_sets") + return await UpdateItemSet(id, payload); + + if (table == "tags") + return await UpdateTag(id, payload); + + var state = LoadState(); + return table switch + { + "storefronts" => Ok(UpdateRow(state, state.Storefronts, id, payload)), + "challenges" => Ok(UpdateRow(state, state.Challenges, id, payload)), + "moderation_reports" => Ok(UpdateRow(state, state.ModerationReports, id, payload)), + "community_board" => Ok(await UpdateCommunityBoard(state, payload)), + _ => NotFound($"Unknown admin panel table '{table}'.") + }; + } + + [HttpPatch("{table}")] + public async Task UpdateNoId(string table, [FromQuery] string key, [FromBody] JsonElement payload) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var state = LoadState(); + return table switch + { + "community_board" => Ok(await UpdateCommunityBoard(state, payload)), + _ => NotFound($"Unknown admin panel table '{table}'.") + }; + } + + [HttpPatch("{table}/{id}")] + public async Task UpdateByKey(string table, string id, [FromQuery] string key, [FromBody] JsonElement payload) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + if (table == "gameconfigs" || table == "serverconfigs") + return Ok(await UpsertConfig(table, id, payload)); + + if (long.TryParse(id, out var rowId)) + return await Update(table, rowId, key, payload); + + return NotFound($"Unknown admin panel table '{table}'."); + } + + private async Task UpsertConfig(string table, string? fallbackKey, JsonElement payload) + { + string? key = null, value = null; + if (payload.ValueKind == JsonValueKind.Object) + { + foreach (var prop in payload.EnumerateObject()) + { + if (prop.Name.Equals("Key", StringComparison.OrdinalIgnoreCase)) + key = prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() : prop.Value.ToString(); + else if (prop.Name.Equals("Value", StringComparison.OrdinalIgnoreCase)) + value = prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() : prop.Value.ToString(); + } + } + + key ??= fallbackKey; + if (string.IsNullOrWhiteSpace(key)) return BadRequest("Config 'Key' is required."); + value ??= ""; + + if (table == "gameconfigs") + { + var row = await db.GameConfig.FirstOrDefaultAsync(x => x.Key == key); + if (row == null) db.GameConfig.Add(new ConfigVal(key, value)); + else row.Value = value; + } + else + { + var row = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == key); + if (row == null) db.ServerConfig.Add(new ServerConfigVal(key, value)); + else row.Value = value; + } + await db.SaveChangesAsync(); + return Ok(new { Key = key, Value = value }); + } + + [HttpDelete("{table}")] + public async Task Delete(string table, [FromQuery] string key, [FromQuery(Name = "in_col")] string? inCol, [FromQuery(Name = "in_vals")] string? inVals, [FromQuery(Name = "eq_col")] string? eqCol, [FromQuery(Name = "eq_val")] string? eqVal, [FromQuery(Name = "room_id")] long? roomId, [FromQuery(Name = "stat_channel")] int? statChannel, [FromQuery(Name = "player_id")] long? playerId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var ids = ParseIds(inVals ?? eqVal); + + if (table == "gameconfigs" || table == "serverconfigs") + { + var cfgKeys = (inVals ?? eqVal ?? "") + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .ToList(); + if (table == "gameconfigs") + { + var rows = await db.GameConfig.Where(x => cfgKeys.Contains(x.Key)).ToListAsync(); + db.GameConfig.RemoveRange(rows); + await db.SaveChangesAsync(); + return Ok(rows); + } + else + { + var rows = await db.ServerConfig.Where(x => cfgKeys.Contains(x.Key)).ToListAsync(); + db.ServerConfig.RemoveRange(rows); + await db.SaveChangesAsync(); + return Ok(rows); + } + } + + if (table == "gift_drops" || table == "storefront_items") + { + var gifts = await db.GiftItems.Where(x => ids.Contains(x.GiftDropId)).ToListAsync(); + db.GiftItems.RemoveRange(gifts); + await db.SaveChangesAsync(); + return Ok(gifts); + } + + if (table == "playlists") + { + var playlists = await db.Playlists.Where(x => ids.Contains(x.PlaylistId)).ToListAsync(); + db.Playlists.RemoveRange(playlists); + await db.SaveChangesAsync(); + return Ok(playlists); + } + + if (table == "item_sets") + { + var sets = await db.ItemSets.Where(x => ids.Contains(x.ItemSetId)).ToListAsync(); + db.ItemSets.RemoveRange(sets); + await db.SaveChangesAsync(); + return Ok(sets); + } + + if (table == "tags") + { + var tags = await db.Tags.Where(x => ids.Contains(x.TagId)).ToListAsync(); + db.Tags.RemoveRange(tags); + await db.SaveChangesAsync(); + return Ok(tags); + } + + if (table == "player_events") + return Ok(await DeletePlayerEvents(ids)); + + if (table == "ip_bans") + { + var ipRows = await db.IpBans.Where(x => ids.Contains(x.Id)).ToListAsync(); + db.IpBans.RemoveRange(ipRows); + await db.SaveChangesAsync(); + return Ok(ipRows); + } + + if (table == "hardware_bans") + { + var hwRows = await db.HardwareBans.Where(x => ids.Contains(x.Id)).ToListAsync(); + db.HardwareBans.RemoveRange(hwRows); + await db.SaveChangesAsync(); + return Ok(hwRows); + } + + if (table == "account_creation_blocks") + { + var blockRows = await db.AccountCreationBlocks.Where(x => ids.Contains(x.Id)).ToListAsync(); + db.AccountCreationBlocks.RemoveRange(blockRows); + await db.SaveChangesAsync(); + return Ok(blockRows); + } + + if (table == "game_blocks") + { + var gameBlockRows = await db.GameBlocks.Where(x => ids.Contains(x.Id)).ToListAsync(); + db.GameBlocks.RemoveRange(gameBlockRows); + await db.SaveChangesAsync(); + + if (gameBlockRows.Count > 0) GameBlockMonitor.Invalidate(); + return Ok(gameBlockRows); + } + + if (table == "room_comments") + { + var commentRows = await db.RoomComments.Where(x => ids.Contains(x.Id)).ToListAsync(); + db.RoomComments.RemoveRange(commentRows); + await db.SaveChangesAsync(); + return Ok(commentRows); + } + + if (table == "leaderboard_scores") + { + + + + if (roomId.HasValue && statChannel.HasValue) + { + + + + var query = db.LeaderboardScores.Where(x => x.StatChannel == statChannel.Value); + if (roomId.Value > 0) + query = query.Where(x => x.RoomId == roomId.Value); + if (playerId.HasValue) + query = query.Where(x => x.PlayerId == playerId.Value); + + var boardRows = await query.ToListAsync(); + db.LeaderboardScores.RemoveRange(boardRows); + await db.SaveChangesAsync(); + return Ok(boardRows); + } + + var scoreRows = await db.LeaderboardScores.Where(x => ids.Contains(x.Id)).ToListAsync(); + db.LeaderboardScores.RemoveRange(scoreRows); + await db.SaveChangesAsync(); + return Ok(scoreRows); + } + + var state = LoadState(); + return table switch + { + "storefronts" => Ok(DeleteRows(state, state.Storefronts, ids)), + "challenges" => Ok(DeleteRows(state, state.Challenges, ids)), + "notes" => Ok(DeleteRows(state, state.Notes, ids)), + "announcements" => Ok(await DeleteAnnouncements(ids)), + "clubs" => Ok(await DeleteClubs(ids)), + _ => NotFound($"Unknown admin panel table '{table}'.") + }; + } + + private async Task> DeleteClubs(List ids) + { + var deleted = new List(); + foreach (var id in ids) + { + if (id <= 0) continue; + if (await ClubStore.DeleteAsync(db, id)) deleted.Add(new { club_id = id }); + } + return deleted; + } + + private async Task> DeleteAnnouncements(List ids) + { + var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "Announcement"); + if (config == null || string.IsNullOrWhiteSpace(config.Value)) + return new List(); + + try + { + var list = config.Value.FromJson>() ?? new List(); + var removed = new List(); + + + var toRemove = ids.Count > 0 + ? list.Where(x => ids.Contains(ReadAnnouncementId(x) ?? 0)).ToList() + : list; + + foreach (var item in toRemove) + { + list.Remove(item); + removed.Add(item); + } + + config.Value = JsonSerializer.Serialize(list); + await db.SaveChangesAsync(); + + foreach (var deleted in toRemove) + { + try { await Notify.SendGlobalNotify(AnnouncementBroadcast.Delete(ToAnnouncement(deleted))); } + catch (Exception) { } + } + + + + + var touchedMaintenance = toRemove.Any(x => string.Equals(x["type"]?.ToString(), "Maintenance", StringComparison.OrdinalIgnoreCase)) + || list.Any(x => string.Equals(x["type"]?.ToString(), "Maintenance", StringComparison.OrdinalIgnoreCase)); + if (touchedMaintenance) + await RecomputeMaintenanceFromAnnouncements(list); + + return removed.Cast().ToList(); + } + catch + { + return new List(); + } + } + + + + + + private async Task ApplyMaintenanceToConfig(int startsInMinutes) + { + var config = await db.Config.FirstOrDefaultAsync(); + if (config == null) return; + + config.ServerMaintenance = new ServerMaintenance { StartsInMinutes = Math.Clamp(startsInMinutes, 0, 61) }.ToJson(); + await db.SaveChangesAsync(); + + if (MainConfig.ActiveConfig != null) + MainConfig.ActiveConfig.ServerMaintenance = config.ServerMaintenance; + } + + + + + + private async Task RecomputeMaintenanceFromAnnouncements(List list) + { + var config = await db.Config.FirstOrDefaultAsync(); + if (config == null) return; + + var now = DateTime.UtcNow; + int minutes = 0; + foreach (var item in list) + { + if (!string.Equals(item["type"]?.ToString(), "Maintenance", StringComparison.OrdinalIgnoreCase)) continue; + + if (item["expires_at"]?.ToString() is string expiresRaw && + DateTime.TryParse(expiresRaw, out var expiry) && expiry <= now) continue; + + var itemMinutes = item["minutes"]?.GetValue(); + if (itemMinutes.HasValue && itemMinutes.Value > 0) + minutes = Math.Min(61, Math.Max(minutes, itemMinutes.Value)); + } + + config.ServerMaintenance = new ServerMaintenance { StartsInMinutes = minutes }.ToJson(); + await db.SaveChangesAsync(); + + if (MainConfig.ActiveConfig != null) + MainConfig.ActiveConfig.ServerMaintenance = config.ServerMaintenance; + } + + private async Task> GetPlayers(string? order, bool ascending) + { + var profiles = await db.Profiles.ToListAsync(); + var playerIds = profiles.Select(x => x.accountId).ToList(); + var players = await db.Players.Where(x => playerIds.Contains(x.AccountId)).ToDictionaryAsync(x => x.AccountId); + var progression = await db.Progression.Where(x => playerIds.Contains(x.PlayerId)).ToDictionaryAsync(x => x.PlayerId); + var reputations = await db.Reputations.Where(x => playerIds.Contains(x.AccountId)).ToDictionaryAsync(x => x.AccountId); + var presences = await db.Presences.Where(x => playerIds.Contains(x.playerId)).ToDictionaryAsync(x => x.playerId); + var moderation = await db.Moderation.Where(x => playerIds.Contains(x.PlayerId) && x.IsBan).ToListAsync(); + + + + var expiredBans = moderation.Where(m => m.ExpireDuration < DateTime.UtcNow).ToList(); + foreach (var expired in expiredBans) + { + db.Moderation.Remove(expired); + moderation.Remove(expired); + } + if (expiredBans.Count > 0) await db.SaveChangesAsync(); + var activeBans = moderation.ToDictionary(x => x.PlayerId); + + + var hardwareBans = await db.HardwareBans.ToListAsync(); + var ipBans = await db.IpBans.ToListAsync(); + var accountCreationBlocks = await db.AccountCreationBlocks.ToListAsync(); + var settings = await db.PlayerSettings.Where(x => playerIds.Contains(x.AccountId)).ToDictionaryAsync(x => x.AccountId); + var currencies = await db.Currency.Where(x => playerIds.Contains(x.AccountId)).ToDictionaryAsync(x => x.AccountId); + var cached = await db.CachedLogins.Where(x => playerIds.Contains(x.accountId)).ToListAsync(); + + + + + + var connectedAccounts = new HashSet(Notify.SnapshotClients().Select(x => x.AccountId)); + + var rows = profiles.Select(profile => + { + players.TryGetValue(profile.accountId, out var player); + progression.TryGetValue(profile.accountId, out var progress); + reputations.TryGetValue(profile.accountId, out var rep); + presences.TryGetValue(profile.accountId, out var presence); + activeBans.TryGetValue(profile.accountId, out var ban); + var progressJson = progress?.GetProgressionJson(); + + + + + + var hardwareBan = player != null && !string.IsNullOrWhiteSpace(player.DeviceId) + ? hardwareBans.FirstOrDefault(h => h.DeviceId == player.DeviceId && (!h.ExpiresAt.HasValue || h.ExpiresAt.Value >= DateTime.UtcNow)) + : null; + var ipBan = player != null && !string.IsNullOrWhiteSpace(player.LastIpAddress) + ? ipBans.FirstOrDefault(ip => ip.IpAddress == player.LastIpAddress && (!ip.ExpiresAt.HasValue || ip.ExpiresAt.Value >= DateTime.UtcNow)) + : null; + var ipBanned = ipBan != null; + + + + var accountCreationBlocked = player != null && !string.IsNullOrWhiteSpace(player.LastIpAddress) + ? accountCreationBlocks.Any(b => b.IpAddress == player.LastIpAddress && (!b.ExpiresAt.HasValue || b.ExpiresAt.Value >= DateTime.UtcNow)) + : false; + + settings.TryGetValue(profile.accountId, out var playerSettings); + currencies.TryGetValue(profile.accountId, out var currency); + var login = cached.FirstOrDefault(x => x.accountId == profile.accountId); + + var birthday = player?.Birthday; + var hasRealBirthday = birthday != null && birthday.Value.Year > 1900; + var age = hasRealBirthday ? (int?)Math.Floor((DateTime.UtcNow - birthday!.Value).TotalDays / 365.2425) : null; + var settingsList = playerSettings?.DeserializeSettings() ?? new List(); + var settingsMap = settingsList + .Where(x => !string.IsNullOrWhiteSpace(x.Key)) + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Last().Value); + + + var nameHistoryList = new List(); + if (settingsMap.TryGetValue("NameHistory", out var nhRaw)) + { + try { nameHistoryList = nhRaw.FromJson>() ?? new List(); } catch { nameHistoryList = new List(); } + } + + bool isOnline = presence?.isOnline == true && connectedAccounts.Contains(profile.accountId); + + return new + { + id = profile.accountId, + name = profile.displayName, + original_name = profile.username, + username = profile.username, + platform = login?.platform.ToString() ?? "Steam", + platform_id = login?.platformId ?? "", + joined_at = DateTime.SpecifyKind(profile.createdAt, DateTimeKind.Utc), + last_online_at = isOnline ? DateTime.UtcNow : DateTime.SpecifyKind(login?.lastLoginTime ?? profile.createdAt, DateTimeKind.Utc), + bio = profile.bio, + email = player?.Email ?? "", + level = progress?.Level ?? 0, + xp = progress?.XP ?? 0, + last_xp_earned_at = progressJson?.LastXpEarnedAt, + xp_earned_today = progressJson?.XpEarnedToday ?? 0, + reputation = rep?.ReputationScore ?? Reputation.DefaultReputation, + + + trust = rep?.Trust ?? "Trusted", + report_count = rep?.ReportCount ?? 0, + noteriety = rep?.Noteriety ?? 0, + cheer_general = rep?.CheerGeneral ?? 0, + cheer_helpful = rep?.CheerHelpful ?? 0, + cheer_great_host = rep?.CheerGreatHost ?? 0, + cheer_sportsman = rep?.CheerSportsman ?? 0, + cheer_creative = rep?.CheerCreative ?? 0, + cheer_credit = rep?.CheerCredit ?? 0, + subscriber_count = 0, + subscribed_count = 0, + name_history = nameHistoryList, + settings = settingsMap, + + + avatar_image = ImageUrl(profile.profileImage), + avatar_color = (string?)null, + is_online = isOnline, + online = isOnline, + banned = ban != null || hardwareBan != null || ipBan != null, + ban_reason = ban?.Message ?? hardwareBan?.Reason ?? ipBan?.Reason ?? "", + ban_type = ban != null + ? (ban.ExpireDuration >= DateTime.MaxValue.AddYears(-10) || ban.ExpireDuration - DateTime.UtcNow > TimeSpan.FromDays(365 * 10) + ? "Permanent" + : "Temporary") + : (hardwareBan != null + ? (hardwareBan.ExpiresAt.HasValue ? "Temporary" : "Permanent") + : (ipBan != null ? (ipBan.ExpiresAt.HasValue ? "Temporary" : "Permanent") : "Temporary")), + + + ban_until = ban != null + ? DateTime.SpecifyKind(ban.ExpireDuration, DateTimeKind.Utc) + : (hardwareBan != null && hardwareBan.ExpiresAt.HasValue + ? DateTime.SpecifyKind(hardwareBan.ExpiresAt.Value, DateTimeKind.Utc) + : (ipBan != null && ipBan.ExpiresAt.HasValue + ? DateTime.SpecifyKind(ipBan.ExpiresAt.Value, DateTimeKind.Utc) + : (DateTime?)null)), + ban_method = ban != null && !string.IsNullOrWhiteSpace(ban.BanMethod) + ? ban.BanMethod + : (hardwareBan != null ? "Hardware Ban" : (ipBan != null ? "IP Ban" : "None")), + + + ban_is_moderator = ban?.Message?.StartsWith("Moderator.", StringComparison.OrdinalIgnoreCase) == true, + ban_category = ban?.Category ?? (hardwareBan != null ? ReportCategory.Misc : (ipBan != null ? ReportCategory.Misc : ReportCategory.Unknown)), + appeal_code = ban?.AppealCode, + ban_action_type = ModerationPolicy.IsVoiceModeration(ban?.BanMethod) ? "VoiceModeration" : (ModerationPolicy.IsTimeout(ban?.BanMethod) ? "Timeout" : "Moderation"), + account_locked = ban != null && string.Equals(ban.BanMethod, "Account Lock", StringComparison.OrdinalIgnoreCase), + + ban_display_reason = ban?.Message != null && ModerationPolicy.IsVoiceModeration(ban.BanMethod) + ? $"{ModerationPolicy.AutomaticVoiceModerationBanText}\n\n{ban.Message}" + : ban?.Message, + ban_started_at = ban != null ? DateTime.SpecifyKind(ban.CreatedAt, DateTimeKind.Utc) : (hardwareBan != null ? DateTime.SpecifyKind(hardwareBan.CreatedAt, DateTimeKind.Utc) : (ipBan != null ? DateTime.SpecifyKind(ipBan.CreatedAt, DateTimeKind.Utc) : (DateTime?)null)), + ban_related_account_username = hardwareBan?.BannedAccountId.HasValue == true && hardwareBan.BannedAccountId.Value != profile.accountId + ? profiles.FirstOrDefault(p => p.accountId == hardwareBan.BannedAccountId.Value)?.username + : null, + ban_note = ban?.ModeratorNote, + ip_banned = ipBanned, + hardware_banned = hardwareBan != null, + account_creation_blocked = accountCreationBlocked, + + + device_class = player?.DeviceClass switch + { + DeviceClass.VR or DeviceClass.VRLow or DeviceClass.Quest2 => "VR", + DeviceClass.Screen or DeviceClass.Mobile => "Screen", + _ => "Screen" + }, + device_id = player?.DeviceId ?? "", + ip = player?.LastIpAddress ?? "", + is_junior = profile.isJunior, + jr_state = player?.JuniorState.ToString() ?? "N/A", + name_locked = profile.NameLocked, + bio_locked = profile.BioLocked, + parent_account_id = player?.Parent, + has_birthday = hasRealBirthday, + age = age, + is_parental_locked = settingsMap.ContainsKey("IsParentalLocked") && settingsMap["IsParentalLocked"] == "true", + is_coach = false, + coach_status = "", + roles = profile.role?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(r => r.Trim()).ToList() ?? new List(), + rec_tokens = currency?.RecTokens ?? 0, + laser_tag_tickets = currency?.LaserTagTickets ?? 0, + gold = currency?.Gold ?? 0, + dracula_silver = currency?.DraculaSilver ?? 0 + }; + }).Cast().ToList(); + + return OrderRows(rows, order, ascending); + } + + private async Task> GetStorefrontItems(string? eqCol, string? eqVal, string? order, bool ascending) + { + var storefrontId = eqCol == "storefront_id" && long.TryParse(eqVal, out var parsed) ? parsed : (long?)null; + + if (storefrontId is > 0 && StorefrontCatalog.StorefrontIds.Contains(storefrontId.Value)) + return OrderRows(CatalogStorefrontItems(storefrontId.Value), order, ascending); + + var query = db.GiftItems.AsQueryable(); + + + + + if (storefrontId != null) + query = query.Where(x => x.StorefrontId == storefrontId.Value); + else + query = query.Where(x => x.StorefrontId > 0); + + var giftDrops = await query.ToListAsync(); + var storefrontNames = GetStorefrontDisplayNames(); + + var rows = giftDrops.Select(g => new + { + id = g.GiftDropId, + name = g.Name, + start_time = g.StartTime, + end_time = g.EndTime, + storefront_id = g.StorefrontId, + storefront_name = storefrontNames.TryGetValue(g.StorefrontId, out var sfName) ? sfName : "", + description = g.Description, + currency_amount = g.CurrencyAmount, + xp_amount = g.XPAmount, + avatar_items = g.AvatarItems, + equipment_items = g.EquipmentItems, + consumable_items = g.ConsumableItems, + purchasable_item_id = g.PurchasableItemId, + is_featured = g.IsFeatured, + price = g.Price, + currency_type = g.CurrencyType.ToString(), + rarity = g.Rarity.ToString(), + context = g.Context.ToString(), + avatar_item_type = g.AvatarItemType.ToString(), + equipment_modification_guid = g.EquipmentModificationGuid, + tooltip = g.Tooltip, + is_query = g.IsQuery, + unique = g.Unique, + subscribers_only = g.SubscribersOnly, + item_set_id = g.ItemSetId, + item_set_friendly_name = g.ItemSetFriendlyName + }).Cast().ToList(); + + return OrderRows(rows, order, ascending); + } + + private static List CatalogStorefrontItems(long storefrontId) + { + var storefrontName = StorefrontDisplayName(storefrontId); + var rows = new List(); + + foreach (var item in StorefrontCatalog.GetItems(storefrontId)) + { + var gift = item.GiftDrop; + var price = item.Prices.FirstOrDefault() ?? item.SubscriberPrices.FirstOrDefault(); + + rows.Add(new + { + id = item.PurchasableItemId, + name = gift?.FriendlyName ?? "", + start_time = (DateTime?)null, + end_time = (DateTime?)null, + storefront_id = storefrontId, + storefront_name = storefrontName, + description = "", + currency_amount = gift?.Currency ?? 0, + xp_amount = gift?.Xp ?? 0, + avatar_items = gift?.AvatarItemDesc ?? "", + equipment_items = gift?.EquipmentPrefabName ?? "", + consumable_items = gift?.ConsumableItemDesc ?? "", + gift_drop_id = gift?.GiftDropId ?? 0, + purchasable_item_id = item.PurchasableItemId, + is_featured = item.IsFeatured, + price = price?.Price ?? 0, + currency_type = (price?.CurrencyType ?? CurrencyType.RecCenterTokens).ToString(), + rarity = (gift?.Rarity ?? Rarity.Common).ToString(), + context = (gift?.Context ?? GiftContext.Commerce_Purchase).ToString(), + avatar_item_type = (gift?.AvatarItemType ?? AvatarItemType.Outfit).ToString(), + equipment_modification_guid = gift?.EquipmentModificationGuid ?? "", + tooltip = gift?.Tooltip ?? "", + is_query = gift?.IsQuery ?? false, + unique = gift?.Unique ?? false, + subscribers_only = gift?.SubscribersOnly ?? false, + item_set_id = gift?.ItemSetId ?? 0, + item_set_friendly_name = gift?.ItemSetFriendlyName ?? "", + is_shipped = true + }); + } + + return rows; + } + + + + + + private async Task> GetGiftDrops(string? order, bool ascending) + { + var giftDrops = await db.GiftItems.ToListAsync(); + + + + var storefrontNames = GetStorefrontDisplayNames(); + + var rows = giftDrops.Select(g => new + { + id = g.GiftDropId, + name = g.Name, + start_time = g.StartTime, + end_time = g.EndTime, + storefront_id = g.StorefrontId, + storefront_name = storefrontNames.TryGetValue(g.StorefrontId, out var sfName) ? sfName : "", + description = g.Description, + currency_amount = g.CurrencyAmount, + xp_amount = g.XPAmount, + avatar_items = g.AvatarItems, + equipment_items = g.EquipmentItems, + consumable_items = g.ConsumableItems, + purchasable_item_id = g.PurchasableItemId, + is_featured = g.IsFeatured, + price = g.Price, + currency_type = g.CurrencyType.ToString(), + rarity = g.Rarity.ToString(), + context = g.Context.ToString(), + avatar_item_type = g.AvatarItemType.ToString(), + equipment_modification_guid = g.EquipmentModificationGuid, + tooltip = g.Tooltip, + is_query = g.IsQuery, + unique = g.Unique, + subscribers_only = g.SubscribersOnly, + item_set_id = g.ItemSetId, + item_set_friendly_name = g.ItemSetFriendlyName + }).Cast().ToList(); + + return OrderRows(rows, order, ascending); + } + + + + + private static readonly JsonSerializerOptions CatalogJsonOptions = new() + { + PropertyNameCaseInsensitive = true, + Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } + }; + + private List GetEquipmentItems() + { + var defaultItems = DBConfig.DefaultEquipmentItems.FromJson>(CatalogJsonOptions) ?? new List(); + var rows = defaultItems + .Where(x => x != null && !string.IsNullOrWhiteSpace(x.PrefabName)) + .Select(item => new + { + equipment_prefab_name = item.PrefabName, + modification_guid = item.ModificationGuid ?? "", + friendly_name = string.IsNullOrWhiteSpace(item.FriendlyName) ? item.PrefabName : item.FriendlyName, + tooltip = item.Tooltip ?? "", + rarity = item.Rarity.ToString() + }).ToList(); + if (rows.Count > 0) + return rows.Cast().ToList(); + + var legacyItems = DBConfig.DefaultEquipmentItems.FromJson>(CatalogJsonOptions) ?? new List(); + return legacyItems + .Where(x => x != null && !string.IsNullOrWhiteSpace(x.EquipmentPrefabName)) + .Select(item => new + { + equipment_prefab_name = item.EquipmentPrefabName, + modification_guid = "", + friendly_name = string.IsNullOrWhiteSpace(item.FriendlyName) ? item.EquipmentPrefabName : item.FriendlyName, + tooltip = item.Tooltip ?? "", + rarity = item.Rarity.ToString() + }).Cast().ToList(); + } + + private List GetConsumableItems() + { + var items = DBConfig.DefaultConsumableItems.FromJson>(CatalogJsonOptions) ?? new List(); + return items + .Where(x => x != null && !string.IsNullOrWhiteSpace(x.ConsumableItemDesc)) + .Select(item => new + { + consumable_item_desc = item.ConsumableItemDesc, + friendly_name = string.IsNullOrWhiteSpace(item.FriendlyName) ? item.ConsumableItemDesc : item.FriendlyName, + tooltip = item.Tooltip ?? "", + rarity = item.Rarity.ToString() + }).Cast().ToList(); + } + + private async Task> GetIpBans(string? order, bool ascending) + { + + var now = DateTime.UtcNow; + var expired = await db.IpBans.Where(x => x.ExpiresAt != null && x.ExpiresAt < now).ToListAsync(); + if (expired.Count > 0) + { + db.IpBans.RemoveRange(expired); + await db.SaveChangesAsync(); + } + + var rows = await db.IpBans.AsNoTracking() + .Select(x => new + { + id = x.Id, + ip = x.IpAddress, + reason = x.Reason, + created_at = x.CreatedAt, + expires_at = x.ExpiresAt, + is_permanent = x.ExpiresAt == null + }) + .ToListAsync(); + + return OrderRows(rows.Cast().ToList(), order, ascending); + } + + private async Task> GetHardwareBans(string? order, bool ascending) + { + + var now = DateTime.UtcNow; + var expired = await db.HardwareBans.Where(x => x.ExpiresAt != null && x.ExpiresAt < now).ToListAsync(); + if (expired.Count > 0) + { + db.HardwareBans.RemoveRange(expired); + await db.SaveChangesAsync(); + } + + var rows = await db.HardwareBans.AsNoTracking() + .Select(x => new + { + id = x.Id, + device_id = x.DeviceId, + reason = x.Reason, + created_at = x.CreatedAt, + expires_at = x.ExpiresAt, + is_permanent = x.ExpiresAt == null + }) + .ToListAsync(); + + return OrderRows(rows.Cast().ToList(), order, ascending); + } + + private async Task AddIpBan(JsonElement payload) + { + var ip = GetJsonString(payload, "ip") ?? GetJsonString(payload, "ip_address") ?? ""; + var reason = GetJsonString(payload, "reason") ?? "Banned via admin panel"; + var expiresAt = TryGetJsonDate(payload, "expires_at"); + + var ipBan = new IpBan + { + IpAddress = ip, + Reason = reason, + CreatedAt = DateTime.UtcNow, + + ExpiresAt = expiresAt + }; + + await db.IpBans.AddAsync(ipBan); + await db.SaveChangesAsync(); + + return new JsonObject + { + ["id"] = ipBan.Id, + ["ip"] = ipBan.IpAddress, + ["reason"] = ipBan.Reason, + ["created_at"] = ipBan.CreatedAt, + ["expires_at"] = ipBan.ExpiresAt, + ["is_permanent"] = ipBan.ExpiresAt == null + }; + } + + private async Task AddHardwareBan(JsonElement payload) + { + var deviceId = GetJsonString(payload, "device_id") ?? ""; + var reason = GetJsonString(payload, "reason") ?? "Banned via admin panel"; + var expiresAt = TryGetJsonDate(payload, "expires_at"); + + var hardwareBan = new HardwareBan + { + DeviceId = deviceId, + Reason = reason, + CreatedAt = DateTime.UtcNow, + + ExpiresAt = expiresAt + }; + + await db.HardwareBans.AddAsync(hardwareBan); + await db.SaveChangesAsync(); + + return new JsonObject + { + ["id"] = hardwareBan.Id, + ["device_id"] = hardwareBan.DeviceId, + ["reason"] = hardwareBan.Reason, + ["created_at"] = hardwareBan.CreatedAt, + ["expires_at"] = hardwareBan.ExpiresAt, + ["is_permanent"] = hardwareBan.ExpiresAt == null + }; + } + + private async Task> GetAccountCreationBlocks(string? order, bool ascending) + { + + var now = DateTime.UtcNow; + var expired = await db.AccountCreationBlocks.Where(x => x.ExpiresAt != null && x.ExpiresAt < now).ToListAsync(); + if (expired.Count > 0) + { + db.AccountCreationBlocks.RemoveRange(expired); + await db.SaveChangesAsync(); + } + + var rows = await db.AccountCreationBlocks.AsNoTracking() + .Select(x => new + { + id = x.Id, + ip = x.IpAddress, + reason = x.Reason, + created_at = x.CreatedAt, + expires_at = x.ExpiresAt, + is_permanent = x.ExpiresAt == null + }) + .ToListAsync(); + + return OrderRows(rows.Cast().ToList(), order, ascending); + } + + private async Task AddAccountCreationBlock(JsonElement payload) + { + var ip = GetJsonString(payload, "ip") ?? GetJsonString(payload, "ip_address") ?? ""; + var reason = GetJsonString(payload, "reason") ?? "Blocked via admin panel"; + var expiresAt = TryGetJsonDate(payload, "expires_at"); + + var block = new AccountCreationBlock + { + IpAddress = ip, + Reason = reason, + CreatedAt = DateTime.UtcNow, + ExpiresAt = expiresAt + }; + + await db.AccountCreationBlocks.AddAsync(block); + await db.SaveChangesAsync(); + + return new JsonObject + { + ["id"] = block.Id, + ["ip"] = block.IpAddress, + ["reason"] = block.Reason, + ["created_at"] = block.CreatedAt, + ["expires_at"] = block.ExpiresAt, + ["is_permanent"] = block.ExpiresAt == null + }; + } + + private async Task> GetGameBlocks(string? order, bool ascending) + { + + var now = DateTime.UtcNow; + var expired = await db.GameBlocks.Where(x => x.ExpiresAt != null && x.ExpiresAt < now).ToListAsync(); + if (expired.Count > 0) + { + db.GameBlocks.RemoveRange(expired); + await db.SaveChangesAsync(); + } + + var rows = await db.GameBlocks.AsNoTracking() + .Select(x => new + { + id = x.Id, + ip = x.IpAddress, + reason = x.Reason, + created_at = x.CreatedAt, + expires_at = x.ExpiresAt, + is_permanent = x.ExpiresAt == null + }) + .ToListAsync(); + + return OrderRows(rows.Cast().ToList(), order, ascending); + } + + private async Task AddGameBlock(JsonElement payload) + { + var ip = GameBlockMonitor.NormalizeIp(GetJsonString(payload, "ip") ?? GetJsonString(payload, "ip_address") ?? ""); + if (string.IsNullOrWhiteSpace(ip)) + return BadRequest("An IP address is required to create a game block."); + var reason = GetJsonString(payload, "reason") ?? "Blocked via admin panel"; + var expiresAt = TryGetJsonDate(payload, "expires_at"); + + var block = new GameBlock + { + IpAddress = ip, + Reason = reason, + CreatedAt = DateTime.UtcNow, + + ExpiresAt = expiresAt + }; + + await db.GameBlocks.AddAsync(block); + await db.SaveChangesAsync(); + + + + GameBlockMonitor.Invalidate(); + + return new JsonObject + { + ["id"] = block.Id, + ["ip"] = block.IpAddress, + ["reason"] = block.Reason, + ["created_at"] = block.CreatedAt, + ["expires_at"] = block.ExpiresAt, + ["is_permanent"] = block.ExpiresAt == null + }; + } + + + + + + + + private async Task> GetLeaderboardScores(string? eqCol, string? eqVal, string? order, bool ascending) + { + var query = db.LeaderboardScores.AsNoTracking(); + if (eqCol == "room_id" && long.TryParse(eqVal, out var roomId)) + query = query.Where(x => x.RoomId == roomId); + + var rows = await query.OrderByDescending(x => x.SubmittedAt).Take(5000).ToListAsync(); + var playerNames = await LoadPlayerNames(rows.Select(x => x.PlayerId).Distinct().ToList()); + var roomIds = rows.Select(x => x.RoomId).Distinct().ToList(); + var rooms = await db.Rooms.AsNoTracking() + .Where(r => roomIds.Contains(r.RoomId)) + .ToDictionaryAsync(r => r.RoomId, r => r.Name); + + var result = rows.Select(s => (object)new + { + id = s.Id, + room_id = s.RoomId, + room_name = rooms.TryGetValue(s.RoomId, out var roomName) ? roomName : $"Room {s.RoomId}", + sub_room_id = s.SubRoomId, + player_id = s.PlayerId, + player_name = playerNames.GetValueOrDefault(s.PlayerId) ?? $"Player {s.PlayerId}", + stat_channel = s.StatChannel, + score = s.Score, + time = s.Time, + submitted_at = DateTime.SpecifyKind(s.SubmittedAt, DateTimeKind.Utc) + }).ToList(); + + return OrderRows(result, order, ascending); + } + + + + private async Task> LoadPlayerNames(List playerIds) + { + if (playerIds.Count == 0) return new Dictionary(); + + var profiles = await db.Profiles.AsNoTracking() + .Where(x => playerIds.Contains(x.accountId)) + .ToDictionaryAsync(x => x.accountId); + + return profiles.ToDictionary( + kv => kv.Key, + kv => kv.Value.displayName ?? kv.Value.username ?? $"Player {kv.Key}"); + } + + + + + + + + private List GetStorefronts(string? order, bool ascending) + { + var state = LoadState(); + + + + + var rows = new List(); + var seen = new HashSet(); + + foreach (var id in StorefrontCatalog.StorefrontIds) + { + seen.Add(id); + var existing = state.Storefronts.FirstOrDefault(s => (s["id"]?.GetValue() ?? 0) == id); + + rows.Add(StorefrontRow(id, StorefrontLabel(id), existing, StorefrontCatalog.GetItems(id).Count, shipped: true)); + } + + foreach (var sf in state.Storefronts) + { + var id = sf["id"]?.GetValue() ?? 0; + if (id <= 0 || !seen.Add(id)) continue; + + rows.Add(StorefrontRow(id, sf["type"]?.ToString(), sf, db.GiftItems.Count(x => x.StorefrontId == id), shipped: false)); + } + + foreach (var id in db.GiftItems.Where(x => x.StorefrontId > 0).Select(x => x.StorefrontId).Distinct().ToList()) + { + if (!seen.Add(id)) continue; + + rows.Add(StorefrontRow(id, null, null, db.GiftItems.Count(x => x.StorefrontId == id), shipped: false)); + } + + return OrderRows(rows, order, ascending); + } + + private static object StorefrontRow(long id, string? type, JsonObject? stateRow, int giftDropCount, bool shipped) + { + var label = string.IsNullOrWhiteSpace(type) ? StorefrontLabel(id) : type; + var friendlyName = stateRow?["friendly_name"]?.ToString(); + + return new + { + id, + type = label, + next_update = stateRow?["next_update"]?.ToString() ?? "", + new_until = stateRow?["new_until"]?.ToString() ?? "", + friendly_name = string.IsNullOrWhiteSpace(friendlyName) ? label : friendlyName, + gift_drop_count = giftDropCount, + is_auto_detected = shipped + }; + } + + private static string? StorefrontTypeName(long id) + { + if (id <= 0 || id > int.MaxValue) return null; + if (id == (long)StorefrontTypes.None) return null; + if (!Enum.IsDefined(typeof(StorefrontTypes), (int)id)) return null; + + return ((StorefrontTypes)id).ToString(); + } + + private static string StorefrontLabel(long id) => StorefrontTypeName(id) ?? $"#{id}"; + + private static string StorefrontDisplayName(long id) => + StorefrontTypeName(id) is { } name ? $"{name} ({id})" : $"#{id}"; + + + + + + private Dictionary GetStorefrontDisplayNames() + { + var names = new Dictionary(); + + foreach (var type in Enum.GetValues()) + { + if (type == StorefrontTypes.None) continue; + names[(long)type] = $"{type} ({(long)type})"; + } + + foreach (var id in StorefrontCatalog.StorefrontIds) + names[id] = StorefrontDisplayName(id); + + var state = LoadState(); + foreach (var sf in state.Storefronts) + { + var id = sf["id"]?.GetValue() ?? 0; + if (id > 0 && !names.ContainsKey(id)) + { + names[id] = $"{sf["type"]?.ToString() ?? $"#{id}"} ({id})"; + } + } + + foreach (var id in db.GiftItems.Where(x => x.StorefrontId > 0).Select(x => x.StorefrontId).Distinct().ToList()) + names.TryAdd(id, StorefrontDisplayName(id)); + + return names; + } + + private List GetChallenges(string? order, bool ascending) + { + var state = LoadState(); + return OrderRows(state.Challenges.Cast().ToList(), order, ascending); + } + + private List FilterByPlayer(List rows, string? eqCol, string? eqVal, string? order, bool ascending) + { + if (!string.IsNullOrWhiteSpace(eqCol) && eqCol == "player_id" && eqVal != null) + { + rows = rows.Where(x => x["player_id"]?.ToString() == eqVal).ToList(); + } + return OrderRows(rows.Cast().ToList(), order, ascending); + } + + private static List OrderRows(List rows, string? order, bool ascending) + { + if (string.IsNullOrWhiteSpace(order)) + return rows; + + var sorted = rows.OrderBy(x => 0); + if (rows.Count > 0 && rows[0] is JsonObject rowObj) + { + var first = rows[0] as JsonObject; + if (first != null && first.ContainsKey(order)) + { + sorted = ascending + ? rows.OrderBy(x => (x as JsonObject)?[order]?.ToString()) + : rows.OrderByDescending(x => (x as JsonObject)?[order]?.ToString()); + } + } + return sorted.ToList(); + } + + private static JsonObject AddStorefront(AdminPanelState state, JsonElement payload) + { + var row = new JsonObject(); + foreach (var prop in payload.EnumerateObject()) + { + try + { + row[prop.Name] = JsonNode.Parse(prop.Value.GetRawText()); + } + catch + { + + if (prop.Value.ValueKind == JsonValueKind.String) + row[prop.Name] = prop.Value.GetString(); + else if (prop.Value.ValueKind == JsonValueKind.Number) + row[prop.Name] = prop.Value.GetRawText(); + else if (prop.Value.ValueKind == JsonValueKind.True) + row[prop.Name] = true; + else if (prop.Value.ValueKind == JsonValueKind.False) + row[prop.Name] = false; + else if (prop.Value.ValueKind == JsonValueKind.Null) + row[prop.Name] = null; + else + row[prop.Name] = prop.Value.GetRawText(); + } + } + if (!row.ContainsKey("id")) + { + + + + + var rawType = row["type"]?.ToString() ?? ""; + long id = 0; + foreach (var type in Enum.GetValues()) + { + if (type == StorefrontTypes.None) continue; + if (string.Equals(type.ToString(), rawType, StringComparison.OrdinalIgnoreCase)) + { + id = (long)type; + break; + } + } + if (id == 0) + { + long maxId = 10000; + foreach (var existing in state.Storefronts) + { + var rawId = existing["id"]?.ToString(); + if (rawId != null && long.TryParse(rawId, out var parsedId) && parsedId >= maxId) + maxId = parsedId + 1; + } + id = maxId; + } + row["id"] = id; + } + state.Storefronts.Add(row); + SaveState(state); + return row; + } + + private static JsonObject AddChallenge(AdminPanelState state, JsonElement payload) + { + var row = new JsonObject(); + foreach (var prop in payload.EnumerateObject()) + { + try + { + row[prop.Name] = JsonNode.Parse(prop.Value.GetRawText()); + } + catch + { + if (prop.Value.ValueKind == JsonValueKind.String) + row[prop.Name] = prop.Value.GetString(); + else if (prop.Value.ValueKind == JsonValueKind.Number) + row[prop.Name] = prop.Value.GetRawText(); + else if (prop.Value.ValueKind == JsonValueKind.True) + row[prop.Name] = true; + else if (prop.Value.ValueKind == JsonValueKind.False) + row[prop.Name] = false; + else if (prop.Value.ValueKind == JsonValueKind.Null) + row[prop.Name] = null; + else + row[prop.Name] = prop.Value.GetRawText(); + } + } + if (!row.ContainsKey("id")) + { + row["id"] = state.Challenges.Count > 0 ? state.Challenges.Max(x => x["id"]?.GetValue() ?? 0) + 1 : 1; + } + state.Challenges.Add(row); + SaveState(state); + return row; + } + + + + + private static object RowsResponse(JsonElement payload, List rows) + { + if (payload.ValueKind == JsonValueKind.Array) + return rows; + return rows.Count > 0 ? rows[0] : new JsonObject(); + } + + + + + + + + private IActionResult InsertLoggedRows(AdminPanelState state, List targetList, JsonElement payload, string table) + { + var rows = AddRows(state, targetList, payload); + NotifyPanelActionDiscord(table, payload); + return Ok(RowsResponse(payload, rows)); + } + + + private async Task InsertGiftLogRows(AdminPanelState state, JsonElement payload) + { + var rows = await AddGiftLogRows(state, payload); + NotifyPanelActionDiscord("gift_log", payload); + return Ok(RowsResponse(payload, rows)); + } + + + + + + + private void NotifyPanelActionDiscord(string table, JsonElement payload) + { + var items = new List(); + if (payload.ValueKind == JsonValueKind.Array) + { + foreach (var el in payload.EnumerateArray()) items.Add(el); + } + else + { + items.Add(payload); + } + + foreach (var item in items) + { + if (item.ValueKind != JsonValueKind.Object) continue; + + var action = GetJsonString(item, "action") ?? table; + var admin = GetJsonString(item, "performed_by") ?? GetJsonString(item, "admin_id") ?? "Unknown"; + var playerId = GetJsonString(item, "player_id") ?? ""; + + string? details = null; + if (item.TryGetProperty("details", out var d)) + { + details = d.ValueKind == JsonValueKind.String ? d.GetString() : d.GetRawText(); + } + else if (table == "gift_log") + { + var giftParts = new List(); + foreach (var name in new[] { "context", "rarity", "avatar", "equip", "cons", "currency", "currency_type", "xp", "message" }) + { + var val = GetJsonString(item, name); + if (!string.IsNullOrWhiteSpace(val) && val != "null") + giftParts.Add($"{name}: {val}"); + } + details = string.Join(", ", giftParts); + } + + var fullDetails = string.IsNullOrWhiteSpace(playerId) ? details : $"Player {playerId} — {details}"; + AdminActionDiscord.LogAction(action, admin, fullDetails); + } + } + + + + + + + private async Task GetPanelAdminUsernameAsync() + { + if (Request.Cookies.TryGetValue(AdminSession.CookieName, out var token)) + { + var (ok, userId) = await AdminSession.ValidateAsync(db, token); + if (ok) + { + var user = await db.AdminUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId); + if (user != null) return user.Username; + } + } + return "Admin"; + } + + private static List AddRows(AdminPanelState state, List targetList, JsonElement payload) + { + + + + var items = new List(); + if (payload.ValueKind == JsonValueKind.Array) + { + foreach (var el in payload.EnumerateArray()) items.Add(el); + } + else + { + items.Add(payload); + } + + var added = new List(); + foreach (var item in items) + { + if (item.ValueKind != JsonValueKind.Object) continue; + + var row = new JsonObject(); + foreach (var prop in item.EnumerateObject()) + { + try + { + row[prop.Name] = JsonNode.Parse(prop.Value.GetRawText()); + } + catch + { + if (prop.Value.ValueKind == JsonValueKind.String) + row[prop.Name] = prop.Value.GetString(); + else if (prop.Value.ValueKind == JsonValueKind.Number) + row[prop.Name] = prop.Value.GetRawText(); + else if (prop.Value.ValueKind == JsonValueKind.True) + row[prop.Name] = true; + else if (prop.Value.ValueKind == JsonValueKind.False) + row[prop.Name] = false; + else if (prop.Value.ValueKind == JsonValueKind.Null) + row[prop.Name] = null; + else + row[prop.Name] = prop.Value.GetRawText(); + } + } + if (!row.ContainsKey("id")) + { + + + long nextId = 1; + if (targetList.Count > 0) + { + foreach (var existing in targetList) + { + var rawId = existing["id"]?.ToString(); + if (rawId != null && long.TryParse(rawId, out var parsedId) && parsedId >= nextId) + nextId = parsedId + 1; + } + } + row["id"] = nextId; + } + if (!row.ContainsKey("created_at")) + row["created_at"] = DateTime.UtcNow.ToString("O"); + targetList.Add(row); + added.Add(row); + } + SaveState(state); + return added; + } + + private static JsonObject UpdateRow(AdminPanelState state, List targetList, long id, JsonElement payload) + { + var row = targetList.FirstOrDefault(x => x["id"]?.GetValue() == id); + if (row == null) + return new JsonObject { ["error"] = "Row not found" }; + + foreach (var prop in payload.EnumerateObject()) + { + try + { + row[prop.Name] = JsonNode.Parse(prop.Value.GetRawText()); + } + catch + { + if (prop.Value.ValueKind == JsonValueKind.String) + row[prop.Name] = prop.Value.GetString(); + else if (prop.Value.ValueKind == JsonValueKind.Number) + row[prop.Name] = prop.Value.GetRawText(); + else if (prop.Value.ValueKind == JsonValueKind.True) + row[prop.Name] = true; + else if (prop.Value.ValueKind == JsonValueKind.False) + row[prop.Name] = false; + else if (prop.Value.ValueKind == JsonValueKind.Null) + row[prop.Name] = null; + else + row[prop.Name] = prop.Value.GetRawText(); + } + } + SaveState(state); + return row; + } + + private static List DeleteRows(AdminPanelState state, List targetList, List ids) + { + var toRemove = targetList.Where(x => ids.Contains(x["id"]?.GetValue() ?? 0)).ToList(); + foreach (var item in toRemove) + { + targetList.Remove(item); + } + SaveState(state); + return toRemove; + } + + private static List ParseIds(string? vals) + { + if (string.IsNullOrWhiteSpace(vals)) + return new List(); + + return vals.Split(',') + .Select(x => long.TryParse(x.Trim(), out var id) ? id : 0) + .Where(x => x > 0) + .ToList(); + } + + private async Task CreateGiftDrop(JsonElement payload) + { + + + var giftDrop = new GiftDropDB + { + Name = GetJsonString(payload, "name") ?? "Gift Drop", + StartTime = TryGetJsonDate(payload, "start_time") ?? DateTime.UtcNow, + EndTime = TryGetJsonDate(payload, "end_time") ?? DateTime.UtcNow.AddDays(30), + StorefrontId = TryGetJsonLong(payload, "storefront_id") ?? 0, + Description = GetJsonString(payload, "description") ?? GetJsonString(payload, "tooltip") ?? "", + CurrencyAmount = TryGetJsonInt(payload, "currency_amount") ?? TryGetJsonInt(payload, "currency") ?? 0, + XPAmount = TryGetJsonInt(payload, "xp_amount") ?? 0, + AvatarItems = GetJsonString(payload, "avatar_items") ?? GetJsonString(payload, "avatar") ?? "", + EquipmentItems = GetJsonString(payload, "equipment_items") ?? GetJsonString(payload, "equip") ?? "", + ConsumableItems = GetJsonString(payload, "consumable_items") ?? GetJsonString(payload, "consumable") ?? "", + PurchasableItemId = TryGetJsonInt(payload, "purchasable_item_id") ?? 0, + IsFeatured = TryGetJsonBool(payload, "is_featured") ?? TryGetJsonBool(payload, "featured") ?? false, + Price = TryGetJsonInt(payload, "price") ?? 0, + CurrencyType = ParseCurrencyType(GetJsonString(payload, "currency_type")) ?? CurrencyType.RecCenterTokens, + Rarity = ParseRarity(GetJsonString(payload, "rarity")) ?? Rarity.Common, + Context = ParseGiftContext(GetJsonString(payload, "context")) ?? GiftContext.Game_Drop, + AvatarItemType = ParseAvatarItemType(GetJsonString(payload, "avatar_item_type")) ?? GetJsonString(payload, "item_type") switch + { + "HairDye" => AvatarItemType.HairDye, + _ => AvatarItemType.Outfit + }, + EquipmentModificationGuid = GetJsonString(payload, "equipment_modification_guid") ?? GetJsonString(payload, "prefab_modify") ?? "", + Tooltip = GetJsonString(payload, "tooltip") ?? "", + IsQuery = TryGetJsonBool(payload, "is_query") ?? false, + Unique = TryGetJsonBool(payload, "unique") ?? false, + SubscribersOnly = TryGetJsonBool(payload, "subscribers_only") ?? TryGetJsonBool(payload, "subonly") ?? false, + ItemSetId = TryGetJsonLong(payload, "item_set_id") ?? 0, + ItemSetFriendlyName = GetJsonString(payload, "item_set_friendly_name") ?? "" + }; + + db.GiftItems.Add(giftDrop); + await db.SaveChangesAsync(); + + + + + if (giftDrop.PurchasableItemId == 0) + { + giftDrop.PurchasableItemId = (int)(giftDrop.GiftDropId % int.MaxValue); + await db.SaveChangesAsync(); + } + + return GiftDropToJson(giftDrop); + } + + private async Task UpdateGiftDrop(long id, JsonElement payload) + { + var giftDrop = await db.GiftItems.FirstOrDefaultAsync(x => x.GiftDropId == id); + if (giftDrop == null) + return new JsonObject { ["error"] = "Gift drop not found" }; + + ApplyGiftDropPayload(giftDrop, payload); + + + + if (giftDrop.PurchasableItemId == 0) + giftDrop.PurchasableItemId = (int)(giftDrop.GiftDropId % int.MaxValue); + + await db.SaveChangesAsync(); + + return GiftDropToJson(giftDrop); + } + + private static void ApplyGiftDropPayload(GiftDropDB giftDrop, JsonElement payload) + { + var name = GetJsonString(payload, "name"); + if (name != null) giftDrop.Name = name; + + var startTime = TryGetJsonDate(payload, "start_time"); + if (startTime != null) giftDrop.StartTime = startTime.Value; + + var endTime = TryGetJsonDate(payload, "end_time"); + if (endTime != null) giftDrop.EndTime = endTime.Value; + + var storefrontId = TryGetJsonLong(payload, "storefront_id"); + if (storefrontId != null) giftDrop.StorefrontId = storefrontId.Value; + + var description = GetJsonString(payload, "description") ?? GetJsonString(payload, "tooltip"); + if (description != null) giftDrop.Description = description; + + var currencyAmount = TryGetJsonInt(payload, "currency_amount") ?? TryGetJsonInt(payload, "currency"); + if (currencyAmount != null) giftDrop.CurrencyAmount = currencyAmount.Value; + + var xpAmount = TryGetJsonInt(payload, "xp_amount"); + if (xpAmount != null) giftDrop.XPAmount = xpAmount.Value; + + var avatarItems = GetJsonString(payload, "avatar_items") ?? GetJsonString(payload, "avatar"); + if (avatarItems != null) giftDrop.AvatarItems = avatarItems; + + var equipmentItems = GetJsonString(payload, "equipment_items") ?? GetJsonString(payload, "equip"); + if (equipmentItems != null) giftDrop.EquipmentItems = equipmentItems; + + var consumableItems = GetJsonString(payload, "consumable_items") ?? GetJsonString(payload, "consumable"); + if (consumableItems != null) giftDrop.ConsumableItems = consumableItems; + + var purchasableItemId = TryGetJsonInt(payload, "purchasable_item_id"); + if (purchasableItemId != null && purchasableItemId > 0) giftDrop.PurchasableItemId = purchasableItemId.Value; + + var isFeatured = TryGetJsonBool(payload, "is_featured") ?? TryGetJsonBool(payload, "featured"); + if (isFeatured != null) giftDrop.IsFeatured = isFeatured.Value; + + var price = TryGetJsonInt(payload, "price"); + if (price != null) giftDrop.Price = price.Value; + + var currencyType = ParseCurrencyType(GetJsonString(payload, "currency_type")); + if (currencyType != null) giftDrop.CurrencyType = currencyType.Value; + + var rarity = ParseRarity(GetJsonString(payload, "rarity")); + if (rarity != null) giftDrop.Rarity = rarity.Value; + + var context = ParseGiftContext(GetJsonString(payload, "context")); + if (context != null) giftDrop.Context = context.Value; + + var avatarItemType = ParseAvatarItemType(GetJsonString(payload, "avatar_item_type")); + if (avatarItemType != null) giftDrop.AvatarItemType = avatarItemType.Value; + + var equipMod = GetJsonString(payload, "equipment_modification_guid") ?? GetJsonString(payload, "prefab_modify"); + if (equipMod != null) giftDrop.EquipmentModificationGuid = equipMod; + + var tooltip = GetJsonString(payload, "tooltip"); + if (tooltip != null) giftDrop.Tooltip = tooltip; + + var isQuery = TryGetJsonBool(payload, "is_query"); + if (isQuery != null) giftDrop.IsQuery = isQuery.Value; + + var unique = TryGetJsonBool(payload, "unique"); + if (unique != null) giftDrop.Unique = unique.Value; + + var subscribersOnly = TryGetJsonBool(payload, "subscribers_only") ?? TryGetJsonBool(payload, "subonly"); + if (subscribersOnly != null) giftDrop.SubscribersOnly = subscribersOnly.Value; + + var itemSetId = TryGetJsonLong(payload, "item_set_id"); + if (itemSetId != null) giftDrop.ItemSetId = itemSetId.Value; + + var itemSetName = GetJsonString(payload, "item_set_friendly_name"); + if (itemSetName != null) giftDrop.ItemSetFriendlyName = itemSetName; + } + + private static JsonObject GiftDropToJson(GiftDropDB g) + { + return new JsonObject + { + ["id"] = g.GiftDropId, + ["name"] = g.Name, + ["start_time"] = g.StartTime, + ["end_time"] = g.EndTime, + ["storefront_id"] = g.StorefrontId, + ["description"] = g.Description, + ["currency_amount"] = g.CurrencyAmount, + ["xp_amount"] = g.XPAmount, + ["avatar_items"] = g.AvatarItems, + ["equipment_items"] = g.EquipmentItems, + ["consumable_items"] = g.ConsumableItems, + ["purchasable_item_id"] = g.PurchasableItemId, + ["is_featured"] = g.IsFeatured, + ["price"] = g.Price, + ["currency_type"] = g.CurrencyType.ToString(), + ["rarity"] = g.Rarity.ToString(), + ["context"] = g.Context.ToString(), + ["avatar_item_type"] = g.AvatarItemType.ToString(), + ["equipment_modification_guid"] = g.EquipmentModificationGuid, + ["tooltip"] = g.Tooltip, + ["is_query"] = g.IsQuery, + ["unique"] = g.Unique, + ["subscribers_only"] = g.SubscribersOnly, + ["item_set_id"] = g.ItemSetId, + ["item_set_friendly_name"] = g.ItemSetFriendlyName + }; + } + + private static string? GetJsonString(JsonElement el, string name) + { + if (el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String) + return v.GetString(); + return null; + } + + private static int? TryGetJsonInt(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v)) return null; + try + { + if (v.ValueKind == JsonValueKind.Number) return v.GetInt32(); + if (v.ValueKind == JsonValueKind.String && int.TryParse(v.GetString(), out var i)) return i; + } + catch { } + return null; + } + + private static long? TryGetJsonLong(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v)) return null; + try + { + if (v.ValueKind == JsonValueKind.Number) return v.GetInt64(); + if (v.ValueKind == JsonValueKind.String && long.TryParse(v.GetString(), out var l)) return l; + } + catch { } + return null; + } + + private static DateTime? TryGetJsonDate(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v)) return null; + try + { + if (v.ValueKind == JsonValueKind.String && DateTime.TryParse(v.GetString(), out var d)) return d.ToUniversalTime(); + } + catch { } + return null; + } + + private static bool? TryGetJsonBool(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v)) return null; + try + { + if (v.ValueKind == JsonValueKind.True) return true; + if (v.ValueKind == JsonValueKind.False) return false; + if (v.ValueKind == JsonValueKind.String && bool.TryParse(v.GetString(), out var b)) return b; + } + catch { } + return null; + } + + private static CurrencyType? ParseCurrencyType(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + if (Enum.TryParse(raw.Trim(), true, out var type) && Enum.IsDefined(type) && type != CurrencyType.Invalid) + return type; + return null; + } + + private static Rarity? ParseRarity(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + if (Enum.TryParse(raw.Trim(), true, out var rarity) && Enum.IsDefined(rarity)) + return rarity; + return null; + } + + private static GiftContext? ParseGiftContext(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + if (Enum.TryParse(raw.Trim(), true, out var context) && Enum.IsDefined(context)) + return context; + return null; + } + + private static AvatarItemType? ParseAvatarItemType(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + if (Enum.TryParse(raw.Trim(), true, out var type) && Enum.IsDefined(type)) + return type; + return null; + } + + + + private static int? TryGetJsonInt(JsonObject obj, string key) + { + if (obj[key] is not JsonValue v) return null; + try + { + if (v.TryGetValue(out var i)) return i; + if (v.TryGetValue(out var s) && int.TryParse(s, out var parsed)) return parsed; + if (v.TryGetValue(out var d)) return (int)d; + } + catch { } + return null; + } + + + + + private static long NewGiftDropId() + { + long value = BitConverter.ToInt64(Guid.NewGuid().ToByteArray(), 0); + return value switch + { + > 0 => value, + > long.MinValue => -value + 1, + _ => 1 + }; + } + + private static GiftContext? TryParseGiftContext(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + if (Enum.TryParse(raw.Trim(), true, out var context) && Enum.IsDefined(context)) + return context; + return null; + } + + private static Rarity? TryParseRarity(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + if (Enum.TryParse(raw.Trim(), true, out var rarity) && Enum.IsDefined(rarity)) + return rarity; + return null; + } + + private async Task UpdatePlayer(long id, JsonElement payload) + { + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (profile == null) return NotFound(); + + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == id); + var progress = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == id); + int? priorLevel = progress?.Level; + var rep = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == id); + var settings = await db.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == id); + + if (TryGetString(payload, "name", out var name)) profile.displayName = name; + if (TryGetString(payload, "original_name", out var username) || TryGetString(payload, "username", out username)) profile.username = username; + if (TryGetString(payload, "bio", out var bio)) profile.bio = bio; + + + if (payload.TryGetProperty("name_locked", out var nameLockedElem) && nameLockedElem.ValueKind == JsonValueKind.True) + profile.NameLocked = true; + else if (payload.TryGetProperty("name_locked", out nameLockedElem) && nameLockedElem.ValueKind == JsonValueKind.False) + profile.NameLocked = false; + if (payload.TryGetProperty("bio_locked", out var bioLockedElem) && bioLockedElem.ValueKind == JsonValueKind.True) + profile.BioLocked = true; + else if (payload.TryGetProperty("bio_locked", out bioLockedElem) && bioLockedElem.ValueKind == JsonValueKind.False) + profile.BioLocked = false; + + + + + if (TryGetString(payload, "password", out var newPassword) && !string.IsNullOrWhiteSpace(newPassword)) + { + if (player == null) return BadRequest("Player data not found - cannot reset password."); + player.Hashword = PasswordHasher.HashPassword(player, newPassword); + + + + + + var staleLogins = await db.CachedLogins.Where(x => x.accountId == id).ToListAsync(); + if (staleLogins.Count > 0) + { + db.CachedLogins.RemoveRange(staleLogins); + } + + + + + player.TokenVersion++; + TokenRevocation.Bump(id, player.TokenVersion); + + + + AdminActionDiscord.LogAction("Player Password Reset", await GetPanelAdminUsernameAsync(), + $"Player {id} ({(profile.displayName ?? profile.username) ?? id.ToString()})"); + + + + + await Notify.SendNotify(id, new Notification { Id = NotificationType.ModerationQuitGame, Msg = null }); + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == id); + if (presence != null) + { + presence.isOnline = false; + presence.roomInstance = null; + presence.LoginLock = null; + } + } + + + + + if (payload.TryGetProperty("avatar_image", out var avatarImageElem)) + { + if (avatarImageElem.ValueKind == JsonValueKind.String) + { + var avatarImage = avatarImageElem.GetString(); + if (!string.IsNullOrWhiteSpace(avatarImage)) + { + if (avatarImage.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + var savedName = SaveDataUrlFile(avatarImage, DBConfig.CDNImages); + if (savedName != null) profile.profileImage = savedName; + } + else + { + + var trimmed = avatarImage; + var schemeIndex = trimmed.IndexOf("://", StringComparison.Ordinal); + if (schemeIndex >= 0) + { + var lastSlash = trimmed.LastIndexOf('/'); + if (lastSlash > schemeIndex) trimmed = trimmed.Substring(lastSlash + 1); + } + if (!string.IsNullOrWhiteSpace(trimmed)) + profile.profileImage = trimmed; + } + } + } + else if (avatarImageElem.ValueKind == JsonValueKind.Null && !string.IsNullOrWhiteSpace(profile.profileImage)) + { + + if (profile.profileImage != DBConfig.DefaultProfileImage) + { + profile.profileImage = DBConfig.DefaultProfileImage; + } + } + } + if (payload.TryGetProperty("is_junior", out var isJunior)) + { + var junior = isJunior.GetBoolean(); + var wasJunior = profile.isJunior; + profile.isJunior = junior; + if (player != null) + { + player.IsJunior = junior; + player.JuniorState = junior ? JuniorState.ModerationOverride : JuniorState.BasedOnBirthday; + } + + if (wasJunior != junior) + { + + + + if (player != null) player.TokenVersion++; + await db.SaveChangesAsync(); + if (player != null) TokenRevocation.Bump(id, player.TokenVersion); + + await NotifyAccountChanged(id, profile, player, refreshSession: true); + } + } + if (payload.TryGetProperty("roles", out var roles)) + { + var oldRoles = profile.role; + profile.role = JoinRoles(roles); + + if (oldRoles != profile.role) + { + if (player != null) + { + player.TokenVersion++; + } + + + + + + await db.SaveChangesAsync(); + if (player != null) + TokenRevocation.Bump(id, player.TokenVersion); + + await NotifyAccountChanged(id, profile, player, refreshSession: true); + } + } + + + + + bool banPenaltyApplied = false; + + + if (payload.TryGetProperty("banned", out var banned)) + { + var isBanned = banned.GetBoolean(); + var banReason = TryGetString(payload, "ban_reason", out var reason) ? reason : "Banned via admin panel"; + + + + var banIsModerator = payload.TryGetProperty("ban_is_moderator", out var isMod) && isMod.ValueKind == JsonValueKind.True; + if (banIsModerator && !string.IsNullOrWhiteSpace(banReason) && + !banReason.StartsWith("Moderator.", StringComparison.OrdinalIgnoreCase)) + { + banReason = "Moderator. " + banReason; + } + var banMethod = TryGetString(payload, "ban_method", out var method) && !string.IsNullOrWhiteSpace(method) + ? method.Trim() + : (isBanned ? "Admin Panel" : "None"); + var moderatorNote = TryGetString(payload, "moderator_note", out var note) ? note?.Trim() : null; + var banType = TryGetString(payload, "ban_type", out var type) ? type : (isBanned ? "Temporary" : ""); + var banCategory = TryGetEnum(payload, "ban_category", out ReportCategory category) ? category : ReportCategory.Misc; + + + var banIsTimeout = isBanned + && payload.TryGetProperty("ban_is_timeout", out var isTimeoutFlag) + && isTimeoutFlag.ValueKind == JsonValueKind.True; + if (banIsTimeout) + { + banMethod = ModerationPolicy.AutomaticTimeoutBanMethod; + banType = "Temporary"; + } + + var isAccountLock = string.Equals(banMethod, "Account Lock", StringComparison.OrdinalIgnoreCase); + if (isAccountLock && isBanned) + { + banType = "Permanent"; + banCategory = ReportCategory.Moderator; + banReason = AccountLock.Reason; + } + + + + + + DateTime? banDuration = null; + if (isBanned) + { + if (string.Equals(banType, "Permanent", StringComparison.OrdinalIgnoreCase)) + { + banDuration = DateTime.MaxValue; + } + else if (TryGetString(payload, "ban_until", out var banUntil)) + { + if (string.Equals(banUntil, "permanent", StringComparison.OrdinalIgnoreCase) || banUntil == "-1") + { + banDuration = DateTime.MaxValue; + } + else if (DateTime.TryParse(banUntil, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsedUntil)) + { + + + banDuration = parsedUntil; + } + else + { + + + banDuration = DateTime.UtcNow.AddDays(30); + } + } + else + { + banDuration = DateTime.UtcNow.AddDays(30); + } + + if (banIsTimeout) + { + + var requested = banDuration!.Value - DateTime.UtcNow; + banDuration = DateTime.UtcNow + ModerationPolicy.ClampTimeout(requested > TimeSpan.Zero ? requested : TimeSpan.Zero); + } + } + + if (banIsTimeout) + banReason = PlayerReportingAPI.GetTimeoutDisplayReason(banMethod, banReason, banCategory); + + + + string playerIp = TryGetString(payload, "ip", out var payloadIp) && !string.IsNullOrWhiteSpace(payloadIp) + ? payloadIp + : (player?.LastIpAddress ?? ""); + string playerDevice = TryGetString(payload, "device_id", out var payloadDevice) && !string.IsNullOrWhiteSpace(payloadDevice) + ? payloadDevice + : (player?.DeviceId ?? ""); + + var existingBan = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == id && x.IsBan); + + + + + bool banChanged = false; + + if (isBanned) + { + if (string.Equals(banMethod, "IP Ban", StringComparison.OrdinalIgnoreCase)) + { + + + + + if (string.IsNullOrWhiteSpace(playerIp)) + { + + + return BadRequest("No IP address is known for this player. Ask them to log in once, then try the IP ban again."); + } + + + + + if (existingBan != null) + { + db.Moderation.Remove(existingBan); + existingBan = null; + } + + var existingIpBan = await db.IpBans.FirstOrDefaultAsync(x => x.IpAddress == playerIp); + if (existingIpBan == null) + { + await db.IpBans.AddAsync(new IpBan + { + IpAddress = playerIp, + Reason = banReason, + CreatedAt = DateTime.UtcNow, + ExpiresAt = banDuration == DateTime.MaxValue ? null : banDuration + }); + banChanged = true; + } + else + { + if (existingIpBan.Reason != banReason || existingIpBan.ExpiresAt != (banDuration == DateTime.MaxValue ? null : banDuration)) + banChanged = true; + existingIpBan.Reason = banReason; + existingIpBan.ExpiresAt = banDuration == DateTime.MaxValue ? null : banDuration; + } + await db.SaveChangesAsync(); + + + + await db.VoteKicks.AddAsync(new VoteKickLog + { + PlayerId = id, + Reason = banReason, + Category = banCategory, + IsForce = true, + CreatedAt = DateTime.UtcNow, + ExpiresAt = banDuration ?? DateTime.MaxValue + }); + await db.SaveChangesAsync(); + + + + + + + + + + await PlayerUtils.KickOrPromptAsync(db, id, PlayerUtils.BuildBlockDetails(true, banCategory, banReason, banDuration, startedAtUtc: DateTime.UtcNow)); + } + else + { + + + + + + + + + var appealable = ModerationPolicy.IssuesAppealCode(banMethod); + var appealCode = appealable ? existingBan?.AppealCode : null; + string? appealError = null; + if (appealable && string.IsNullOrWhiteSpace(appealCode)) + { + var (newCode, cooldownError) = await PlayerUtils.GetAppealForNewBanAsync(db, id); + appealCode = newCode; + appealError = cooldownError; + banChanged = true; + } + var banStartedAt = DateTime.UtcNow; + var banWasModified = existingBan == null + || existingBan.Message != banReason + || existingBan.ExpireDuration != banDuration!.Value + || existingBan.BanMethod != banMethod + || existingBan.ModeratorNote != moderatorNote + || existingBan.Category != banCategory; + if (existingBan == null) + { + await db.Moderation.AddAsync(new ModerationDB + { + PlayerId = id, + Category = banCategory, + Message = banReason, + ModeratorNote = moderatorNote, + CreatedAt = banStartedAt, + ExpireDuration = banDuration!.Value, + IsBan = true, + BanMethod = banMethod, + AppealCode = appealCode + }); + banChanged = true; + } + else + { + + if (banWasModified) + { + banChanged = true; + existingBan.CreatedAt = banStartedAt; + } + existingBan.Category = banCategory; + existingBan.Message = banReason; + existingBan.ModeratorNote = moderatorNote; + existingBan.ExpireDuration = banDuration!.Value; + existingBan.IsBan = true; + existingBan.BanMethod = banMethod; + existingBan.AppealCode = appealCode; + } + + + + + + + if (string.Equals(banMethod, "Hardware Ban", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(playerDevice)) + { + var existingHardwareBan = await db.HardwareBans.FirstOrDefaultAsync(x => x.DeviceId == playerDevice); + if (existingHardwareBan == null) + { + await db.HardwareBans.AddAsync(new HardwareBan + { + DeviceId = playerDevice, + Reason = banReason, + CreatedAt = DateTime.UtcNow, + ExpiresAt = banDuration == DateTime.MaxValue ? null : banDuration, + BannedAccountId = id + }); + } + else + { + existingHardwareBan.Reason = banReason; + existingHardwareBan.ExpiresAt = banDuration == DateTime.MaxValue ? null : banDuration; + existingHardwareBan.BannedAccountId = id; + } + } + + await db.SaveChangesAsync(); + + + + await db.VoteKicks.AddAsync(new VoteKickLog + { + PlayerId = id, + Reason = banReason, + Category = banCategory, + IsForce = true, + CreatedAt = DateTime.UtcNow, + ExpiresAt = banDuration ?? DateTime.MaxValue + }); + await db.SaveChangesAsync(); + + + + + + + + + + var effectiveBan = existingBan ?? await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == id && x.IsBan); + var blockDetails = PlayerUtils.BuildBlockDetails(true, banCategory, banReason, banDuration, appealCode: appealCode, appealError: appealError, startedAtUtc: effectiveBan?.CreatedAt ?? DateTime.UtcNow); + + PlayerUtils.ApplyClientBanKind(blockDetails, banMethod); + await PlayerUtils.KickOrPromptAsync(db, id, blockDetails); + } + + + + + + if (banChanged) + { + if (!isAccountLock) + { + if (rep == null) + { + rep = new Reputation { AccountId = id }; + await db.Reputations.AddAsync(rep); + } + rep.ApplyBanPenalty( + permanent: banDuration == DateTime.MaxValue, + cheating: string.Equals(banMethod, "Anti-Cheat", StringComparison.OrdinalIgnoreCase)); + } + banPenaltyApplied = true; + + + + + + await AdminActionDiscord.LogBanAsync(db, id, + await GetPanelAdminUsernameAsync(), + causedByReports: false, + reason: banReason, + category: banCategory, + expiry: banDuration, + isModerator: banIsModerator, + banMethod: banMethod); + } + } + else if (existingBan != null) + { + db.Moderation.Remove(existingBan); + + + + if (string.Equals(existingBan.BanMethod, "IP Ban", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(playerIp)) + await db.IpBans.Where(x => x.IpAddress == playerIp).ExecuteDeleteAsync(); + if (string.Equals(existingBan.BanMethod, "Hardware Ban", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(playerDevice)) + await db.HardwareBans.Where(x => x.DeviceId == playerDevice).ExecuteDeleteAsync(); + + await db.SaveChangesAsync(); + + + await BestEffort("the refresh-login notification", id, + () => Notify.SendNotify(id, new Notification { Id = NotificationType.RefreshLogin, Msg = profile })); + await BestEffort("the moderation-clear push", id, + () => PlayerUtils.ReleaseModerationHoldAsync(id)); + } + else + { + + + + + var clearedIpBan = !string.IsNullOrWhiteSpace(playerIp) + && await db.IpBans.Where(x => x.IpAddress == playerIp).ExecuteDeleteAsync() > 0; + var clearedHwBan = !string.IsNullOrWhiteSpace(playerDevice) + && await db.HardwareBans.Where(x => x.DeviceId == playerDevice).ExecuteDeleteAsync() > 0; + + if (clearedIpBan || clearedHwBan) + { + await BestEffort("the refresh-login notification", id, + () => Notify.SendNotify(id, new Notification { Id = NotificationType.RefreshLogin, Msg = profile })); + await BestEffort("the moderation-clear push", id, + () => PlayerUtils.ReleaseModerationHoldAsync(id)); + } + } + } + + if (player != null) + { + if (TryGetString(payload, "email", out var email)) + { + player.Email = email; + + + + player.EmailVerified = !string.IsNullOrWhiteSpace(email); + player.PendingEmail = null; + player.EmailVerifyToken = null; + } + if (TryGetString(payload, "device_id", out var deviceId)) player.DeviceId = deviceId; + if (TryGetEnum(payload, "jr_state", out JuniorState juniorState)) player.JuniorState = juniorState; + if (payload.TryGetProperty("is_junior", out var playerIsJunior)) player.IsJunior = playerIsJunior.GetBoolean(); + + + + if (payload.TryGetProperty("parent_account_id", out var parentProp)) + { + if (parentProp.ValueKind == JsonValueKind.Null || (parentProp.ValueKind == JsonValueKind.String && string.IsNullOrWhiteSpace(parentProp.GetString()))) + { + player.Parent = null; + } + else if (parentProp.ValueKind == JsonValueKind.Number && parentProp.TryGetInt64(out var rawParent)) + { + player.Parent = rawParent > 0 ? rawParent : null; + } + else if (parentProp.ValueKind == JsonValueKind.String && long.TryParse(parentProp.GetString(), out var parsedParent)) + { + player.Parent = parsedParent > 0 ? parsedParent : null; + } + } + } + + + + if (payload.TryGetProperty("name_history", out var nameHistory) && nameHistory.ValueKind == JsonValueKind.Array) + { + var history = nameHistory.EnumerateArray() + .Select(x => x.GetString() ?? string.Empty) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToList(); + + if (settings == null) + { + settings = new PlayerSettings { AccountId = id }; + await db.PlayerSettings.AddAsync(settings); + } + var existingSettings = settings.DeserializeSettings() ?? new List(); + existingSettings.RemoveAll(x => x.Key == "NameHistory"); + existingSettings.Add(new Setting { Key = "NameHistory", Value = System.Text.Json.JsonSerializer.Serialize(history) }); + settings.Settings = existingSettings.ToJson(); + } + + + + + if (payload.TryGetProperty("settings", out var settingsProp) && settingsProp.ValueKind == JsonValueKind.Object) + { + var settingList = new List(); + foreach (var prop in settingsProp.EnumerateObject()) + { + string? val = null; + if (prop.Value.ValueKind == JsonValueKind.String) val = prop.Value.GetString(); + else if (prop.Value.ValueKind == JsonValueKind.True) val = "true"; + else if (prop.Value.ValueKind == JsonValueKind.False) val = "false"; + else if (prop.Value.ValueKind == JsonValueKind.Number) val = prop.Value.GetRawText(); + else val = prop.Value.GetRawText(); + + if (val != null) + settingList.Add(new Setting { Key = prop.Name, Value = val }); + } + + if (settings == null) + { + settings = new PlayerSettings { AccountId = id }; + await db.PlayerSettings.AddAsync(settings); + } + settings.Settings = settingList.ToJson(); + } + + + var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == id); + bool hasBalanceField = HasAny(payload, "rec_tokens", "laser_tag_tickets", "gold", "dracula_silver"); + if (currency == null && hasBalanceField) + { + currency = new Currency { AccountId = id }; + await db.Currency.AddAsync(currency); + } + var balanceEdits = new List<(CurrencyType Type, long Balance, long Granted)>(); + if (currency != null) + { + if (TryGetLong(payload, "rec_tokens", out var recTokens)) + { + balanceEdits.Add((CurrencyType.RecCenterTokens, recTokens, recTokens - currency.RecTokens)); + currency.RecTokens = recTokens; + } + if (TryGetLong(payload, "laser_tag_tickets", out var laserTickets)) + { + balanceEdits.Add((CurrencyType.LaserTagTickets, laserTickets, laserTickets - currency.LaserTagTickets)); + currency.LaserTagTickets = laserTickets; + } + if (TryGetLong(payload, "gold", out var gold)) + { + balanceEdits.Add((CurrencyType.LostSkullsGold, gold, gold - currency.Gold)); + currency.Gold = gold; + } + if (TryGetLong(payload, "dracula_silver", out var draculaSilver)) + { + balanceEdits.Add((CurrencyType.DraculaSilver, draculaSilver, draculaSilver - currency.DraculaSilver)); + currency.DraculaSilver = draculaSilver; + } + } + + if (progress == null && (payload.TryGetProperty("level", out _) || payload.TryGetProperty("xp", out _))) + { + progress = new ProgressionData { PlayerId = id }; + await db.Progression.AddAsync(progress); + } + + if (progress != null) + { + if (TryGetInt(payload, "level", out var level)) progress.Level = level; + if (TryGetInt(payload, "xp", out var xp)) progress.XP = xp; + } + + if (rep == null && HasAny(payload, "reputation", "noteriety", "cheer_general", "cheer_helpful", "cheer_great_host", "cheer_sportsman", "cheer_creative", "cheer_credit")) + { + rep = new Reputation { AccountId = id }; + await db.Reputations.AddAsync(rep); + } + + if (rep != null) + { + + + + if (!banPenaltyApplied && TryGetDouble(payload, "reputation", out var reputationScore)) + rep.ReputationScore = reputationScore; + if (TryGetDouble(payload, "noteriety", out var noteriety)) rep.Noteriety = noteriety; + if (TryGetInt(payload, "cheer_general", out var cheerGeneral)) rep.CheerGeneral = cheerGeneral; + if (TryGetInt(payload, "cheer_helpful", out var cheerHelpful)) rep.CheerHelpful = cheerHelpful; + if (TryGetInt(payload, "cheer_great_host", out var cheerGreatHost)) rep.CheerGreatHost = cheerGreatHost; + if (TryGetInt(payload, "cheer_sportsman", out var cheerSportsman)) rep.CheerSportsman = cheerSportsman; + if (TryGetInt(payload, "cheer_creative", out var cheerCreative)) rep.CheerCreative = cheerCreative; + if (TryGetInt(payload, "cheer_credit", out var cheerCredit)) rep.CheerCredit = cheerCredit; + } + + await db.SaveChangesAsync(); + + foreach (var edit in balanceEdits) + await BestEffort($"the balance update notification ({edit.Type})", id, + () => StorefrontShared.SendBalanceUpdateAsync(id, edit.Type, edit.Balance, edit.Granted, StorefrontBalanceAddTypes.Manual_Thanks)); + + if (progress != null) + { + await BestEffort("level-up gift delivery", id, + () => LevelUpGiftService.DeliverLevelUpGiftsAsync(db, id, recomputeFromXp: true, fromLevel: priorLevel, notify: true)); + + await BestEffort("progression update push", id, + () => RoomProgressionService.PushProgressionUpdateAsync(db, id)); + } + + return Ok(new { success = true }); + } + + private static async Task BestEffort(string what, long playerId, Func action) + { + try + { + await action(); + } + catch (Exception) + { + } + } + + private static bool TryGetString(JsonElement payload, string prop, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? value) + { + if (payload.TryGetProperty(prop, out var elem) && elem.ValueKind == JsonValueKind.String) + { + value = elem.GetString()!; + return true; + } + value = null; + return false; + } + + + + + + + + + + + + + + [HttpPost("players/{playerId:long}/quitgame")] + public async Task QuitPlayerGame(long playerId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound("Player does not exist."); + + var display = !string.IsNullOrWhiteSpace(profile.displayName) ? profile.displayName : profile.username; + + Notify.SendReliably(playerId, new Notification { Id = NotificationType.ModerationQuitGame, Msg = "" }, + TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1), Notify.EmptyObjectPayload(NotificationType.ModerationQuitGame)); + + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence != null) + { + presence.isOnline = false; + presence.roomInstance = null; + await db.SaveChangesAsync(); + } + + AdminActionDiscord.LogAction("Quit Game", await GetPanelAdminUsernameAsync(), + $"Forced player {playerId} ({display}) to quit the game."); + + return Ok(new { success = true, message = $"Sent quit-game to {display}." }); + } + + [HttpPost("players/{playerId:long}/forcelogout")] + public async Task ForceLogoutPlayer(long playerId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound("Player does not exist."); + + var display = !string.IsNullOrWhiteSpace(profile.displayName) ? profile.displayName : profile.username; + + await Notify.SendNotify(playerId, Notify.EmptyObjectPayload(NotificationType.Logout)); + + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (player != null) + { + player.TokenVersion++; + TokenRevocation.Bump(playerId, player.TokenVersion); + } + + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence != null) + { + presence.isOnline = false; + presence.roomInstance = null; + } + + await db.SaveChangesAsync(); + + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromMilliseconds(300)); + Notify.DisconnectAccount(playerId, "force logout"); + } + catch { } + }); + + AdminActionDiscord.LogAction("Force Logout", await GetPanelAdminUsernameAsync(), + $"Forced player {playerId} ({display}) to log out."); + + return Ok(new { success = true, message = $"Sent force-logout to {display}." }); + } + + [HttpPost("players/{playerId:long}/forcejoin")] + public async Task ForceJoinPlayer(long playerId, [FromQuery] string key, [FromQuery(Name = "target_player_id")] long targetPlayerId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + + var hostProfile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (hostProfile == null) return NotFound("Host player does not exist."); + + var targetProfile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == targetPlayerId); + if (targetProfile == null) return NotFound("Target player does not exist."); + + var hostDisplay = !string.IsNullOrWhiteSpace(hostProfile.displayName) ? hostProfile.displayName : hostProfile.username; + var targetDisplay = !string.IsNullOrWhiteSpace(targetProfile.displayName) ? targetProfile.displayName : targetProfile.username; + + var hostPresence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (hostPresence == null || !hostPresence.roomInstance.HasValue) + return BadRequest(new { success = false, message = $"{hostDisplay} is not currently in a room." }); + + var roomInstanceId = hostPresence.roomInstance.Value; + var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId); + if (roomInstance == null) + return BadRequest(new { success = false, message = $"The room instance {roomInstanceId} could not be found." }); + + var matchResult = await MatchmakingUtils.JoinRoom(targetPlayerId, roomInstanceId); + if (matchResult.errorCode != JoinStatus.Success) + { + return BadRequest(new { success = false, message = $"Failed to move {targetDisplay} into the room: {matchResult.errorCode}." }); + } + + db.ChangeTracker.Clear(); + var targetPresence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == targetPlayerId); + if (targetPresence != null) + { + try + { + await Notify.SendNotify(targetPlayerId, new Notification + { + Id = "PresenceUpdate", + Msg = await Presence.CreateAsync(db, targetPresence, roomInstance) + }); + } + catch (Exception) + { + } + } + + var targetBan = await db.ResolveBanAsync(targetPlayerId); + if (targetBan != null && !await db.IsDeveloper(targetPlayerId)) + { + var banDetails = PlayerUtils.BuildBlockDetails(true, targetBan.Category, targetBan.Reason, targetBan.ExpiryUtc, + appealCode: targetBan.AppealCode, startedAtUtc: targetBan.StartedAtUtc, + relatedUsername: await PlayerUtils.RelatedAccountUsernameAsync(db, targetBan.BannedAccountId, targetPlayerId)); + PlayerUtils.ApplyClientBanKind(banDetails, targetBan.BanMethod); + await Notify.SendNotify(targetPlayerId, new Notification { Id = NotificationType.ModerationKick, Msg = banDetails }); + } + + AdminActionDiscord.LogAction("Force Join", await GetPanelAdminUsernameAsync(), + $"Force-joined player {targetPlayerId} ({targetDisplay}) into room {roomInstance.name} (instance {roomInstanceId}) where player {playerId} ({hostDisplay}) is."); + + return Ok(new + { + success = true, + message = $"{targetDisplay} has been moved to {hostDisplay}'s room ({roomInstance.name}).", + room_name = roomInstance.name, + room_instance_id = roomInstanceId + }); + } + + [HttpPost("players/{playerId:long}/warn")] + public async Task WarnPlayer(long playerId, [FromQuery] string key, [FromQuery] int reportCategory, [FromQuery] string? reason, [FromQuery] string? note) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound("Player does not exist."); + + var display = !string.IsNullOrWhiteSpace(profile.displayName) ? profile.displayName : profile.username; + + if (!Enum.IsDefined(typeof(ReportCategory), reportCategory)) + return BadRequest(new { success = false, message = $"Unknown report category {reportCategory}." }); + + var category = (ReportCategory)reportCategory; + var displayReason = string.IsNullOrWhiteSpace(reason) ? null : reason.Trim(); + var moderatorNote = string.IsNullOrWhiteSpace(note) ? null : note.Trim(); + + var warning = await PlayerReportingAPI.IssueWarningAsync(db, playerId, category, displayReason, moderatorNote, 1); + + if (warning == null) + { + var connected = Notify.GetClient(playerId) != null; + + AdminActionDiscord.LogAction("Warn Player (awaiting decision)", await GetPanelAdminUsernameAsync(), + $"Warned developer {playerId} ({display}) - {category} - parked until they accept or deny" + + (connected ? string.Empty : " (no live notify connection at the time)")); + + return Ok(new + { + success = true, + message = connected + ? $"{display} is a developer: the accept/deny prompt was sent to their client and the warning is not applied unless they accept it." + : $"{display} is a developer and has no live connection right now, so the accept/deny prompt could not be shown. The warning is parked (nothing written) and prompts again on the next action aimed at them.", + warning_id = (long?)null + }); + } + + AdminActionDiscord.LogAction("Warn Player", await GetPanelAdminUsernameAsync(), + $"Warned player {playerId} ({display}) - {category}" + + (displayReason == null ? string.Empty : $": {displayReason}") + + (moderatorNote == null ? string.Empty : $" | note: {moderatorNote}")); + + return Ok(new + { + success = true, + message = $"{display} was warned: moved to their dorm and shown the warning until they acknowledge it.", + warning_id = warning.Id + }); + } + + [HttpPost("players/{playerId:long}/voicerestriction")] + public async Task ApplyVoiceChatRestriction(long playerId, [FromQuery] string key, [FromQuery] int minutes, [FromQuery] int? reportCategory, [FromQuery] string? reason, [FromQuery] string? note) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound("Player does not exist."); + + var display = !string.IsNullOrWhiteSpace(profile.displayName) ? profile.displayName : profile.username; + + var category = reportCategory.HasValue && Enum.IsDefined(typeof(ReportCategory), reportCategory.Value) + ? (ReportCategory)reportCategory.Value + : ReportCategory.Misc; + + var duration = TimeSpan.FromMinutes(minutes > 0 ? minutes : VoiceChatRestrictions.DefaultDuration.TotalMinutes); + var restriction = await VoiceChatRestrictions.ApplyAsync(db, playerId, + string.IsNullOrWhiteSpace(reason) ? "Restricted from voice chat for a Code of Conduct violation." : reason.Trim(), + category, duration, VoiceChatRestrictions.Method, + await GetPanelAdminUsernameAsync(), string.IsNullOrWhiteSpace(note) ? null : note.Trim()); + + await VoiceChatRestrictions.PushAsync(playerId, restriction); + + AdminActionDiscord.LogAction("Voice Restriction", await GetPanelAdminUsernameAsync(), + $"Restricted {display} ({playerId}) from voice chat for {Math.Round((restriction.ExpiresAt - DateTime.UtcNow).TotalMinutes)} minutes ({category})" + + (string.IsNullOrWhiteSpace(reason) ? "" : $": {reason.Trim()}")); + + return Ok(new + { + success = true, + message = $"{display} is restricted from voice chat until {restriction.ExpiresAt:u}.", + expires_at = DateTime.SpecifyKind(restriction.ExpiresAt, DateTimeKind.Utc), + minutes = (int)Math.Max(0, (restriction.ExpiresAt - DateTime.UtcNow).TotalMinutes) + }); + } + + [HttpDelete("players/{playerId:long}/voicerestriction")] + public async Task ClearVoiceChatRestriction(long playerId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound("Player does not exist."); + + var display = !string.IsNullOrWhiteSpace(profile.displayName) ? profile.displayName : profile.username; + + var cleared = await VoiceChatRestrictions.ClearAsync(db, playerId, await GetPanelAdminUsernameAsync()); + if (!cleared) + return Ok(new { success = true, message = $"{display} is not restricted from voice chat." }); + + await VoiceChatRestrictions.PushAsync(playerId, null); + + AdminActionDiscord.LogAction("Voice Restriction Lifted", await GetPanelAdminUsernameAsync(), + $"Lifted the voice chat restriction on {display} ({playerId})."); + + return Ok(new { success = true, message = $"{display} can use voice chat again." }); + } + + [HttpPost("players/{playerId:long}/togglejunior")] + public async Task ToggleJunior(long playerId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound("Player does not exist."); + + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == playerId); + var display = !string.IsNullOrWhiteSpace(profile.displayName) ? profile.displayName : profile.username; + + var makeJunior = !profile.isJunior; + profile.isJunior = makeJunior; + if (player != null) + { + player.IsJunior = makeJunior; + player.JuniorState = makeJunior ? JuniorState.ModerationOverride : JuniorState.BasedOnBirthday; + } + + await db.SaveChangesAsync(); + + if (player != null) player.TokenVersion++; + await db.SaveChangesAsync(); + if (player != null) TokenRevocation.Bump(playerId, player.TokenVersion); + + await NotifyAccountChanged(playerId, profile, player, refreshSession: true); + + AdminActionDiscord.LogAction(makeJunior ? "Junior Account" : "Lift Junior", await GetPanelAdminUsernameAsync(), + $"Set player {playerId} ({display}) junior status to {makeJunior}."); + + return Ok(new + { + success = true, + is_junior = makeJunior, + jr_state = player?.JuniorState.ToString() ?? profile.isJunior.ToString(), + message = makeJunior ? $"{display} is now a Junior account." : $"Junior status removed from {display}." + }); + } + + private static bool TryGetInt(JsonElement payload, string prop, out int value) + { + value = 0; + if (!payload.TryGetProperty(prop, out var elem)) return false; + + switch (elem.ValueKind) + { + case JsonValueKind.Number: + if (elem.TryGetInt64(out var asLong)) + { + value = (int)Math.Clamp(asLong, int.MinValue, int.MaxValue); + return true; + } + if (elem.TryGetDouble(out var asDouble)) + { + value = (int)Math.Clamp(asDouble, int.MinValue, int.MaxValue); + return true; + } + return false; + case JsonValueKind.String: + if (int.TryParse(elem.GetString(), out var parsed)) + { + value = parsed; + return true; + } + if (double.TryParse(elem.GetString(), out var parsedDouble)) + { + value = (int)Math.Clamp(parsedDouble, int.MinValue, int.MaxValue); + return true; + } + return false; + default: + return false; + } + } + + private static bool TryGetLong(JsonElement payload, string prop, out long value) + { + value = 0; + if (!payload.TryGetProperty(prop, out var elem)) return false; + + switch (elem.ValueKind) + { + case JsonValueKind.Number: + if (elem.TryGetInt64(out var asLong)) + { + value = asLong; + return true; + } + if (elem.TryGetDouble(out var asDouble)) + { + value = (long)Math.Clamp(asDouble, (double)long.MinValue, (double)long.MaxValue); + return true; + } + return false; + case JsonValueKind.String: + if (long.TryParse(elem.GetString(), out var parsed)) + { + value = parsed; + return true; + } + if (double.TryParse(elem.GetString(), out var parsedDouble)) + { + value = (long)Math.Clamp(parsedDouble, (double)long.MinValue, (double)long.MaxValue); + return true; + } + return false; + default: + return false; + } + } + + /// + /// An account id out of a panel payload, where blank means "none". Distinct from + /// because a field the panel can clear has to accept "" (and a + /// cleared field must not silently keep the old id), and because a value that is neither a + /// number nor digits - the panel posts account ids as text to keep eighteen digits exact - + /// is reported rather than quietly dropped. + /// + /// Returns false only when the property is absent, so a caller leaves the stored value alone + /// unless the panel actually sent the field. When it returns true, is + /// non-null for a value that cannot be an id, and the caller must not write it. + /// + private static bool TryGetAccountId(JsonElement payload, string prop, out long value, out string? error) + { + value = 0; + error = null; + if (!payload.TryGetProperty(prop, out var elem)) return false; + + var text = elem.ValueKind switch + { + JsonValueKind.Number => elem.GetRawText(), + JsonValueKind.String => elem.GetString()?.Trim(), + JsonValueKind.Null => null, + _ => "" + }; + + if (string.IsNullOrEmpty(text)) return true; + if (long.TryParse(text, out var parsed) && parsed >= 0) + { + value = parsed; + return true; + } + + error = $"'{prop}' must be an account id in digits, or blank for no sender."; + return true; + } + + private static bool TryGetDouble(JsonElement payload, string prop, out double value) + { + value = 0; + if (!payload.TryGetProperty(prop, out var elem)) return false; + + return elem.ValueKind switch + { + JsonValueKind.Number => elem.TryGetDouble(out value), + JsonValueKind.String => double.TryParse(elem.GetString(), out value), + _ => false, + }; + } + + private static bool TryGetEnum(JsonElement payload, string prop, out T value) where T : struct + { + if (payload.TryGetProperty(prop, out var elem) && elem.ValueKind == JsonValueKind.String && Enum.TryParse(elem.GetString(), true, out var parsed)) + { + value = parsed; + return true; + } + value = default; + return false; + } + + private static bool HasAny(JsonElement payload, params string[] props) + { + return props.Any(p => payload.TryGetProperty(p, out _)); + } + + private static string[] SplitRoles(string? roles) => string.IsNullOrWhiteSpace(roles) ? [] : roles.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + private async Task NotifyAccountChanged(long id, Profile profile, PlayerData? player, bool refreshSession = false) + { + await Notify.SendNotify(id, new Notification { Id = "AccountUpdate", Msg = profile }); + await Notify.UpdateSubscriptions(id, Notifs.SubscriptionUpdateProfile, profile); + if (player != null) + await Notify.SendNotify(id, new Notification { Id = "SelfAccountUpdate", Msg = PlayerUtils.BuildSelfAccount(profile, player) }); + if (refreshSession) + await Notify.SendNotify(id, new Notification { Id = NotificationType.RefreshLogin, Msg = profile }); + } + + private static string JoinRoles(JsonElement roles) + { + if (roles.ValueKind != JsonValueKind.Array) return roles.ToString(); + return string.Join(",", roles.EnumerateArray().Select(x => x.ToString()).Where(x => !string.IsNullOrWhiteSpace(x))); + } + + private async Task> GetRooms(string? order, bool ascending) + { + var roomsList = await db.Rooms.ToListAsync(); + var roomIds = roomsList.Select(r => r.RoomId).ToList(); + var stats = await db.RoomStats.Where(s => roomIds.Contains(s.RoomId)).ToDictionaryAsync(s => s.RoomId); + var giftRows = await db.RoomJoinGifts.Where(g => roomIds.Contains(g.RoomId)).ToListAsync(); + var giftsByRoom = giftRows.GroupBy(g => g.RoomId).ToDictionary(g => g.Key, g => g.ToList()); + + object RoomRow(RoomData r, RoomStats? stat, bool banned, string banReason) => new + { + id = r.RoomId, + name = r.Name, + description = r.Description, + image_name = r.ImageName, + custom_warning = r.CustomWarning, + creator_account_id = r.CreatorAccountId, + publish_state = r.State.ToString(), + accessibility = r.Accessibility.ToString(), + is_radicade_approved = false, + banned = banned, + ban_reason = banReason, + exclude_from_lists = false, + exclude_from_search = false, + supports_screens = r.SupportsScreens, + supports_walk_vr = r.SupportsWalkVR, + supports_teleport_vr = r.SupportsTeleportVR, + supports_vr_low = r.SupportsVRLow, + supports_quest2 = r.SupportsQuest2, + supports_mobile = r.SupportsMobile, + supports_juniors = r.SupportsJuniors, + min_level = r.MinLevel, + age_rating = 0, + created_at = r.CreatedAt, + published_at = r.PublishedAt, + cheer_count = stat?.CheerCount ?? 0, + favorite_count = stat?.FavoriteCount ?? 0, + visitor_count = stat?.VisitorCount ?? 0, + visit_count = stat?.VisitCount ?? 0, + ranking_context = "", + is_dorm = r.IsDorm, + is_place_play = false, + max_player_calculation_mode = (int)r.MaxPlayerCalculationMode, + max_players = r.MaxPlayers, + cloning_allowed = r.CloningAllowed, + disable_mic_auto_mute = r.DisableMicAutoMute, + disable_room_comments = r.DisableRoomComments, + encrypt_voice_chat = r.EncryptVoiceChat, + join_gift_enabled = r.JoinGiftEnabled, + join_gift_once_per_player = r.JoinGiftOncePerPlayer, + join_gift_version = r.JoinGiftVersion, + join_gift_title = r.JoinGiftTitle, + join_gift_message = r.JoinGiftMessage, + join_gift_avatar_item = r.JoinGiftAvatarItem, + join_gift_equipment_item = r.JoinGiftEquipmentItem, + join_gift_consumable_item = r.JoinGiftConsumableItem, + join_gift_currency_amount = r.JoinGiftCurrencyAmount, + join_gift_currency_type = r.JoinGiftCurrencyType.ToString(), + join_gift_xp = r.JoinGiftXp, + join_gift_context = r.JoinGiftContext.ToString(), + join_gift_rarity = r.JoinGiftRarity.ToString(), + join_gift_avatar_item_type = r.JoinGiftAvatarItemType.ToString(), + join_gift_equipment_modification_guid = r.JoinGiftEquipmentModificationGuid, + join_gift_from_player_id = Math.Max(0, r.JoinGiftFromPlayerId).ToString(CultureInfo.InvariantCulture), + // The room's whole list of boxes. A room saved before the list existed answers with + // the one box its row still describes, so the editor always opens on something real. + join_gifts = JoinGiftRows(r, giftsByRoom.GetValueOrDefault(r.RoomId)), + toxmod_enabled = false, + load_screen_locked = false, + persistence_version = 0, + min_ugc_sub_version = 0, + auto_localize_room = false, + is_developer_owned = false, + tags = r.Tags, + featured = SafeRoomTags(r.Tags).Any(t => string.Equals(t, "featured", StringComparison.OrdinalIgnoreCase)) + }; + + var rooms = roomsList.Select(r => + { + stats.TryGetValue(r.RoomId, out var stat); + return RoomRow(r, stat, false, ""); + }).ToList(); + + var liveRoomIds = roomsList.Select(r => r.RoomId).ToHashSet(); + foreach (var entry in RoomBanStore.Load()) + { + if (liveRoomIds.Contains(entry.RoomId)) continue; + rooms.Add(RoomRow(entry.Room, entry.Stats, true, entry.Reason)); + } + + return OrderRows(rooms, order, ascending); + } + + private async Task> GetRoomComments(string? order, bool ascending) + { + var rows = await db.RoomComments + .AsNoTracking() + .Select(c => new + { + id = c.Id, + comment = c.Comment, + comment_type = c.CommentType, + room_id = c.RoomId, + creator_account_id = c.CreatorAccountId, + created_at = c.CreatedAt, + position_x = c.PositionX, + position_y = c.PositionY, + position_z = c.PositionZ + }) + .ToListAsync(); + + return OrderRows(rows.Cast().ToList(), order, ascending); + } + + private List GetCommunityBoard() + { + var state = LoadState(); + if (state.CommunityBoard == null) + { + state.CommunityBoard = new JsonObject + { + ["id"] = 1, + ["featured_player_id"] = null, + ["instagram_images"] = new JsonArray(), + ["board_message"] = "", + ["announcement_url"] = "", + ["videos"] = new JsonArray(), + ["featured_rooms"] = new JsonArray() + }; + SaveState(state); + } + + return new List { state.CommunityBoard }; + } + + private async Task UpdateCommunityBoard(AdminPanelState state, JsonElement payload) + { + if (state.CommunityBoard == null) + { + state.CommunityBoard = new JsonObject + { + ["id"] = 1, + ["featured_player_id"] = null, + ["instagram_images"] = new JsonArray(), + ["board_message"] = "", + ["announcement_url"] = "", + ["videos"] = new JsonArray(), + ["featured_rooms"] = new JsonArray() + }; + } + + foreach (var prop in payload.EnumerateObject()) + { + try + { + state.CommunityBoard[prop.Name] = JsonNode.Parse(prop.Value.GetRawText()); + } + catch + { + if (prop.Value.ValueKind == JsonValueKind.String) + state.CommunityBoard[prop.Name] = prop.Value.GetString(); + else if (prop.Value.ValueKind == JsonValueKind.Number) + state.CommunityBoard[prop.Name] = prop.Value.GetRawText(); + else if (prop.Value.ValueKind == JsonValueKind.True) + state.CommunityBoard[prop.Name] = true; + else if (prop.Value.ValueKind == JsonValueKind.False) + state.CommunityBoard[prop.Name] = false; + else if (prop.Value.ValueKind == JsonValueKind.Null) + state.CommunityBoard[prop.Name] = null; + else + state.CommunityBoard[prop.Name] = prop.Value.GetRawText(); + } + } + + + + + var instagramNode = state.CommunityBoard["instagram_images"]; + if (instagramNode != null && instagramNode.GetValueKind() == JsonValueKind.Array) + { + foreach (var img in instagramNode.AsArray()) + { + var url = img?["image_url"]?.ToString(); + var existingName = img?["image_name"]?.ToString(); + if (string.IsNullOrWhiteSpace(existingName) + && url != null + && url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + var saved = SaveDataUrlFile(url, DBConfig.CDNImages); + if (saved != null) + img!["image_name"] = saved; + } + } + } + + + SaveState(state); + + + var boardConfig = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "CommunityBoard"); + + + var instagramImages = new List(); + if (instagramNode != null && instagramNode.GetValueKind() == JsonValueKind.Array) + { + foreach (var img in instagramNode.AsArray()) + { + if (img == null) continue; + instagramImages.Add(new RecNet.Board.InstagramImage + { + ImageName = img["image_name"]?.ToString() ?? "", + ImageUrl = img["image_url"]?.ToString() ?? "" + }); + } + } + + + + var videos = new List(); + var videosNode = state.CommunityBoard["videos"]; + if (videosNode != null && videosNode.GetValueKind() == JsonValueKind.Array) + { + foreach (var vid in videosNode.AsArray()) + { + if (vid == null) continue; + + + + var blob = Path.GetFileName(vid["blob_name"]?.ToString() ?? ""); + if (!string.IsNullOrWhiteSpace(blob) && !System.IO.File.Exists(Path.Join(DBConfig.CDNVideos, blob))) + blob = ""; + + var thumb = vid["thumbnail_blob_name"]?.ToString(); + var thumbName = thumb != null && thumb.StartsWith("data:", StringComparison.OrdinalIgnoreCase) + ? SaveDataUrlFile(thumb, DBConfig.CDNImages) ?? thumb + : thumb ?? ""; + + videos.Add(new RecNet.Board.BoardVideo + { + BlobName = blob, + Title = vid["title"]?.ToString() ?? "", + Description = vid["description"]?.ToString() ?? "", + ThumbnailBlobName = thumbName, + SourceUrl = vid["source_url"]?.ToString() ?? "" + }); + } + } + + var communityBoard = new RecNet.Board.CommunityBoard + { + FeaturedPlayer = new RecNet.Board.FeaturedPlayer + { + Id = (int)(state.CommunityBoard["featured_player_id"]?.GetValue() ?? 0), + TitleOverride = state.CommunityBoard["featured_player_title"]?.ToString() ?? "", + UrlOverride = state.CommunityBoard["featured_player_url"]?.ToString() ?? "" + }, + FeaturedRoomGroup = await BuildFeaturedRoomGroup(state.CommunityBoard), + CurrentAnnouncement = new RecNet.Board.BoardAnnouncement + { + Message = state.CommunityBoard["board_message"]?.ToString() ?? "", + MoreInfoUrl = state.CommunityBoard["announcement_url"]?.ToString() ?? "" + }, + InstagramImages = instagramImages, + Videos = videos + }; + + if (boardConfig == null) + { + boardConfig = new RecNet.Config.ServerConfigVal { Key = "CommunityBoard", Value = JsonSerializer.Serialize(communityBoard) }; + db.ServerConfig.Add(boardConfig); + } + else + { + boardConfig.Value = JsonSerializer.Serialize(communityBoard); + } + await db.SaveChangesAsync(); + + + await CommunityBoardBroadcast.PushAsync(communityBoard); + + return state.CommunityBoard; + } + + + + private static long TryGetEntryId(JsonNode? entry) + { + if (entry == null) return 0; + foreach (var key in new[] { "id", "room_id", "RoomId" }) + { + var raw = entry[key]?.ToString(); + if (raw != null && long.TryParse(raw, out var parsed) && parsed > 0) return parsed; + } + return 0; + } + + + + + + + + private async Task BuildFeaturedRoomGroup(JsonObject board) + { + var rooms = new List(); + + var featuredRooms = board["featured_rooms"]; + if (featuredRooms != null && featuredRooms.GetValueKind() == JsonValueKind.Array) + { + var roomIds = new List(); + foreach (var entry in featuredRooms.AsArray()) + { + if (entry == null) continue; + + var id = TryGetEntryId(entry); + if (id > 0) roomIds.Add(id); + } + + + var dbRooms = roomIds.Count > 0 + ? await db.Rooms.Where(x => roomIds.Contains(x.RoomId)).ToDictionaryAsync(x => x.RoomId) + : new Dictionary(); + + foreach (var entry in featuredRooms.AsArray()) + { + if (entry == null) continue; + + var id = TryGetEntryId(entry); + if (id <= 0) continue; + + var name = entry["name"]?.ToString() ?? entry["room_name"]?.ToString() ?? entry["RoomName"]?.ToString() ?? ""; + dbRooms.TryGetValue(id, out var dbRoom); + if (dbRoom != null) + { + if (string.IsNullOrWhiteSpace(name)) name = dbRoom.Name; + } + + rooms.Add(new RecNet.Board.FeaturedRoom + { + RoomId = id, + RoomName = name, + ImageName = dbRoom?.ImageName ?? "" + }); + } + } + + return new RecNet.Board.FeaturedRoomGroup + { + + FeaturedRoomGroupId = 1, + Name = board["featured_rooms_title"]?.ToString() ?? "Top Picks", + Rooms = rooms + }; + } + + private static Announcement ToAnnouncement(JsonObject row) + { + string? Text(string key) => row[key]?.ToString(); + return new Announcement + { + AnnouncementId = ReadAnnouncementId(row) ?? 0, + AnnouncementStatus = TryGetJsonInt(row, "AnnouncementStatus") ?? 0, + AnnouncementType = TryGetJsonInt(row, "AnnouncementType") ?? 0, + Title = Text("Title") ?? Text("title") ?? string.Empty, + Body = Text("Body") ?? Text("message") ?? string.Empty, + ImageName = Text("ImageName") ?? string.Empty, + LinkName = Text("LinkName") ?? Text("link_name") ?? string.Empty, + LinkType = TryGetJsonInt(row, "LinkType") ?? 0, + LinkUri = Text("LinkUri") ?? Text("link_uri") ?? string.Empty, + Platform = Platforms.All, + CreatedAt = DateTime.UtcNow + }; + } + + private static long? ReadAnnouncementId(JsonObject row) + { + if (row["id"] is not JsonValue value) return null; + if (value.TryGetValue(out var asLong)) return asLong; + if (value.TryGetValue(out var asDouble) && asDouble <= long.MaxValue && asDouble >= long.MinValue) return (long)asDouble; + if (value.TryGetValue(out var asString) && long.TryParse(asString, out var parsed)) return parsed; + return null; + } + + private List GetAnnouncements() + { + + + var config = db.ServerConfig.FirstOrDefault(x => x.Key == "Announcement"); + if (config == null || string.IsNullOrWhiteSpace(config.Value)) + { + return new List(); + } + try + { + var deserialized = config.Value.FromJson>(); + return deserialized?.Cast().ToList() ?? new List(); + } + catch + { + return new List(); + } + } + + private async Task UpdateAnnouncements(JsonElement payload) + { + try + { + + var items = new List(); + if (payload.ValueKind == JsonValueKind.Array) + { + foreach (var el in payload.EnumerateArray()) items.Add(el); + } + else + { + items.Add(payload); + } + if (items.Count == 0) return Ok(new JsonObject()); + + var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "Announcement"); + if (config == null) + { + config = new RecNet.Config.ServerConfigVal { Key = "Announcement", Value = "[]" }; + db.ServerConfig.Add(config); + } + + + List list; + try + { + list = (config.Value ?? "[]").FromJson>() ?? new List(); + } + catch + { + list = new List(); + } + long nextId = 1; + foreach (var existing in list) + { + var existingId = ReadAnnouncementId(existing); + if (existingId == null) continue; + existing["id"] = existingId.Value; + if (existingId.Value >= nextId) nextId = existingId.Value + 1; + } + var pendingNotifs = new List(); + int? maintenanceMinutes = null; + + JsonObject? created = null; + foreach (var announcement in items) + { + var type = announcement.TryGetProperty("type", out var typeProp) + ? (typeProp.GetString() ?? "Regular Announcement") + : "Regular Announcement"; + var message = announcement.TryGetProperty("message", out var msgProp) ? msgProp.GetString() ?? "" : ""; + var imageRaw = announcement.TryGetProperty("image", out var imgProp) && imgProp.ValueKind == JsonValueKind.String + ? imgProp.GetString() + : null; + var minutes = TryGetInt(announcement, "minutes", out var minutesValue) ? minutesValue : (int?)null; + var expiresAt = announcement.TryGetProperty("expires_at", out var expProp) && expProp.ValueKind == JsonValueKind.String + ? expProp.GetString() + : null; + + var linkName = announcement.TryGetProperty("link_name", out var lnProp) ? lnProp.GetString() : null; + var linkTypeRaw = TryGetInt(announcement, "link_type", out var linkTypeValue) ? linkTypeValue : (int?)null; + var linkUri = announcement.TryGetProperty("link_uri", out var luProp) ? luProp.GetString() : null; + var title = announcement.TryGetProperty("title", out var tProp) ? tProp.GetString() : null; + var normalized = type.ToLower().Replace(" ", "_"); + + + string? imageName = null; + if (!string.IsNullOrWhiteSpace(imageRaw)) + { + imageName = SaveDataUrlFile(imageRaw, DBConfig.CDNImages); + imageName ??= imageRaw; + } + + + + + var panelImage = imageRaw; + if (!string.IsNullOrWhiteSpace(imageName) && imageName != imageRaw && !imageName.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + panelImage = $"{DBConfig.ServerUrl}/images/img/{imageName}"; + + var buttonType = linkTypeRaw ?? 0; + var platformType = -1; + var buttonName = string.IsNullOrWhiteSpace(linkName) + ? (normalized == "maintenance" || normalized == "game_update" ? string.Empty : "Learn More") + : linkName; + var buttonUri = string.IsNullOrWhiteSpace(linkUri) ? string.Empty : linkUri; + var annStatus = normalized == "maintenance" ? 4 : 0; + + var row = new JsonObject + { + ["id"] = nextId, + ["type"] = type, + ["message"] = message, + ["image"] = panelImage, + ["minutes"] = minutes, + ["expires_at"] = expiresAt, + ["created_at"] = DateTime.UtcNow, + ["title"] = string.IsNullOrWhiteSpace(title) ? type : title, + ["link_name"] = buttonName, + ["link_type"] = buttonType, + ["link_uri"] = buttonUri, + + ["AnnouncementId"] = nextId, + ["AnnouncementStatus"] = annStatus, + ["AnnouncementType"] = buttonType, + ["Title"] = string.IsNullOrWhiteSpace(title) ? type : title, + ["Body"] = message, + ["ImageName"] = imageName ?? "", + ["IsMostRecent"] = true, + ["LinkName"] = buttonName, + ["LinkType"] = platformType, + ["LinkUri"] = buttonUri, + + + + ["Platform"] = -1, + ["CreatedAt"] = DateTime.UtcNow + }; + list.Add(row); + created = row; + + + + var annObject = new Announcement + { + AnnouncementId = nextId, + AnnouncementStatus = annStatus, + AnnouncementType = buttonType, + Title = string.IsNullOrWhiteSpace(title) ? type : title, + Body = message, + ImageName = imageName ?? "", + IsMostRecent = true, + LinkName = buttonName, + LinkType = platformType, + LinkUri = buttonUri, + Platform = Platforms.All, + CreatedAt = DateTime.UtcNow + }; + nextId++; + + + + + + + Notification? notif = null; + if (normalized == "maintenance") + { + + + var startsInMinutes = Math.Clamp(minutes ?? 30, 0, 61); + notif = new Notification + { + Id = NotificationType.ServerMaintenance, + Msg = new ServerMaintenance { StartsInMinutes = startsInMinutes } + }; + maintenanceMinutes = startsInMinutes; + } + else if (normalized == "game_update") + { + notif = Notify.EmptyObjectPayload(GameUpdateVersionCheck.AppVersionUpdateNotificationId); + } + else + { + notif = AnnouncementBroadcast.Update(annObject); + } + if (notif != null) pendingNotifs.Add(notif); + } + + config.Value = JsonSerializer.Serialize(list); + await db.SaveChangesAsync(); + + + + + + + if (maintenanceMinutes.HasValue) + await ApplyMaintenanceToConfig(maintenanceMinutes.Value); + + + + foreach (var notif in pendingNotifs) + { + try { await Notify.SendGlobalNotify(notif); } + catch (Exception) { } + } + + return Ok(created ?? new JsonObject()); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new JsonObject { ["error"] = ex.Message }); + } + } + + private List GetModerationReports(string? order, bool ascending) + { + var state = LoadState(); + if (state.ModerationReports.Count == 0) + { + state.ModerationReports = new List(); + SaveState(state); + } + + return OrderRows(state.ModerationReports.Cast().ToList(), order, ascending); + } + + private List GetAvatarItems() + { + + + + var avatarItems = DBConfig.DefaultAvatarCatalog.FromJson>() ?? new List(); + return avatarItems + .Where(item => item != null && !string.IsNullOrWhiteSpace(item.AvatarItemDesc)) + .Select(item => new + { + avatar_item_desc = item.AvatarItemDesc, + unlocked_level = item.UnlockedLevel, + platform_mask = item.PlatformMask, + friendly_name = item.FriendlyName ?? "", + tooltip = item.Tooltip ?? "", + rarity = ((Rarity)item.Rarity).ToString(), + // The picture the game shows for this item. The panel builds its own URL from + // it (the same /images/img/ the server serves), so an item that has one + // is shown as a tile rather than as a line of text. + thumbnail_image = item.ThumbnailImage ?? "" + }).Cast().ToList(); + } + + private async Task> GetPlayerEvents(string? order, bool ascending) + { + + + var events = await db.PlayerEvents.ToListAsync(); + + var rows = events.Select(ev => new + { + id = ev.PlayerEventId, + creator_player_id = ev.CreatorPlayerId, + image_name = ev.ImageName, + room_id = ev.RoomId, + name = ev.Name, + description = ev.Description, + sub_room_id = ev.SubRoomId, + club_id = (long?)null, + start_time = ev.StartTime, + end_time = ev.EndTime, + attendee_count = ev.AttendeeCount, + state = 0, + moderation_state = ev.ModerationState, + accessibility = (int)ev.Accessibility, + is_multi_instance = false, + support_multi_instance_room_chat = false, + broadcasting_room_instance_id = (long?)null, + default_broadcast_permissions = 0, + can_request_broadcast_permissions = 0, + recurrence_schedule = (string?)null, + created_at = ev.CreatedAt + }).Cast().ToList(); + + return OrderRows(rows, order, ascending); + } + + private async Task> GetPlayerEventResponses(string? eqCol, string? eqVal, string? order, bool ascending) + { + var query = db.PlayerEventResponses.AsQueryable(); + if (eqCol == "player_event_id" && long.TryParse(eqVal, out var eventId)) + query = query.Where(x => x.PlayerEventId == eventId); + + var responses = await query.ToListAsync(); + var playerIds = responses.Select(r => r.PlayerId).Distinct().ToList(); + var profiles = await db.Profiles.Where(p => playerIds.Contains(p.accountId)).ToDictionaryAsync(p => p.accountId); + + var rows = responses.Select(r => new + { + id = r.PlayerEventResponseId, + player_event_id = r.PlayerEventId, + player_id = r.PlayerId, + player_name = profiles.TryGetValue(r.PlayerId, out var profile) ? profile.displayName : ("Player " + r.PlayerId), + type = (int)r.Type, + type_name = r.Type.ToString(), + created_at = r.CreatedAt + }).Cast().ToList(); + + return OrderRows(rows, order, ascending); + } + + private async Task> GetEventInvitations(string? eqCol, string? eqVal, string? order, bool ascending) + { + var query = db.Messages.Where(m => m.Type == MessageType.PlayerEventInvitation); + if (eqCol == "player_event_id" && long.TryParse(eqVal, out var eventId)) + query = query.Where(m => m.PlayerEventId == eventId); + + var invitations = await query.ToListAsync(); + var playerIds = invitations.SelectMany(m => new[] { m.FromPlayerId, m.ToPlayerId }).Distinct().ToList(); + var profiles = await db.Profiles.Where(p => playerIds.Contains(p.accountId)).ToDictionaryAsync(p => p.accountId); + + var rows = invitations.Select(m => new + { + id = m.Id, + player_event_id = m.PlayerEventId, + from_player_id = m.FromPlayerId, + from_name = profiles.TryGetValue(m.FromPlayerId, out var from) ? from.displayName : ("Player " + m.FromPlayerId), + to_player_id = m.ToPlayerId, + to_name = profiles.TryGetValue(m.ToPlayerId, out var to) ? to.displayName : ("Player " + m.ToPlayerId), + sent_time = m.SentTime + }).Cast().ToList(); + + return OrderRows(rows, order, ascending); + } + + private async Task UpdatePlayerEvent(long id, JsonElement payload) + { + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == id); + if (ev == null) return NotFound(); + + if (TryGetString(payload, "name", out var name)) ev.Name = name; + if (TryGetString(payload, "description", out var description)) ev.Description = description; + if (TryGetString(payload, "image_name", out var imageName)) ev.ImageName = imageName; + if (TryGetLong(payload, "room_id", out var roomId) && roomId > 0) ev.RoomId = roomId; + if (TryGetDate(payload, "start_time", out var startTime)) ev.StartTime = startTime; + if (TryGetDate(payload, "end_time", out var endTime)) ev.EndTime = endTime; + if (TryGetInt(payload, "attendee_count", out var attendeeCount)) ev.AttendeeCount = attendeeCount; + if (TryGetInt(payload, "accessibility", out var accessibility)) ev.Accessibility = (Accessibility)accessibility; + if (TryGetLong(payload, "creator_player_id", out var creatorId) && creatorId > 0) ev.CreatorPlayerId = creatorId; + if (TryGetInt(payload, "moderation_state", out var moderationState)) ev.ModerationState = moderationState; + if (payload.TryGetProperty("sub_room_id", out var subRoomElem) && subRoomElem.ValueKind != JsonValueKind.Undefined) + { + if (subRoomElem.ValueKind == JsonValueKind.Null) ev.SubRoomId = null; + else if (subRoomElem.ValueKind == JsonValueKind.Number && subRoomElem.TryGetInt64(out var subRoomId)) ev.SubRoomId = subRoomId; + else if (subRoomElem.ValueKind == JsonValueKind.String && long.TryParse(subRoomElem.GetString(), out var subRoomStr)) ev.SubRoomId = subRoomStr; + } + + await db.SaveChangesAsync(); + return Ok(new { success = true }); + } + + private async Task> DeletePlayerEvents(List ids) + { + var events = await db.PlayerEvents.Where(x => ids.Contains(x.PlayerEventId)).ToListAsync(); + if (events.Count == 0) return new List(); + + var eventIds = events.Select(x => x.PlayerEventId).ToList(); + var responses = await db.PlayerEventResponses.Where(x => eventIds.Contains(x.PlayerEventId)).ToListAsync(); + db.PlayerEventResponses.RemoveRange(responses); + db.PlayerEvents.RemoveRange(events); + await db.SaveChangesAsync(); + + return events.Select(ev => new { id = ev.PlayerEventId, name = ev.Name }).Cast().ToList(); + } + + private static bool TryGetDate(JsonElement payload, string prop, out DateTime value) + { + if (payload.TryGetProperty(prop, out var elem) && elem.ValueKind == JsonValueKind.String && DateTime.TryParse(elem.GetString(), out var parsed)) + { + value = parsed.ToUniversalTime(); + return true; + } + value = default; + return false; + } + + /// + /// A room's join gift boxes as the panel reads them. A room with no list answers with the + /// single box its own row describes, so a room configured before the list existed opens on the + /// box it actually hands out rather than on an empty editor. + /// + private static List JoinGiftRows(RoomData room, List? rows) + { + var source = rows is { Count: > 0 } ? rows : [RoomJoinGiftUtils.Snapshot(room)]; + return source + .OrderBy(x => x.SortOrder) + .ThenBy(x => x.RoomJoinGiftId) + .Select(JoinGiftRow) + .ToList(); + } + + private static object JoinGiftRow(RoomJoinGift gift) => new + { + id = gift.RoomJoinGiftId, + sort_order = gift.SortOrder, + version = gift.Version, + enabled = gift.Enabled, + once_per_player = gift.OncePerPlayer, + title = gift.Title, + message = gift.Message, + // Digits in a string, for the same reason the room's own sender is: an account id is + // eighteen digits and a JSON number would arrive in the panel rounded. + from_player_id = Math.Max(0, gift.FromPlayerId).ToString(CultureInfo.InvariantCulture), + avatar_item = gift.AvatarItem, + avatar_item_type = gift.AvatarItemType.ToString(), + equipment_item = gift.EquipmentItem, + equipment_modification_guid = gift.EquipmentModificationGuid, + consumable_item = gift.ConsumableItem, + currency_type = gift.CurrencyType.ToString(), + currency_amount = gift.CurrencyAmount, + xp = gift.Xp, + context = gift.Context.ToString(), + rarity = gift.Rarity.ToString() + }; + + /// + /// Replaces a room's list of join gift boxes with the list the panel sent. A box that is new is + /// inserted; a box whose contents changed keeps its identity but gets a new version, so players + /// who already opened the old box are handed the new one; a box the panel left out is deleted. + /// Anything that cannot be read refuses the save before a row is touched, so a mistyped id never + /// leaves the room half-written. + /// + private async Task ApplyJoinGiftListAsync(RoomData room, JsonElement list) + { + var wanted = new List(); + var index = 0; + foreach (var element in list.EnumerateArray()) + { + var gift = ReadJoinGift(element, index, out var error); + if (gift == null) return BadRequest(error); + gift.SortOrder = index; + wanted.Add(gift); + index++; + } + + if (wanted.Count > MaxJoinGiftsPerRoom) + return BadRequest($"A room can hold at most {MaxJoinGiftsPerRoom} join gift boxes."); + + var existing = await db.RoomJoinGifts.Where(x => x.RoomId == room.RoomId).ToListAsync(); + var kept = new HashSet(); + var changed = false; + + foreach (var gift in wanted) + { + RoomJoinGift? row = null; + if (gift.RoomJoinGiftId > 0) + { + row = existing.FirstOrDefault(x => x.RoomJoinGiftId == gift.RoomJoinGiftId); + if (row == null) + return BadRequest($"join_gifts[{gift.SortOrder}].id {gift.RoomJoinGiftId} is not one of this room's gift boxes."); + } + + if (row == null) + { + var added = new RoomJoinGift + { + RoomId = room.RoomId, + SortOrder = gift.SortOrder, + Version = 1 + }; + CopyJoinGiftValues(gift, added); + db.RoomJoinGifts.Add(added); + changed = true; + continue; + } + + kept.Add(row.RoomJoinGiftId); + if (!SameJoinGift(row, gift)) + { + CopyJoinGiftValues(gift, row); + row.Version++; + changed = true; + } + + // Moving a box up or down the list reissues nothing: the box itself did not change. + row.SortOrder = gift.SortOrder; + } + + var removed = existing.Where(x => !kept.Contains(x.RoomJoinGiftId)).ToList(); + if (removed.Count > 0) + { + db.RoomJoinGifts.RemoveRange(removed); + changed = true; + } + + // The room's own fields describe its first box, so a reader that only knows those fields - + // and the editor of a room saved before the list existed - still sees what it hands out. + var first = wanted.OrderBy(x => x.SortOrder).FirstOrDefault(); + room.JoinGiftEnabled = wanted.Any(x => x.Enabled); + room.JoinGiftOncePerPlayer = first?.OncePerPlayer ?? true; + room.JoinGiftTitle = first?.Title ?? "Room Visit Gift"; + room.JoinGiftMessage = first?.Message ?? ""; + room.JoinGiftAvatarItem = first?.AvatarItem ?? ""; + room.JoinGiftAvatarItemType = first?.AvatarItemType ?? AvatarItemType.Outfit; + room.JoinGiftEquipmentItem = first?.EquipmentItem ?? ""; + room.JoinGiftEquipmentModificationGuid = first?.EquipmentModificationGuid ?? ""; + room.JoinGiftConsumableItem = first?.ConsumableItem ?? ""; + room.JoinGiftCurrencyType = first?.CurrencyType ?? CurrencyType.RecCenterTokens; + room.JoinGiftCurrencyAmount = first?.CurrencyAmount ?? 0; + room.JoinGiftXp = first?.Xp ?? 0; + room.JoinGiftContext = first?.Context ?? GiftContext.Game_Drop; + room.JoinGiftRarity = first?.Rarity ?? Rarity.Common; + room.JoinGiftFromPlayerId = first?.FromPlayerId ?? 0; + + if (changed) room.JoinGiftVersion++; + + await db.SaveChangesAsync(); + return null; + } + + private const int MaxJoinGiftsPerRoom = 20; + + /// One box as the panel sent it, or null with the reason it could not be read. + private static RoomJoinGift? ReadJoinGift(JsonElement element, int index, out string? error) + { + error = null; + if (element.ValueKind != JsonValueKind.Object) + { + error = $"join_gifts[{index}] must be a gift box."; + return null; + } + + var gift = new RoomJoinGift { RoomJoinGiftId = Math.Max(0, TryGetJsonLong(element, "id") ?? 0) }; + + if (TryGetJsonBool(element, "enabled") is { } enabled) gift.Enabled = enabled; + if (TryGetJsonBool(element, "once_per_player") is { } oncePerPlayer) gift.OncePerPlayer = oncePerPlayer; + if (GetJsonString(element, "title") is { } title) gift.Title = title; + if (GetJsonString(element, "message") is { } message) gift.Message = message; + if (GetJsonString(element, "avatar_item") is { } avatarItem) gift.AvatarItem = avatarItem; + if (ParseAvatarItemType(GetJsonString(element, "avatar_item_type")) is { } avatarItemType) gift.AvatarItemType = avatarItemType; + if (GetJsonString(element, "equipment_item") is { } equipmentItem) gift.EquipmentItem = equipmentItem; + if (GetJsonString(element, "equipment_modification_guid") is { } modificationGuid) gift.EquipmentModificationGuid = modificationGuid; + if (GetJsonString(element, "consumable_item") is { } consumableItem) gift.ConsumableItem = consumableItem; + if (ParseCurrencyType(GetJsonString(element, "currency_type")) is { } currencyType) gift.CurrencyType = currencyType; + if (TryGetJsonInt(element, "currency_amount") is { } currencyAmount) gift.CurrencyAmount = Math.Max(0, currencyAmount); + if (TryGetJsonInt(element, "xp") is { } xp) gift.Xp = Math.Max(0, xp); + if (ParseGiftContext(GetJsonString(element, "context")) is { } context) gift.Context = context; + if (ParseRarity(GetJsonString(element, "rarity")) is { } rarity) gift.Rarity = rarity; + + // Named with the box it belongs to: an eighteen-digit account id has to stay digits, and a + // refusal has to say which box it came from. + if (TryGetAccountId(element, "from_player_id", out var fromPlayerId, out var fromError)) + { + if (fromError != null) + { + error = $"join_gifts[{index}].from_player_id must be an account id in digits, or blank for no sender."; + return null; + } + + gift.FromPlayerId = fromPlayerId; + } + + return gift; + } + + private static bool SameJoinGift(RoomJoinGift row, RoomJoinGift sent) + { + return row.Enabled == sent.Enabled && + row.OncePerPlayer == sent.OncePerPlayer && + string.Equals(row.Title ?? "", sent.Title ?? "", StringComparison.Ordinal) && + string.Equals(row.Message ?? "", sent.Message ?? "", StringComparison.Ordinal) && + string.Equals(row.AvatarItem ?? "", sent.AvatarItem ?? "", StringComparison.Ordinal) && + row.AvatarItemType == sent.AvatarItemType && + string.Equals(row.EquipmentItem ?? "", sent.EquipmentItem ?? "", StringComparison.Ordinal) && + string.Equals(row.EquipmentModificationGuid ?? "", sent.EquipmentModificationGuid ?? "", StringComparison.Ordinal) && + string.Equals(row.ConsumableItem ?? "", sent.ConsumableItem ?? "", StringComparison.Ordinal) && + row.CurrencyType == sent.CurrencyType && + row.CurrencyAmount == sent.CurrencyAmount && + row.Xp == sent.Xp && + row.Context == sent.Context && + row.Rarity == sent.Rarity && + row.FromPlayerId == sent.FromPlayerId; + } + + /// The fields that make a box a box - never its id, room, order or version. + private static void CopyJoinGiftValues(RoomJoinGift from, RoomJoinGift to) + { + to.Enabled = from.Enabled; + to.OncePerPlayer = from.OncePerPlayer; + to.Title = from.Title ?? ""; + to.Message = from.Message ?? ""; + to.AvatarItem = from.AvatarItem ?? ""; + to.AvatarItemType = from.AvatarItemType; + to.EquipmentItem = from.EquipmentItem ?? ""; + to.EquipmentModificationGuid = from.EquipmentModificationGuid ?? ""; + to.ConsumableItem = from.ConsumableItem ?? ""; + to.CurrencyType = from.CurrencyType; + to.CurrencyAmount = from.CurrencyAmount; + to.Xp = from.Xp; + to.Context = from.Context; + to.Rarity = from.Rarity; + to.FromPlayerId = from.FromPlayerId; + } + + private async Task UpdateRoom(long id, JsonElement payload) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) + { + if (payload.TryGetProperty("banned", out var liftBan) && liftBan.ValueKind == JsonValueKind.False) + { + var restored = await RoomBanStore.RestoreAsync(db, id); + if (restored) return Ok(new { success = true, room_id = id, banned = false, restored = true }); + } + + return NotFound("Room does not exist."); + } + + if (TryGetString(payload, "name", out var name)) room.Name = name; + if (TryGetString(payload, "description", out var description)) room.Description = description; + if (TryGetString(payload, "image_name", out var imageName)) room.ImageName = imageName; + if (TryGetString(payload, "custom_warning", out var customWarning)) room.CustomWarning = customWarning; + + if (payload.TryGetProperty("supports_screens", out var supportsScreens)) room.SupportsScreens = supportsScreens.GetBoolean(); + if (payload.TryGetProperty("supports_walk_vr", out var supportsWalk)) room.SupportsWalkVR = supportsWalk.GetBoolean(); + if (payload.TryGetProperty("supports_teleport_vr", out var supportsTeleport)) room.SupportsTeleportVR = supportsTeleport.GetBoolean(); + if (payload.TryGetProperty("supports_vr_low", out var supportsVrLow)) room.SupportsVRLow = supportsVrLow.GetBoolean(); + if (payload.TryGetProperty("supports_quest2", out var supportsQuest2)) room.SupportsQuest2 = supportsQuest2.GetBoolean(); + if (payload.TryGetProperty("supports_mobile", out var supportsMobile)) room.SupportsMobile = supportsMobile.GetBoolean(); + if (payload.TryGetProperty("supports_juniors", out var supportsJuniors)) room.SupportsJuniors = supportsJuniors.GetBoolean(); + if (payload.TryGetProperty("cloning_allowed", out var cloningAllowed)) room.CloningAllowed = cloningAllowed.GetBoolean(); + if (payload.TryGetProperty("disable_mic_auto_mute", out var disableMicAutoMute)) room.DisableMicAutoMute = disableMicAutoMute.GetBoolean(); + if (payload.TryGetProperty("disable_room_comments", out var disableRoomComments)) room.DisableRoomComments = disableRoomComments.GetBoolean(); + if (payload.TryGetProperty("encrypt_voice_chat", out var encryptVoiceChat)) room.EncryptVoiceChat = encryptVoiceChat.GetBoolean(); + if (payload.TryGetProperty("is_dorm", out var isDorm)) room.IsDorm = isDorm.GetBoolean(); + + if (TryGetInt(payload, "min_level", out var minLevel)) room.MinLevel = minLevel; + if (TryGetInt(payload, "max_players", out var maxPlayers)) room.MaxPlayers = maxPlayers; + if (TryGetInt(payload, "max_player_calculation_mode", out var maxCalcMode)) room.MaxPlayerCalculationMode = (MaxPlayerMode)maxCalcMode; + + // A room's boxes arrive as a list. When the panel sends one, it is the whole truth about + // that room's boxes and the single-box fields below are not read: the list is written to + // RoomJoinGifts and the room's own fields are then filled in from its first box, so every + // older reader of those fields still sees the box the room hands out first. + if (payload.TryGetProperty("join_gifts", out var joinGiftList) && joinGiftList.ValueKind == JsonValueKind.Array) + { + var refusal = await ApplyJoinGiftListAsync(room, joinGiftList); + if (refusal != null) return refusal; + } + else + { + var giftChanged = false; + if (payload.TryGetProperty("join_gift_enabled", out var joinGiftEnabled)) room.JoinGiftEnabled = joinGiftEnabled.GetBoolean(); + if (payload.TryGetProperty("join_gift_once_per_player", out var joinGiftOnce)) room.JoinGiftOncePerPlayer = joinGiftOnce.GetBoolean(); + if (TryGetString(payload, "join_gift_title", out var giftTitle)) { room.JoinGiftTitle = giftTitle ?? ""; giftChanged = true; } + if (TryGetString(payload, "join_gift_message", out var giftMessage)) { room.JoinGiftMessage = giftMessage ?? ""; giftChanged = true; } + if (TryGetString(payload, "join_gift_avatar_item", out var giftAvatar)) { room.JoinGiftAvatarItem = giftAvatar ?? ""; giftChanged = true; } + if (TryGetString(payload, "join_gift_equipment_item", out var giftEquip)) { room.JoinGiftEquipmentItem = giftEquip ?? ""; giftChanged = true; } + if (TryGetString(payload, "join_gift_consumable_item", out var giftCons)) { room.JoinGiftConsumableItem = giftCons ?? ""; giftChanged = true; } + if (TryGetInt(payload, "join_gift_currency_amount", out var giftCurrencyAmount)) { room.JoinGiftCurrencyAmount = Math.Max(0, giftCurrencyAmount); giftChanged = true; } + if (TryGetInt(payload, "join_gift_xp", out var giftXp)) { room.JoinGiftXp = Math.Max(0, giftXp); giftChanged = true; } + if (TryGetString(payload, "join_gift_currency_type", out var giftCurrencyTypeRaw) && + StorefrontShared.TryParseCurrencyType(giftCurrencyTypeRaw, out var giftCurrencyType)) + { + room.JoinGiftCurrencyType = giftCurrencyType; + giftChanged = true; + } + if (TryGetString(payload, "join_gift_context", out var giftContextRaw) && ParseGiftContext(giftContextRaw) is { } giftContext) + { + room.JoinGiftContext = giftContext; + giftChanged = true; + } + if (TryGetString(payload, "join_gift_rarity", out var giftRarityRaw) && ParseRarity(giftRarityRaw) is { } giftRarity) + { + room.JoinGiftRarity = giftRarity; + giftChanged = true; + } + if (TryGetString(payload, "join_gift_avatar_item_type", out var giftAvatarTypeRaw) && ParseAvatarItemType(giftAvatarTypeRaw) is { } giftAvatarType) + { + room.JoinGiftAvatarItemType = giftAvatarType; + giftChanged = true; + } + if (TryGetString(payload, "join_gift_equipment_modification_guid", out var giftEquipMod)) + { + room.JoinGiftEquipmentModificationGuid = giftEquipMod ?? ""; + giftChanged = true; + } + if (TryGetAccountId(payload, "join_gift_from_player_id", out var giftFrom, out var giftFromError)) + { + if (giftFromError != null) return BadRequest(giftFromError); + // Only a sender that actually changed re-issues the box: this field is not carried by + // every panel that saves a room, so it must not bump the gift version on its own. + if (room.JoinGiftFromPlayerId != giftFrom) + { + room.JoinGiftFromPlayerId = giftFrom; + giftChanged = true; + } + } + if (TryGetInt(payload, "join_gift_version", out var giftVersion) && giftVersion > 0) + room.JoinGiftVersion = giftVersion; + else if (giftChanged) + room.JoinGiftVersion++; + } + + + var stats = await db.RoomStats.FirstOrDefaultAsync(x => x.RoomId == room.RoomId); + if (stats == null && HasAny(payload, "cheer_count", "favorite_count", "visitor_count", "visit_count")) + { + stats = new RadicadeServer.RecNet.Rooms.RoomStats { RoomId = room.RoomId }; + db.RoomStats.Add(stats); + } + if (stats != null) + { + if (TryGetInt(payload, "cheer_count", out var cheerCount)) stats.CheerCount = cheerCount; + if (TryGetInt(payload, "favorite_count", out var favoriteCount)) stats.FavoriteCount = favoriteCount; + if (TryGetInt(payload, "visitor_count", out var visitorCount)) stats.VisitorCount = visitorCount; + if (TryGetInt(payload, "visit_count", out var visitCount)) stats.VisitCount = visitCount; + } + + + + if (TryGetString(payload, "publish_state", out var publishState) && Enum.TryParse(publishState, true, out var state)) + room.State = state; + if (payload.TryGetProperty("banned", out var banned)) + { + if (banned.ValueKind == JsonValueKind.True) + { + var banReason = TryGetString(payload, "ban_reason", out var reason) ? reason ?? "" : ""; + await RoomBanStore.BanAsync(db, room, banReason, await GetPanelAdminUsernameAsync()); + return Ok(new { success = true, room_id = id, banned = true, reason = banReason }); + } + + room.State = RoomState.Active; + } + + if (payload.TryGetProperty("accessibility", out var accessibilityProp)) + { + var accessibilityText = accessibilityProp.ValueKind switch + { + JsonValueKind.Number => accessibilityProp.GetRawText(), + JsonValueKind.String => accessibilityProp.GetString(), + _ => null + }; + if (TryParseRoomAccessibility(accessibilityText, out var parsedAccessibility)) + room.Accessibility = parsedAccessibility; + } + + var featuredFlag = TryGetJsonBool(payload, "featured") ?? TryGetJsonBool(payload, "is_featured"); + if (featuredFlag != null) + { + var tags = StringTools.ReadTags(room.Tags); + var featuredTag = tags.FirstOrDefault(x => string.Equals(x.Tag, "featured", StringComparison.OrdinalIgnoreCase)); + if (featuredFlag.Value) + { + if (featuredTag == null) + tags.Add(new ItemTag { Type = TagType.Auto, Tag = "featured" }); + } + else if (featuredTag != null) + { + tags.Remove(featuredTag); + } + + room.Tags = StringTools.CreateTagString(tags); + } + else if (payload.TryGetProperty("tags", out var tagsElement) || payload.TryGetProperty("Tags", out tagsElement)) + { + var tagString = ParseRoomTags(tagsElement); + if (tagString != null) room.Tags = tagString; + } + + await db.SaveChangesAsync(); + return Ok(new { success = true }); + } + + private static string? ParseRoomTags(JsonElement element) + { + try + { + var tags = new List(); + + if (element.ValueKind == JsonValueKind.String) + { + AddTagsFromString(element.GetString()); + } + else if (element.ValueKind == JsonValueKind.Array) + { + foreach (var entry in element.EnumerateArray()) + { + if (entry.ValueKind == JsonValueKind.String) + { + AddTagsFromString(entry.GetString()); + continue; + } + + if (entry.ValueKind != JsonValueKind.Object) continue; + var name = (GetJsonString(entry, "Tag") ?? GetJsonString(entry, "tag"))?.Trim().TrimStart('#'); + if (string.IsNullOrWhiteSpace(name)) continue; + + var type = TagType.General; + var rawType = GetJsonString(entry, "Type") ?? GetJsonString(entry, "type"); + if (rawType != null) + { + if (int.TryParse(rawType, out var typeValue) && Enum.IsDefined(typeof(TagType), typeValue)) + type = (TagType)typeValue; + else if (Enum.TryParse(rawType, true, out var parsedType)) + type = parsedType; + } + + tags.Add(new ItemTag { Tag = name, Type = type }); + } + } + else + { + return null; + } + + return StringTools.CreateTagString(tags); + + void AddTagsFromString(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return; + foreach (var part in raw.Split(new[] { ',', ' ', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var name = part.TrimStart('#'); + if (!string.IsNullOrWhiteSpace(name)) + tags.Add(new ItemTag { Tag = name, Type = TagType.General }); + } + } + } + catch + { + return null; + } + } + + + + + + + + private async Task> AddGiftLogRows(AdminPanelState state, JsonElement payload) + { + var added = AddRows(state, state.GiftLog, payload); + + foreach (var gift in added) + { + var playerId = long.TryParse(gift["player_id"]?.ToString(), out var pid) ? pid : 0; + if (playerId > 0) + { + await ProcessGiftRowAsync(playerId, gift); + } + else + { + gift["error"] = "Invalid or missing player_id."; + } + } + + SaveState(state); + return added; + } + + + + + + + + + + private async Task ProcessGiftRowAsync(long playerId, JsonObject gift) + { + long giftDropId = 0; + try + { + var message = gift["message"]?.ToString() ?? "A gift from the Radicade Team!"; + var currencyAmount = TryGetJsonInt(gift, "currency") ?? 0; + var xpAmount = TryGetJsonInt(gift, "xp") ?? 0; + var avatar = gift["avatar"]?.ToString() ?? ""; + var cons = gift["cons"]?.ToString() ?? ""; + var equip = gift["equip"]?.ToString() ?? ""; + var currencyTypeRaw = gift["currency_type"]?.ToString() ?? gift["currencyType"]?.ToString() ?? ""; + + var fromPlayerId = long.TryParse(gift["from_player_id"]?.ToString(), out var typedFrom) && typedFrom > 0 + ? typedFrom + : RadicadeTeamAccountId; + + var equipModificationGuid = gift["equipment_modification_guid"]?.ToString() + ?? gift["prefab_modify"]?.ToString() + ?? ""; + + + + + bool isRoomCurrency = string.Equals(currencyTypeRaw, "RoomCurrency", StringComparison.OrdinalIgnoreCase); + var currencyType = StorefrontShared.TryParseCurrencyType(currencyTypeRaw, out var parsed) ? parsed : CurrencyType.Invalid; + + if (currencyAmount > 0 && currencyType == CurrencyType.Invalid && !isRoomCurrency) + throw new InvalidOperationException($"Unknown currency type '{currencyTypeRaw}' - gift not routed to avoid crediting the wrong wallet."); + + + + + if (isRoomCurrency && currencyAmount > 0) + throw new InvalidOperationException("RoomCurrency gifts are not supported yet: no room currency storage exists and no room_id was supplied."); + + bool hasReward = currencyAmount > 0 || xpAmount > 0 + || !string.IsNullOrWhiteSpace(avatar) || !string.IsNullOrWhiteSpace(cons) || !string.IsNullOrWhiteSpace(equip); + if (!hasReward) + throw new InvalidOperationException("Gift has no rewards (item, currency or XP)."); + + + + + giftDropId = NewGiftDropId(); + + var giftBox = new Gift + { + GiftDropId = giftDropId, + ItemSetId = 0, + ItemSetFriendlyName = "Radicade Gift", + FriendlyName = gift["title"]?.ToString() ?? "A Gift from Radicade!", + Tooltip = message, + IsQuery = false, + Unique = false, + SubscribersOnly = false, + ConsumableItemDesc = cons, + AvatarItemDesc = avatar, + AvatarItemType = AvatarItemType.Outfit, + EquipmentPrefabName = equip, + EquipmentModificationGuid = equipModificationGuid, + CurrencyType = currencyType, + Currency = currencyAmount, + Xp = xpAmount, + Context = TryParseGiftContext(gift["context"]?.ToString()) ?? GiftContext.Game_Drop, + Rarity = TryParseRarity(gift["rarity"]?.ToString()) ?? Rarity.Common, + FromPlayerId = fromPlayerId + }; + + await PersistGiftBoxAsync(playerId, giftBox, gift); + + + gift["sent"] = true; + gift["sent_at"] = DateTime.UtcNow; + gift["gift_drop_id"] = giftDropId; + gift["boxed"] = true; + + gift["from_player_id"] = fromPlayerId; + } + catch (Exception ex) + { + + gift["error"] = ex.Message; + } + return giftDropId; + } + + + + + + private async Task PersistGiftBoxAsync(long playerId, Gift giftBox, JsonObject? logRow) + { + var overwritten = await StorefrontShared.PersistGiftBoxAsync(db, playerId, giftBox); + if (overwritten != 0 && logRow != null) + logRow["overwrote_gift_drop_id"] = overwritten; + } + + private async Task> ProcessGiftLog(string? eqCol, string? eqVal, string? order, bool ascending) + { + var state = LoadState(); + var filtered = state.GiftLog.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(eqCol) && eqVal != null) + filtered = filtered.Where(x => x[eqCol]?.ToString() == eqVal); + + + + + var pendingGifts = filtered.Where(x => !string.Equals(x["sent"]?.ToString(), "true", StringComparison.OrdinalIgnoreCase)).ToList(); + foreach (var gift in pendingGifts) + { + var playerId = long.TryParse(gift["player_id"]?.ToString(), out var pid) ? pid : 0; + if (playerId > 0) + { + await ProcessGiftRowAsync(playerId, gift); + } + } + + SaveState(state); + return OrderRows(filtered.Cast().ToList(), order, ascending); + } +} + +public class WeeklyScheduleRequest +{ + public string Revision { get; set; } = ""; + public string Title { get; set; } = "Weekly Challenges"; + public bool Enabled { get; set; } = true; + public string RewardKey { get; set; } = ""; + public int FallbackTokens { get; set; } = 300; + public int ChallengeMapId { get; set; } + public List ChallengeIds { get; set; } = new(); +} + +public class AdminPanelState +{ + public List Storefronts { get; set; } = new(); + public List Challenges { get; set; } = new(); + public List ModerationLog { get; set; } = new(); + public List GiftLog { get; set; } = new(); + public List Notes { get; set; } = new(); + public List AuditLog { get; set; } = new(); + public List RoomComments { get; set; } = new(); + public JsonObject? CommunityBoard { get; set; } + public List ModerationReports { get; set; } = new(); + public List PlayerEvents { get; set; } = new(); +} diff --git a/Server/Admin/AdminProfileAPI.cs b/Server/Admin/AdminProfileAPI.cs new file mode 100644 index 0000000..3354e75 --- /dev/null +++ b/Server/Admin/AdminProfileAPI.cs @@ -0,0 +1,477 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Cors; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using RadicadeServer.API.Notifications; +using RadicadeServer.Auth; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin +{ + [Route("admin/profiles")] + [EnableCors("AdminPanel")] + public class AdminProfileAPI(RecDBContext db) : Controller + { + private Task RejectInvalidKey(string key) + { + return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); + } + + private async Task NotifyUser(long playerId) + { + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) + return; + + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (plrData == null) + return; + + await Notify.SendNotify(playerId, new Notification + { + Id = "AccountUpdate", + Msg = profile + }); + await Notify.UpdateSubscriptions(playerId, Notifs.SubscriptionUpdateProfile, profile); + + await Notify.SendNotify(playerId, new Notification + { + Id = "SelfAccountUpdate", + Msg = new SelfAccount + { + juniorState = plrData.Birthday != null && plrData.Birthday.Value.Year > 1900 + ? JuniorState.BasedOnBirthday : JuniorState.PendingOverride, + accountId = playerId, + parentAccountId = plrData.Parent, + availableUsernameChanges = 99, + bio = profile.bio, + displayName = profile.displayName, + email = plrData.Email, + createdAt = profile.createdAt, + isJunior = profile.isJunior, + phone = plrData.Phone, + platforms = profile.platforms, + profileImage = profile.profileImage, + username = profile.username + } + }); + } + [HttpGet("whitelist")] + public async Task WhitelistUser([FromQuery] string key, [FromQuery] long playerId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound(); + + var cachedLogins = await db.CachedLogins.Where(x => x.accountId == playerId).ToListAsync(); + + string addedIds = ""; + List ids = new(); + foreach (var cached in cachedLogins) + { + if (cached.platform != Platforms.Steam) continue; + if (!SteamWhitelist.Contains(cached.platformId)) + { + SteamWhitelist.Add(cached.platformId); + addedIds += cached.platformId + ","; + ids.Add(cached.platformId); + } + } + + addedIds = addedIds.Trim().TrimEnd(','); + + return Ok($"Whitelisted @{profile.username}! Added Steam IDs: {addedIds}"); + } + [HttpGet("whitelist/toggle")] + public async Task ToggleWhitelist([FromQuery] string key, [FromQuery] bool enabled) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + SteamWhitelist.Enabled = enabled; + + return Ok(enabled + ? "Steam whitelist is now ENABLED. Only approved Steam IDs can sign in.\nConfig: " + SteamWhitelist.FilePath + : "Steam whitelist is now DISABLED. Anyone can sign in."); + } + [HttpGet("whitelist/list")] + public async Task GetWhitelist([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + return Ok($"Whitelist {(SteamWhitelist.Enabled ? "ENABLED" : "disabled")} ({SteamWhitelist.SteamIds.Count} IDs)\nFilename: {SteamWhitelist.FilePath}\n```json\n{new SteamWhitelist.WhitelistFile { Enabled = SteamWhitelist.Enabled, SteamIds = SteamWhitelist.SteamIds.ToList() }.ToJson()}\n```"); + } + [HttpGet("whitelist/add")] + public async Task AddWhitelistId([FromQuery] string key, [FromQuery] string steamId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + if (string.IsNullOrWhiteSpace(steamId)) return BadRequest("No steamId given."); + + var added = SteamWhitelist.Add(steamId); + return Ok(added + ? $"Steam ID {steamId} was added to the whitelist." + : $"Steam ID {steamId} is already on the whitelist."); + } + [HttpGet("whitelist/remove")] + public async Task RemoveWhitelistId([FromQuery] string key, [FromQuery] string steamId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + if (string.IsNullOrWhiteSpace(steamId)) return BadRequest("No steamId given."); + + var removed = SteamWhitelist.Remove(steamId); + return Ok(removed + ? $"Steam ID {steamId} was removed from the whitelist." + : $"Steam ID {steamId} is not on the whitelist."); + } + [HttpGet("set/uname")] + public async Task SetUsername([FromQuery] string key, [FromQuery] long playerId, [FromQuery] string newName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound(); + + profile.username = newName; + + await db.SaveChangesAsync(); + await NotifyUser(profile.accountId); + + return Ok("Updated profile!\n```json" + profile.ToJson() + "```"); + } + [HttpGet("set/dname")] + public async Task SetDisplayName([FromQuery] string key, [FromQuery] long playerId, [FromQuery] string newName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound(); + + profile.displayName = newName; + + await db.SaveChangesAsync(); + await NotifyUser(profile.accountId); + + return Ok("Updated profile!\n```json" + profile.ToJson() + "```"); + } + [HttpGet("set/image")] + public async Task SetProfileImage([FromQuery] string key, [FromQuery] long playerId, [FromQuery] string imgName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound(); + + profile.profileImage = imgName; + + await db.SaveChangesAsync(); + await NotifyUser(profile.accountId); + + return Ok("Updated profile!\n```json" + profile.ToJson() + "```"); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [HttpGet("profiles/{profileId}")] + public async Task GetRoom([FromQuery] string key, long profileId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == profileId); + if (profile == null) return NotFound(); + return Ok(profile); + } + [HttpGet("ban")] + public async Task BanUser([FromQuery] string key, [FromQuery] long userId, [FromQuery] ReportCategory category, [FromQuery] int duration, [FromQuery] string reason) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return NotFound("User does not exist."); + + bool didBan = false; + + + DateTime banExpiry = DateTime.MaxValue; + string appealCode = string.Empty; + var existingModeration = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == userId && x.IsBan); + if (existingModeration != null) + { + if (duration > 0) + { + + var expireTime = DateTime.UtcNow.AddMinutes(duration); + existingModeration.ExpireDuration = expireTime; + existingModeration.Message = reason; + existingModeration.Category = category; + existingModeration.IsBan = true; + didBan = true; + banExpiry = expireTime; + } + else if (duration == -1) + { + + existingModeration.ExpireDuration = DateTime.MaxValue; + existingModeration.Message = reason; + existingModeration.Category = category; + existingModeration.IsBan = true; + didBan = true; + banExpiry = DateTime.MaxValue; + } + else + { + db.Moderation.Remove(existingModeration); + didBan = false; + + + + + await Notify.SendNotify(userId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile }); + + await PlayerUtils.ReleaseModerationHoldAsync(userId); + } + } + else + { + if (duration > 0) + { + + + + var expireTime = DateTime.UtcNow.AddMinutes(duration); + var (newCode, _) = await PlayerUtils.GetAppealForNewBanAsync(db, userId); + var newModeration = new ModerationDB + { + Category = category, + ExpireDuration = expireTime, + IsBan = true, + Message = reason, + PlayerId = userId, + AppealCode = newCode + }; + await db.Moderation.AddAsync(newModeration); + appealCode = newCode ?? string.Empty; + didBan = true; + banExpiry = expireTime; + } + else if (duration == -1) + { + + var expireTime = DateTime.MaxValue; + var (newCode, _) = await PlayerUtils.GetAppealForNewBanAsync(db, userId); + var newModeration = new ModerationDB + { + Category = category, + ExpireDuration = expireTime, + IsBan = true, + Message = reason, + PlayerId = userId, + AppealCode = newCode + }; + await db.Moderation.AddAsync(newModeration); + appealCode = newCode ?? string.Empty; + didBan = true; + banExpiry = expireTime; + } + } + + + + if (didBan) + { + + + + + + + var activeBan = existingModeration ?? await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == userId && x.IsBan); + if (activeBan != null && string.IsNullOrWhiteSpace(activeBan.AppealCode)) + { + var (newCode, _) = await PlayerUtils.GetAppealForNewBanAsync(db, userId); + activeBan.AppealCode = newCode; + appealCode = newCode ?? string.Empty; + } + else if (string.IsNullOrWhiteSpace(appealCode) && activeBan != null) + { + appealCode = activeBan.AppealCode ?? string.Empty; + } + + await db.VoteKicks.AddAsync(new VoteKickLog + { + PlayerId = userId, + Reason = reason, + Category = category, + IsForce = true, + CreatedAt = DateTime.UtcNow, + ExpiresAt = banExpiry + }); + + + + var rep = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == userId); + if (rep != null) + rep.ApplyBanPenalty(permanent: duration == -1, cheating: false); + } + + await db.SaveChangesAsync(); + + + + + if (didBan) + { + await AdminActionDiscord.LogBanAsync(db, userId, + bannedBy: "Admin (Profile API)", + causedByReports: false, + reason: reason, + category: category, + expiry: banExpiry, + isModerator: false); + } + + + + + + + + if (didBan) + { + var banStart = (await db.Moderation.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerId == userId && x.IsBan))?.CreatedAt; + await PlayerUtils.KickOrPromptAsync(db, userId, PlayerUtils.BuildBlockDetails(true, category, reason, banExpiry, appealCode: appealCode, startedAtUtc: banStart ?? DateTime.UtcNow)); + } + + if (didBan) + { + + return Ok(duration == -1 + ? $"Successfully banned @{profile.username} permanently!" + : $"Successfully banned @{profile.username} for {duration} minutes!"); + } + else + return Ok($"Successfully unbanned @{profile.username}!"); + } + + [HttpGet("coach")] + public async Task SendCoachMessage([FromQuery] string key, [FromQuery] long userId, [FromQuery] string message) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return NotFound("User does not exist."); + + + + + await Notify.SendMessage(userId, new Message + { + Type = MessageType.CoachMessage, + Data = message, + FromPlayerId = 1 + }); + + return Ok($"Successfully sent coach message to @{profile.username}!"); + } + + [HttpGet("coach/all")] + public async Task SendCoachMessageToAll([FromQuery] string key, [FromQuery] string message) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profiles = await db.Profiles.ToListAsync(); + long playerCount = 0; + foreach (var profile in profiles) + { + + if (profile.accountId == 1) continue; + + await Notify.SendMessage(profile.accountId, new Message + { + Type = MessageType.CoachMessage, + Data = message, + FromPlayerId = 1 + }); + playerCount++; + } + + return Ok($"Successfully sent coach message to {playerCount} players!"); + } + + [HttpGet("forcequit")] + public async Task ForceQuitPlayer([FromQuery] string key, [FromQuery] long userId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return NotFound("User does not exist."); + + + + Notify.SendReliably(userId, new Notification { Id = NotificationType.ModerationQuitGame, Msg = "" }, + TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1), Notify.EmptyObjectPayload(NotificationType.ModerationQuitGame)); + + + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId); + if (presence != null) + { + presence.isOnline = false; + presence.roomInstance = null; + presence.LoginLock = null; + await db.SaveChangesAsync(); + } + + return Ok($"Successfully force quit @{profile.username}!"); + } + } +} diff --git a/Server/Admin/AdminRoomAPI.cs b/Server/Admin/AdminRoomAPI.cs new file mode 100644 index 0000000..27e3bab --- /dev/null +++ b/Server/Admin/AdminRoomAPI.cs @@ -0,0 +1,1042 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Data; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin +{ + [Route("admin/rooms")] + [EnableCors("AdminPanel")] + public class AdminRoomAPI(RecDBContext db) : Controller + { + private Task RejectInvalidKey(string key) + { + return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); + } + + [HttpGet("room/{roomId}")] + public async Task GetRoom([FromQuery] string key, long roomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await RoomUtils.GetRoom(roomId); + if (room == null) return NotFound(); + + return Ok(room); + } + + [HttpPost("create/room")] + public async Task CreateRoom([FromQuery] string key, [FromQuery] long newOwner, [FromBody] JsonNode fullRoom) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var roomData = new RoomData + { + EncryptVoiceChat = fullRoom.Get("EncryptVoiceChat", false), + Accessibility = fullRoom.Get("Accessibility", Accessibility.Private), + CloningAllowed = fullRoom.Get("CloningAllowed", false), + + CreatedAt = fullRoom["CreatedAt"]?.GetValue() ?? DateTime.UtcNow, + + CreatorAccountId = newOwner, + CustomWarning = fullRoom.Get("CustomWarning", ""), + Description = fullRoom.Get("Description", ""), + + DisableMicAutoMute = fullRoom.Get("DisableMicAutoMute", false), + DisableRoomComments = fullRoom.Get("DisableRoomComments", false), + + ImageName = fullRoom.Get("ImageName", ""), + + IsDorm = fullRoom.Get("IsDorm", false), + IsRRO = fullRoom.Get("IsRRO", false), + + IsDeveloperOwned = fullRoom["IsDeveloperOwned"] != null + ? fullRoom.Get("IsDeveloperOwned", false) + : await db.IsDeveloper(newOwner), + + loadScreen = fullRoom["LoadScreens"]?.ToJson() ?? "[]", + + MaxPlayerCalculationMode = fullRoom.Get("MaxPlayerCalculationMode", MaxPlayerMode.AllSubrooms), + MaxPlayers = fullRoom.Get("MaxPlayers", 10), + MinLevel = fullRoom.Get("MinLevel", 0), + + Name = fullRoom.Get("Name", ""), + State = fullRoom.Get("State", RoomState.Active), + + SupportsJuniors = fullRoom.Get("SupportsJuniors", false), + SupportsLevelVoting = fullRoom.Get("SupportsLevelVoting", false), + SupportsMobile = fullRoom.Get("SupportsMobile", false), + SupportsQuest2 = fullRoom.Get("SupportsQuest2", false), + SupportsScreens = fullRoom.Get("SupportsScreens", false), + SupportsTeleportVR = fullRoom.Get("SupportsTeleportVR", false), + SupportsVRLow = fullRoom.Get("SupportsVRLow", false), + SupportsWalkVR = fullRoom.Get("SupportsWalkVR", false), + + Tags = StringTools.CreateTagString(fullRoom.Get>("Tags", [])), + + Version = 1, + WarningMask = fullRoom.Get("WarningMask", WarningMask.None) + }; + + await db.Rooms.AddAsync(roomData); + + await db.SaveChangesAsync(); + + foreach (var subRoom in fullRoom["SubRooms"]?.AsArray() ?? new JsonArray()) + { + if (subRoom == null) continue; + var subRoomSceneId = subRoom["UnitySceneId"]?.ToString() ?? SceneLocations.MakerRoom; + if (!SceneLocations.IsValid(subRoomSceneId)) + subRoomSceneId = SceneLocations.MakerRoom; + + // A new room's subroom can arrive with its Unity bundle already uploaded, + // named exactly as it was imported (see UploadRoom). + var bundleName = subRoom["UnityAssetId"]?.ToString(); + if (!string.IsNullOrWhiteSpace(bundleName)) + { + if (!AdminUtils.TryResolveBundleName(bundleName, out var resolvedBundle, out var bundleError)) + return BadRequest(bundleError); + bundleName = resolvedBundle; + } + + await db.AddAsync(new SubRoom + { + Accessibility = (Accessibility)(subRoom["Accessibility"]?.GetValue() ?? 0), + CreatedAt = subRoom["CreatedAt"]?.GetValue() ?? DateTime.UtcNow, + SavedByAccountId = newOwner, + IsSandbox = subRoom["IsSandbox"]?.GetValue() ?? false, + MaxPlayers = subRoom["MaxPlayers"]?.GetValue() ?? 0, + Name = subRoom["Name"]?.ToString() ?? "", + RoomId = roomData.RoomId, + UnitySceneId = subRoomSceneId, + DataBlob = subRoom["DataBlob"]?.ToString() ?? "", + UnityAssetId = bundleName ?? "", + }); + } + + await db.RoomRoles.AddAsync(new RoomRole + { + AccountId = newOwner, + InvitedRole = RoleType.None, + Role = RoleType.Creator, + RoomId = roomData.RoomId + }); + + await db.SaveChangesAsync(); + + return Ok($"Created as {roomData.RoomId}!"); + } + + [HttpGet("rename/room")] + public async Task RenameRoom([FromQuery] string key, [FromQuery] long roomId, [FromQuery] string newName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + room.Name = newName; + + await db.SaveChangesAsync(); + + return Ok($"Changed to ^{newName}!"); + } + + [HttpGet("delete/room")] + public async Task DeleteRoom([FromQuery] string key, [FromQuery] long roomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + if (room.IsRRO || room.IsDorm || room.CreatorAccountId == 1) return Ok("You cannot delete RROs."); + + db.Rooms.Remove(room); + await db.SaveChangesAsync(); + + return Ok($"Deleted."); + } + + [HttpGet("set/datablob")] + public async Task SetDatablob([FromQuery] string key, [FromQuery] long roomId, [FromQuery] string subRoomName, [FromQuery] string datablobName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.Name == subRoomName); + if (subRoom == null) return NotFound("Subroom not found."); + + subRoom.DataBlob = datablobName; + + await db.SaveChangesAsync(); + + return Ok($"Set datablob to {datablobName} for {subRoom.Name}!"); + } + + // The other half of importing a subroom: its .assetbundle. The file name is + // the asset id - RoomServerAPI serves /rooms/unity_assets/ out of the CDN + // by that name - so binding means pointing the subroom at an uploaded bundle, + // and an empty name clears it again. BindAssetBundle below is the narrower, + // Studio-side counterpart for 40-character manifest ids; this one takes the + // '.assetbundle' files an import actually produces. + [HttpGet("set/bundle")] + public async Task SetBundle([FromQuery] string key, [FromQuery] long roomId, [FromQuery] string subRoomName, [FromQuery] string bundleName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.Name == subRoomName); + if (subRoom == null) return NotFound("Subroom not found."); + + if (string.IsNullOrWhiteSpace(bundleName)) + { + subRoom.UnityAssetId = ""; + await db.SaveChangesAsync(); + return Ok($"Cleared the Unity bundle for {subRoom.Name}!"); + } + + if (!AdminUtils.TryResolveBundleName(bundleName, out var resolvedBundle, out var bundleError)) + return BadRequest(bundleError); + + subRoom.UnityAssetId = resolvedBundle ?? ""; + + await db.SaveChangesAsync(); + + return Ok($"Set bundle to {subRoom.UnityAssetId} for {subRoom.Name}!"); + } + + [HttpPost("create/subroom")] + public async Task CreateSubRoom([FromQuery] string key, [FromQuery] long roomId, [FromBody] JsonNode subRoom) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + var name = (subRoom.Get("Name", "") ?? "").Trim(); + if (string.IsNullOrWhiteSpace(name)) return BadRequest("Subroom name is required."); + + if (await db.SubRooms.AnyAsync(x => x.RoomId == roomId && x.Name == name)) + return BadRequest($"A subroom named \"{name}\" already exists in this room."); + + // The Unity bundle the subroom loads, if one was imported alongside the save. + var bundleName = subRoom.Get("UnityAssetId", "") ?? ""; + if (!string.IsNullOrWhiteSpace(bundleName)) + { + if (!AdminUtils.TryResolveBundleName(bundleName, out var resolvedBundle, out var bundleError)) + return BadRequest(bundleError); + bundleName = resolvedBundle ?? ""; + } + + var mainSubRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + + var maxPlayers = subRoom.Get("MaxPlayers", mainSubRoom?.MaxPlayers ?? 10); + if (maxPlayers <= 0) maxPlayers = mainSubRoom?.MaxPlayers ?? 10; + + var unitySceneId = subRoom.Get("UnitySceneId", ""); + if (string.IsNullOrWhiteSpace(unitySceneId)) + unitySceneId = mainSubRoom?.UnitySceneId ?? "a75f7547-79eb-47c6-8986-6767abcb4f92"; + + var dbSubRoom = new SubRoom + { + Accessibility = (Accessibility)(subRoom["Accessibility"]?.GetValue() ?? (int)Accessibility.Public), + CreatedAt = subRoom.Get("CreatedAt", DateTime.UtcNow), + SavedByAccountId = room.CreatorAccountId, + IsSandbox = subRoom.Get("IsSandbox", true), + MaxPlayers = maxPlayers, + Name = name, + RoomId = roomId, + UnitySceneId = unitySceneId, + DataBlob = subRoom.Get("DataBlob", ""), + UnityAssetId = bundleName, + }; + + await db.AddAsync(dbSubRoom); + await db.SaveChangesAsync(); + + return Ok($"Created as {dbSubRoom.SubRoomId}!"); + } + + [HttpGet("rename/subroom")] + public async Task RenameSubRoom([FromQuery] string key, [FromQuery] long roomId, [FromQuery] long subRoomId, [FromQuery] string newName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId && x.RoomId == roomId); + if (subRoom == null) return NotFound("Subroom not found."); + + newName = (newName ?? "").Trim(); + if (string.IsNullOrWhiteSpace(newName)) return BadRequest("Subroom name is required."); + + if (await db.SubRooms.AnyAsync(x => x.RoomId == roomId && x.SubRoomId != subRoomId && x.Name == newName)) + return BadRequest($"A subroom named \"{newName}\" already exists in this room."); + + subRoom.Name = newName; + + await db.SaveChangesAsync(); + + return Ok($"Renamed to {newName}!"); + } + + [HttpGet("delete/subroom")] + public async Task DeleteSubRoom([FromQuery] string key, [FromQuery] long roomId, [FromQuery] long subRoomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId && x.RoomId == roomId); + if (subRoom == null) return NotFound("Subroom not found."); + + + var subroomCount = await db.SubRooms.CountAsync(x => x.RoomId == roomId); + if (subroomCount <= 1) return BadRequest("A room must keep at least one subroom."); + + var history = await db.SubRoomHistory.Where(x => x.RoomId == roomId && x.SubRoomId == subRoomId).ToListAsync(); + foreach (var historyItem in history) + db.SubRoomHistory.Remove(historyItem); + + db.SubRooms.Remove(subRoom); + + await db.SaveChangesAsync(); + + return Ok($"Deleted {subRoom.Name}!"); + } + + [HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/assetbundle")] + public async Task BindAssetBundle([FromQuery] string key, long roomId, long subRoomId, + [FromBody] string assetId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + if (!ModelState.IsValid || !RadicadeServer.API.Studio.StudioUnityAssetRegistry.IsValidAssetId(assetId)) + return BadRequest("Expected a 40-character Unity asset ID."); + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); + if (subRoom == null) return NotFound("Subroom not found in this room."); + + + + if (!System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, assetId + ".assetbundle"))) + return BadRequest("Upload the bundle as .assetbundle before linking it."); + if (System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, assetId))) + return BadRequest("This ID belongs to a Studio manifest; choose an ID for the archived bundle."); + + var previousAssetId = subRoom.UnityAssetId; + subRoom.UnityAssetId = assetId; + await db.SaveChangesAsync(); + return Ok(new { RoomId = roomId, SubRoomId = subRoomId, PreviousUnityAssetId = previousAssetId, UnityAssetId = assetId }); + } + + [HttpPost("upload/datablob")] + [RequestSizeLimit(256L * 1024 * 1024)] + public async Task UploadDatablob([FromQuery] string key, [FromQuery] string name, [FromForm] IFormFile file) + { + + if (!ModelState.IsValid) return BadRequest("Invalid upload request. Use a multipart form no larger than 256 MiB."); + + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var path = Path.Join(DBConfig.CDNStorage, name); + + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Ok($"Uploaded {name}!\nURL: {DBConfig.ServerUrl}/cdn/room/{name}"); + } + + [HttpPost("upload/image")] + public async Task UploadImage([FromQuery] string key, [FromQuery] long roomId, [FromForm] IFormFile file) + { + + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + if (file == null || file.Length == 0) return BadRequest("No file provided."); + + + var allowed = new[] { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" }; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (string.IsNullOrEmpty(ext) || !allowed.Contains(ext)) + return BadRequest($"Unsupported image format '{(string.IsNullOrEmpty(ext) ? "no extension" : ext)}'. Use png/jpg/jpeg/gif/webp."); + + string guid = Guid.NewGuid().ToString(); + var path = Path.Join(DBConfig.CDNImages, guid + ext); + + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + room.ImageName = guid + ext; + + await db.SaveChangesAsync(); + + return Ok($"Uploaded {guid}{ext}!\nURL: {DBConfig.ServerUrl}/images/img/{guid}{ext}"); + } + + [HttpGet("set/image")] + public async Task SetImage([FromQuery] string key, [FromQuery] long roomId, [FromQuery] string imgName) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + + imgName = Path.GetFileName(imgName ?? "").Trim(); + + + room.ImageName = imgName; + + await db.SaveChangesAsync(); + + return Ok($"Set to {imgName}!\nURL: {DBConfig.ServerUrl}/images/img/{imgName}"); + } + + [HttpGet("set/role/{roleType}")] + public async Task SetRoleOfUser([FromQuery] string key, [FromQuery] long roomId, [FromQuery] long target, RoleType roleType) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == target && x.RoomId == roomId); + + if (existingRole == null) + { + var newRole = new RoomRole + { + AccountId = target, + InvitedRole = RoleType.None, + Role = roleType, + RoomId = roomId + }; + + await db.RoomRoles.AddAsync(newRole); + } + else + { + if (existingRole.Role == roleType) + return Ok($"Player already has {Enum.GetName(roleType)}."); + + existingRole.Role = roleType; + } + + await db.SaveChangesAsync(); + + try + { + var updatedRoom = await RoomUtils.GetRoom(roomId); + if (updatedRoom != null) + await Notify.SendNotify(target, new Notification { Id = "RoomUpdate", Msg = updatedRoom }); + } + catch (Exception) { } + + return Ok($"Set to {Enum.GetName(roleType)}!"); + } + + [HttpGet("set/rro")] + public async Task SetRRO([FromQuery] string key, [FromQuery] long roomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + room.IsRRO = true; + var tags = StringTools.ReadTags(room.Tags); + if (tags.FirstOrDefault(x => x.Tag == "rro" && x.Type == TagType.Auto) == null) + tags.Add(new ItemTag { Type = TagType.Auto, Tag = "rro" }); + room.Tags = StringTools.CreateTagString(tags); + + await db.SaveChangesAsync(); + + return Ok($"Set to RRO {room.Name}!"); + } + + [HttpGet("remove/rro")] + public async Task RemoveRRO([FromQuery] string key, [FromQuery] long roomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + room.IsRRO = false; + var tags = StringTools.ReadTags(room.Tags); + var existingRROTag = tags.FirstOrDefault(x => x.Tag == "rro" && x.Type == TagType.Auto); + if (existingRROTag != null) + tags.Remove(existingRROTag); + room.Tags = StringTools.CreateTagString(tags); + + await db.SaveChangesAsync(); + + return Ok($"Removed RRO from {room.Name}!"); + } + + [HttpGet("fulldelete/room")] + public async Task FullyDeleteRoom([FromQuery] string key, [FromQuery] long roomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + if (room.IsRRO || room.IsDorm || room.CreatorAccountId == 1) return Ok("You cannot delete RROs."); + + var roles = await db.RoomRoles.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var role in roles) + { + db.RoomRoles.Remove(role); + } + + var interactions = await db.RoomInteractions.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var i in interactions) + { + db.RoomInteractions.Remove(i); + } + + var keys = await db.RoomKeys.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var k in keys) + { + db.RoomKeys.Remove(k); + } + + var stats = await db.RoomStats.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (stats != null) + db.RoomStats.Remove(stats); + + var subrooms = await db.SubRooms.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var subroom in subrooms) + { + db.SubRooms.Remove(subroom); + } + + var prevSubrooms = await db.SubRoomHistory.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var subroom in prevSubrooms) + { + db.SubRoomHistory.Remove(subroom); + } + + await db.SaveChangesAsync(); + + return Ok("Deleted."); + } + + [HttpGet("set/owner")] + public async Task SetRoomOwner([FromQuery] string key, [FromQuery] long roomId, [FromQuery] long target) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + long oldOwnerId = room.CreatorAccountId; + room.CreatorAccountId = target; + + var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == target && x.RoomId == roomId); + + if (existingRole == null) + { + var newRole = new RoomRole + { + AccountId = target, + InvitedRole = RoleType.None, + Role = RoleType.Creator, + RoomId = roomId + }; + + await db.RoomRoles.AddAsync(newRole); + } + else + { + existingRole.Role = RoleType.Creator; + } + + + + if (oldOwnerId != target) + { + var oldOwnerRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == oldOwnerId && x.RoomId == roomId); + if (oldOwnerRole != null && oldOwnerRole.Role == RoleType.Creator) + oldOwnerRole.Role = RoleType.CoOwner; + } + + await db.SaveChangesAsync(); + + return Ok($"Set to owner!"); + } + + + + + + [HttpGet("instances/live")] + public async Task GetLiveRoomInstances([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var instances = await db.RoomInstances.ToListAsync(); + var roomIds = instances.Select(i => i.roomId).Distinct().ToList(); + var rooms = roomIds.Count > 0 + ? await db.Rooms.Where(r => roomIds.Contains(r.RoomId)).ToDictionaryAsync(r => r.RoomId) + : new Dictionary(); + + + var presences = await db.Presences.Where(p => p.isOnline && p.roomInstance != null).ToListAsync(); + var profileIds = presences.Select(p => p.playerId).Distinct().ToList(); + var profiles = profileIds.Count > 0 + ? await db.Profiles.Where(x => profileIds.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId) + : new Dictionary(); + + var result = instances.Select(instance => + { + var players = presences + .Where(p => p.roomInstance == instance.roomInstanceId) + .Select(p => new + { + player_id = p.playerId, + name = profiles.TryGetValue(p.playerId, out var profile) ? profile.displayName : $"Player {p.playerId}" + }) + .OrderBy(p => p.player_id) + .ToList(); + + return new + { + instance_id = instance.roomInstanceId, + room_id = instance.roomId, + room_name = rooms.TryGetValue(instance.roomId, out var room) ? room.Name : "Unknown Room", + sub_room_id = instance.subRoomId, + event_id = instance.eventId, + instance_type = instance.roomInstanceType.ToString(), + location = instance.location ?? "", + room_code = instance.roomCode ?? "", + photon_room_id = instance.photonRoomId, + is_private = instance.isPrivate, + is_in_progress = instance.isInProgress, + is_full = instance.isFull, + max_capacity = instance.maxCapacity, + player_count = players.Count, + players + }; + }) + .OrderByDescending(i => i.player_count) + .ToList(); + + return Ok(new { count = result.Count, instances = result }); + } + + + + + + [HttpGet("instances/{instanceId:long}")] + public async Task GetLiveInstanceDetail(long instanceId, [FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var instance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId); + if (instance == null) return NotFound("Instance not found."); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == instance.roomId); + var subrooms = await db.SubRooms.Where(s => s.RoomId == instance.roomId).ToListAsync(); + + var presences = await db.Presences.Where(p => p.isOnline && p.roomInstance == instanceId).ToListAsync(); + var profileIds = presences.Select(p => p.playerId).Distinct().ToList(); + var profiles = profileIds.Count > 0 + ? await db.Profiles.Where(x => profileIds.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId) + : new Dictionary(); + var playerData = profileIds.Count > 0 + ? await db.Players.Where(x => profileIds.Contains(x.AccountId)).ToDictionaryAsync(x => x.AccountId) + : new Dictionary(); + var progression = profileIds.Count > 0 + ? await db.Progression.Where(x => profileIds.Contains(x.PlayerId)).ToDictionaryAsync(x => x.PlayerId) + : new Dictionary(); + + var playerRows = presences.Select(p => + { + profiles.TryGetValue(p.playerId, out var profile); + playerData.TryGetValue(p.playerId, out var player); + progression.TryGetValue(p.playerId, out var progress); + return new + { + player_id = p.playerId, + name = profile?.displayName ?? $"Player {p.playerId}", + username = profile?.username ?? "", + level = progress?.Level ?? 0, + device_class = p.deviceClass.ToString(), + is_online = p.isOnline + }; + }).OrderBy(p => p.player_id).ToList(); + + return Ok(new + { + instance = new + { + instance_id = instance.roomInstanceId, + room_id = instance.roomId, + room_name = room?.Name ?? "Unknown Room", + sub_room_id = instance.subRoomId, + instance_type = instance.roomInstanceType.ToString(), + location = instance.location ?? "", + room_code = instance.roomCode ?? "", + is_private = instance.isPrivate, + is_in_progress = instance.isInProgress, + is_full = instance.isFull, + max_capacity = instance.maxCapacity, + player_count = playerRows.Count + }, + players = playerRows, + subrooms = subrooms.Select(s => new + { + sub_room_id = s.SubRoomId, + name = s.Name, + max_players = s.MaxPlayers + }).ToList() + }); + } + + + + + + [HttpPost("instances/{instanceId:long}/kick")] + public async Task KickFromInstance(long instanceId, [FromQuery] string key, [FromQuery] long playerId, [FromQuery] string? reason = null) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var instance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId); + if (instance == null) return NotFound("Instance not found."); + + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence == null || presence.roomInstance != instanceId) return NotFound("Player is not in this instance."); + + + var dorm = await RoomUtils.GetOrCreateDorm(playerId); + RoomInstance? dormInstance = null; + if (dorm != null) + dormInstance = await MatchmakingUtils.CreateRoomInstance(dorm.RoomId, true, null, null, null); + if (dormInstance == null) return StatusCode(500, "Could not resolve a dorm instance for this player."); + + presence.roomInstance = dormInstance.roomInstanceId; + presence.errorCode = JoinStatus.Success; + presence.isOnline = true; + MatchmakingUtils.MarkInRoom(playerId); + + await RecomputeInstanceCapacity(instance); + await db.SaveChangesAsync(); + + + + + try + { + await Notify.SendNotify(playerId, new Notification + { + Id = NotificationType.ModerationKick, + Msg = PlayerUtils.BuildBlockDetails(false, ReportCategory.Misc, reason ?? "Kicked from room", null, isHostKick: true) + }); + } + catch (Exception) { } + + try + { + await Notify.SendNotify(playerId, new Notification + { + Id = "PresenceUpdate", + Msg = await Presence.CreateAsync(db, presence, dormInstance) + }); + } + catch (Exception) { } + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + return Ok(new + { + player_id = playerId, + name = profile?.displayName ?? $"Player {playerId}", + kicked = true, + instance_id = instanceId + }); + } + + + + + + + [HttpPost("instances/{instanceId:long}/move")] + public async Task MovePlayerToSubRoom(long instanceId, [FromQuery] string key, [FromQuery] long playerId, [FromQuery] long toSubRoomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var instance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId); + if (instance == null) return NotFound("Instance not found."); + + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence == null || presence.roomInstance != instanceId) return NotFound("Player is not in this instance."); + + + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == instance.roomId && x.SubRoomId == toSubRoomId); + if (subRoom == null) return BadRequest("Target subroom does not belong to this room."); + if (instance.subRoomId == toSubRoomId) + return Ok(new { player_id = playerId, already_in_subroom = true, instance_id = instanceId }); + + + + var targetInstance = await db.RoomInstances + .FirstOrDefaultAsync(x => x.roomId == instance.roomId && x.subRoomId == toSubRoomId && !x.isFull); + bool created = false; + if (targetInstance == null) + { + targetInstance = await MatchmakingUtils.CreateRoomInstance(instance.roomId, instance.isPrivate, toSubRoomId, instance.clubId, instance.eventId); + if (targetInstance == null) return StatusCode(500, "Could not create the target subroom instance."); + + + db.RoomInstances.Attach(targetInstance); + created = true; + } + + presence.roomInstance = targetInstance.roomInstanceId; + presence.errorCode = JoinStatus.Success; + presence.isOnline = true; + MatchmakingUtils.MarkInRoom(playerId); + + await RecomputeInstanceCapacity(instance); + await RecomputeInstanceCapacity(targetInstance); + await db.SaveChangesAsync(); + + try + { + await Notify.SendNotify(playerId, new Notification + { + Id = "PresenceUpdate", + Msg = await Presence.CreateAsync(db, presence, targetInstance) + }); + } + catch (Exception) { } + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + return Ok(new + { + player_id = playerId, + name = profile?.displayName ?? $"Player {playerId}", + instance_id = targetInstance.roomInstanceId, + sub_room_id = toSubRoomId, + created + }); + } + + + + + private async Task RecomputeInstanceCapacity(RoomInstance instance) + { + var count = await MatchmakingUtils.GetPlayerCountInInstance(instance.roomInstanceId); + instance.isFull = count >= instance.maxCapacity; + } + + [HttpGet("roles")] + public async Task GetRoomRoles([FromQuery] string key, [FromQuery] long roomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + var roles = await db.RoomRoles.Where(x => x.RoomId == roomId).ToListAsync(); + var accountIds = roles.Select(r => r.AccountId).Distinct().ToList(); + var profiles = accountIds.Count > 0 + ? await db.Profiles.Where(x => accountIds.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId) + : new Dictionary(); + + string? NameOf(long id) => profiles.TryGetValue(id, out var p) + ? $"{p.displayName} (@{p.username})" + : id.ToString(); + + var result = roles.Select(r => new + { + account_id = r.AccountId, + name = NameOf(r.AccountId), + role = r.Role.ToString(), + role_value = (int)r.Role, + invited_role = r.InvitedRole.ToString(), + is_creator = r.AccountId == room.CreatorAccountId || r.Role == RoleType.Creator + }).ToList(); + + + if (!result.Any(x => x.account_id == room.CreatorAccountId)) + { + result.Insert(0, new + { + account_id = room.CreatorAccountId, + name = NameOf(room.CreatorAccountId), + role = "Creator", + role_value = (int)RoleType.Creator, + invited_role = "None", + is_creator = true + }); + } + + return Ok(new { room_id = roomId, creator_account_id = room.CreatorAccountId, roles = result }); + } + + + + + + [HttpGet("room/{roomId}/bans")] + public async Task GetRoomBans([FromQuery] string key, long roomId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + + var bans = await db.RoomBans + .Where(x => x.roomId == roomId && (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow)) + .OrderByDescending(x => x.bannedAt) + .ToListAsync(); + + var ids = bans.Select(b => b.bannedPlayerId).Concat(bans.Select(b => b.actorId)).Distinct().ToList(); + var profiles = ids.Count > 0 + ? await db.Profiles.AsNoTracking().Where(x => ids.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId) + : new Dictionary(); + + string? NameOf(long id) => profiles.TryGetValue(id, out var p) ? p.displayName : null; + + var result = bans.Select(b => new + { + ban_id = b.BanId, + room_id = b.roomId, + player_id = b.bannedPlayerId, + player_name = NameOf(b.bannedPlayerId), + banned_by_id = b.actorId, + banned_by_name = string.IsNullOrWhiteSpace(b.bannedByName) ? (NameOf(b.actorId) ?? $"Player {b.actorId}") : b.bannedByName, + reason = b.reason, + banned_at = b.bannedAt, + expires_at = b.expiresAt + }).ToList(); + + return Ok(new { room_id = roomId, room_name = room.Name, count = result.Count, bans = result }); + } + + + + + + + [HttpPost("room/{roomId}/bans")] + public async Task BanPlayerFromRoom([FromQuery] string key, long roomId, [FromBody] JsonNode body) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + long playerId = body["player_id"]?.GetValue() ?? 0; + if (playerId <= 0) return BadRequest("player_id is required."); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return NotFound("Player does not exist."); + + var existing = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == roomId && x.bannedPlayerId == playerId && + (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow)); + if (existing != null) return Ok($"Player is already banned from this room."); + + var reason = body["reason"]?.GetValue()?.Trim() ?? ""; + var ban = new RoomBan + { + roomId = roomId, + actorId = 1, + bannedPlayerId = playerId, + bannedByName = "Admin", + reason = reason, + bannedAt = DateTime.UtcNow, + expiresAt = null + }; + + await db.RoomBans.AddAsync(ban); + await db.SaveChangesAsync(); + + + await MatchmakingUtils.KickPlayerFromRoom(playerId, roomId, + string.IsNullOrWhiteSpace(reason) ? "You were banned from this room." : $"You were banned from this room: {reason}"); + + return Ok(new + { + ban_id = ban.BanId, + room_id = roomId, + player_id = playerId, + player_name = profile.displayName, + reason = reason, + banned_at = ban.bannedAt + }); + } + + + + + [HttpDelete("room/{roomId}/bans/{playerId}")] + public async Task UnbanPlayerFromRoom([FromQuery] string key, long roomId, long playerId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return NotFound("Room does not exist."); + + var ban = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == roomId && x.bannedPlayerId == playerId); + if (ban == null) return NotFound("Player is not banned from this room."); + + db.RoomBans.Remove(ban); + await db.SaveChangesAsync(); + + return Ok($"Ban lifted for player {playerId}."); + } + } +} diff --git a/Server/Admin/AdminRuntime.cs b/Server/Admin/AdminRuntime.cs new file mode 100644 index 0000000..d58a99f --- /dev/null +++ b/Server/Admin/AdminRuntime.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.Utils +{ + public static class AdminRuntime + { + public static readonly DateTime ServerStartedAtUtc = DateTime.UtcNow; + } +} diff --git a/Server/Admin/AdminStatusAPI.cs b/Server/Admin/AdminStatusAPI.cs new file mode 100644 index 0000000..22ff13b --- /dev/null +++ b/Server/Admin/AdminStatusAPI.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin +{ + [Route("admin/status")] + [EnableCors("AdminPanel")] + public class AdminStatusAPI(RecDBContext db) : Controller + { + private Task RejectInvalidKey(string key) + { + return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); + } + + [HttpGet("players/active")] + public async Task GetActivePlayerCount([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var activePlayerCount = await db.Presences.CountAsync(x => x.isOnline); + + return Ok(new + { + activePlayerCount + }); + } + + [HttpGet("")] + public async Task GetServerStatus([FromQuery] string key) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var activePlayerCount = await db.Presences.CountAsync(x => x.isOnline); + var playersInRoomsCount = await db.Presences.CountAsync(x => x.isOnline && x.roomInstance != null); + var activeRoomInstanceCount = await db.RoomInstances.CountAsync(); + var gameVersion = await db.ServerConfig + .Where(x => x.Key == "GameVersion") + .Select(x => x.Value) + .FirstOrDefaultAsync() ?? DBConfig.GameVersion; + var barrelVersion = await db.ServerConfig + .Where(x => x.Key == "BarrelVersion") + .Select(x => x.Value) + .FirstOrDefaultAsync(); + var barrelKey = await db.ServerConfig + .Where(x => x.Key == "BarrelKey") + .Select(x => x.Value) + .FirstOrDefaultAsync(); + var barrelCheck = await db.ServerConfig + .Where(x => x.Key == "BarrelCheck") + .Select(x => x.Value) + .FirstOrDefaultAsync(); + var config = await db.Config.FirstOrDefaultAsync(); + + return Ok(new + { + status = "online", + serverUrl = DBConfig.ServerUrl, + activePlayerCount, + playersInRoomsCount, + activeRoomInstanceCount, + gameVersion, + barrelVersion, + barrelKey, + barrelCheck, + maintenance = config?.ServerMaintenance, + startedAtUtc = AdminRuntime.ServerStartedAtUtc, + uptimeSeconds = (long)(DateTime.UtcNow - AdminRuntime.ServerStartedAtUtc).TotalSeconds + }); + } + } +} diff --git a/Server/Admin/AdminStorefrontsAPI.cs b/Server/Admin/AdminStorefrontsAPI.cs new file mode 100644 index 0000000..158bf4e --- /dev/null +++ b/Server/Admin/AdminStorefrontsAPI.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Cors; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Store; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin; + +[Route("admin/store")] +[EnableCors("AdminPanel")] +public class AdminStorefrontsAPI(RecDBContext db) : Controller +{ + private Task RejectInvalidKey(string key) + { + return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); + } + + [HttpGet("{key}/itemset/{itemSetId}")] + public async Task GetItemSet(string key, long itemSetId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + return Ok("Item sets not implemented for GiftDropDB."); + } + + [HttpGet("{key}/gift/{giftId}")] + public async Task GetGift(string key, long giftId) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var gift = await db.GiftItems.FirstOrDefaultAsync(x => x.GiftDropId == giftId); + if (gift == null) return Ok("Not found."); + + return Ok(gift); + } + + [HttpGet("{key}/gift/{giftId}/set")] + public async Task SetGift() + { + return Ok("todo."); + } + + [HttpGet("{key}/gift/new")] + public async Task CreateGift() + { + return Ok("todo."); + } +} \ No newline at end of file diff --git a/Server/Admin/AdminUtils.cs b/Server/Admin/AdminUtils.cs new file mode 100644 index 0000000..c3de911 --- /dev/null +++ b/Server/Admin/AdminUtils.cs @@ -0,0 +1,122 @@ +using System.Security.Cryptography; +using System.Text; +using RadicadeServer.DB; + +namespace RadicadeServer.Utils +{ + public static class AdminUtils + { + /// + /// The ceiling every admin upload is held to, on the request and on the multipart form. + /// + /// Kestrel's own default is 30 MB, and no room import fits under it: an .assetbundle or + /// a .room save routinely runs to tens or hundreds of megabytes. A route that does not + /// raise the limit has its connection killed part-way through the body, so the panel sees + /// a broken connection instead of an answer - which is what made importing a large room + /// look like an authentication failure. Named once here so the next upload route cannot + /// quietly forget it. + /// + public const long MaxUploadBytes = 256L * 1024 * 1024; + + /// + /// A room file being assembled from parts lives beside its final name under this suffix + /// until the last part lands. Nothing serves or lists one, so a half-imported bundle can + /// never be fetched by the client, and a name ending in this suffix is refused outright. + /// + public const string PartSuffix = ".uploading"; + + /// The multipart ceiling this server answers with, in the words the panel shows an admin. + public const string MaxUploadLabel = "256 MiB"; + + public static bool IsUploadPart(string? name) => + !string.IsNullOrEmpty(name) && name.EndsWith(PartSuffix, StringComparison.OrdinalIgnoreCase); + + private static bool SecretMatches(string? given, string? stored) + { + if (string.IsNullOrEmpty(given) || string.IsNullOrEmpty(stored)) + return false; + + return CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(given), + Encoding.UTF8.GetBytes(stored)); + } + + public static bool IsAdminKey(this string? key) + { + return SecretMatches(key, DBConfig.AdminApiKey); + } + + public static bool IsSupportKey(this string? key) + { + return SecretMatches(key, DBConfig.AdminApiKey) + || SecretMatches(key, DBConfig.SupportApiKey); + } + + public static bool TryResolveUploadName(string? requested, out string? name, out string? error) + { + name = null; + error = null; + + if (string.IsNullOrWhiteSpace(requested)) return true; + + var trimmed = requested.Trim(); + + if (trimmed.Length > 128) + { + error = "Name is too long."; + return false; + } + + if (trimmed != Path.GetFileName(trimmed) || trimmed.Contains("..")) + { + error = "Name must be a bare file name."; + return false; + } + + foreach (var c in trimmed) + { + if (!char.IsLetterOrDigit(c) && c != '-' && c != '_' && c != '.') + { + error = "Name may only contain letters, digits, '-', '_' and '.'."; + return false; + } + } + + name = trimmed; + return true; + } + + /// A subroom's Unity bundle is served under the name it is stored as + /// (RoomServerAPI's unity_assets route hands the client that file name, and + /// the client then fetches it from the CDN by name), so binding accepts any + /// uploaded '.assetbundle'. That is deliberately wider than + /// StudioUnityAssetRegistry.IsValidAssetId, which only covers the + /// 40-character manifest ids the Studio pipeline mints. + public static bool TryResolveBundleName(string? requested, out string? name, out string? error) + { + if (!TryResolveUploadName(requested, out name, out error)) return false; + + if (name == null) + { + error = "A bundle file name is required."; + return false; + } + + if (!name.EndsWith(".assetbundle", StringComparison.OrdinalIgnoreCase)) + { + error = $"'{name}' is not a .assetbundle file."; + name = null; + return false; + } + + if (!System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, name))) + { + error = $"No uploaded bundle named '{name}'."; + name = null; + return false; + } + + return true; + } + } +} diff --git a/Server/Admin/AdminVersionAPI.cs b/Server/Admin/AdminVersionAPI.cs new file mode 100644 index 0000000..a99483a --- /dev/null +++ b/Server/Admin/AdminVersionAPI.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Cors; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using RadicadeServer.Utils; + +namespace RadicadeServer.Admin +{ + [Route("admin/version")] + [EnableCors("AdminPanel")] + public class AdminVersionAPI(RecDBContext db) : Controller + { + private Task RejectInvalidKey(string key) + { + return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); + } + + private async Task SetServerConfigValue(string keyName, string key, string value) + { + var rejection = await RejectInvalidKey(key); + if (rejection != null) return rejection; + + var existingValue = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == keyName); + + if (existingValue != null) + { + existingValue.Value = value; + } + else + { + await db.ServerConfig.AddAsync(new ServerConfigVal { Key = keyName, Value = value }); + } + + await db.SaveChangesAsync(); + + return Ok("Set to: " + value); + } + + [HttpGet("setgameversion")] + public async Task SetGameVersion([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("GameVersion", key, value); + } + [HttpGet("setversion")] + public async Task SetVersion([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("BarrelVersion", key, value); + } + [HttpGet("setkey")] + public async Task SetKey([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("BarrelKey", key, value); + } + [HttpGet("setcheck")] + public async Task SetCheck([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("BarrelCheck", key, value); + } + [HttpGet("setmanifestversion")] + public async Task SetManifestVersion([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("ApprovedManifestVersion", key, value); + } + [HttpGet("setmanifestsha256")] + public async Task SetManifestSha256([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("ApprovedManifestSha256", key, value); + } + [HttpGet("setgamedownloadurl")] + public async Task SetGameDownloadUrl([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("GameDownloadUrl", key, value); + } + [HttpGet("setlaunchticket")] + public async Task SetLaunchTicket([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("LaunchTicket", key, value); + } + [HttpGet("setlaunchticketrequired")] + public async Task SetLaunchTicketRequired([FromQuery] string key, [FromQuery] bool value) + { + return await SetServerConfigValue("LaunchTicketRequired", key, value.ToString()); + } + [HttpGet("setlaunchticketurl")] + public async Task SetLaunchTicketUrl([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("LaunchTicketUrl", key, value); + } + [HttpGet("setlaunchticketaudience")] + public async Task SetLaunchTicketAudience([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("LaunchTicketAudience", key, value); + } + [HttpGet("setlaunchticketnonce")] + public async Task SetLaunchTicketNonce([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("LaunchTicketNonce", key, value); + } + [HttpGet("setgamepackagesha256")] + public async Task SetGamePackageSha256([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("GamePackageSha256", key, value); + } + [HttpGet("setgamepackagesize")] + public async Task SetGamePackageSize([FromQuery] string key, [FromQuery] long value) + { + return await SetServerConfigValue("GamePackageSize", key, value.ToString()); + } + [HttpGet("setgameextractsubdirectory")] + public async Task SetGameExtractSubdirectory([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("GameExtractSubdirectory", key, value); + } + [HttpGet("setlaunchermanifestnotes")] + public async Task SetLauncherManifestNotes([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("LauncherManifestNotes", key, value); + } + [HttpGet("setlaunchermanifestdelete")] + public async Task SetLauncherManifestDelete([FromQuery] string key, [FromQuery] string value) + { + return await SetServerConfigValue("LauncherManifestDelete", key, value); + } + } +} diff --git a/Server/Admin/RoomBanStore.cs b/Server/Admin/RoomBanStore.cs new file mode 100644 index 0000000..d63da6e --- /dev/null +++ b/Server/Admin/RoomBanStore.cs @@ -0,0 +1,201 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Admin; + +public static class RoomBanStore +{ + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + private static readonly object FileLock = new(); + + private const long CoachAccountId = 1; + + private static string ClosedMessage(string roomName, string reason) => + $"We have closed the room '{roomName}' for: {reason}. We encourage creators to review our Creator Code of Conduct to help make Radicade a fun and safe place for everyone."; + + private static string RestoredMessage(string roomName) => + $"We have restored the room '{roomName}' after a thorough review of the previous accusations. Thank you for continuing to follow the Creator Code of Conduct, and making Radicade a better place."; + + public static string FilePath => Path.Join(DBConfig.PersistentLogs, "banned-rooms.json"); + + public static List Load() + { + lock (FileLock) return LoadLocked(); + } + + public static BannedRoom? Find(long roomId) => Load().FirstOrDefault(x => x.RoomId == roomId); + + public static bool IsBanned(long roomId) => Find(roomId) != null; + + public static async Task BanAsync(RecDBContext db, RoomData room, string reason, string bannedBy) + { + var entry = new BannedRoom + { + RoomId = room.RoomId, + Name = room.Name, + Reason = reason, + BannedBy = bannedBy, + BannedAt = DateTime.UtcNow, + PublishedAt = room.PublishedAt, + Room = room, + SubRooms = await db.SubRooms.AsNoTracking().Where(x => x.RoomId == room.RoomId).ToListAsync(), + Roles = await db.RoomRoles.AsNoTracking().Where(x => x.RoomId == room.RoomId).ToListAsync(), + Stats = await db.RoomStats.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == room.RoomId) + }; + + Upsert(entry); + + db.Rooms.Remove(room); + await db.SaveChangesAsync(); + + var why = string.IsNullOrWhiteSpace(reason) ? "a breach of the Creator Code of Conduct" : reason; + await TellOwnerAsync(room.CreatorAccountId, ClosedMessage(room.Name, why)); + + return entry; + } + + public static async Task RestoreAsync(RecDBContext db, long roomId) + { + var entry = Find(roomId); + if (entry == null) return false; + + await InsertRoomWithItsIdAsync(db, entry.Room, entry.PublishedAt); + + Remove(roomId); + + await TellOwnerAsync(entry.Room.CreatorAccountId, RestoredMessage(entry.Name)); + return true; + } + + private static async Task InsertRoomWithItsIdAsync(RecDBContext db, RoomData room, DateTime publishedAt) + { + var entity = db.Model.FindEntityType(typeof(RoomData)) + ?? throw new InvalidOperationException("RoomData is not part of the model."); + + var table = entity.GetTableName() ?? nameof(RoomData); + var qualified = entity.GetSchema() is { Length: > 0 } schema ? $"[{schema}].[{table}]" : $"[{table}]"; + + var columns = new List(); + var cells = new List(); + var parameters = new List(); + foreach (var property in entity.GetProperties()) + { + if (property.IsShadowProperty() || property.PropertyInfo == null) continue; + + var value = property.Name == nameof(RoomData.PublishedAt) + ? publishedAt + : property.PropertyInfo.GetValue(room); + + columns.Add($"[{property.GetColumnName()}]"); + + var sqlValue = ToSqlValue(value); + if (sqlValue == null) + { + cells.Add("NULL"); + } + else + { + cells.Add("{" + parameters.Count + "}"); + parameters.Add(sqlValue); + } + } + + var sql = + $"SET IDENTITY_INSERT {qualified} ON;\n" + + $"INSERT INTO {qualified} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", cells)});\n" + + $"SET IDENTITY_INSERT {qualified} OFF;"; + + await db.Database.ExecuteSqlRawAsync(sql, parameters); + } + + private static async Task TellOwnerAsync(long ownerAccountId, string message) + { + if (ownerAccountId <= 0) return; + + try + { + await Notify.SendMessage(ownerAccountId, new Message + { + Type = MessageType.CoachMessage, + Data = message, + FromPlayerId = CoachAccountId + }); + } + catch (Exception) + { + } + } + + private static object? ToSqlValue(object? value) => value switch + { + null => null, + Enum enumValue => Convert.ChangeType(enumValue, Enum.GetUnderlyingType(enumValue.GetType())), + _ => value + }; + + private static void Upsert(BannedRoom entry) + { + lock (FileLock) + { + var rooms = LoadLocked(); + rooms.RemoveAll(x => x.RoomId == entry.RoomId); + rooms.Add(entry); + SaveLocked(rooms); + } + } + + private static void Remove(long roomId) + { + lock (FileLock) + { + var rooms = LoadLocked(); + if (rooms.RemoveAll(x => x.RoomId == roomId) == 0) return; + SaveLocked(rooms); + } + } + + private static List LoadLocked() + { + if (!File.Exists(FilePath)) return new List(); + + try + { + var json = File.ReadAllText(FilePath); + var rooms = JsonSerializer.Deserialize>(json, JsonOptions) ?? new List(); + + foreach (var room in rooms) room.Room.PublishedAt = room.PublishedAt; + + return rooms; + } + catch (Exception) + { + return new List(); + } + } + + private static void SaveLocked(List rooms) + { + Directory.CreateDirectory(DBConfig.PersistentLogs); + File.WriteAllText(FilePath, JsonSerializer.Serialize(rooms, JsonOptions)); + } +} + +public class BannedRoom +{ + public long RoomId { get; set; } + public string Name { get; set; } = ""; + public string Reason { get; set; } = ""; + public string BannedBy { get; set; } = ""; + public DateTime BannedAt { get; set; } + public DateTime PublishedAt { get; set; } + + public RoomData Room { get; set; } = new(); + public List SubRooms { get; set; } = new(); + public List Roles { get; set; } = new(); + public RoomStats? Stats { get; set; } +} diff --git a/Server/Analytics/DataCollectionAPI.cs b/Server/Analytics/DataCollectionAPI.cs new file mode 100644 index 0000000..92e65fb --- /dev/null +++ b/Server/Analytics/DataCollectionAPI.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.API.Analytics +{ + [Route("datacollection")] + public class DataCollectionAPI : Controller + { + [HttpPost("data/event")] + [HttpPost("data/heartbeat")] + public IActionResult Event() + { + return Ok(new { }); + } + + [HttpPost("data/events")] + public IActionResult Events() + { + return Ok(Array.Empty()); + } + + [HttpGet("sampling")] + public IActionResult Sampling() + { + return Ok(new { }); + } + } +} diff --git a/Server/Analytics/GameLogsAPI.cs b/Server/Analytics/GameLogsAPI.cs new file mode 100644 index 0000000..542c6dd --- /dev/null +++ b/Server/Analytics/GameLogsAPI.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; + +namespace RadicadeServer.API.Logs +{ + [Route("gamelogs")] + public class GameLogsAPI : Controller + { + private const int HeadChars = 400; + + private const long MaxAttachmentBytes = 20 * 1024 * 1024; + + [HttpPut("reports/{id}")] + [HttpPost("reports/{id}")] + public async Task UploadReport(string id) + { + var body = await ReadAsync(); + + return Ok(new { ReportId = id, Success = true }); + } + + [HttpPut("reports/{id}/attachments/{name}")] + [HttpPost("reports/{id}/attachments/{name}")] + public async Task UploadAttachment(string id, string name) + { + var body = await ReadBytesAsync(); + var path = await StoreAsync(id, name, body); + + return Ok(new { ReportId = id, AttachmentName = name, Size = body.Length, Success = true }); + } + + [HttpPost("reports")] + public async Task CreateReport() + { + var body = await ReadAsync(); + var id = Guid.NewGuid().ToString(); + + return Ok(new { ReportId = id, Success = true }); + } + + private async Task ReadAsync() + { + using var reader = new StreamReader(Request.Body); + return await reader.ReadToEndAsync(); + } + + private async Task ReadBytesAsync() + { + using var buffer = new MemoryStream(); + var chunk = new byte[81920]; + int read; + while ((read = await Request.Body.ReadAsync(chunk)) > 0) + { + buffer.Write(chunk, 0, read); + if (buffer.Length >= MaxAttachmentBytes) break; + } + return buffer.ToArray(); + } + + private static async Task StoreAsync(string reportId, string name, byte[] body) + { + if (body.Length == 0 || body.Length > MaxAttachmentBytes) return null; + + try + { + Directory.CreateDirectory(AttachmentFolder); + var path = Path.Combine(AttachmentFolder, $"{Sanitize(reportId)}-{Sanitize(name)}"); + await System.IO.File.WriteAllBytesAsync(path, body); + return path; + } + catch (Exception) + { + return null; + } + } + + private static string AttachmentFolder => Path.Combine(DBConfig.PrivateData, "BugReports"); + + private static string Sanitize(string value) + { + var clean = new string((value ?? string.Empty).Where(c => !Path.GetInvalidFileNameChars().Contains(c)).ToArray()); + return clean.Length == 0 ? "upload" : clean; + } + + private static string Summarize(byte[] body) + { + foreach (var b in body) + { + if (b == 0) return "(binary)"; + } + return Head(System.Text.Encoding.UTF8.GetString(body, 0, Math.Min(body.Length, 4096))); + } + + private static string Head(string body) + { + var flat = body.Replace('\r', ' ').Replace('\n', ' ').Trim(); + if (flat.Length == 0) return string.Empty; + return flat.Length <= HeadChars ? flat : flat[..HeadChars] + "..."; + } + } +} diff --git a/Server/Analytics/GameSight.cs b/Server/Analytics/GameSight.cs new file mode 100644 index 0000000..7072764 --- /dev/null +++ b/Server/Analytics/GameSight.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.RecNet.GameSight; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Analytics +{ + [Route("api/gamesight/")] + public class GameSight : Controller + { + [HttpPost("event")] + public Success SendEvent([FromForm] string type, [FromForm] string user_id) + { + return new Success(true, null, null); + } + } +} diff --git a/Server/Analytics/GameSightEvent.cs b/Server/Analytics/GameSightEvent.cs new file mode 100644 index 0000000..e00e7a0 --- /dev/null +++ b/Server/Analytics/GameSightEvent.cs @@ -0,0 +1,18 @@ +namespace RadicadeServer.RecNet.GameSight +{ + public class GameSightIdentifiers + { + public string resolution { get; set; } = null!; + public string language { get; set; } = null!; + public string timezone { get; set; } = null!; + } + public class GameSightEvent + { + public string type { get; set; } = null!; + public string user_id { get; set; } = null!; + public GameSightIdentifiers identifiers { get; set; } = null!; + public string ip { get; set; } = null!; + public string gameplay_session_id { get; set; } = null!; + public string transaction_id { get; set; } = null!; + } +} diff --git a/Server/Auth/AdminLoginThrottle.cs b/Server/Auth/AdminLoginThrottle.cs new file mode 100644 index 0000000..7572339 --- /dev/null +++ b/Server/Auth/AdminLoginThrottle.cs @@ -0,0 +1,108 @@ +using System.Collections.Concurrent; + +namespace RadicadeServer.Auth; + + + + + + + + +public static class AdminLoginThrottle +{ + + public const int MaxFailedAttempts = 5; + + + public static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15); + + private sealed record Entry(int Failures, DateTime FirstFailureAt, DateTime LockedUntil); + + private static readonly ConcurrentDictionary Attempts = new(); + + + + public static bool IsLocked(string username, string ip, out TimeSpan remaining) + { + remaining = TimeSpan.Zero; + var byUser = LockedRemaining("u:" + username); + var byIp = LockedRemaining("ip:" + ip); + + if (byUser == null && byIp == null) return false; + remaining = Max(byUser, byIp)!.Value; + return true; + } + + public static void RecordFailure(string username, string ip) + { + var now = DateTime.UtcNow; + foreach (var key in Keys(username, ip)) + { + Attempts.AddOrUpdate(key, + _ => new Entry(1, now, DateTime.MinValue), + (_, existing) => + { + + + if (now - existing.FirstFailureAt > LockoutDuration) + return new Entry(1, now, DateTime.MinValue); + + var failures = existing.Failures + 1; + + + var lockedUntil = failures >= MaxFailedAttempts ? now + LockoutDuration : DateTime.MinValue; + return new Entry(failures, existing.FirstFailureAt, lockedUntil); + }); + } + Prune(now); + } + + public static void Clear(string username, string ip) + { + foreach (var key in Keys(username, ip)) + Attempts.TryRemove(key, out _); + } + + private static TimeSpan? LockedRemaining(string key) + { + if (string.IsNullOrWhiteSpace(key)) return null; + if (!Attempts.TryGetValue(key, out var entry)) return null; + + + if (entry.Failures < MaxFailedAttempts) return null; + + var now = DateTime.UtcNow; + if (now >= entry.LockedUntil) + { + Attempts.TryRemove(key, out _); + return null; + } + return entry.LockedUntil - now; + } + + private static IEnumerable Keys(string username, string ip) + { + if (!string.IsNullOrWhiteSpace(username)) yield return "u:" + username; + if (!string.IsNullOrWhiteSpace(ip)) yield return "ip:" + ip; + } + + private static TimeSpan? Max(TimeSpan? a, TimeSpan? b) + { + if (a == null) return b; + if (b == null) return a; + return a > b ? a : b; + } + + private static void Prune(DateTime now) + { + + foreach (var kv in Attempts) + { + var e = kv.Value; + var lockActive = e.Failures >= MaxFailedAttempts && now < e.LockedUntil; + if (!lockActive && now - e.FirstFailureAt > LockoutDuration) + Attempts.TryRemove(kv.Key, out _); + } + } +} diff --git a/Server/Auth/AdminSecurity.cs b/Server/Auth/AdminSecurity.cs new file mode 100644 index 0000000..43fa3f1 --- /dev/null +++ b/Server/Auth/AdminSecurity.cs @@ -0,0 +1,78 @@ +using System.Security.Cryptography; +using System.Text; + +namespace RadicadeServer.Auth; + + + + + +public static class AdminSecurity +{ + + + private const int Pbkdf2Iterations = 600_000; + private const int SaltSize = 16; + private const int HashSize = 32; + + public static string HashPassword(string password) + { + var salt = RandomNumberGenerator.GetBytes(SaltSize); + var hash = Rfc2898DeriveBytes.Pbkdf2( + Encoding.UTF8.GetBytes(password), salt, Pbkdf2Iterations, HashAlgorithmName.SHA256, HashSize); + return $"pbkdf2${Pbkdf2Iterations}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}"; + } + + public static bool VerifyPassword(string storedHash, string password) + { + if (string.IsNullOrWhiteSpace(storedHash)) return false; + + var parts = storedHash.Split('$'); + if (parts.Length != 4 || parts[0] != "pbkdf2") return false; + if (!int.TryParse(parts[1], out var iterations)) return false; + + try + { + var salt = Convert.FromBase64String(parts[2]); + var expected = Convert.FromBase64String(parts[3]); + var actual = Rfc2898DeriveBytes.Pbkdf2( + Encoding.UTF8.GetBytes(password), salt, iterations, HashAlgorithmName.SHA256, expected.Length); + return CryptographicOperations.FixedTimeEquals(actual, expected); + } + catch + { + return false; + } + } + + public static bool NeedsRehash(string storedHash) + { + if (string.IsNullOrWhiteSpace(storedHash)) return false; + + var parts = storedHash.Split('$'); + if (parts.Length != 4 || parts[0] != "pbkdf2") return true; + return !int.TryParse(parts[1], out var iterations) || iterations < Pbkdf2Iterations; + } + + + + + + public static string? ValidatePasswordPolicy(string? password, string? username = null) + { + if (string.IsNullOrWhiteSpace(password) || password.Length < 12) + return "Password must be at least 12 characters long."; + if (!password.Any(char.IsUpper)) + return "Password must contain at least one uppercase letter."; + if (!password.Any(char.IsLower)) + return "Password must contain at least one lowercase letter."; + if (!password.Any(char.IsDigit)) + return "Password must contain at least one number."; + if (!password.Any(ch => !char.IsLetterOrDigit(ch))) + return "Password must contain at least one symbol (e.g. ! @ # $ %)."; + if (!string.IsNullOrWhiteSpace(username) && username.Length >= 3 && + password.Contains(username, StringComparison.OrdinalIgnoreCase)) + return "Password must not contain your username."; + return null; + } +} diff --git a/Server/Auth/AdminSession.cs b/Server/Auth/AdminSession.cs new file mode 100644 index 0000000..0b56516 --- /dev/null +++ b/Server/Auth/AdminSession.cs @@ -0,0 +1,162 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.DB; + +namespace RadicadeServer.Auth; + + + + + + + + +public static class AdminSession +{ + public const string CookieName = "radicade_admin_session"; + public const string StateCookieName = "radicade_oauth_state"; + public const string RedirectCookieName = "radicade_oauth_redirect"; + + + public static readonly TimeSpan Lifetime = TimeSpan.FromHours(12); + + public static readonly TimeSpan AbsoluteLifetime = TimeSpan.FromDays(7); + + + private static readonly TimeSpan RotationGrace = TimeSpan.FromMinutes(2); + + + public static async Task CreateAsync(RecDBContext db, long adminUserId) + { + var token = NewToken(); + db.AdminSessions.Add(new AdminSessionRecord + { + AdminUserId = adminUserId, + TokenHash = Hash(token), + CreatedAt = DateTime.UtcNow, + LastSeenAt = DateTime.UtcNow, + ExpiresAt = DateTime.UtcNow.Add(Lifetime) + }); + await db.SaveChangesAsync(); + return token; + } + + + + + + public static async Task<(bool Ok, long UserId)> ValidateAsync(RecDBContext db, string? token) + { + if (string.IsNullOrWhiteSpace(token)) return (false, 0); + + var now = DateTime.UtcNow; + var hash = Hash(token); + + var record = await db.AdminSessions.FirstOrDefaultAsync(s => s.TokenHash == hash); + if (record == null) + { + + record = await db.AdminSessions.FirstOrDefaultAsync(s => + s.PreviousTokenHash == hash && s.LastSeenAt > now - RotationGrace); + if (record == null) return (false, 0); + } + + if (record.RevokedAt.HasValue || record.ExpiresAt <= now) + return (false, 0); + + var ceiling = record.CreatedAt.Add(AbsoluteLifetime); + if (now >= ceiling) return (false, 0); + + record.LastSeenAt = now; + record.ExpiresAt = now.Add(Lifetime) < ceiling ? now.Add(Lifetime) : ceiling; + await db.SaveChangesAsync(); + + return (true, record.AdminUserId); + } + + + + + private static readonly object RotateLock = new(); + + + + + + + + + + public static (string Token, bool Rotated) TryRotate(RecDBContext db, string? currentToken) + { + if (string.IsNullOrWhiteSpace(currentToken)) return ("", false); + + lock (RotateLock) + { + var record = db.AdminSessions.FirstOrDefault(s => s.TokenHash == Hash(currentToken)); + if (record == null) + { + + + + record = db.AdminSessions.FirstOrDefault(s => s.PreviousTokenHash == Hash(currentToken)); + if (record != null && record.LastSeenAt > DateTime.UtcNow - RotationGrace) + return (currentToken, false); + return (currentToken, false); + } + + var now = DateTime.UtcNow; + var ceiling = record.CreatedAt.Add(AbsoluteLifetime); + if (now >= ceiling) return (currentToken, false); + + var next = NewToken(); + record.PreviousTokenHash = record.TokenHash; + record.TokenHash = Hash(next); + record.LastSeenAt = now; + record.ExpiresAt = now.Add(Lifetime) < ceiling ? now.Add(Lifetime) : ceiling; + db.SaveChanges(); + return (next, true); + } + } + + public static async Task RevokeAsync(RecDBContext db, string? token) + { + if (string.IsNullOrWhiteSpace(token)) return; + + var record = await db.AdminSessions.FirstOrDefaultAsync(s => s.TokenHash == Hash(token)); + if (record == null) return; + + record.RevokedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + + public static async Task RevokeAllAsync(RecDBContext db, long adminUserId) + { + var sessions = await db.AdminSessions + .Where(s => s.AdminUserId == adminUserId && !s.RevokedAt.HasValue) + .ToListAsync(); + foreach (var session in sessions) + session.RevokedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + + public static async Task PruneAsync(RecDBContext db) + { + var cutoff = DateTime.UtcNow.AddDays(-2); + var stale = await db.AdminSessions + .Where(s => s.RevokedAt.HasValue || s.ExpiresAt < cutoff) + .ToListAsync(); + if (stale.Count == 0) return; + db.AdminSessions.RemoveRange(stale); + await db.SaveChangesAsync(); + } + + private static string NewToken() => Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); + + private static string Hash(string token) => + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token))); +} diff --git a/Server/Auth/AuthAPI.cs b/Server/Auth/AuthAPI.cs new file mode 100644 index 0000000..3e6340a --- /dev/null +++ b/Server/Auth/AuthAPI.cs @@ -0,0 +1,1116 @@ +using Azure; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Auth.Connection; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Radicade.Mail; +using RadicadeServer.Radicade.Responses; +using RadicadeServer.Utils; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace RadicadeServer.Auth +{ + [Route("auth")] + public class AuthAPI : Controller + { + + public class WebLoginRequest + { + public string? Username { get; set; } + public string? Password { get; set; } + } + + + + public const int MinPasswordLength = 6; + + RecDBContext db; + PasswordHasher hasher; + + public AuthAPI(RecDBContext context) { + this.db = context; + this.hasher = new PasswordHasher(); + } + + [HttpGet("account/me/haspassword"), Authorize] + public async Task GetHasPassword() + { + var userId = User.GetUserId(); + + var user = await db.Players.FirstAsync(x => x.AccountId == userId); + + return !string.IsNullOrEmpty(user.Hashword); + } + + [HttpPost("account/me/changepassword"), Authorize] + public async Task ChangePassword([FromForm] string oldPassword, [FromForm] string newPassword) + { + if (string.IsNullOrWhiteSpace(newPassword)) return new Success(false, "New password is required.", null); + if (newPassword.Length < MinPasswordLength) return new Success(false, $"Password must be at least {MinPasswordLength} characters.", null); + + var userId = User.GetUserId(); + + var user = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (user == null) return new Success(false, "User does not exist.", "damn."); + + + + if (!string.IsNullOrWhiteSpace(user.Hashword)) + { + if (string.IsNullOrEmpty(oldPassword)) + return new Success(false, "Current password is required.", null); + var check = hasher.VerifyHashedPassword(user, user.Hashword, oldPassword); + if (check != PasswordVerificationResult.Success && check != PasswordVerificationResult.SuccessRehashNeeded) + return new Success(false, "Current password is incorrect.", null); + } + + user.Hashword = hasher.HashPassword(user, newPassword); + + await db.SaveChangesAsync(); + + AuthSecurityMonitor.NoteCredentialChange(user.AccountId, "password changed"); + return new Success(true, null, null); + } + + + + + + + + + + [HttpPost("account/recoverpassword")] + public async Task RecoverPassword() + { + + var email = ""; + if (Request.HasFormContentType) + email = Request.Form["email"].FirstOrDefault() ?? ""; + if (string.IsNullOrWhiteSpace(email)) + { + using var reader = new System.IO.StreamReader(Request.Body); + var raw = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(raw)) + { + try + { + using var doc = System.Text.Json.JsonDocument.Parse(raw); + if (doc.RootElement.TryGetProperty("email", out var e) && e.ValueKind == System.Text.Json.JsonValueKind.String) + email = e.GetString() ?? ""; + } + catch { } + } + } + email = email.Trim(); + + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) + return new Success(false, "Enter a valid email address.", null); + + + + var plrData = await db.Players.FirstOrDefaultAsync(x => x.Email.ToLower() == email.ToLower()); + if (plrData != null) + { + plrData.PasswordResetToken = Guid.NewGuid().ToString("N"); + plrData.PasswordResetExpires = DateTime.UtcNow.AddHours(1); + await db.SaveChangesAsync(); + + SendResetEmail(plrData); + AuthSecurityMonitor.NoteCredentialChange(plrData.AccountId, "password reset requested"); + } + + + return new Success(true, null, null); + } + + private void SendResetEmail(PlayerData plrData) + { + if (string.IsNullOrWhiteSpace(plrData.PasswordResetToken)) return; + + var link = $"https://radicade.app/reset?code={plrData.PasswordResetToken}"; + var html = + "
" + + "\"Radicade\"" + + "

Reset your Radicade password

" + + "

Hi there,

" + + "

We received a request to reset the password on your Radicade account from the game. If that was you, click the button below to choose a new password:

" + + "

Reset my password

" + + "

This link expires in one hour. If you didn't request this, you can safely ignore this email.

"; + + _ = RadicadeMail.SendAsync(plrData.Email, "Reset your password - Radicade", html); + } + + + + + + + + + [HttpPost("web/login")] + public async Task WebLogin([FromBody] WebLoginRequest req) + { + if (string.IsNullOrWhiteSpace(req?.Username)) return Ok(new { success = false, message = "No username was given." }); + if (string.IsNullOrWhiteSpace(req?.Password)) return Ok(new { success = false, message = "No password was given." }); + + var loginName = req.Username.Trim().TrimStart('@'); + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.username == loginName || x.displayName == loginName); + if (profile == null) + return Ok(new { success = false, message = "Invalid username or password." }); + + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == profile.accountId); + if (plrData == null) + return Ok(new { success = false, message = "Invalid username or password." }); + + + + + + if (string.IsNullOrWhiteSpace(plrData.Hashword)) + return Ok(new { success = false, message = "Please make a password in-game!" }); + + var loginIp = Request.GetClientIp(); + var result = hasher.VerifyHashedPassword(plrData, plrData.Hashword, req.Password ?? ""); + if (result != PasswordVerificationResult.Success) + { + AuthSecurityMonitor.RecordFailedLogin(profile.accountId, loginIp); + return Ok(new { success = false, message = "Invalid username or password." }); + } + + if (await AccountLock.IsLockedAsync(db, profile.accountId)) + { + var lockedUsername = await AccountLock.ResolveUsernameAsync(db, profile.accountId) ?? profile.username; + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] refused web sign-in for locked account {profile.accountId}"); + return Ok(new { success = false, error = AccountLock.ErrorCode, message = AccountLock.LoginErrorMessage(lockedUsername) }); + } + AuthSecurityMonitor.RecordSuccessfulLogin(profile.accountId, loginIp, false, false); + + + + if (!await db.Presences.AnyAsync(x => x.playerId == profile.accountId)) + { + db.Presences.Add(new RealPresence + { + playerId = profile.accountId, + deviceClass = DeviceClass.Screen, + statusVisibility = StatusVisibility.Public, + vRMovementMode = VRMovementModes.TELEPORT + }); + await db.SaveChangesAsync(); + } + + var newToken = await GenerateToken(db, profile.accountId, "web", DeviceClass.Screen, Platforms.Steam, "web:" + profile.accountId); + if (!newToken.success || newToken.value == null) + return Ok(new { success = false, message = newToken.error ?? "Failed to sign in." }); + + return Ok(new + { + success = true, + token = (string)newToken.value, + username = profile.username, + displayName = profile.displayName, + email = plrData.Email, + emailVerified = plrData.EmailVerified + }); + } + + [HttpPost("connect/token")] + public async Task> ConnectWithToken([FromForm] TokenConnectData data) + { + + + + + + data.platform_id = data.platform_id?.Trim(); + data.device_id = data.device_id?.Trim(); + + var platformAuth = data.platform_auth?.Trim() ?? string.Empty; + if (data.platform == Platforms.Standalone) + { + if (string.IsNullOrWhiteSpace(data.device_id) || + data.device_id.Length > 256 || data.client_id != "recroom") + return Unauthorized(new AuthResponse("Failed to sign in.", "Sign in with your Radicade username and password.")); + data.platform_id = "radicade-quest:" + data.device_id; + if (data.grant_type == "password" && !string.IsNullOrWhiteSpace(data.username) && + !string.IsNullOrWhiteSpace(data.password)) + data.account_id = 0; + else if (data.grant_type is "cached_login" or "refresh_token") + { + var remembered = await QuestRememberedSession.ValidateAsync(db, + Request.Headers[QuestRememberedSession.Header].ToString(), data.device_id, data.account_id); + if (!remembered.HasValue) + return Unauthorized(new AuthResponse("invalid_grant", "Sign in with your Radicade username and password.")); + data.account_id = remembered.Value; + data.grant_type = "refresh_token"; + } + else return Unauthorized(new AuthResponse("invalid_grant", "Sign in with your Radicade username and password.")); + } + if (data.platform == Platforms.Steam && !string.IsNullOrEmpty(platformAuth) && + !TryValidateSteamPlatformAuth(platformAuth, out var platformAuthReason)) + { + return SteamIdentityRefusal("platform_auth shape", platformAuthReason, data); + } + + if (data.platform == Platforms.Steam && (data.grant_type is "cached_login" or "create_account" or "password")) + { + if (string.IsNullOrWhiteSpace(platformAuth)) + { + return SteamIdentityRefusal("platform_auth", + "platform_auth was absent or blank", data); + } + if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey)) + { + if (!DBConfig.TrustUnverifiedSteamIdentity) + { + return SteamIdentityRefusal("steam api key", + "no SteamApiKey is configured and TrustUnverifiedSteamIdentity is off", data); + } + } + else + { + if (!TryReadSteamTicket(platformAuth, out var steamTicket)) + { + return SteamIdentityRefusal("platform_auth.Ticket", + "platform_auth carried no readable string 'Ticket'", data); + } + + string? verifiedSteamId; + try + { + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted); + timeout.CancelAfter(TimeSpan.FromSeconds(10)); + verifiedSteamId = await AuthenticateSteamTicketAsync(steamTicket, GetSteamAppId(platformAuth), timeout.Token); + } + catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException) + { + return SteamIdentityRefusal("steam ticket check", + $"Steam could not be reached ({ex.GetType().Name})", data); + } + + if (string.IsNullOrWhiteSpace(verifiedSteamId)) + { + return SteamIdentityRefusal("steam ticket check", + "Steam would not confirm the ticket (stale ticket, or one issued for an app it will not check)", data); + } + if (!string.Equals(verifiedSteamId, data.platform_id?.Trim(), StringComparison.Ordinal)) + { + return SteamIdentityRefusal("identity match", + $"the ticket is for {verifiedSteamId}, not for the posted identity {data.platform_id?.Trim()}", data); + } + } + } + + var isWhitelisted = await AuthShared.IsWhitelisted(data.platform_id ?? string.Empty); + if (!isWhitelisted) + { + return new AuthResponse("Error", "You are not whitelisted."); + } + + + + + + + AuthResponse response = new AuthResponse(); + + switch (data.grant_type) + { + default: + return Unauthorized(); + + case "cached_login": + { + if (data.account_id <= 0) + return StatusCode(400, new AuthResponse("invalid_request", "missing account_id")); + + if (!await db.Players.AnyAsync(player => player.AccountId == data.account_id) || + !await db.Profiles.AnyAsync(profile => profile.accountId == data.account_id) || + !await db.Presences.AnyAsync(presence => presence.playerId == data.account_id)) + return StatusCode(400, new AuthResponse("invalid_grant", "unknown or incomplete account_id")); + + if (!await db.CachedLogins.AnyAsync(login => login.accountId == data.account_id && + login.platform == data.platform && login.platformId == data.platform_id)) + return StatusCode(400, new AuthResponse("invalid_grant", "no linked account for this platform identity")); + + response = await TokenRefresh(data); + return Ok(response); + } + + case "create_account": + { + var carriesSignUpPayload = HasSignUpPayload(data); + + + + + + using var identity = await RegistrationGuard.LockIdentityAsync( + RegistrationGuard.IdentityKey(data.platform.ToString(), data.platform_id, data.device_id)); + + var existingLink = await db.CachedLogins + .AsNoTracking() + .Where(login => login.platform == data.platform && login.platformId == data.platform_id) + .Where(login => data.account_id <= 0 || login.accountId == data.account_id) + .OrderByDescending(login => login.lastLoginTime) + .FirstOrDefaultAsync(); + + if (data.account_id > 0 && existingLink == null) + return StatusCode(400, new AuthResponse("invalid_grant", "no linked account for this platform identity")); + if (existingLink != null && carriesSignUpPayload) + { + var linkedSettings = await db.PlayerSettings.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == existingLink.accountId); + bool finished = linkedSettings?.DeserializeSettings().Any(x => + string.Equals(x.Key, "Recroom.AccountCreation.HasFinished", StringComparison.OrdinalIgnoreCase) && + string.Equals(x.Value, "true", StringComparison.OrdinalIgnoreCase)) == true; + if (finished) + existingLink = null; + } + + if (existingLink != null) + { + data.account_id = existingLink.accountId; + + if (carriesSignUpPayload) + await ApplySignUpPayloadAsync(data, existingLink.accountId); + else if (!await PlayerUtils.HasFinishedAccountCreationAsync(db, existingLink.accountId)) + { + + + + + + await PlayerUtils.RefreshAccountCreationMarkersAsync(db, existingLink.accountId); + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Onboarding] account {existingLink.accountId} resumed " + + "before its creation flow finished; started marker restored so the client runs the flow"); + } + } + else + { + var adoptable = await PlayerUtils.FindAdoptableAccountForDeviceAsync(db, data.device_id?.Trim()); + if (adoptable != null) + { + data.account_id = adoptable.AccountId; + await PlayerUtils.AdoptDeviceAccountAsync(db, adoptable.AccountId, new AccountCreationData + { + platform = data.platform, + platformId = data.platform_id ?? string.Empty, + deviceId = data.device_id ?? string.Empty + }, markStarted: true); + if (carriesSignUpPayload) + await ApplySignUpPayloadAsync(data, adoptable.AccountId); + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Auth] create_account adopted this device's account " + + $"{adoptable.AccountId} for platform {data.platform} identity {data.platform_id}"); + } + else + { + + + + + + var refusal = await TryProvisionOnboardingAccountAsync(data); + if (refusal != null) + { + return TokenRefusal(refusal); + } + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Auth] create_account made account {data.account_id} " + + $"for platform {data.platform} identity {data.platform_id}"); + } + } + + response = await TokenRefresh(data); + return Ok(response); + } + + case "refresh_token": + response = await TokenRefresh(data); + return Ok(response); + + case "password": + if (data.username == null) return Ok(new AuthResponse { error = "Failed to sign in.", error_description = "No username was given." }); + if (data.password == null) return Ok(new AuthResponse { error = "Failed to sign in.", error_description = "No password was given." }); + + var loginIp = Request.GetClientIp(); + if (AuthSecurityMonitor.IsSourceThrottled(loginIp)) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] throttled sign-in source {loginIp} (suspected spraying)"); + return StatusCode(429, new AuthResponse("too_many_requests", "Too many sign-in attempts from this connection. Please try again later.")); + } + + + + + + + var loginName = data.username.Trim().TrimStart('@'); + var existingProfile = await db.Profiles.FirstOrDefaultAsync(x => x.username == loginName || x.displayName == loginName); + if (existingProfile != null) + { + data.account_id = existingProfile.accountId; + } + + var playerData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == data.account_id); + if (playerData != null) + { + var result = hasher.VerifyHashedPassword(playerData, playerData.Hashword, data.password); + if (result == PasswordVerificationResult.Success) + { + var alreadyCached = await db.CachedLogins.FirstOrDefaultAsync(x => x.accountId == data.account_id && x.platform == data.platform && x.platformId == data.platform_id); + if (await AccountLock.IsLockedAsync(db, data.account_id) && alreadyCached == null) + { + var lockedUsername = await AccountLock.ResolveUsernameAsync(db, data.account_id); + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] refused password sign-in for locked account {data.account_id}"); + return Ok(new AuthResponse(AccountLock.ErrorCode, AccountLock.LoginErrorMessage(lockedUsername))); + } + if (alreadyCached == null) + { + await db.AddAsync(new CachedLogin { lastLoginTime = DateTime.UtcNow, accountId = data.account_id, platform = data.platform, platformId = data.platform_id! }); + await db.SaveChangesAsync(); + } + + var newDevice = !string.IsNullOrWhiteSpace(playerData.DeviceId) + && !string.Equals(playerData.DeviceId, data.device_id, StringComparison.Ordinal); + var newIp = !string.IsNullOrWhiteSpace(playerData.LastIpAddress) + && !string.Equals(playerData.LastIpAddress, loginIp, StringComparison.Ordinal); + AuthSecurityMonitor.RecordSuccessfulLogin(data.account_id, loginIp, newDevice, newIp); + + response = await TokenRefresh(data, verifiedByPassword: true); + return Ok(response); + } + } + + var failures = AuthSecurityMonitor.RecordFailedLogin(data.account_id, loginIp); + var failureDelay = AuthSecurityMonitor.ProgressiveDelayMs(failures); + if (failureDelay > 0) await Task.Delay(failureDelay); + return Ok(new AuthResponse { error = "Failed to sign in.", error_description = "Unknown error." }); + } + + } + + [NonAction] + private async Task ApplySignUpPayloadAsync(TokenConnectData data, long accountId) + { + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == accountId); + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == accountId); + if (profile == null || plrData == null) + { + return; + } + + var applied = new List(); + var ignored = new List(); + bool isOnboarding = await PlayerUtils.IsChoosingTheirAccountNameAsync(db, accountId) || PlayerUtils.IsNewPlayer(plrData); + + + + + + bool nameApplied = false; + bool nameChosen = false; + var chosen = UsernameRules.Parse(data.username, out var nameRejection); + if (nameRejection != null) + { + + + + ignored.Add($"name=refused({nameRejection})"); + } + else if (chosen == null) + { + ignored.Add("name=absent"); + } + else if (string.Equals(chosen, profile.username, StringComparison.Ordinal)) + { + ignored.Add($"name={chosen}(already its name)"); + nameChosen = true; + } + else if (profile.NameLocked) + { + ignored.Add($"name={chosen}(locked)"); + } + else if (!await PlayerUtils.IsChoosingTheirAccountNameAsync(db, accountId) && !PlayerUtils.IsNewPlayer(plrData)) + { + ignored.Add($"name={chosen}(flow over for account '{profile.username}')"); + } + else if (await PlayerUtils.IsNameTakenAsync(db, chosen)) + { + ignored.Add($"name={chosen}(taken)"); + } + else + { + var previous = profile.username; + profile.username = chosen; + profile.displayName = chosen; + applied.Add($"name={chosen}(was '{previous}')"); + nameApplied = true; + nameChosen = true; + } + + var birthday = PlayerUtils.ReadBirthday(data.birthday ?? data.birthdayUTC); + if (birthday == null) + { + ignored.Add("birthday=absent"); + } + else if (plrData.Birthday != null && plrData.Birthday.Value.Year > 1900) + { + ignored.Add($"birthday={birthday:yyyy-MM-dd}(account already gave {plrData.Birthday:yyyy-MM-dd})"); + } + else + { + plrData.Birthday = birthday; + plrData.IsJunior = PlayerUtils.IsJuniorByBirthday(birthday); + profile.isJunior = plrData.IsJunior; + applied.Add($"birthday={birthday:yyyy-MM-dd}"); + } + + if (string.IsNullOrWhiteSpace(data.password)) + { + ignored.Add("password=absent"); + } + else if (!string.IsNullOrWhiteSpace(plrData.Hashword)) + { + ignored.Add("password=(account already has one)"); + } + else + { + plrData.Hashword = new PasswordHasher().HashPassword(plrData, data.password); + applied.Add("password=set"); + } + + if (applied.Count > 0 || isOnboarding) await db.SaveChangesAsync(); + + + + + + + if (isOnboarding || nameApplied) + await PlayerUtils.RefreshAccountCreationMarkersAsync(db, accountId, nameChosen ? true : null); + + var line = + $"[SignUp] account {accountId} resumed its sign-up on an identity that was already linked: " + + $"applied [{string.Join(", ", applied)}], ignored [{string.Join(", ", ignored)}]; " + + $"name is now '{profile.username}', birthday {(plrData.Birthday == null ? "none" : plrData.Birthday.Value.ToString("yyyy-MM-dd"))}, " + + $"password {(string.IsNullOrWhiteSpace(plrData.Hashword) ? "unset" : "set")}."; + } + + [NonAction] + private async Task TryProvisionOnboardingAccountAsync(TokenConnectData data) + { + var platformId = data.platform_id?.Trim() ?? string.Empty; + var deviceId = data.device_id?.Trim() ?? string.Empty; + + + + if (platformId.Length == 0 || deviceId.Length == 0 || platformId.Length > 64 || deviceId.Length > 256) + { + return new AuthResponse("Account creation failed.", "A valid platform ID and device ID are required."); + } + + var clientIp = Request.GetClientIp(); + if (await db.IsIpBanned(clientIp)) + return new AuthResponse("Account creation failed.", "This IP address is banned from creating new accounts."); + if (await db.IsAccountCreationBlocked(clientIp)) + return new AuthResponse("Account creation failed.", "This IP address is blocked from creating new accounts."); + if (await db.IsHardwareBanned(deviceId)) + return new AuthResponse("Account creation failed.", "This device is banned."); + + + + + var throttled = RegistrationGuard.Check(clientIp, deviceId); + if (throttled != null) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Registration] refused a new account for platform " + + $"{data.platform} identity {platformId} ({RegistrationGuard.Describe(clientIp, deviceId)}): {throttled}"); + return new AuthResponse("Account creation failed.", throttled); + } + + data.platform_id = platformId; + data.device_id = deviceId; + + var created = await PlayerUtils.CreateAccount(new AccountCreationData + { + platform = data.platform, + platformId = platformId, + deviceId = deviceId, + deviceClass = data.device_class, + password = data.password, + username = data.username, + birthday = data.birthday, + birthdayUTC = data.birthdayUTC, + email = data.email, + avatar = data.avatar, + avatarJson = data.avatarJson + }, clientIp, accountCreationInProgress: true); + + if (!created.success || created.value is not Profile profile) + return new AuthResponse("Account creation failed.", created.error ?? "Could not create the account."); + + data.account_id = profile.accountId; + return null; + } + + [NonAction] + private static bool HasSignUpPayload(TokenConnectData data) => + !string.IsNullOrWhiteSpace(data.username) || + !string.IsNullOrWhiteSpace(data.password) || + data.birthday != null || + data.birthdayUTC != null || + !string.IsNullOrWhiteSpace(data.avatar) || + !string.IsNullOrWhiteSpace(data.avatarJson) || + !string.IsNullOrWhiteSpace(data.email); + + [NonAction] + private static ObjectResult TokenRefusal(AuthResponse refusal) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Auth] sign-in refused: error={refusal.error} detail=\"{refusal.error_description}\""); + return new(refusal) { StatusCode = 400 }; + } + + private const string SteamAppId = "471710"; + private static readonly string[] SteamAllowedAppIds = ["480", SteamAppId]; + + [NonAction] + protected virtual async Task AuthenticateSteamTicketAsync(string ticket, string appId, CancellationToken cancellationToken) + { + + + + + + foreach (var candidate in CandidateSteamAppIds(appId)) + { + var result = await PlatformIdentity.AuthenticateTicketAsync(ticket, candidate, cancellationToken); + if (result.OtherApp) continue; + return result.Ok ? result.SteamId : null; + } + + return null; + } + + [NonAction] + private static IEnumerable CandidateSteamAppIds(string appId) => + new[] { appId }.Concat(SteamAllowedAppIds) + .Where(candidate => !string.IsNullOrWhiteSpace(candidate)) + .Distinct(StringComparer.Ordinal); + + [NonAction] + private static string GetSteamAppId(string platformAuth) + { + using var document = JsonDocument.Parse(platformAuth); + var appId = document.RootElement.GetProperty("AppId"); + return appId.ValueKind == JsonValueKind.String ? appId.GetString() ?? SteamAppId : appId.ToString(); + } + + [NonAction] + private static bool TryReadSteamTicket(string platformAuth, out string ticket) + { + ticket = string.Empty; + try + { + using var document = JsonDocument.Parse(platformAuth); + if (!document.RootElement.TryGetProperty("Ticket", out var ticketElement) || + ticketElement.ValueKind != JsonValueKind.String) + return false; + + ticket = ticketElement.GetString() ?? string.Empty; + return ticket.Length > 0; + } + catch (JsonException) + { + return false; + } + } + + + + + + + + [NonAction] + private ActionResult SteamIdentityRefusal(string stage, string detail, TokenConnectData data) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Auth] connect/token refused at {stage} for " + + $"platform {data.platform} identity {OrAbsent(data.platform_id)} " + + $"grant_type {OrAbsent(data.grant_type)}: {detail}"); + return Unauthorized(); + } + + [NonAction] + private static string OrAbsent(string? value) => + string.IsNullOrWhiteSpace(value) ? "" : value.Trim(); + + [NonAction] + private static bool TryValidateSteamPlatformAuth(string platformAuth, out string reason) + { + reason = "unknown"; + if (platformAuth[0] != '{' || platformAuth[^1] != '}') + { + reason = $"not a JSON object (len={platformAuth.Length})"; + return false; + } + + try + { + using var doc = JsonDocument.Parse(platformAuth); + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object) + { + reason = "platform_auth is valid JSON but not an object"; + return false; + } if (!root.TryGetProperty("Ticket", out var ticketElement) || ticketElement.ValueKind != JsonValueKind.String) + { + reason = $"no string 'Ticket' property (properties: {DescribeProperties(root)})"; + return false; + } + + var ticket = ticketElement.GetString() ?? string.Empty; + if (ticket.Length == 0 || !ticket.StartsWith("140", StringComparison.Ordinal)) + { + reason = $"'Ticket' has an unexpected format (len={ticket.Length})"; + return false; + } + + if (!root.TryGetProperty("AppId", out var appIdElement)) + { + reason = $"no 'AppId' property (properties: {DescribeProperties(root)})"; + return false; + } + + var appId = appIdElement.ValueKind == JsonValueKind.String ? appIdElement.GetString() : appIdElement.ToString(); + if (!SteamAllowedAppIds.Contains(appId, StringComparer.Ordinal)) + { + reason = "'AppId' is not supported for Steam ticket verification"; + return false; + } + + reason = "ok"; + return true; + } + catch (JsonException ex) + { + reason = $"invalid JSON ({ex.GetType().Name})"; + return false; + } + } + + + [NonAction] + private static string DescribeProperties(JsonElement root) + { + var names = new List(); + foreach (var property in root.EnumerateObject()) + names.Add(property.Name); + return names.Count == 0 ? "none" : string.Join(",", names); + } + + [NonAction] + private async Task TokenRefresh(TokenConnectData data, bool verifiedByPassword = false) + { + + + + + + + if (data.account_id <= 0) + { + long resolvedAccountId = 0; + + + + if (!string.IsNullOrWhiteSpace(data.refresh_token) && + Guid.TryParse(data.refresh_token, out var tokenGuid)) + { + var tokenBytes = tokenGuid.ToByteArray(); + var embedded = BitConverter.ToInt64(tokenBytes, 0); + if (embedded > 0 && await db.Players.AnyAsync(x => x.AccountId == embedded)) + resolvedAccountId = embedded; + } + + + + if (resolvedAccountId <= 0) + { + var headerAuth = base.Request.Headers.Authorization.ToString(); + if (!string.IsNullOrWhiteSpace(headerAuth)) + { + var headerUserId = headerAuth.GetUserId(); + if (headerUserId.HasValue && headerUserId.Value > 0 && await db.Players.AnyAsync(x => x.AccountId == headerUserId.Value)) + resolvedAccountId = headerUserId.Value; + } + } + + + + + if (resolvedAccountId <= 0) + { + if (!string.IsNullOrWhiteSpace(data.device_id)) + { + var devicePlayer = await db.Players + .Where(x => x.DeviceId == data.device_id) + .OrderByDescending(x => x.AccountId) + .FirstOrDefaultAsync(); + if (devicePlayer != null) resolvedAccountId = devicePlayer.AccountId; + } + else if (!string.IsNullOrWhiteSpace(data.platform_id)) + { + var recentLogin = await db.CachedLogins + .Where(x => x.platformId == data.platform_id && x.platform == data.platform) + .OrderByDescending(x => x.lastLoginTime) + .FirstOrDefaultAsync(); + if (recentLogin != null) resolvedAccountId = recentLogin.accountId; + } + } + + if (resolvedAccountId > 0) data.account_id = resolvedAccountId; } PlayerData? player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == data.account_id); + + if (player == null) + { + var hasIdentity = await db.CachedLogins.AnyAsync(l => l.accountId == data.account_id) + || await db.Profiles.AnyAsync(p => p.accountId == data.account_id); + if (hasIdentity) + { + + player = new PlayerData + { + AccountId = data.account_id, + Phone = "", + AppVersion = data.ver ?? DBConfig.GameVersion, + DeviceClass = data.device_class, + NameChanges = 99, + Email = "", + Hashword = "", + JuniorState = JuniorState.BasedOnBirthday, + Visibility = StatusVisibility.Public, + DeviceId = data.device_id ?? "", + LastIpAddress = Request.GetClientIp() ?? "" + }; + await db.AddRangeAsync( + player, + new PlayerAvatar { Avatar = DBConfig.DefaultAvatar, Outfits = "[]", AccountId = data.account_id }, + new Reputation { AccountId = data.account_id }, + new PlayerSettings { AccountId = data.account_id, Settings = DBConfig.NewAccountSettings2023 }, + new ProgressionData { PlayerId = data.account_id, XP = 0, Level = 1, Progress = new ProgressionJson().ToJson() }, + new RealPresence { playerId = data.account_id, deviceClass = data.device_class, statusVisibility = StatusVisibility.Public, vRMovementMode = VRMovementModes.TELEPORT }, + new Currency { AccountId = data.account_id }, + new PlayerUnlocks { AccountId = data.account_id, AvatarItems = DBConfig.DefaultAvatarUnlocks } + ); + await db.SaveChangesAsync(); + await RoomUtils.GetOrCreateDorm(data.account_id); + } + } + + CachedLogin? cachedLogin = await db.CachedLogins.FirstOrDefaultAsync( + l => l.accountId == data.account_id && + l.platformId == data.platform_id && + l.platform == data.platform); + + if (data.account_id == 1) + { + return new AuthResponse("Failed to login.", "System account cannot log in."); + } + if (player == null) + { + return new AuthResponse("Failed to login.", "Missing player data."); + } + + if (await AccountLock.IsLockedAsync(db, data.account_id) && cachedLogin == null) + { + var lockedUsername = await AccountLock.ResolveUsernameAsync(db, data.account_id); + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] refused {data.grant_type} sign-in for locked account {data.account_id} (platform={data.platform}, platformId={data.platform_id})"); + return new AuthResponse(AccountLock.ErrorCode, AccountLock.LoginErrorMessage(lockedUsername)); + } + if (cachedLogin == null) + { + return new AuthResponse("Failed to login.", "Missing cached login."); + } + + AuthResponse response = new AuthResponse(); + string authorization = data.platform == Platforms.Standalone ? string.Empty : base.Request.Headers.Authorization.ToString(); + + bool hasToken = !string.IsNullOrEmpty(authorization); + bool isPlatform = false; + + if (cachedLogin != null) + { + if (cachedLogin.platformId == data.platform_id && + cachedLogin.platform == data.platform && + (player.DeviceId == data.device_id || string.IsNullOrEmpty(player.DeviceId)) && + data.client_id == "recroom") + { + isPlatform = true; + } + } + + if (!hasToken && !isPlatform && !verifiedByPassword) + { + base.Response.StatusCode = 401; + return new AuthResponse("Failed to login.", "Invalid platform session."); + } + else + { + long userId = data.account_id; + if (hasToken) + { + var headerUserId = authorization.GetUserId(); + if (headerUserId.HasValue) + { + userId = headerUserId.Value; + } + else + { + hasToken = false; + } + } + + Success newToken = await GenerateToken(db, data.account_id, data.ver!, data.device_class, data.platform, data.platform_id); + player.DeviceId = data.device_id!; + + + if (data.device_class != DeviceClass.Unknown) + player.DeviceClass = data.device_class; + + + var loginIp = Request.GetClientIp(); + if (!string.IsNullOrWhiteSpace(loginIp)) + player.LastIpAddress = loginIp; + await db.SaveChangesAsync(); + + if (!newToken.success || newToken.value == null) + { + response = new AuthResponse + { + error = "Error", + error_description = newToken.error + }; + return response; + } + else + { + response.access_token = (string)newToken.value; + if (data.platform == Platforms.Standalone) + Response.Headers[QuestRememberedSession.Header] = response.access_token; + + + + + + + + var accountBytes = BitConverter.GetBytes(data.account_id); + var refreshGuidBytes = new byte[16]; + Array.Copy(accountBytes, 0, refreshGuidBytes, 0, 8); response.refresh_token = new Guid(refreshGuidBytes).ToString(); + response.key = "c3Vja215ZGlja25nYQ=="; + + + + + + + return response; + } + } + } + + [NonAction] + internal static async Task GenerateToken(RecDBContext db, long userId, string version, DeviceClass deviceClass, Platforms platform, string platformId) + { + + CachedLogin? cachedLogin = await db.CachedLogins.FirstOrDefaultAsync( + login => login.accountId == userId && + login.platformId == platformId && + login.platform == platform); + + Profile? profile = await db.Profiles.FirstOrDefaultAsync((plr) => plr.accountId == userId); + + if (profile == null) + return AuthResponses.MissingProfile; + + if (await AccountLock.IsLockedAsync(db, userId) && cachedLogin == null) + return new Success(false, AccountLock.ErrorCode, null); + RealPresence? presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId); + if (presence == null) return AuthResponses.MissingPresence; + presence.deviceClass = deviceClass; + + PlayerData? player = await db.Players.FirstOrDefaultAsync((plr) => plr.AccountId == userId); + if (player == null) return AuthResponses.MissingPlayer; + player.AppVersion = version; + + if (cachedLogin != null) + { + cachedLogin.lastLoginTime = DateTime.UtcNow; + } + await db.SaveChangesAsync(); + + MatchmakingUtils.ClearInRoom(userId); + + List claims = new List(); + + claims.Add(new Claim("iss", DBConfig.TokenIssuer)); + claims.Add(new Claim("client_id", "recroom")); + claims.Add(new Claim("role", "gameClient")); + if (RadicadeServer.RecNet.Keepsakes.KeepsakeFeature.Enabled) + { + claims.Add(new Claim("role", "keepsake")); + claims.Add(new Claim("role", "livekeepsakeeventroomsaveoverride")); + } + claims.Add(new Claim("rn.plat", ((int)platform).ToString())); + claims.Add(new Claim("rn.platid", platformId)); + claims.Add(new Claim("rn.deviceclass", deviceClass.ToString())); + claims.Add(new Claim("rn.ver", version)); + claims.Add(new Claim("idp", "local")); + claims.Add(new Claim("userId", userId.ToString())); + claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); + + + + claims.Add(new Claim("tver", player.TokenVersion.ToString())); + if (platform == Platforms.Standalone) + claims.Add(new Claim("q.pwd", QuestRememberedSession.PasswordVersion(player.Hashword))); + + + if (profile.isJunior || PlayerUtils.IsJuniorByBirthday(player.Birthday)) + claims.Add(new Claim("role", "junior")); + if (profile.HasRole("developer")) claims.Add(new Claim("role", "developer")); + if (profile.HasRole("betastudio")) claims.Add(new Claim("role", "betastudio")); + if (profile.HasRole("moderator")) claims.Add(new Claim("role", "moderator")); + if (profile.HasRole("screenshare")) claims.Add(new Claim("role", "screenshare")); + if (profile.HasRole("influencer")) claims.Add(new Claim("role", "influencer")); + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(DBConfig.JwtKey); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims.ToArray()), + Expires = DateTime.Now.AddDays(7), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + var tokenWritten = tokenHandler.WriteToken(token); + return new Success(true, null, tokenWritten); + } + } +} diff --git a/Server/Auth/AuthShared.cs b/Server/Auth/AuthShared.cs new file mode 100644 index 0000000..9528e23 --- /dev/null +++ b/Server/Auth/AuthShared.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; + +namespace RadicadeServer.Auth; + +public class AuthShared +{ + public static IDbContextFactory DbFactory = null!; + + public static Task IsWhitelisted(string platformId) + { + + + + if (!SteamWhitelist.Enabled) return Task.FromResult(true); + return Task.FromResult(SteamWhitelist.Contains(platformId)); + } +} \ No newline at end of file diff --git a/Server/Auth/CachedLogin.cs b/Server/Auth/CachedLogin.cs new file mode 100644 index 0000000..aebe42e --- /dev/null +++ b/Server/Auth/CachedLogin.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.RecNet.Auth.Cached +{ + public class CachedLogin + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), JsonIgnore] + public long loginId { get; set; } + + public long accountId { get; set; } + + public Platforms platform { get; set; } + + public string platformId { get; set; } = null!; + + public DateTime lastLoginTime { get; set; } + + public bool requirePassword { get; set; } + } +} diff --git a/Server/Auth/CachedLoginAPI.cs b/Server/Auth/CachedLoginAPI.cs new file mode 100644 index 0000000..1545f8d --- /dev/null +++ b/Server/Auth/CachedLoginAPI.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.Auth.Cached +{ + [Route("auth/cachedlogin")] + public class CachedLoginAPI : Controller + { + RecDBContext db; + public CachedLoginAPI(RecDBContext context) + { + this.db = context; + } + + [HttpGet("forplatformid/{platform}/{platformId}")] + public async Task GetCachedLoginsForPlatformId(Platforms platform, string platformId) + { + if (platform == Platforms.Standalone) return Ok(new List()); + var cached = await db.CachedLogins.AsNoTracking() + .Where(login => login.platformId == platformId && login.platform == platform) + .Where(login => db.Players.Any(player => player.AccountId == login.accountId)) + .Where(login => db.Profiles.Any(profile => profile.accountId == login.accountId)) + .Where(login => db.Presences.Any(presence => presence.playerId == login.accountId)) + .OrderByDescending(login => login.lastLoginTime) + .ToListAsync(); + return Ok(cached); + } + + [HttpPost("forplatformids")] + public async Task> GetCachedLoginsForPlatformIds([FromForm] string[] id) + { + if (id == null || id.Length <= 0) return []; + + var requestedIds = id.Where(platformId => !string.IsNullOrWhiteSpace(platformId)) + .Distinct(StringComparer.Ordinal).ToArray(); + var cachedLogins = new List(); + foreach (var platformId in requestedIds) + { + var cached = await db.CachedLogins.AsNoTracking() + .Where(login => login.platformId == platformId && login.platform != Platforms.Standalone) + .Where(login => db.Players.Any(player => player.AccountId == login.accountId)) + .Where(login => db.Profiles.Any(profile => profile.accountId == login.accountId)) + .Where(login => db.Presences.Any(presence => presence.playerId == login.accountId)) + .OrderByDescending(login => login.lastLoginTime) + .FirstOrDefaultAsync(); + if (cached != null) cachedLogins.Add(cached); + } + + return cachedLogins; + } + } +} diff --git a/Server/Auth/DeviceApprovalAPI.cs b/Server/Auth/DeviceApprovalAPI.cs new file mode 100644 index 0000000..a6e6ef8 --- /dev/null +++ b/Server/Auth/DeviceApprovalAPI.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; + +namespace RadicadeServer.Auth +{ + [Route("/")] + public class DeviceApprovalAPI : Controller + { + + public DeviceApprovalAPI() { + } + + [HttpGet("device")] + public IActionResult Device([FromQuery(Name = "user_code")] string userCode) + { + var (found, approved, accountId, expiresUtc) = StudioAuthAPI.DescribeUserCode(userCode); + + return Content(DevicePage(userCode, found, approved, accountId, expiresUtc), "text/html"); + } + + private static string DevicePage(string userCode, bool found, bool approved, long accountId, DateTime expiresUtc) + { + var glyph = found && approved ? "✅" : found ? "⏳" : "⚠️"; + var color = found && approved ? "#16a34a" : found ? "#d97706" : "#dc2626"; + var title = found && approved ? "Approved" : found ? "Waiting for approval" : "Code not recognised"; + var body = found && approved + ? "This server signs Rec Room Studio in automatically, so this code is already approved as account " + accountId + ". Close this tab and go back to Unity." + : found + ? "The code is valid, but it has not been approved yet. Return to Unity and try again." + : "This server has no pending sign-in for that code - it may have expired, or it may already have been used (device codes are single use). Start the sign-in again from Unity."; + + var codeBlock = string.IsNullOrWhiteSpace(userCode) + ? "

No user_code was supplied. Unity sends one when it opens this page.

" + : "

Your code

" + + "

" + + System.Net.WebUtility.HtmlEncode(userCode) + "

"; + + var expiry = found && expiresUtc > DateTime.UtcNow + ? "

Code expires " + expiresUtc.ToString("u") + " UTC.

" + : ""; + + return "" + + "Radicade - Rec Room Studio sign-in" + + "" + + "
" + + "
" + glyph + "
" + + "

" + title + "

" + + codeBlock + + "

" + body + "

" + + expiry + + "

" + DBConfig.ServerUrl + " - local Radicade server

" + + "
"; + } + } +} diff --git a/Server/Auth/EACApi.cs b/Server/Auth/EACApi.cs new file mode 100644 index 0000000..6531dfd --- /dev/null +++ b/Server/Auth/EACApi.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.Auth +{ + [Route("auth/eac")] + public class EACApi : Controller + { + [HttpGet("challenge")] + public IActionResult GetFakeEACChallenge() + { + return new JsonResult("AQAAAOmBNw8TMlSZs9NsuyGxsU7WMoKVcieTW8IyoSMEGcbTIPg7DZ4JGZTCfg4b1h58FOOkM7r0LUhRVwlq8wo528o3LR9g/kHzlrVJA6H0+HaYTQzjKEe+zl5/VupqSXk8PXQ0BlNiRED3x5YMCGiM+F0fYmd0NU4K0d5hY+A="); + } + } +} diff --git a/Server/Auth/PhotonAuthAPI.cs b/Server/Auth/PhotonAuthAPI.cs new file mode 100644 index 0000000..c12349d --- /dev/null +++ b/Server/Auth/PhotonAuthAPI.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.RecNet.Auth.Photon; + +namespace RadicadeServer.Auth.Photon +{ + [ApiController, Route("auth/photon")] + public class PhotonAuthAPI : Controller + { + [AllowAnonymous] + [HttpGet("authenticate")] + [HttpPost("authenticate")] + public ActionResult AuthenticatePhoton() + { + return Ok(new PhotonAuthResult { ResultCode = 1 }); + } + } +} diff --git a/Server/Auth/PhotonAuthResult.cs b/Server/Auth/PhotonAuthResult.cs new file mode 100644 index 0000000..e017ec2 --- /dev/null +++ b/Server/Auth/PhotonAuthResult.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Auth.Photon +{ + public class PhotonAuthResult + { + public int ResultCode { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? UserId { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Message { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Nickname { get; set; } + } +} diff --git a/Server/Auth/PlatformIdentity.cs b/Server/Auth/PlatformIdentity.cs new file mode 100644 index 0000000..7c57451 --- /dev/null +++ b/Server/Auth/PlatformIdentity.cs @@ -0,0 +1,242 @@ +using System.Text.Json; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.Auth; + +public static class PlatformIdentity +{ + public static readonly string[] AllowedSteamAppIds = ["480", "471710"]; + + public static HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(10) }; + + public sealed record Proof(bool Verified, string? SteamId, string Detail) + { + public static Proof Ok(string? steamId) => new(true, steamId, "proved"); + public static Proof No(string detail) => new(false, null, detail); + } + + public static async Task ProveAsync( + Platforms platform, + string? platformId, + string? steamTicketHeader, + string? platformAuth, + CancellationToken cancellationToken = default) + { + var id = platformId?.Trim() ?? string.Empty; + if (id.Length == 0) return Proof.No("A platform id is required."); + + return platform switch + { + Platforms.Steam => await ProveSteamAsync(id, steamTicketHeader, platformAuth, cancellationToken), + Platforms.Oculus => ProveOculus(id, platformAuth), + _ => Proof.No("Account creation is only available from Steam or Oculus on this server.") + }; + } + + private static async Task ProveSteamAsync( + string platformId, + string? steamTicketHeader, + string? platformAuth, + CancellationToken cancellationToken) + { + var ticket = string.IsNullOrWhiteSpace(steamTicketHeader) + ? ReadString(platformAuth, "Ticket") + : steamTicketHeader.Trim(); + + if (string.IsNullOrWhiteSpace(ticket)) + return Proof.No("No Steam ticket was sent (x-steam-ticket header or platform_auth.Ticket)."); + + if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey)) + { + if (!DBConfig.TrustUnverifiedSteamIdentity) + return Proof.No("Steam identity cannot be proved: no SteamApiKey is configured."); + + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Identity] accepted Steam identity {platformId} " + + "UNPROVED - no SteamApiKey is configured and TrustUnverifiedSteamIdentity is on."); + return Proof.Ok(null); + } + + var attempted = new List(); + foreach (var appId in CandidateAppIds(ReadString(platformAuth, "AppId"))) + { + attempted.Add(appId); + var result = await AuthenticateTicketAsync(ticket, appId, cancellationToken); + + if (result.OtherApp) continue; + + if (!result.Ok) + return Proof.No(result.Detail); + + if (!string.Equals(result.SteamId, platformId, StringComparison.Ordinal)) + return Proof.No($"The Steam ticket is for {result.SteamId}, not for the identity it was sent with."); + + return Proof.Ok(result.SteamId); + } + + return Proof.No("The Steam ticket is not for an app this server accepts (tried " + + string.Join(", ", attempted) + ")."); + } + + private static Proof ProveOculus(string platformId, string? platformAuth) + { + var nonce = ReadString(platformAuth, "Nonce"); + if (string.IsNullOrWhiteSpace(nonce)) + return Proof.No("No Oculus nonce was sent (platform_auth.Nonce)."); + + return Proof.No("Oculus identity cannot be proved yet: no Oculus app secret is configured, " + + "so the nonce cannot be validated."); + } + + public static async Task CheckSteamStandingAsync(string steamId, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey)) return null; + if (string.IsNullOrWhiteSpace(steamId)) return null; + + var key = Uri.EscapeDataString(DBConfig.SteamApiKey); + var id = Uri.EscapeDataString(steamId); + + try + { + using var summaryDoc = await GetJsonAsync( + $"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key={key}&steamids={id}", + cancellationToken); + if (summaryDoc == null) return null; + + if (!summaryDoc.RootElement.TryGetProperty("response", out var summary) || + !summary.TryGetProperty("players", out var players) || + players.ValueKind != JsonValueKind.Array || + players.GetArrayLength() == 0) + return "Your Steam profile was not found, so an account cannot be created for it."; + + var player = players[0]; + + if (player.TryGetProperty("profilestate", out var profileState) && + profileState.ValueKind == JsonValueKind.Number && + profileState.TryGetInt32(out var state) && state != 1) + return "Your Steam profile has to be set up before you can create an account."; + + using var bansDoc = await GetJsonAsync( + $"https://api.steampowered.com/ISteamUser/GetPlayerBans/v1/?key={key}&steamids={id}", + cancellationToken); + if (bansDoc == null) return null; + + if (!bansDoc.RootElement.TryGetProperty("players", out var bans) || + bans.ValueKind != JsonValueKind.Array || bans.GetArrayLength() == 0) + return "Steam would not report this account's ban status, so an account cannot be created for it."; + + var entry = bans[0]; + if (entry.TryGetProperty("CommunityBanned", out var banned) && banned.ValueKind == JsonValueKind.True) + return "Steam accounts with an active community ban are not allowed to create an account."; + } + catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Identity] Steam standing check could not be " + + $"completed for {steamId}: {ex.GetType().Name}: {ex.Message}"); + return null; + } + + return null; + } + + public sealed record TicketResult(bool Ok, bool OtherApp, string? SteamId, string Detail); + + public static async Task AuthenticateTicketAsync(string ticket, string appId, CancellationToken cancellationToken) + { + try + { + using var doc = await GetJsonAsync( + "https://api.steampowered.com/ISteamUserAuth/AuthenticateUserTicket/v1/?key=" + + $"{Uri.EscapeDataString(DBConfig.SteamApiKey)}&appid={Uri.EscapeDataString(appId)}" + + $"&ticket={Uri.EscapeDataString(ticket)}", + cancellationToken); + + if (doc == null) return new TicketResult(false, false, null, "Steam did not answer the ticket check."); + + if (!doc.RootElement.TryGetProperty("response", out var response)) + return new TicketResult(false, false, null, "Steam did not answer the ticket check."); + + if (response.TryGetProperty("error", out var error)) + { + var code = error.TryGetProperty("errorcode", out var errorCode) && errorCode.TryGetInt32(out var value) + ? value + : 0; + var description = error.TryGetProperty("errordesc", out var desc) ? desc.GetString() : null; + + if (code == 102) + return new TicketResult(false, true, null, $"ticket for app {appId}"); + + return new TicketResult(false, false, null, + $"Steam refused the ticket ({(description ?? "no description")})."); + } + + if (!response.TryGetProperty("params", out var parameters) || + parameters.ValueKind != JsonValueKind.Object || + !parameters.TryGetProperty("result", out var result) || + result.GetString() != "OK" || + !parameters.TryGetProperty("steamid", out var steamId) || + steamId.ValueKind != JsonValueKind.String) + return new TicketResult(false, false, null, "Steam did not accept the ticket."); + + return new TicketResult(true, false, steamId.GetString(), "ok"); + } + catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException) + { + return new TicketResult(false, false, null, + $"Steam could not be reached to check the ticket ({ex.GetType().Name})."); + } + } + + private static async Task GetJsonAsync(string url, CancellationToken cancellationToken) + { + using var response = await Client.GetAsync(url, cancellationToken); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Identity] Steam answered {(int)response.StatusCode} for " + + $"{new Uri(url).AbsolutePath}."); + return null; + } + + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + return await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); + } + + private static IEnumerable CandidateAppIds(string? claimed) + { + var seen = new HashSet(StringComparer.Ordinal); + var ordered = new List(); + void Add(string? candidate) + { + var value = candidate?.Trim(); + if (string.IsNullOrEmpty(value) || !seen.Add(value)) return; + ordered.Add(value); + } + + Add(claimed); + foreach (var id in AllowedSteamAppIds) Add(id); + return ordered; + } + + private static string? ReadString(string? json, string property) + { + if (string.IsNullOrWhiteSpace(json)) return null; + + try + { + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.ValueKind != JsonValueKind.Object) return null; + if (!doc.RootElement.TryGetProperty(property, out var value)) return null; + + return value.ValueKind switch + { + JsonValueKind.String => value.GetString(), + JsonValueKind.Number => value.ToString(), + _ => null + }; + } + catch (JsonException) + { + return null; + } + } +} diff --git a/Server/Auth/PlatformIds.cs b/Server/Auth/PlatformIds.cs new file mode 100644 index 0000000..1e20fe6 --- /dev/null +++ b/Server/Auth/PlatformIds.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Auth.Cached +{ + public class PlatformIds + { + public long Platform { get; set; } + public long PlatformId { get; set; } + } +} diff --git a/Server/Auth/PlayerData.cs b/Server/Auth/PlayerData.cs new file mode 100644 index 0000000..67f089f --- /dev/null +++ b/Server/Auth/PlayerData.cs @@ -0,0 +1,90 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.RecNet.Auth +{ + public class PlayerData + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long AccountId { get; set; } + public string Email { get; set; } = ""; + + + + + + public bool EmailVerified { get; set; } + + + + + + + [JsonIgnore] + public string? EmailVerifyToken { get; set; } + + + + + + + + [JsonIgnore] + public string? PendingEmail { get; set; } + + public string Phone { get; set; } = ""; + public string AppVersion { get; set; } = ""; + public long? Parent { get; set; } + public int NameChanges { get; set; } = 3; + public JuniorState JuniorState { get; set; } + public bool IsJunior { get; set; } + [JsonIgnore] + public string Hashword { get; set; } = ""; + + + + + + + + [JsonIgnore] + public int TokenVersion { get; set; } + + + + + + + [JsonIgnore] + public string? PasswordResetToken { get; set; } + + + + + + [JsonIgnore] + public DateTime? PasswordResetExpires { get; set; } + public DateTime? Birthday { get; set; } + + + + + + public bool CodeOfConductAccepted { get; set; } + public StatusVisibility Visibility { get; set; } + public DeviceClass DeviceClass { get; set; } + public bool Developer { get; set; } + [JsonIgnore] + public string DeviceId { get; set; } = ""; + + + + + + + [JsonIgnore] + public string LastIpAddress { get; set; } = ""; + } +} diff --git a/Server/Auth/PlayerRoleTypes.cs b/Server/Auth/PlayerRoleTypes.cs new file mode 100644 index 0000000..947fed2 --- /dev/null +++ b/Server/Auth/PlayerRoleTypes.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Auth.Enums; + +public enum PlayerRoleTypes +{ + none, + influencer, + screenshare, + moderator, + developer +} \ No newline at end of file diff --git a/Server/Auth/Profile.cs b/Server/Auth/Profile.cs new file mode 100644 index 0000000..cf2cc60 --- /dev/null +++ b/Server/Auth/Profile.cs @@ -0,0 +1,69 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth.Enums; + +namespace RadicadeServer.RecNet.Auth +{ + public class Profile + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long accountId { get; set; } + + public string profileImage { get; set; } = null!; + + public string? bannerImage { get; set; } + + public bool isRecentHistoryVisible { get; set; } = true; + + public bool isJunior { get; set; } + + public int platforms { get; set; } + + public string username { get; set; } = null!; + public string displayName { get; set; } = null!; + + public string? bio { get; set; } + public int personalPronouns { get; set; } + public int identityFlags { get; set; } + + public string? displayEmoji { get; set; } + + + + + + + public bool NameLocked { get; set; } + public bool BioLocked { get; set; } + + public DateTime createdAt { get; set; } + + public string? role { get; set; } + + + + + + public bool developer => HasRole("developer"); + public bool moderator => HasRole("moderator"); + public bool screenshare => HasRole("screenshare"); + public bool influencer => HasRole("influencer"); + + public bool HasRole(string expectedRoleName) + { + if (string.IsNullOrWhiteSpace(role)) return false; + + + var normalized = expectedRoleName switch + { + "screenshare" => "sharescreen", + "influencer" => "imfluencer", + _ => expectedRoleName + }; + return role.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Any(r => r.Equals(expectedRoleName, StringComparison.OrdinalIgnoreCase) || r.Equals(normalized, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/Server/Auth/QuestRememberedSession.cs b/Server/Auth/QuestRememberedSession.cs new file mode 100644 index 0000000..3b580a4 --- /dev/null +++ b/Server/Auth/QuestRememberedSession.cs @@ -0,0 +1,51 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using System.Security.Cryptography; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using RadicadeServer.DB; + +namespace RadicadeServer.Auth; + +internal static class QuestRememberedSession +{ + internal const string Header = "X-Radicade-Quest-Session"; + + internal static async Task ValidateAsync(RecDBContext db, string token, string deviceId, long requestedAccount) + { + if (string.IsNullOrWhiteSpace(token) || token.Length > 8192) return null; + try + { + var handler = new JwtSecurityTokenHandler { MapInboundClaims = false }; + var principal = handler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(DBConfig.JwtKey)), + ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256 }, + ValidateIssuer = true, + ValidIssuer = DBConfig.TokenIssuer, + ValidateAudience = false, + ValidateLifetime = true, + RequireExpirationTime = true, + ClockSkew = TimeSpan.Zero + }, out _); + string? Claim(string name) => principal.FindFirst(name)?.Value; + if (Claim("client_id") != "recroom" || Claim("rn.plat") != "7" || + Claim("rn.platid") != "radicade-quest:" + deviceId || + !long.TryParse(Claim("userId"), out var account) || account <= 0 || + (requestedAccount > 0 && requestedAccount != account) || + !int.TryParse(Claim("tver"), out var version)) return null; + var player = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == account); + return player != null && player.TokenVersion == version && + Claim("q.pwd") == PasswordVersion(player.Hashword) ? account : null; + } + catch (Exception ex) when (ex is SecurityTokenException or ArgumentException) + { + return null; + } + } + + internal static string PasswordVersion(string? hash) => + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(hash ?? ""))); +} diff --git a/Server/Auth/RecAuth.cs b/Server/Auth/RecAuth.cs new file mode 100644 index 0000000..f7f965f --- /dev/null +++ b/Server/Auth/RecAuth.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Reflection; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Auth; + +namespace RadicadeServer.Utils.Attributes +{ + + + + + + + + + + + + + + + + + + + + + + +} diff --git a/Server/Auth/RolesAPI.cs b/Server/Auth/RolesAPI.cs new file mode 100644 index 0000000..283043d --- /dev/null +++ b/Server/Auth/RolesAPI.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; + +namespace RadicadeServer.API +{ + [Route("role")] + [Route("auth/role")] + public class RolesAPI(RecDBContext db) : Controller + { + [HttpGet("{roleType}/{id}")] + public async Task GetDeveloperRole(string roleType, long id) + { + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (player != null) + { + return player.HasRole(roleType); + } + return false; + } + } +} diff --git a/Server/Auth/SteamWhitelist.cs b/Server/Auth/SteamWhitelist.cs new file mode 100644 index 0000000..250ec31 --- /dev/null +++ b/Server/Auth/SteamWhitelist.cs @@ -0,0 +1,192 @@ +using System.Text.Json; + +namespace RadicadeServer.Auth; + + + + + + + +public static class SteamWhitelist +{ + public sealed class WhitelistFile + { + public bool Enabled { get; set; } + public List SteamIds { get; set; } = new(); + } + + private static readonly JsonSerializerOptions CaseInsensitive = new() + { + PropertyNameCaseInsensitive = true + }; + + private static readonly object Lock = new(); + private static WhitelistFile? _data; + private static string _filePath = ""; + private static bool _loaded; + private static DateTime _lastWriteUtc = DateTime.MinValue; + + + public static string FilePath => _filePath; + + + + + + public static bool CreatedEmptyConfig { get; private set; } + + + public static void Load() + { + lock (Lock) + { + if (_loaded) return; + + _filePath = Path.GetFullPath(Path.Join(DBConfig.PrivateData, "steamwhitelist.json")); + _loaded = true; + CreatedEmptyConfig = !File.Exists(_filePath); + + try + { + if (CreatedEmptyConfig) + { + + + + + _data = new WhitelistFile { Enabled = false, SteamIds = new List() }; + } + else + { + var raw = File.ReadAllText(_filePath); + _data = JsonSerializer.Deserialize(raw, CaseInsensitive) ?? new WhitelistFile(); + _data.SteamIds ??= new List(); + _lastWriteUtc = File.GetLastWriteTimeUtc(_filePath); + } + } + catch (Exception) + { + _data = new WhitelistFile { Enabled = false, SteamIds = new List() }; + } + } + } + + + public static bool Enabled + { + get { EnsureLoaded(); lock (Lock) return _data!.Enabled; } + set { EnsureLoaded(); lock (Lock) { _data!.Enabled = value; SaveLocked(); } } + } + + + public static IReadOnlyCollection SteamIds + { + get { EnsureLoaded(); lock (Lock) return _data!.SteamIds.ToList(); } + } + + + public static void SetSteamIds(IEnumerable steamIds) + { + EnsureLoaded(); + lock (Lock) + { + _data!.SteamIds = steamIds + .Where(id => !string.IsNullOrWhiteSpace(id)) + .Select(id => id.Trim()) + .Distinct() + .ToList(); + SaveLocked(); + } + } + + + public static bool Contains(string steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) return false; + EnsureLoaded(); + lock (Lock) return _data!.SteamIds.Contains(steamId.Trim()); + } + + + public static bool Add(string steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) return false; + steamId = steamId.Trim(); + + EnsureLoaded(); + lock (Lock) + { + if (_data!.SteamIds.Contains(steamId)) return false; + _data.SteamIds.Add(steamId); + SaveLocked(); + return true; + } + } + + + public static bool Remove(string steamId) + { + if (string.IsNullOrWhiteSpace(steamId)) return false; + steamId = steamId.Trim(); + + EnsureLoaded(); + lock (Lock) + { + var removed = _data!.SteamIds.Remove(steamId); + if (removed) SaveLocked(); + return removed; + } + } + + private static void EnsureLoaded() + { + if (!_loaded) { Load(); return; } + + + + + + lock (Lock) + { + DateTime lastWriteUtc; + try { lastWriteUtc = File.GetLastWriteTimeUtc(_filePath); } + catch { return; } + + if (lastWriteUtc > _lastWriteUtc) + { + try + { + var raw = File.ReadAllText(_filePath); + var reloaded = JsonSerializer.Deserialize(raw, CaseInsensitive); + if (reloaded != null) + { + reloaded.SteamIds ??= new List(); + _data = reloaded; + _lastWriteUtc = lastWriteUtc; + } + } + catch (Exception) + { + } + } + } + } + + private static void SaveLocked() + { + try + { + var dir = Path.GetDirectoryName(_filePath); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + + var options = new JsonSerializerOptions { WriteIndented = true }; + File.WriteAllText(_filePath, JsonSerializer.Serialize(_data, options)); + _lastWriteUtc = File.GetLastWriteTimeUtc(_filePath); + } + catch (Exception) + { + } + } +} \ No newline at end of file diff --git a/Server/Auth/StudioAuthAPI.cs b/Server/Auth/StudioAuthAPI.cs new file mode 100644 index 0000000..66de019 --- /dev/null +++ b/Server/Auth/StudioAuthAPI.cs @@ -0,0 +1,317 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.Utils; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Rooms; +using System.Collections.Concurrent; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace RadicadeServer.Auth +{ + [Route("auth")] + public class StudioAuthAPI : Controller + { + private const int DeviceCodeLifetimeSeconds = 900; + private const int PollIntervalSeconds = 2; + private const int TokenLifetimeSeconds = 604800; + + private static readonly ConcurrentDictionary Pending = new(StringComparer.Ordinal); + private const string UserCodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + + private sealed class PendingDeviceAuthorization + { + public string UserCode = string.Empty; + public string Scope = "studio"; + public DateTime ExpiresUtc = DateTime.UtcNow; + public long AccountId; + public bool Approved; + } + + RecDBContext db; + private readonly IConfiguration config; + + public StudioAuthAPI(RecDBContext context,IConfiguration configuration) { + this.db = context; + this.config = configuration; + } + + [HttpGet("connect/deviceauthorization")] + [HttpPost("connect/deviceauthorization")] + public async Task DeviceAuthorization() + { + var fields = await ReadRequestFields(); + + if (!IsDeviceFlowAllowed()) + { + return BadRequest(new { error = "access_denied", error_description = "Rec Room Studio sign-in is only available from the machine running the server." }); + } + + var accountId = await ResolveStudioAccountId(fields); + if (accountId <= 0) + { + return BadRequest(new { error = "access_denied", error_description = "No account is configured for Rec Room Studio on this server." }); + } + + PruneExpired(); + + var deviceCode = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant(); + var pending = new PendingDeviceAuthorization + { + UserCode = GenerateUserCode(), + Scope = fields.TryGetValue("scope", out var scope) && !string.IsNullOrWhiteSpace(scope) ? scope : "studio", + ExpiresUtc = DateTime.UtcNow.AddSeconds(DeviceCodeLifetimeSeconds), + AccountId = accountId, + Approved = true + }; + Pending[deviceCode] = pending; + + return Ok(new + { + device_code = deviceCode, + user_code = pending.UserCode, + verification_uri = $"{DBConfig.ServerUrl}/device", + verification_uri_complete = $"{DBConfig.ServerUrl}/device?user_code={pending.UserCode}", + expires_in = DeviceCodeLifetimeSeconds, + interval = PollIntervalSeconds, + scope = pending.Scope + }); + } + + [HttpGet("connect/studiotoken")] + [HttpPost("connect/studiotoken")] + public async Task StudioToken() + { + var fields = await ReadRequestFields(); + var grantType = fields.GetValueOrDefault("grant_type") ?? string.Empty; + + if (IsDeviceCodeGrant(grantType)) + return await CompleteDeviceAuthorization(fields); + + if (IsRefreshGrant(grantType)) + return await RefreshStudioToken(fields); + + if (grantType.Length == 0) + { + return BadRequest(new { error = "invalid_request", error_description = "grant_type is required." }); + } + return BadRequest(new { error = "unsupported_grant_type", error_description = $"grant_type '{grantType}' is not supported." }); + } + + private static bool IsDeviceCodeGrant(string grantType) => + grantType.Replace("-", "_").Contains("device_code", StringComparison.OrdinalIgnoreCase); + + private static bool IsRefreshGrant(string grantType) => + grantType.Replace("-", "_").Contains("refresh_token", StringComparison.OrdinalIgnoreCase); + + internal static (bool Found, bool Approved, long AccountId, DateTime ExpiresUtc) DescribeUserCode(string userCode) + { + if (string.IsNullOrWhiteSpace(userCode)) return (false, false, 0, default); + + var wanted = NormalizeUserCode(userCode); + var now = DateTime.UtcNow; + foreach (var entry in Pending) + { + if (entry.Value.ExpiresUtc <= now) continue; + if (NormalizeUserCode(entry.Value.UserCode) != wanted) continue; + return (true, entry.Value.Approved, entry.Value.AccountId, entry.Value.ExpiresUtc); + } + + return (false, false, 0, default); + } + + private static string NormalizeUserCode(string userCode) => + new string(userCode.Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant(); + + private async Task CompleteDeviceAuthorization(Dictionary fields) + { + var deviceCode = fields.GetValueOrDefault("device_code") ?? string.Empty; + if (!Pending.TryGetValue(deviceCode, out var pending)) + return BadRequest(new { error = "invalid_grant", error_description = "Unknown device code." }); + + if (pending.ExpiresUtc <= DateTime.UtcNow) + { + Pending.TryRemove(deviceCode, out _); + return BadRequest(new { error = "expired_token", error_description = "The device code has expired." }); + } + + if (!pending.Approved) + return BadRequest(new { error = "authorization_pending", error_description = "The user has not approved this sign-in yet." }); + + Pending.TryRemove(deviceCode, out _); + return await IssueStudioTokens(pending.AccountId); + } + + private async Task RefreshStudioToken(Dictionary fields) + { + var refreshToken = fields.GetValueOrDefault("refresh_token") ?? string.Empty; + var accountId = AccountIdFromRefreshToken(refreshToken); + + if (accountId > 0 && !await db.Players.AnyAsync(x => x.AccountId == accountId)) + accountId = 0; + + if (accountId <= 0) + { + return BadRequest(new { error = "invalid_grant", error_description = "Unknown refresh token." }); + } + + return await IssueStudioTokens(accountId); + } + + private async Task IssueStudioTokens(long accountId) + { + await EnsurePresence(accountId); + + var token = await AuthAPI.GenerateToken(db, accountId, DBConfig.GameVersion, DeviceClass.Screen, Platforms.Steam, $"studio:{accountId}"); + if (!token.success || token.value is not string accessToken) + { + return BadRequest(new { error = "invalid_grant", error_description = token.error ?? "Could not create a studio token." }); + } + + return Ok(new + { + access_token = accessToken, + refresh_token = RefreshTokenFor(accountId), + token_type = "Bearer", + expires_in = TokenLifetimeSeconds, + scope = "studio" + }); + } + + private static string RefreshTokenFor(long accountId) + { + var bytes = new byte[16]; + Array.Copy(BitConverter.GetBytes(accountId), 0, bytes, 0, 8); + return new Guid(bytes).ToString(); + } + + private static long AccountIdFromRefreshToken(string refreshToken) + { + if (!Guid.TryParse(refreshToken, out var guid)) return 0; + var embedded = BitConverter.ToInt64(guid.ToByteArray(), 0); + return embedded > 0 ? embedded : 0; + } + + private async Task EnsurePresence(long accountId) + { + if (await db.Presences.AnyAsync(x => x.playerId == accountId)) return; + + db.Presences.Add(new RealPresence + { + playerId = accountId, + deviceClass = DeviceClass.Screen, + statusVisibility = StatusVisibility.Public, + vRMovementMode = VRMovementModes.TELEPORT + }); + await db.SaveChangesAsync(); + } + + private async Task ResolveStudioAccountId(Dictionary fields) + { + var configured = config["Radicade:Studio:AutoApproveAccountId"]; + if (long.TryParse(configured, out var configuredId) && configuredId > 0 && await db.Players.AnyAsync(x => x.AccountId == configuredId)) + return configuredId; + + if (long.TryParse(fields.GetValueOrDefault("account_id"), out var requestedId) && requestedId > 0 && await db.Players.AnyAsync(x => x.AccountId == requestedId)) + return requestedId; + + var developer = await db.Profiles.Where(p => p.role != null && p.role.Contains("developer")) + .OrderBy(p => p.accountId) + .FirstOrDefaultAsync(); + if (developer != null) return developer.accountId; + + return 0; + } + + private bool IsDeviceFlowAllowed() + { + if (bool.TryParse(config["Radicade:Studio:AllowRemoteDeviceAuth"], out var allowRemote) && allowRemote) return true; + + var ip = Request.GetClientIp(); + if (string.IsNullOrWhiteSpace(ip)) return false; + if (IPAddress.TryParse(ip, out var parsed)) return IPAddress.IsLoopback(parsed); + return ip.Equals("localhost", StringComparison.OrdinalIgnoreCase); + } + + private static string GenerateUserCode() + { + var buffer = new char[9]; + var bytes = RandomNumberGenerator.GetBytes(8); + for (var i = 0; i < 8; i++) + { + buffer[i < 4 ? i : i + 1] = UserCodeAlphabet[bytes[i] % UserCodeAlphabet.Length]; + } + buffer[4] = '-'; + return new string(buffer); + } + + private static void PruneExpired() + { + var now = DateTime.UtcNow; + foreach (var entry in Pending) + { + if (entry.Value.ExpiresUtc <= now) Pending.TryRemove(entry.Key, out _); + } + } + + private async Task> ReadRequestFields() + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var query in Request.Query) + fields[query.Key] = query.Value.ToString(); + + if (Request.HasFormContentType) + { + foreach (var form in Request.Form) + fields[form.Key] = form.Value.ToString(); + } + else + { + Request.EnableBuffering(); + Request.Body.Position = 0; + using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true); + var raw = await reader.ReadToEndAsync(); + Request.Body.Position = 0; + + if (!string.IsNullOrWhiteSpace(raw)) + { + try + { + using var document = JsonDocument.Parse(raw); + if (document.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var property in document.RootElement.EnumerateObject()) + { + fields[property.Name] = property.Value.ValueKind == JsonValueKind.String + ? property.Value.GetString() ?? string.Empty + : property.Value.ToString(); + } + } + } + catch (JsonException) + { + fields["raw"] = raw; + } + } + } + + return fields; + } + + private static string Describe(Dictionary fields) + { + var parts = fields.Select(field => field.Key.Contains("secret", StringComparison.OrdinalIgnoreCase) + || field.Key.Contains("password", StringComparison.OrdinalIgnoreCase) + ? $"{field.Key}=" + : $"{field.Key}={field.Value}"); + return string.Join(", ", parts); + } + } +} diff --git a/Server/Auth/TokenConnectData.cs b/Server/Auth/TokenConnectData.cs new file mode 100644 index 0000000..dd1050c --- /dev/null +++ b/Server/Auth/TokenConnectData.cs @@ -0,0 +1,36 @@ +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.RecNet.Auth.Connection +{ + public class TokenConnectData + { + public string grant_type { get; set; } = null!; + public long account_id { get; set; } + public string ver { get; set; } = null!; + public DeviceClass device_class { get; set; } + public string device_id { get; set; } = null!; + public Platforms platform { get; set; } + public string platform_id { get; set; } = null!; + public string client_id { get; set; } = null!; + + public string? username { get; set; } + public string? password { get; set; } + + public DateTime? birthday { get; set; } + + public DateTime? birthdayUTC { get; set; } + + public string? email { get; set; } + + public string? avatar { get; set; } + + public string? avatarJson { get; set; } + + public string? platform_auth { get; set; } + + + + + public string? refresh_token { get; set; } + } +} diff --git a/Server/Avatars/Avatar.cs b/Server/Avatars/Avatar.cs new file mode 100644 index 0000000..fc737e5 --- /dev/null +++ b/Server/Avatars/Avatar.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Avatar +{ + public class Avatar + { + public string OutfitSelections { get; set; } = null!; + public string HairColor { get; set; } = null!; + public string SkinColor { get; set; } = null!; + public string FaceFeatures { get; set; } = null!; + } +} diff --git a/Server/Avatars/AvatarItem.cs b/Server/Avatars/AvatarItem.cs new file mode 100644 index 0000000..3b745cc --- /dev/null +++ b/Server/Avatars/AvatarItem.cs @@ -0,0 +1,18 @@ +namespace RadicadeServer.RecNet.Avatar +{ + public class AvatarItem + { + public string AvatarItemDesc { get; set; } = null!; + public int UnlockedLevel { get; set; } + public int PlatformMask { get; set; } = -1; + public string FriendlyName { get; set; } = string.Empty; + public string Tooltip { get; set; } = string.Empty; + public int Rarity { get; set; } + public int AvatarItemType { get; set; } + public int AvatarItemId { get; set; } + public bool IsBaseAvatarItem { get; set; } + public string? TagList { get; set; } + public DateTime CreatedAt { get; set; } + public string ThumbnailImage { get; set; } = string.Empty; + } +} diff --git a/Server/Avatars/AvatarItemDescription.cs b/Server/Avatars/AvatarItemDescription.cs new file mode 100644 index 0000000..80ee274 --- /dev/null +++ b/Server/Avatars/AvatarItemDescription.cs @@ -0,0 +1,69 @@ +using System.Text; + +namespace RadicadeServer.RecNet.Avatar; + +public static class AvatarItemDescription +{ + public static string Normalize(string? description) + { + if (string.IsNullOrWhiteSpace(description)) + { + return description?.Trim() ?? string.Empty; + } + + var trimmed = description.Trim(); + var segments = trimmed.Split(','); + if (segments[0].Length == 0) + { + return trimmed; + } + + var hasEmptySlot = false; + for (var index = 1; index < segments.Length - 1; index++) + { + if (segments[index].Length == 0) + { + hasEmptySlot = true; + break; + } + } + + if (!hasEmptySlot) + { + return trimmed; + } + + var builder = new StringBuilder(segments[0]).Append(','); + for (var index = 1; index < segments.Length; index++) + { + var material = segments[index].Trim(); + if (material.Length == 0) + { + continue; + } + + builder.Append(material).Append(','); + } + + return builder.ToString(); + } + + public static bool HasEmptyMaterialSlot(string? description) + { + if (string.IsNullOrWhiteSpace(description)) + { + return false; + } + + var segments = description.Trim().Split(','); + for (var index = 1; index < segments.Length - 1; index++) + { + if (segments[index].Length == 0) + { + return true; + } + } + + return false; + } +} diff --git a/Server/Avatars/AvatarItemV4.cs b/Server/Avatars/AvatarItemV4.cs new file mode 100644 index 0000000..178d01c --- /dev/null +++ b/Server/Avatars/AvatarItemV4.cs @@ -0,0 +1,16 @@ +namespace RadicadeServer.RecNet.Avatar; + +public sealed class AvatarItemV4(AvatarItem item) +{ + public int AvatarItemId { get; } = item.AvatarItemId; + public string AvatarItemDesc { get; } = item.AvatarItemDesc; + public int AvatarItemType { get; } = item.AvatarItemType; + public int PlatformMask { get; } = item.PlatformMask; + public string FriendlyName { get; } = item.FriendlyName ?? string.Empty; + public string Tooltip { get; } = item.Tooltip ?? string.Empty; + public int Rarity { get; } = item.Rarity; + public string TagList { get; } = item.TagList ?? string.Empty; + public bool IsBaseAvatarItem { get; } = item.IsBaseAvatarItem; + public string ThumbnailImage { get; } = item.ThumbnailImage ?? string.Empty; + public DateTime CreatedAt { get; } = item.CreatedAt; +} diff --git a/Server/Avatars/AvatarUnlockCatalog.cs b/Server/Avatars/AvatarUnlockCatalog.cs new file mode 100644 index 0000000..f64b2b3 --- /dev/null +++ b/Server/Avatars/AvatarUnlockCatalog.cs @@ -0,0 +1,256 @@ +using RadicadeServer.Extensions; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Avatar; + +public static class AvatarUnlockCatalog +{ + public static string LoadStarterJson() + { + using var stream = typeof(AvatarUnlockCatalog).Assembly.GetManifestResourceStream("StarterAvatarItems.json") + ?? throw new InvalidOperationException("The starter avatar inventory resource is missing."); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public static List GetStarterItems() + { + var starters = EnrichWithMetadata(ParseCatalog(LoadStarterJson()), DBConfig.DefaultAvatarCatalog); + starters.AddRange(ParseCatalog(DBConfig.DefaultAvatarCatalog).Where(item => item.IsBaseAvatarItem)); + return KeepClientAvailableItems(KeepResolvableItems(starters)) + .DistinctBy(item => item.AvatarItemDesc, StringComparer.OrdinalIgnoreCase).ToList(); + } + + private static readonly JsonSerializerOptions CatalogOptions = new() + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + + public static List ParseCatalog(string? json) + { + var items = new List(); + if (string.IsNullOrWhiteSpace(json)) return items; + try + { + using var document = JsonDocument.Parse(json, new JsonDocumentOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip + }); + if (document.RootElement.ValueKind != JsonValueKind.Array) + { + return items; + } + var index = 0; + foreach (var entry in document.RootElement.EnumerateArray()) + { + try + { + var item = entry.Deserialize(CatalogOptions); + if (item != null && !string.IsNullOrWhiteSpace(item.AvatarItemDesc)) + items.Add(item); + } + catch (JsonException) + { + } + index++; + } + } + catch (JsonException) + { + } + return KeepSupportedItems(items); + } + + public static bool NeedsMetadataEnrichment(IEnumerable? items) + { + return items != null && items.Any(item => + item == null || item.AvatarItemId <= 0 || string.IsNullOrWhiteSpace(item.ThumbnailImage)); + } + + public static List EnrichWithMetadata(IEnumerable items, string? metadataJson) + { + var metadata = ParseCatalog(metadataJson).ToDictionary(x => x.AvatarItemDesc, StringComparer.OrdinalIgnoreCase); + return items.Select(item => metadata.TryGetValue(item.AvatarItemDesc, out var full) + ? new AvatarItem + { + AvatarItemDesc = item.AvatarItemDesc, UnlockedLevel = item.UnlockedLevel, + PlatformMask = item.PlatformMask, FriendlyName = full.FriendlyName, + Tooltip = full.Tooltip, Rarity = full.Rarity, AvatarItemType = full.AvatarItemType, + AvatarItemId = full.AvatarItemId, IsBaseAvatarItem = full.IsBaseAvatarItem, + TagList = full.TagList, CreatedAt = full.CreatedAt, ThumbnailImage = full.ThumbnailImage + } + : item).ToList(); + } + private static readonly string[] KnownUnsafeDescriptionParts = + [ + "07116e9b-b182-40b4-90c2-99738e7aedd6", + "11b3413f-2c18-41cb-8ef2-55fb825ed476", + "UjSxxUK7z0GGdjvI_NsJ3Q", + "X0nzvhAtgEKqzjQPb9d3qQ", + "6taO0KRM_kuKoaTnOIsHRg", + "KUAMuM41hk-YLZoqTiKncA,LjQ1UKN2xkqJkok40Dxaew", + "5aa7be74-19a9-4dd8-b525-46c5aef34866", + ]; + + private static readonly Lazy> SafeDefaultItems = new(() => + DBConfig.SafeDefaultAvatarUnlocks2023.FromJson>() + ?? new List()); + private static readonly Lazy> SafeDefaultDescriptions = new(() => + SafeDefaultItems.Value + .Where(item => !string.IsNullOrWhiteSpace(item.AvatarItemDesc)) + .Select(item => item.AvatarItemDesc) + .ToHashSet(StringComparer.OrdinalIgnoreCase)); + + public static List GetSafeDefaultItems() + { + var catalogJson = string.Equals(DBConfig.DefaultAvatarCatalog, DBConfig.SafeDefaultAvatarUnlocks2023, + StringComparison.Ordinal) ? DBConfig.DefaultAvatarUnlocks : DBConfig.DefaultAvatarCatalog; + var items = ParseCatalog(catalogJson); + if (items.Count == 0) + items = ParseCatalog(DBConfig.DefaultAvatarUnlocks); + var supportedItems = KeepResolvableItems(items); + var candidates = supportedItems.Count > 0 + ? supportedItems + : SafeDefaultItems.Value.Select(Clone).ToList(); + var available = KeepClientAvailableItems(candidates); + return available.Count > 0 ? available : candidates; + } + + public static List KeepClientAvailableItems(IEnumerable? items) + { + if (items == null) + { + return new List(); + } + + var cutoff = DBConfig.AvatarItemReleaseCutoffUtc; + if (cutoff == default) + { + return items.Where(item => item != null).ToList(); + } + + return items.Where(item => item != null && IsAvailableToClient(item, cutoff)).ToList(); + } + + public static bool IsAvailableToClient(AvatarItem item) + { + return IsAvailableToClient(item, DBConfig.AvatarItemReleaseCutoffUtc); + } + + private static bool IsAvailableToClient(AvatarItem? item, DateTime cutoffUtc) + { + if (item == null || item.CreatedAt == default) + { + return true; + } + + var createdAt = item.CreatedAt.Kind == DateTimeKind.Unspecified + ? DateTime.SpecifyKind(item.CreatedAt, DateTimeKind.Utc) + : item.CreatedAt.ToUniversalTime(); + + return createdAt < cutoffUtc; + } + + public static List KeepSupportedItems(IEnumerable? items) + { + if (items == null) + { + return new List(); + } + + return items + .Where(item => + item != null && + !string.IsNullOrWhiteSpace(item.AvatarItemDesc) && + !IsKnownUnsafe(item.AvatarItemDesc)) + .Select(Clone) + .ToList(); + } + + public static List KeepResolvableItems(IEnumerable? items) + { + var supportedItems = KeepSupportedItems(items); + var resolvable = DBConfig.ResolvableAvatarItemDescs; + if (resolvable == null || resolvable.Count == 0) + { + return supportedItems; + } + + return supportedItems.Where(item => IsClientResolvableDescription(item.AvatarItemDesc)).ToList(); + } + + public static bool IsClientResolvableDescription(string? description) + { + if (string.IsNullOrWhiteSpace(description)) return false; + + var resolvable = DBConfig.ResolvableAvatarItemDescs; + return resolvable == null || resolvable.Count == 0 || + resolvable.Contains(AvatarItemDescription.Normalize(description)); + } + + public static AvatarItem? FindItem(string? avatarItemDesc, string? catalogueJson = null) + { + if (string.IsNullOrWhiteSpace(avatarItemDesc)) return null; + + var wanted = AvatarItemDescription.Normalize(avatarItemDesc); + if (string.IsNullOrWhiteSpace(wanted)) return null; + + var json = string.IsNullOrWhiteSpace(catalogueJson) ? DBConfig.DefaultAvatarCatalog : catalogueJson; + return CatalogueByDescription(json).TryGetValue(wanted, out var item) ? Clone(item) : null; + } + + private static string? _byDescriptionCacheKey; + private static Dictionary? _byDescriptionCache; + + private static Dictionary CatalogueByDescription(string? json) + { + if (_byDescriptionCache != null && string.Equals(_byDescriptionCacheKey, json, StringComparison.Ordinal)) + return _byDescriptionCache; + + var byDescription = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var item in ParseCatalog(json)) + { + if (item == null || string.IsNullOrWhiteSpace(item.AvatarItemDesc)) continue; + if (!byDescription.ContainsKey(item.AvatarItemDesc)) byDescription[item.AvatarItemDesc] = item; + } + + _byDescriptionCache = byDescription; + _byDescriptionCacheKey = json; + return byDescription; + } + + public static bool IsKnownSafe(string avatarItemDesc) + { + return SafeDefaultDescriptions.Value.Contains(avatarItemDesc); + } + + public static bool IsKnownUnsafe(string avatarItemDesc) + { + return KnownUnsafeDescriptionParts.Any(part => + avatarItemDesc.Contains(part, StringComparison.OrdinalIgnoreCase)); + } + + private static AvatarItem Clone(AvatarItem item) + { + return new AvatarItem + { + AvatarItemDesc = AvatarItemDescription.Normalize(item.AvatarItemDesc), + UnlockedLevel = item.UnlockedLevel, + PlatformMask = item.PlatformMask, + FriendlyName = item.FriendlyName, + Tooltip = item.Tooltip, + Rarity = item.Rarity, + AvatarItemType = item.AvatarItemType, + AvatarItemId = item.AvatarItemId, + IsBaseAvatarItem = item.IsBaseAvatarItem, + TagList = item.TagList, + CreatedAt = item.CreatedAt, + ThumbnailImage = item.ThumbnailImage + }; + } +} diff --git a/Server/Avatars/AvatarV1.cs b/Server/Avatars/AvatarV1.cs new file mode 100644 index 0000000..1e9bba0 --- /dev/null +++ b/Server/Avatars/AvatarV1.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.Utils.Attributes; + +namespace RadicadeServer.API.Avatars.V1 +{ + [Route("api/avatar/v1")] + [Route("econ/api/avatar/v1")] + public class AvatarV1 : Controller + { + RecDBContext db; + public AvatarV1(RecDBContext context) + { + this.db = context; + } + + [HttpGet("defaultunlocked")] + public List GetDefaultUnlocks() + { + return AvatarUnlockCatalog.GetStarterItems(); + } + + [HttpGet("defaultbaseavataritems")] + public IActionResult GetDefaultBaseAvatarItems() + { + try + { + return Ok(AvatarUnlockCatalog.GetSafeDefaultItems() + .Where(item => item.IsBaseAvatarItem).ToList()); + } + catch (Exception) + { + return Ok(Array.Empty()); + } + } + + } +} diff --git a/Server/Avatars/AvatarV2.cs b/Server/Avatars/AvatarV2.cs new file mode 100644 index 0000000..6c95315 --- /dev/null +++ b/Server/Avatars/AvatarV2.cs @@ -0,0 +1,546 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.Utils; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Store; + +namespace RadicadeServer.API.Avatars.V2; + +[Route("api/avatar/v2")] +[Route("econ/api/avatar/v2")] +public class AvatarV2(RecDBContext db) : Controller +{ + [HttpGet(""), Authorize] + public async Task GetLocalAvatar() + { + var userId = User.GetUserId(); + if (userId == null) return null; + + var avatar = db.Avatars.FirstOrDefault(x => x.AccountId == userId); + if (avatar != null) + { + if (NormalizeSavedAvatar(avatar)) + { + await db.SaveChangesAsync(); + } + + return avatar.DeserializeAvatar(); + } + + avatar = new RadicadeServer.RecNet.Accounts.Local.PlayerAvatar + { + AccountId = userId.Value, + Avatar = DBConfig.DefaultAvatar, + Outfits = "[]" + }; + await db.Avatars.AddAsync(avatar); + await db.SaveChangesAsync(); + + return avatar.DeserializeAvatar(); + } + + [HttpGet("{accountId:long}")] + public async Task GetAvatarForAccount(long accountId) + { + var saved = await db.Avatars.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == accountId); + var avatar = saved?.DeserializeAvatar(); + if (avatar != null) return avatar; + + return DBConfig.DefaultAvatar.FromJson() ?? new Avatar(); + } + + [HttpPost("set"), Authorize] + public async Task SetLocalAvatar([FromBody] Avatar newAvatar) + { + var userId = User.GetUserId(); + if (userId == null) return null; + + var avatar = await db.Avatars.FirstOrDefaultAsync(x => x.AccountId == userId); + if (avatar == null) + { + avatar = new RadicadeServer.RecNet.Accounts.Local.PlayerAvatar + { + AccountId = userId.Value, + Avatar = DBConfig.DefaultAvatar, + Outfits = "[]" + }; + await db.Avatars.AddAsync(avatar); + } + + avatar.Avatar = newAvatar.ToJson(); + + await db.SaveChangesAsync(); + + return newAvatar; + } [HttpGet("gifts"), Authorize] + public async Task GetGifts() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var gifts = await StorefrontShared.GetPendingGiftsAsync(db, userId.Value); + + return Ok(gifts.Select(g => g.ForClient()).ToList()); + } + + [HttpPost("gifts/claim"), Authorize] + public async Task ClaimGifts() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var requestedIds = await ReadGiftIdsFromBodyAsync(); + + var gifts = await StorefrontShared.GetPendingGiftsAsync(db, userId.Value); + var toClaim = requestedIds.Count > 0 + ? gifts.Where(g => requestedIds.Contains(g.GiftDropId)).ToList() + : gifts; + + if (toClaim.Count == 0) + { + return GiftClaimResponse(new List(), await GetPlayerLevelAsync(userId.Value)); + } + + var claimed = new List(); + var levelUpGifts = new List(); + var balanceGrants = new List(); + var strategy = db.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + try + { + foreach (var gift in toClaim) + { + var grant = await GrantGift(userId.Value, gift); + if (grant != null) balanceGrants.Add(grant); + await StorefrontShared.RemovePendingGiftAsync(db, userId.Value, gift.GiftDropId); + claimed.Add(gift); + } + levelUpGifts.AddRange(await LevelUpGiftService.DeliverLevelUpGiftsAsync(db, userId.Value, recomputeFromXp: true, notify: false)); + await tx.CommitAsync(); + await AnnounceBalancesAsync(userId.Value, balanceGrants, StorefrontBalanceAddTypes.FromGiftBox); + } + catch + { + await tx.RollbackAsync(); + throw; + } + }); + + foreach (var gift in levelUpGifts) + await StorefrontShared.SendGiftPackageNotificationAsync(userId.Value, gift); + return GiftClaimResponse(claimed, await GetPlayerLevelAsync(userId.Value)); + } + + [HttpPost("gifts/generate"), Authorize] + public async Task GenerateGift() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var fields = await ReadGiftGenerateFieldsAsync(); + if (!TryReadGiftContext(fields, "MFPELEJLJLD", "GiftContext", out var giftContext) || + !TryReadOptionalGiftContext(fields, "CIEHFJEIOJJ", "AlternateGiftContext", out var alternateContext)) + return BadRequest(new { error = "invalid_gift_context" }); + + fields.TryGetValue("Message", out var message); + if (message == null && fields.TryGetValue("DPAJDIEBNKM", out var opaqueMessage)) + message = opaqueMessage; + var xp = TryFindField(fields, out var rawXp, "Xp", "XP") && int.TryParse(rawXp, out var parsedXp) + ? parsedXp + : 0; + + var context = (GiftContext)giftContext; + if (context is GiftContext.All_Weekly_Challenge_Complete or GiftContext.Weekly_Challenge_Complete) + { + var mapId = WeeklyChallenges.CreateMap(DateTime.UtcNow).ChallengeMapId; + var state = await db.WeeklyChallenges.AsNoTracking().SingleOrDefaultAsync(x => + x.PlayerId == userId.Value && x.ChallengeMapId == mapId); + var pendingWeekly = await StorefrontShared.GetPendingGiftsAsync(db, userId.Value); + var weeklyGift = pendingWeekly.FirstOrDefault(x => x.GiftDropId == state?.RewardGiftDropId); + return weeklyGift == null + ? Conflict(new { error = "weekly_reward_not_pending" }) + : Ok(weeklyGift.ForClient()); + } + void Trace(string line) => RewardTrace.Log(userId.Value, line); + + var room = RroRewardPools.RoomForContext(context); + var paysReward = room != null && RroRewardPools.IsRewardActivity(room); + RewardTrace.Log(userId.Value, $"stage=Generate claimed={RewardTrace.Context(context)} " + + $"room={(room ?? "")} isActivityReward={paysReward} xp={xp} message=\"{message}\""); + + var gift = await GameRewardRedemption.TakeOutstandingAsync(db, userId.Value, context, message, Trace); + var source = gift == null ? null : "outstanding reward"; + + if (gift == null) + { + gift = await GameRewardRedemption.TakePendingBoxAsync(db, userId.Value, context, Trace); + if (gift != null) source = "box the selection route already handed over"; + } + + if (gift == null) + { + gift = await GameRewardRedemption.TakeRecentlyHandedOverAsync( + db, userId.Value, context, TimeSpan.FromMinutes(10), Trace); + if (gift != null) source = "box the server already handed over for this reward"; + } + + if (gift == null && paysReward) + { + gift = await GameRewardRedemption.BuildRoomRewardAsync(db, userId.Value, context, message, Trace); + if (gift != null) + source = "the activity's own reward, selected by the server"; + else + { + var sets = string.Join(", ", RroRewardPools.SetsFor(context) + .Select(x => string.IsNullOrEmpty(x.Colour) ? x.Family : $"{x.Family} {x.Colour}")); + RewardTrace.Error(userId.Value, + "Unable to resolve the reward for this context. " + + $"Activity={room} Context={RewardTrace.Context(context)} ExpectedPool={sets} " + + $"EligibleItems={RroRewardPools.Resolve(context).Count} " + + "Reason=the activity's own pool, its lower ranks and the non-exclusive pool all " + + "resolved to nothing."); + } + } + + if (gift == null) + { + RewardTrace.Log(userId.Value, $"stage=Fallback claimed={RewardTrace.Context(context)} " + + "is not an activity this server pays a reward for; generating the ordinary box."); + gift = BuildGeneratedGift(userId.Value, context, message, xp); + source = "generated box (no activity reward)"; + } + + await StorefrontShared.PersistGiftBoxAsync(db, userId.Value, gift, notify: false); + RewardTrace.Log(userId.Value, $"stage=Grant source={source} box={RewardTrace.Box(gift)}"); + return Ok(gift.ForClient()); + } + + private async Task> ReadGiftGenerateFieldsAsync() + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var field in form) fields[field.Key] = field.Value.FirstOrDefault() ?? string.Empty; + return fields; + } + + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var raw = (await reader.ReadToEndAsync()).Trim(); + Request.Body.Position = 0; + if (raw.Length == 0) return fields; + + if (raw.StartsWith('{')) + { + try + { + using var document = JsonDocument.Parse(raw); + foreach (var property in document.RootElement.EnumerateObject()) + fields[property.Name] = property.Value.ValueKind == JsonValueKind.String + ? property.Value.GetString() ?? string.Empty + : property.Value.ToString(); + } + catch (JsonException) { } + } + else + { + foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var parts = pair.Split('=', 2); + fields[Uri.UnescapeDataString(parts[0].Replace('+', ' '))] = + parts.Length > 1 ? Uri.UnescapeDataString(parts[1].Replace('+', ' ')) : string.Empty; + } + } + return fields; + } + + private static bool TryReadGiftContext(IReadOnlyDictionary fields, string opaqueName, string friendlyName, out int value) + { + value = 0; + if (!TryFindField(fields, out var raw, opaqueName, friendlyName)) return false; + if (!int.TryParse(raw, out value)) return false; + return Enum.IsDefined(typeof(GiftContext), value) && (GiftContext)value != GiftContext.None; + } + + private static bool TryReadOptionalGiftContext(IReadOnlyDictionary fields, string opaqueName, string friendlyName, out int? value) + { + value = null; + if (!TryFindField(fields, out var raw, opaqueName, friendlyName) || string.IsNullOrWhiteSpace(raw)) return true; + if (!int.TryParse(raw, out var parsed) || !Enum.IsDefined(typeof(GiftContext), parsed)) return false; + value = parsed; + return true; + } + + private static bool TryFindField(IReadOnlyDictionary fields, out string raw, params string[] names) + { + foreach (var name in names) + { + if (fields.TryGetValue(name, out var value)) + { + raw = value; + return true; + } + } + raw = string.Empty; + return false; + } + + private static Gift BuildGeneratedGift(long playerId, GiftContext context, string? message, int xp) + { + int[] tokenAmounts = [10, 25, 50, 100, 250, 500]; + return new Gift + { + GiftDropId = StorefrontShared.NewGiftDropId(), + FromPlayerId = playerId, + Tooltip = message ?? string.Empty, + CurrencyType = CurrencyType.RecCenterTokens, + Currency = tokenAmounts[Random.Shared.Next(tokenAmounts.Length)], + Xp = xp, + Context = context, + BalanceType = 0, + Rarity = Rarity.Rare + }; + } + + [HttpPost("gifts/consume/{giftDropId:long}"), Authorize] + public async Task ConsumeGift(long giftDropId) + => await ConsumePlayerGift(giftDropId, await ReadGiftIdsFromBodyAsync()); + + [HttpPost("gifts/consume"), Authorize] + public async Task ConsumeGiftBody() + => await ConsumePlayerGift(null, await ReadGiftIdsFromBodyAsync()); + + private async Task> ReadGiftIdsFromBodyAsync() + { + var ids = new List(); + try + { + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var raw = (await reader.ReadToEndAsync()).Trim(); + Request.Body.Position = 0; + if (raw.Length == 0) return ids; + + if (raw.StartsWith('{')) + { + using var doc = JsonDocument.Parse(raw); + foreach (var prop in doc.RootElement.EnumerateObject()) + { + if (!string.Equals(prop.Name, "Id", StringComparison.OrdinalIgnoreCase) && + !string.Equals(prop.Name, "giftDropId", StringComparison.OrdinalIgnoreCase)) + continue; + if (TryReadGiftId(prop.Value, out var v)) ids.Add(v); + } + foreach (var prop in doc.RootElement.EnumerateObject()) + { + if (!string.Equals(prop.Name, "giftDropIds", StringComparison.OrdinalIgnoreCase) || + prop.Value.ValueKind != JsonValueKind.Array) + continue; + foreach (var el in prop.Value.EnumerateArray()) + if (TryReadGiftId(el, out var v)) ids.Add(v); + } + } + else + { + foreach (var pair in raw.Split('&')) + { + var kv = pair.Split('=', 2); + var key = Uri.UnescapeDataString(kv[0]).Trim(); + if (!string.Equals(key, "Id", StringComparison.OrdinalIgnoreCase) && + !string.Equals(key, "giftDropId", StringComparison.OrdinalIgnoreCase)) + continue; + var val = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]).Trim() : ""; + if (long.TryParse(val, out var v)) ids.Add(v); + } + if (ids.Count == 0 && long.TryParse(raw, out var bare)) ids.Add(bare); + } + } + catch (Exception) + { + } + return ids.Distinct().ToList(); + } + + private static bool TryReadGiftId(JsonElement el, out long v) + { + if (el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out v)) return true; + if (el.ValueKind == JsonValueKind.String && long.TryParse(el.GetString(), out v)) return true; + v = 0; + return false; + } + + private async Task ConsumePlayerGift(long? giftDropId, List bodyIds) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var requestedIds = new List(); + if (giftDropId is > 0) requestedIds.Add(giftDropId.Value); + if (bodyIds is { Count: > 0 }) requestedIds.AddRange(bodyIds); + + var gifts = await StorefrontShared.GetPendingGiftsAsync(db, userId.Value); + var toConsume = requestedIds.Count > 0 + ? gifts.Where(g => requestedIds.Contains(g.GiftDropId)).ToList() + : gifts; + if (toConsume.Count == 0) + { + var delivered = requestedIds.Count > 0 + ? await HandedOverBoxIdsAsync(userId.Value, requestedIds) + : new List(); + if (delivered.Count > 0) + { + return GiftClaimResponse(new List(), await GetPlayerLevelAsync(userId.Value)); + } + return NotFound(new { error = "unable_to_consume_missing_or_used_gift", giftDropId }); + } + + var consumed = new List(); + var levelUpGifts = new List(); + var balanceGrants = new List(); + var strategy = db.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + try + { + foreach (var gift in toConsume) + { + var grant = await GrantGift(userId.Value, gift); + if (grant != null) balanceGrants.Add(grant); + await StorefrontShared.RemovePendingGiftAsync(db, userId.Value, gift.GiftDropId); + consumed.Add(gift); + } + levelUpGifts.AddRange(await LevelUpGiftService.DeliverLevelUpGiftsAsync(db, userId.Value, recomputeFromXp: true, notify: false)); + await tx.CommitAsync(); + await AnnounceBalancesAsync(userId.Value, balanceGrants, StorefrontBalanceAddTypes.FromGiftBox); + } + catch + { + await tx.RollbackAsync(); + throw; + } + }); + + foreach (var gift in levelUpGifts) + await StorefrontShared.SendGiftPackageNotificationAsync(userId.Value, gift); + + if (consumed.Count > 0) + await ProgressionAwardService.AwardXpAsync(db, userId.Value, + consumed.Count * ProgressionAwardService.XpPerGiftBoxOpened, "gift box"); + return GiftClaimResponse(consumed, await GetPlayerLevelAsync(userId.Value)); + } + + private async Task GetPlayerLevelAsync(long playerId) + { + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + return progression?.Level ?? 1; + } + + private async Task> HandedOverBoxIdsAsync(long playerId, List requestedIds) + { + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + var rewards = data?.GetProgressionJson()?.PendingGameRewards; + if (rewards == null) return new List(); + + return rewards + .Where(r => r.ConsumedGiftDropId != 0 && requestedIds.Contains(r.ConsumedGiftDropId)) + .Select(r => r.ConsumedGiftDropId) + .Distinct() + .ToList(); + } + + private IActionResult GiftClaimResponse(List claimed, int unlockedLevel) + { + var body = new Dictionary + { + ["consumed"] = claimed.Select(g => g.ForClient()).ToList(), + ["UnlockedLevel"] = unlockedLevel + }; + var first = claimed.FirstOrDefault(); + if (first != null) + { + foreach (var kv in first.ForClient().ToJson().FromJson>() ?? new Dictionary()) + body[kv.Key] = kv.Value; + } + return Ok(body); + } + + private async Task GrantGift(long playerId, Gift gift) + { + return await StorefrontShared.GrantGiftAsync(db, playerId, gift); + } + + private static async Task AnnounceBalancesAsync(long playerId, List grants, StorefrontBalanceAddTypes addType) + { + foreach (var grant in grants) + await StorefrontShared.SendBalanceUpdateAsync(playerId, grant.CurrencyType, grant.Balance, grant.Granted, addType); + } + + private static bool NormalizeSavedAvatar(RadicadeServer.RecNet.Accounts.Local.PlayerAvatar avatar) + { + if (string.IsNullOrWhiteSpace(avatar.Avatar)) + { + avatar.Avatar = DBConfig.DefaultAvatar; + return true; + } + + var original = avatar.Avatar; + avatar.Avatar = RepairKnownUnsafeOutfitSelections(avatar.Avatar); + + return avatar.Avatar != original; + } + + private static string RepairKnownUnsafeOutfitSelections(string avatarJson) + { + const string body = "d0a9262f-5504-46a7-bb10-7507503db58e"; + const string accessory = "95e4cc30-cb68-473d-a395-feadf5b51512"; + const string repairedOutfitSelection = body + "," + accessory + + ",0440f08f-ef1d-49d8-942b-523056e8bb45,,1"; + + string[] shortOutfitSelections = + [ + body + ",,,,1", + body + "," + accessory + ",,,1" + ]; + + foreach (var shortOutfitSelection in shortOutfitSelections) + { + avatarJson = avatarJson.Replace( + shortOutfitSelection, + repairedOutfitSelection, + StringComparison.OrdinalIgnoreCase); + } + + return avatarJson; + } +} + +public class ClaimGiftsRequest +{ + public List? GiftDropIds { get; set; } + public long? GiftDropId { get; set; } +} + +public class GenerateGiftRequest +{ + public long? RecipientPlayerId { get; set; } + public long? RecipientId { get; set; } + public int? PurchasableItemId { get; set; } + public int? ItemId { get; set; } + public long? GiftDropId { get; set; } + public StorefrontTypes? Storefront { get; set; } + public CurrencyType? Currency { get; set; } + public long? Price { get; set; } + public string? Message { get; set; } +} diff --git a/Server/Avatars/AvatarV3.cs b/Server/Avatars/AvatarV3.cs new file mode 100644 index 0000000..3258a6e --- /dev/null +++ b/Server/Avatars/AvatarV3.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; + +namespace RadicadeServer.API.Avatars.V3; + +[Route("api/avatar/v3")] +[Route("econ/api/avatar/v3")] +public class AvatarV3(RecDBContext db) : Controller +{ + [HttpGet("saved"), Authorize] + public async Task> GetSavedOutfits() + { + var userId = User.GetUserId(); + if (userId == null) return new List(); + + return await SavedOutfits.GetAsync(db, userId.Value); + } + + [HttpGet("saved/{accountId:long}"), Authorize] + public async Task> GetSavedOutfitsForAccount(long accountId) + { + return await SavedOutfits.GetAsync(db, accountId); + } + + [HttpPost("saved/set"), Authorize] + public async Task SetSavedOutfit([FromBody] Outfit? outfit) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var saved = await SavedOutfits.SaveAsync(db, userId.Value, outfit); + if (saved == null) return BadRequest(new { error = "invalid_slot" }); + + return Ok(saved); + } + + [HttpPost("gifts/generate"), Authorize] + public Task GenerateGift() + { + var compatibilityController = new AvatarV2(db) + { + ControllerContext = ControllerContext + }; + return compatibilityController.GenerateGift(); + } +} diff --git a/Server/Avatars/AvatarV4.cs b/Server/Avatars/AvatarV4.cs new file mode 100644 index 0000000..b1f7f26 --- /dev/null +++ b/Server/Avatars/AvatarV4.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.API.Avatars.V4; + +[Route("api/avatar/v4")] +[Route("econ/api/avatar/v4")] +public class AvatarV4(RecDBContext db) : Controller +{ + [HttpGet("items")] + public async Task>> GetLocalAvatarItems() + { + var userId = User.GetUserId(); + + List defaultItems; + + try + { + defaultItems = LockerAccess.HasFullInventory(db, userId) + ? AvatarUnlockCatalog.GetSafeDefaultItems() + : AvatarUnlockCatalog.GetStarterItems(); + } + catch (Exception) + { + defaultItems = new List(); + } + + var playerItems = new List(); + + if (userId.HasValue) + { + var playerUnlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId); + var unlockedItems = playerUnlocks?.GetAvatarItems(); + playerItems = User.FindFirst("rn.plat")?.Value == ((int)Platforms.Standalone).ToString() + ? AvatarUnlockCatalog.KeepResolvableItems(unlockedItems) + : AvatarUnlockCatalog.KeepSupportedItems(unlockedItems); + } + + var items = defaultItems + .Concat(playerItems) + .Where(x => !string.IsNullOrWhiteSpace(x.AvatarItemDesc)) + .GroupBy(x => x.AvatarItemDesc, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) + .ToList(); + + return Ok(items.Select(item => new AvatarItemV4(item)).ToList()); + } + + [HttpPost("saved/set"), Authorize] + public async Task SetSavedOutfit([FromBody] Outfit? outfit) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var saved = await SavedOutfits.SaveAsync(db, userId.Value, outfit); + if (saved == null) return BadRequest(new { error = "invalid_slot" }); + + return Ok(new { Success = true, Slot = saved.Slot }); + } +} diff --git a/Server/Avatars/Consumable.cs b/Server/Avatars/Consumable.cs new file mode 100644 index 0000000..836e4b1 --- /dev/null +++ b/Server/Avatars/Consumable.cs @@ -0,0 +1,27 @@ +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Avatar +{ + public class Consumable + { + public long Id { get; set; } + + public string ConsumableItemDesc { get; set; } = null!; + + public int Count { get; set; } + + public DateTime CreatedAt { get; set; } + + public int UnlockedLevel { get; set; } + + public int InitialCount { get; set; } + + public bool IsActive { get; set; } + + public int ActiveDurationMinutes { get; set; } + + public ConsumableCategories Category { get; set; } + + public int PlatformMask => -1; + } +} diff --git a/Server/Avatars/ConsumableCatalog.cs b/Server/Avatars/ConsumableCatalog.cs new file mode 100644 index 0000000..996ada7 --- /dev/null +++ b/Server/Avatars/ConsumableCatalog.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RadicadeServer.Extensions; + +namespace RadicadeServer.RecNet.Avatar +{ + public static class ConsumableCatalog + { + private static readonly Dictionary FriendlyToToken = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "20% Off Coupon", "50oCPkzd3EerBd7nYNdkCw" }, + { "87 Flavor Cake", "xwQWBB_fekmTqRc2LB92cg" }, + { "Assorted Donuts", "ZuvkidodzkuOfGLDnTOFyg" }, + { "Black & White Camera Film", "frOMH6WxDEG1fBqC4_83vg" }, + { "Bubble Tea", "JxAl1wVJ1UGAjoT3m3ilug" }, + { "Bubbly", "iiGTvhOCHkOTNJhb16Zbyw" }, + { "Bucket Orange Hair Dye", "xr45B5QC4EyKXThtwZto_A" }, + { "Butterfly Blue Hair Dye", "JwJeh15cjkOc9WWaGRibDQ" }, + { "Cake Pops", "Efd3_dy4ZEqWr7i6j5x0aA" }, + { "Candy Apples", "EmPvh3I6L0uK_1i8Wy_ylQ" }, + { "Cauldron Crimson Hair Dye", "3R1bzI35fkChoFFRFbponQ" }, + { "Celebration Cake", "Sk_1sm88ZU2zpWn01Lv7hw" }, + { "Cheese Pizza", "5hIAZ9wg5EyG1cILf4FS2A" }, + { "Chocolate Cake", "BbJb1_0g4EGJP_CeNrjpew" }, + { "Chocolate Frosted Donuts", "mMCGPgK3tki5S_15q2Z81A" }, + { "Chocolate Popcorn", "lWN_g4OcN0S0C4n6cSZq4g" }, + { "Chocolate Variety Box", "K9RbXo-4U0q6NbGu8VL1Sw" }, + { "Circuit Cerulean Hair Dye", "x9ntSHto50GpNWPwYCLUEg" }, + { "Dawn Camera Film", "m0bVLwWGj0GuIBSb6wCk6Q" }, + { "Dracula Film", "oG7CdvW7p0-S8sVe9w5vRw" }, + { "EDM Emerald Hair Dye", "4L3JoaqkAUa1kz98nciPXw" }, + { "Ego Potion", "Tpxqe_lycUelySRHM8B0Vw" }, + { "Flaming Popcorn", "uswFSANaZES0EJMLfOgNSw" }, + { "Frontier Forest Hair Dye", "YaVzcoefhk6zjbNDELLx8A" }, + { "Ghost Beard Film", "1c0Djlp090uBDczEobYNQw" }, + { "Gizmo Grape Hair Dye", "iEtWDLmv7ES87J4NE_TGJQ" }, + { "Glazed Donuts", "7OZ5AE3uuUyqa0P-2W1ptg" }, + { "Goblin Green Hair Dye", "jA3zzS31zEitBxtRCk7oew" }, + { "Goblin King Film", "6SyCoJCgo0Wd6qlPlnMOtg" }, + { "H2Oasis Blue Hair Dye", "P7HixsdkskmzgFMdFJ-WdA" }, + { "Hawaiian Pizza", "_jnjYGBcyEWY5Ub4OezXcA" }, + { "High Five O'Gold", "-hy0qD-iUk-v4NHxNzanmg" }, + { "High Five O'Gunpowder", "YEfbJTnsR0yT_p7e7tb_kQ" }, + { "High Five O'Lasers", "xHOwjwpXd0GDkvBz2VqieA" }, + { "High Five O'Spells", "VQSgL2pTLkWx4B3kwYG7UA" }, + { "High Five O'Whip", "lag2tZyB90W04lQ7ol4vMw" }, + { "Invasion Donuts", "-_VNAj3P10aWCCJZoVTF4Q" }, + { "Jumbotron Film", "Il4VmrnjDkqjmjzddqoIEw" }, + { "Jungle Jade Hair Dye", "5Ugg8k19n0WH8GPFCDWTCQ" }, + { "KO - Bear Claw", "ZtZnYBpKkECJlhHmkj4MiA" }, + { "KO - Bees", "g3kxdlJv5kO8PuBXveM48w" }, + { "KO - Cola", "wx--2TPTdEuGAqLCjs9Qag" }, + { "KO - Fangs", "XOZcxx-Klkyhe-MDbTqiwA" }, + { "KO - FryingPan", "uGVFydNSokCXFAmXu3aceQ" }, + { "KO - Grenade", "EAhk3ZZdXEmH5wRAXXT24Q" }, + { "KO - Ranger", "Av-wvjXvvkmNVSz7ZZnTiA" }, + { "KO - Shield", "5AJin8T2iEG7BzOPOgx2HA" }, + { "KO - Skull Wings", "U38Qe6rhEk6mFvArHfYjng" }, + { "KO - Star Power", "yvqSbK2czkS2sUCRdrGaEw" }, + { "KO - Tire Track", "J1WqFNUWo0OBi4LGKPDHWw" }, + { "KREQ Red Hair Dye", "P1kCuPlRc0-NAg6JUlEH_g" }, + { "Latte", "P15H1ONBhk-5DYYjid1ttg" }, + { "Lounge Lavender Hair Dye", "Hh_O_RejGE-_PubyM8xHaw" }, + { "Maker Mauve Hair Dye", "s2pxukIEt0uDeGVJmZayOw" }, + { "Mousebot Magenta Hair Dye", "vXL0rrAk70SFxJlaJk0uqQ" }, + { "PVPeach Hair Dye", "AQTYAh1it0GIGdgGO5sUHQ" }, + { "Paula's Periwinkle Hair Dye", "UAOH6ccEbEmehJg8XW_D8w" }, + { "Pepperoni Pizza", "mq23W-RSP0G8iGNLdrcpUw" }, + { "Pirate Gold Hair Dye", "o0hG5O_eaUO0R8vPyW-Hvg" }, + { "Popcorn", "QRx0aSTT9keMFdAJMQHdTg" }, + { "Propulsion Purple Hair Dye", "xRtKWR-D40GUW43R0EFpNg" }, + { "Ranger Roy-Al Blue Hair Dye", "8vlVslaWaUyFS-iDHhxW9g" }, + { "Rebel Rose Hair Dye", "STFKahjHJ0SQJPfoDe4S1g" }, + { "Recreational Raspberry Hair Dye", "eJh_BQ5y4UWwiMJzBKcwMQ" }, + { "Red Apples", "uMHrUPLYFk2rJOW_uop5Aw" }, + { "Red Velvet Cake", "LwaotjVEBUir0-w8126n_g" }, + { "Root Beer", "JfnVXFmilU6ysv-VbTAe3A" }, + { "Salted Pretzels", "InQ25wQMGkG_bvuD5rf2Ag" }, + { "Sepia Camera Film", "A5M-yf9tgUihq1uab3v58g" }, + { "Snowman Donuts", "jdrRJ7deWk2P9aRRrPy3zw" }, + { "Sparklers", "hRdK6NGfAkGBmYSs_xGzXg" }, + { "Standee Sandy Hair Dye", "inIuPzhhOEmz1RI8mZtSLg" }, + { "Supreme Pizza", "wUCIKdJSvEmiQHYMyx4X4w" }, + { "Sushi", "4My42w1D1Uu3-98pXB-x_Q" }, + { "VR Cola", "YpL6eN6sKEquOYIYnbhvew" }, + { "Witch Film", "mL3zCEuy2UWZxAV4V-OsMA" }, + }; + + public static string ToClientDesc(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return value ?? string.Empty; + + var parts = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return string.Join(",", parts.Select(p => + FriendlyToToken.TryGetValue(p, out var token) ? token : p)); + } + + public const int DefaultCount = 5; + + private const long GrantedIdBase = 800_000_000; + private const long GrantedIdSpan = 100_000_000; + + private static readonly DateTime GrantedAt = new(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + private static readonly System.Text.Json.JsonSerializerOptions CatalogReadOptions = new() + { + PropertyNameCaseInsensitive = true, + Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } + }; + + public static List ParseItems(string? catalogJson) + { + if (string.IsNullOrWhiteSpace(catalogJson)) return new List(); + + try + { + return catalogJson.FromJson>(CatalogReadOptions) ?? new List(); + } + catch (System.Text.Json.JsonException) + { + return new List(); + } + } + + public static List DefaultInstances(string? catalogJson) + { + var catalog = ParseItems(catalogJson); + + if (catalog.Count == 0) return new List(); + + var instances = new List(); + var taken = new HashSet(); + foreach (var item in catalog) + { + var desc = ToClientDesc(item.ConsumableItemDesc); + if (string.IsNullOrWhiteSpace(desc)) continue; + + var id = StableId(desc); + while (!taken.Add(id)) id++; + + instances.Add(new Consumable + { + Id = id, + ConsumableItemDesc = desc, + Count = DefaultCount, + InitialCount = DefaultCount, + CreatedAt = GrantedAt, + UnlockedLevel = 0, + IsActive = false, + ActiveDurationMinutes = 0 + }); + } + + return instances; + } + + private static long StableId(string token) + { + uint hash = 2166136261; + foreach (var c in token) + { + hash ^= c; + hash *= 16777619; + } + return GrantedIdBase + (hash % GrantedIdSpan); + } + } +} diff --git a/Server/Avatars/ConsumableItem.cs b/Server/Avatars/ConsumableItem.cs new file mode 100644 index 0000000..04de5d4 --- /dev/null +++ b/Server/Avatars/ConsumableItem.cs @@ -0,0 +1,19 @@ +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Avatar +{ + + + + + + public class ConsumableItem + { + + public string ConsumableItemDesc { get; set; } = ""; + + public string FriendlyName { get; set; } = ""; + public string Tooltip { get; set; } = ""; + public Rarity Rarity { get; set; } = Rarity.Common; + } +} diff --git a/Server/Avatars/ConsumablesV2.cs b/Server/Avatars/ConsumablesV2.cs new file mode 100644 index 0000000..eda94d0 --- /dev/null +++ b/Server/Avatars/ConsumablesV2.cs @@ -0,0 +1,428 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.IO; +using System.Text.Json; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.API.Storefront; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Avatars.V2 +{ + [Route("api/consumables/v2")] + [Route("econ/api/consumables/v2")] + [Route("api/consumables/v1")] + [Route("econ/api/consumables/v1")] + public class ConsumablesV2 : Controller + { + RecDBContext db; + public ConsumablesV2(RecDBContext context) + { + this.db = context; + } + + [HttpGet("getUnlocked"), Authorize] + public async Task?> GetUnlockedConsumables() + { + var userId = User.GetUserId(); + + var consumables = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId); + var owned = consumables?.GetConsumables() ?? new List(); + + var merged = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (LockerAccess.HasFullInventory(db, userId)) + { + foreach (var item in ConsumableCatalog.DefaultInstances(DBConfig.DefaultConsumableItems)) + Add(merged, item); + foreach (var item in owned) + merged.Remove(ConsumableCatalog.ToClientDesc(item.ConsumableItemDesc)); + } + + foreach (var instance in owned) + Add(merged, instance); + + return merged.Values.ToList(); + } + + /// + /// The free gift button's list: the consumables that may be handed to the named player. + /// The client asks for this before it opens the box, so a 404 leaves the button sitting on + /// "Processing" with nothing behind it and the screen never resolves. The body is the same + /// bulk consumable the unlocked list serves - only IsTransferable differs, because that is + /// the member the button filters on - and it answers even for an account that owns nothing, + /// which is what keeps the box offered to a player who has never opened one. + /// + [HttpGet("getTransferable/{playerId:long}"), Authorize] + [HttpGet("getTransferable"), Authorize] + public async Task?> GetTransferable(long playerId = 0) + { + var owner = playerId > 0 ? playerId : User.GetUserId() ?? 0; + + var merged = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var item in ConsumableCatalog.DefaultInstances(DBConfig.DefaultConsumableItems)) + Add(merged, item); + + if (owner > 0) + { + var unlocks = await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == owner); + var owned = unlocks?.GetConsumables() ?? new List(); + + foreach (var item in owned) + merged.Remove(ConsumableCatalog.ToClientDesc(item.ConsumableItemDesc)); + foreach (var item in owned) + Add(merged, item); + } + + var transferable = merged.Values.ToList(); + foreach (var entry in transferable) + entry.IsTransferable = true; + + return transferable; + } + + /// + /// Hands one of the caller's consumables to another player - the half of the free gift + /// button that getTransferable only sets up. The client posts the entry it showed and then + /// waits for this answer; with no route the request 404s and the button stays on + /// "Processing" with the gift already looking sent. One unit leaves the sender's stack (the + /// entry is dropped when that was the last of it) and reaches the recipient as a gift + /// package, the same box the friend box arrives in, so it appears on their watch without a + /// relog. The answer is the two obfuscated members the client actually reads, not a + /// PascalCase body it would treat as a failure. + /// + [HttpPost("transfer"), Authorize] + public async Task TransferConsumable() + { + var senderId = User.GetUserId(); + if (senderId == null) return Unauthorized(); + + var fields = await ReadBodyFieldsAsync(); + var toPlayerId = PickLong(fields, "toPlayerId", "playerId", "playerIdTo", "recipientId", "targetPlayerId", "accountId"); + if (toPlayerId is not > 0) + return Ok(ClientAction.Failure("No player was named to receive the item.")); + if (toPlayerId == senderId.Value) + return Ok(ClientAction.Failure("You cannot hand a consumable to yourself.")); + + if (!await db.Players.AsNoTracking().AnyAsync(player => player.AccountId == toPlayerId.Value)) + return Ok(ClientAction.Failure($"Player {toPlayerId.Value} does not exist.")); + + var sender = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == senderId.Value); + var owned = sender?.GetConsumables() ?? new List(); + var target = MatchConsumable(owned, fields); + if (target == null || target.Count <= 0) + return Ok(ClientAction.Failure("You do not own that consumable.")); + + var message = Pick(fields, "message", "tooltip", "note"); + var box = new Gift + { + FriendlyName = target.ConsumableItemDesc, + Tooltip = string.IsNullOrWhiteSpace(message) ? "A gift for you <3" : message.Trim(), + FromPlayerId = senderId.Value, + ConsumableItemDesc = ConsumableCatalog.ToClientDesc(target.ConsumableItemDesc), + Context = GiftContext.Consumable + }; + + if (target.InitialCount > 1) + { + target.InitialCount -= 1; + target.Count = target.InitialCount; + } + else + { + owned.Remove(target); + } + + sender!.SetConsumables(owned); + await StorefrontShared.PersistGiftBoxAsync(db, toPlayerId.Value, box, notify: true); + + await Notify.SendAccountUpdate(senderId.Value); + + return Ok(ClientAction.Ok()); + } + + /// + /// Marks which of the player's consumables is the active one. The client asks for this as it + /// enters a room, once per entry it wants switched on, and a 404 leaves the effect that the + /// consumable bought (a hair dye, a rank-up potion) unapplied. Only the named entry is turned + /// on, and turning one on is what turns the others off, so a second activation never leaves + /// two effects claiming the same slot. The answer has no body, which is what the route's own + /// wrapper returns. + /// + [HttpPost("updateActive"), Authorize] + public async Task UpdateActive() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var fields = await ReadBodyFieldsAsync(); + var activates = PickBool(fields, "isActive", "active", "activate", "enabled") ?? true; + + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId.Value); + if (unlocks == null) return Ok(); + + var consumables = unlocks.GetConsumables(); + var target = MatchConsumable(consumables, fields); + if (target == null) return Ok(); + + if (activates) + { + var targetDesc = ConsumableCatalog.ToClientDesc(target.ConsumableItemDesc); + foreach (var other in consumables) + { + if (!string.Equals(ConsumableCatalog.ToClientDesc(other.ConsumableItemDesc), targetDesc, + StringComparison.OrdinalIgnoreCase)) + other.IsActive = false; + } + } + + target.IsActive = activates; + unlocks.SetConsumables(consumables); + await db.SaveChangesAsync(); + + return Ok(); + } + + /// + /// The entry the request names, by the id the unlocked list handed out and - when the client + /// only echoed the description back - by its client token, alone or as one member of a list. + /// + private static Consumable? MatchConsumable(List owned, Dictionary fields) + { + var id = PickLong(fields, "id", "consumableItemId", "consumableId", "itemId", "consumableMappingId"); + if (id is > 0) + { + var byId = owned.FirstOrDefault(c => c.Id == id.Value); + if (byId != null) return byId; + } + + var desc = Pick(fields, "consumableItemDesc", "desc", "descriptions", "consumableItem", "itemDesc"); + if (string.IsNullOrWhiteSpace(desc)) return null; + + var wanted = ConsumableCatalog.ToClientDesc(desc); + var candidates = wanted + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(part => !string.IsNullOrWhiteSpace(part)) + .ToList(); + if (candidates.Count == 0) candidates.Add(wanted); + + foreach (var candidate in candidates) + { + var byDesc = owned.FirstOrDefault(c => + string.Equals(ConsumableCatalog.ToClientDesc(c.ConsumableItemDesc), candidate, StringComparison.OrdinalIgnoreCase) + || string.Equals(c.ConsumableItemDesc, candidate, StringComparison.OrdinalIgnoreCase)); + if (byDesc != null) return byDesc; + } + + return null; + } + + /// + /// The request's members as a flat map. The body is a serialized request DTO whose members + /// are not published, and the entry the client names sits one level down in a nested + /// consumable, so every leaf is folded in by name instead of binding to a guessed shape; a + /// form-encoded body is read the same way. + /// + private async Task> ReadBodyFieldsAsync() + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + try + { + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var raw = (await reader.ReadToEndAsync()).Trim(); + if (Request.Body.CanSeek) Request.Body.Position = 0; + if (raw.Length == 0) return fields; + + if (raw.StartsWith('{') || raw.StartsWith('[')) + { + using var document = JsonDocument.Parse(raw); + Flatten(document.RootElement, fields); + return fields; + } + + foreach (var pair in raw.Split('&')) + { + var kv = pair.Split(new[] { '=' }, 2); + if (kv.Length < 2) continue; + fields[Uri.UnescapeDataString(kv[0])] = Uri.UnescapeDataString(kv[1]); + } + } + catch (Exception) + { + } + + return fields; + } + + private static void Flatten(JsonElement element, Dictionary into) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + if (property.Value.ValueKind is JsonValueKind.Object or JsonValueKind.Array) + Flatten(property.Value, into); + else + into[property.Name] = property.Value.ValueKind == JsonValueKind.String + ? property.Value.GetString() ?? string.Empty + : property.Value.ToString(); + } + break; + + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) Flatten(item, into); + break; + } + } + + private static string Pick(Dictionary fields, params string[] names) + { + foreach (var name in names) + if (fields.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)) + return value.Trim(); + return string.Empty; + } + + private static long? PickLong(Dictionary fields, params string[] names) + { + var raw = Pick(fields, names); + return long.TryParse(raw, out var parsed) ? parsed : null; + } + + private static bool? PickBool(Dictionary fields, params string[] names) + { + var raw = Pick(fields, names); + if (raw.Length == 0) return null; + return bool.TryParse(raw, out var parsed) ? parsed : null; + } + + public class ConsumeRequest + { + public long Id { get; set; } + public int DeltaCount { get; set; } = 1; + } + + [HttpPost("consume"), Authorize] + public async Task ConsumeConsumable() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var dto = await ReadConsumeRequestAsync(); + if (dto == null || dto.Id <= 0) return Ok(); + if (dto.DeltaCount <= 0) dto.DeltaCount = 1; + + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId.Value); + if (unlocks == null) + { + unlocks = new PlayerUnlocks { AccountId = userId.Value }; + await db.Unlocks.AddAsync(unlocks); + } + + var consumables = unlocks.GetConsumables(); + var target = consumables.FirstOrDefault(c => c.Id == dto.Id); + + if (target == null) + { + return Ok(); + } + + if (target.InitialCount > dto.DeltaCount) + { + target.InitialCount -= dto.DeltaCount; + target.Count = target.InitialCount; + } + else + { + consumables.Remove(target); + } + + unlocks.SetConsumables(consumables); + await db.SaveChangesAsync(); + return Ok(); + } + + private async Task ReadConsumeRequestAsync() + { + try + { + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var raw = (await reader.ReadToEndAsync()).Trim(); + Request.Body.Position = 0; + if (raw.Length == 0) return null; + + var dto = new ConsumeRequest(); + + if (raw.StartsWith('{')) + { + using var doc = JsonDocument.Parse(raw); + if (doc.RootElement.ValueKind != JsonValueKind.Object) return null; + foreach (var prop in doc.RootElement.EnumerateObject()) + { + if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) && long.TryParse(prop.Value.GetRawText(), out var id)) + dto.Id = id; + if (prop.Name.Equals("DeltaCount", StringComparison.OrdinalIgnoreCase) && int.TryParse(prop.Value.GetRawText(), out var delta)) + dto.DeltaCount = delta; + } + return dto; + } + + foreach (var pair in raw.Split('&')) + { + var kv = pair.Split(new[] { '=' }, 2); + if (kv.Length < 2) continue; + var key = Uri.UnescapeDataString(kv[0]); + var value = Uri.UnescapeDataString(kv[1]); + if (key.Equals("Id", StringComparison.OrdinalIgnoreCase) && long.TryParse(value, out var id)) + dto.Id = id; + if (key.Equals("DeltaCount", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var delta)) + dto.DeltaCount = delta; + } + return dto; + } + catch (Exception) + { + return null; + } + } + + private static void Add(Dictionary merged, Consumable instance) + { + var desc = ConsumableCatalog.ToClientDesc(instance.ConsumableItemDesc); + if (string.IsNullOrWhiteSpace(desc)) return; + + if (!merged.TryGetValue(desc, out var entry)) + { + entry = new BulkConsumableResponse { ConsumableItemDesc = desc }; + merged[desc] = entry; + } + + entry.Ids.Add(instance.Id); + entry.CreatedAts.Add(instance.CreatedAt); + entry.Count += instance.Count; + entry.InitialCount += instance.InitialCount; + entry.IsActive |= instance.IsActive; + entry.ActiveDurationMinutes = Math.Max( + entry.ActiveDurationMinutes ?? 0, instance.ActiveDurationMinutes); + } + + public class BulkConsumableResponse + { + public List Ids { get; set; } = new List(); + public List CreatedAts { get; set; } = new List(); + public string ConsumableItemDesc { get; set; } = ""; + public int Count { get; set; } + public int InitialCount { get; set; } + public bool IsActive { get; set; } + public int? ActiveDurationMinutes { get; set; } + public bool IsTransferable { get; set; } + } + } +} diff --git a/Server/Avatars/Equipment.cs b/Server/Avatars/Equipment.cs new file mode 100644 index 0000000..142ff56 --- /dev/null +++ b/Server/Avatars/Equipment.cs @@ -0,0 +1,16 @@ +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Avatar +{ + public class Equipment + { + public string PrefabName { get; set; } = null!; + public string ModificationGuid { get; set; } = null!; + public int UnlockedLevel { get; set; } + public bool Favorited { get; set; } + public int PlatformMask => -1; + public string FriendlyName { get; set; } = null!; + public string Tooltip { get; set; } = null!; + public Rarity Rarity { get; set; } + } +} diff --git a/Server/Avatars/EquipmentItem.cs b/Server/Avatars/EquipmentItem.cs new file mode 100644 index 0000000..cb5d983 --- /dev/null +++ b/Server/Avatars/EquipmentItem.cs @@ -0,0 +1,19 @@ +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Avatar +{ + + + + + + public class EquipmentItem + { + + public string EquipmentPrefabName { get; set; } = ""; + + public string FriendlyName { get; set; } = ""; + public string Tooltip { get; set; } = ""; + public Rarity Rarity { get; set; } = Rarity.Common; + } +} diff --git a/Server/Avatars/EquipmentSkinCatalog.cs b/Server/Avatars/EquipmentSkinCatalog.cs new file mode 100644 index 0000000..3245c54 --- /dev/null +++ b/Server/Avatars/EquipmentSkinCatalog.cs @@ -0,0 +1,123 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Avatar +{ + public class EquipmentSkinItem + { + [JsonPropertyName("prefab_name")] + public string PrefabName { get; set; } = ""; + + [JsonPropertyName("modification_guid")] + public string ModificationGuid { get; set; } = ""; + + [JsonPropertyName("display_name")] + public string DisplayName { get; set; } = ""; + + [JsonPropertyName("tooltip")] + public string Tooltip { get; set; } = ""; + + [JsonPropertyName("rarity")] + public Rarity Rarity { get; set; } = Rarity.Common; + + [JsonPropertyName("storefront")] + public string Storefront { get; set; } = ""; + + [JsonPropertyName("price")] + public long Price { get; set; } + + [JsonPropertyName("image_name")] + public string ImageName { get; set; } = ""; + } + + public class EquipmentSkinFile + { + [JsonPropertyName("items")] + public List Items { get; set; } = new(); + } + + public static class EquipmentSkinCatalog + { + private static readonly JsonSerializerOptions ReadOptions = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + public static List Parse(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return new List(); + + try + { + var file = JsonSerializer.Deserialize(json, ReadOptions); + return (file?.Items ?? new List()) + .Where(item => item != null + && !string.IsNullOrWhiteSpace(item.PrefabName) + && !string.IsNullOrWhiteSpace(item.ModificationGuid)) + .ToList(); + } + catch (JsonException) + { + return new List(); + } + } + + public static List ToEquipment(IEnumerable skins) + { + return skins.Select(skin => new Equipment + { + PrefabName = skin.PrefabName.Trim(), + ModificationGuid = skin.ModificationGuid.Trim(), + UnlockedLevel = 0, + Favorited = false, + FriendlyName = string.IsNullOrWhiteSpace(skin.DisplayName) + ? skin.PrefabName.Trim() + : skin.DisplayName.Trim(), + Tooltip = skin.Tooltip ?? "", + Rarity = skin.Rarity + }).ToList(); + } + + public static string MergeInto(string? equipmentJson, string? skinsJson) + { + var skins = ToEquipment(Parse(skinsJson)); + if (skins.Count == 0) return equipmentJson ?? "[]"; + + var equipment = ParseEquipment(equipmentJson); + if (equipment == null) return equipmentJson ?? "[]"; + + var known = new HashSet( + equipment.Select(Key), StringComparer.OrdinalIgnoreCase); + + var added = 0; + foreach (var skin in skins) + { + if (!known.Add(Key(skin))) continue; + equipment.Add(skin); + added++; + } + + return added == 0 ? equipmentJson ?? "[]" : equipment.ToJson(); + } + + private static List? ParseEquipment(string? equipmentJson) + { + if (string.IsNullOrWhiteSpace(equipmentJson)) return new List(); + try + { + return equipmentJson.FromJson>(ReadOptions) ?? new List(); + } + catch (JsonException) + { + return null; + } + } + + private static string Key(Equipment item) => + $"{(item.PrefabName ?? "").Trim()}\u001f{(item.ModificationGuid ?? "").Trim()}"; + } +} diff --git a/Server/Avatars/EquipmentV1.cs b/Server/Avatars/EquipmentV1.cs new file mode 100644 index 0000000..dbebc76 --- /dev/null +++ b/Server/Avatars/EquipmentV1.cs @@ -0,0 +1,168 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; + +namespace RadicadeServer.API.Avatars.V1; + +[Route("api/equipment/v1")] +[Route("econ/api/equipment/v1")] +[Authorize] +public class EquipmentV1(RecDBContext db) : Controller +{ + [HttpPost("update")] + [HttpPut("update")] + public async Task UpdateEquipment([FromBody] JsonElement body) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + if (!TryParse(body, out var entries)) + return BadRequest(); + + var slots = entries.Where(entry => entry.HasSlot).ToList(); + var favourites = entries.Where(entry => entry.Favorited.HasValue).ToList(); + if (slots.Count == 0 && favourites.Count == 0) + return Ok(); + + if (slots.Count > 0) + { + var avatar = await db.Avatars.FirstOrDefaultAsync(x => x.AccountId == userId); + if (avatar != null) + { + avatar.EquipmentSlots = slots.Select(entry => (object)new + { + slotIndex = entry.SlotIndex, + equipmentPrefabName = entry.PrefabName ?? string.Empty, + equipmentModificationGuid = entry.ModificationGuid ?? string.Empty + }).ToList().ToJson(); + } + } + + if (favourites.Count > 0) + await ApplyFavouritesAsync(userId.Value, favourites); + + await db.SaveChangesAsync(); + + return Ok(); + } + + private async Task ApplyFavouritesAsync(long userId, List favourites) + { + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId); + if (unlocks == null) + { + unlocks = new PlayerUnlocks { AccountId = userId }; + db.Unlocks.Add(unlocks); + } + + List equipment; + try + { + equipment = unlocks.GetEquipment() ?? new List(); + } + catch (JsonException) + { + equipment = new List(); + } + + foreach (var update in favourites) + { + if (string.IsNullOrWhiteSpace(update.ModificationGuid)) continue; + + var row = equipment.FirstOrDefault(item => string.Equals( + item.ModificationGuid, update.ModificationGuid, StringComparison.OrdinalIgnoreCase)); + + if (row == null) + { + continue; + } + + row.Favorited = update.Favorited == true; + } + + unlocks.Equipment = equipment.ToJson(); + } + + private sealed class EquipmentUpdate + { + public string? PrefabName { get; set; } + public string? ModificationGuid { get; set; } + public bool? Favorited { get; set; } + public int SlotIndex { get; set; } + public bool HasSlot { get; set; } + } + + private static bool TryParse(JsonElement body, out List entries) + { + entries = new List(); + + JsonElement array; + if (body.ValueKind == JsonValueKind.Array) + { + array = body; + } + else if (body.ValueKind == JsonValueKind.Object) + { + var found = false; + array = default; + foreach (var prop in body.EnumerateObject()) + { + if (string.Equals(prop.Name, "equipmentSlots", StringComparison.OrdinalIgnoreCase) + && prop.Value.ValueKind == JsonValueKind.Array) + { + array = prop.Value; + found = true; + break; + } + } + if (!found) return false; + } + else + { + return false; + } + + foreach (var item in array.EnumerateArray()) + { + if (item.ValueKind != JsonValueKind.Object) continue; + + var entry = new EquipmentUpdate(); + foreach (var prop in item.EnumerateObject()) + { + if (string.Equals(prop.Name, "slotIndex", StringComparison.OrdinalIgnoreCase) + || string.Equals(prop.Name, "slot", StringComparison.OrdinalIgnoreCase) + || string.Equals(prop.Name, "index", StringComparison.OrdinalIgnoreCase)) + { + if (prop.Value.ValueKind == JsonValueKind.Number) entry.SlotIndex = prop.Value.GetInt32(); + entry.HasSlot = true; + } + else if (string.Equals(prop.Name, "favorited", StringComparison.OrdinalIgnoreCase) + || string.Equals(prop.Name, "favourite", StringComparison.OrdinalIgnoreCase)) + { + if (prop.Value.ValueKind == JsonValueKind.True) entry.Favorited = true; + else if (prop.Value.ValueKind == JsonValueKind.False) entry.Favorited = false; + } + else if (string.Equals(prop.Name, "modificationGuid", StringComparison.OrdinalIgnoreCase) + || string.Equals(prop.Name, "equipmentModificationGuid", StringComparison.OrdinalIgnoreCase)) + { + if (prop.Value.ValueKind == JsonValueKind.String) entry.ModificationGuid = prop.Value.GetString(); + } + else if (string.Equals(prop.Name, "prefabName", StringComparison.OrdinalIgnoreCase) + || string.Equals(prop.Name, "equipmentPrefabName", StringComparison.OrdinalIgnoreCase) + || string.Equals(prop.Name, "equipment", StringComparison.OrdinalIgnoreCase)) + { + if (prop.Value.ValueKind == JsonValueKind.String) entry.PrefabName = prop.Value.GetString(); + } + } + + entries.Add(entry); + } + + return true; + } +} diff --git a/Server/Avatars/EquipmentV2.cs b/Server/Avatars/EquipmentV2.cs new file mode 100644 index 0000000..5c5b6de --- /dev/null +++ b/Server/Avatars/EquipmentV2.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; + +namespace RadicadeServer.API.Avatars.V2 +{ + [Route("api/equipment/v2")] + [Route("econ/api/equipment/v2")] + public class EquipmentV2 : Controller + { + RecDBContext db; + public EquipmentV2(RecDBContext context) + { + this.db = context; + } + + [HttpGet("getUnlocked"), Authorize] + public List GetUnlockedEquipment() + { + var userId = User.GetUserId(); + + var items = db.Unlocks.FirstOrDefault(x => x.AccountId == userId); + var defaultEquipment = TryReadEquipment(LockerAccess.HasFullInventory(db, userId) + ? EquipmentSkinCatalog.MergeInto(DBConfig.DefaultEquipmentItems, DBConfig.DefaultEquipmentSkinItems) + : DBConfig.DefaultEquipmentUnlocks); + var playerEquipment = GetPlayerEquipment(items); + + return MergeEquipment(defaultEquipment, playerEquipment); + } + + private static List TryReadEquipment(string json) + { + try + { + return (json.FromJson>() ?? new List()) + .Where(x => !string.IsNullOrWhiteSpace(x.PrefabName)) + .Select(NormalizeEquipment) + .ToList(); + } + catch + { + return new List(); + } + } + + private static List GetPlayerEquipment(PlayerUnlocks? items) + { + try + { + return items?.GetEquipment() ?? new List(); + } + catch + { + return new List(); + } + } + + private static List MergeEquipment(IEnumerable defaults, IEnumerable playerEquipment) + { + var merged = new Dictionary(); + + foreach (var item in defaults.Concat(playerEquipment).Select(NormalizeEquipment)) + { + if (string.IsNullOrWhiteSpace(item.PrefabName)) continue; + merged[$"{item.PrefabName}\u001f{item.ModificationGuid}"] = item; + } + + return merged.Values.ToList(); + } + + private static Equipment NormalizeEquipment(Equipment item) + { + item.PrefabName ??= ""; + item.ModificationGuid ??= ""; + item.FriendlyName = string.IsNullOrWhiteSpace(item.FriendlyName) ? item.PrefabName : item.FriendlyName; + item.Tooltip ??= ""; + return item; + } + } +} diff --git a/Server/Avatars/Gift.cs b/Server/Avatars/Gift.cs new file mode 100644 index 0000000..64fc68f --- /dev/null +++ b/Server/Avatars/Gift.cs @@ -0,0 +1,93 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Radicade.Gifts.Enums; + +namespace RadicadeServer.RecNet.Avatar +{ + public class Gift + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long GiftDropId { get; set; } + + public long ItemSetId { get; set; } = 0; + public string ItemSetFriendlyName { get; set; } = string.Empty; + + public string FriendlyName { get; set; } = string.Empty; + public string Tooltip { get; set; } = string.Empty; + + public bool IsQuery { get; set; } = false; + public bool Unique { get; set; } = false; + public bool SubscribersOnly { get; set; } = false; + + public string ConsumableItemDesc { get; set; } = string.Empty; + + public string AvatarItemDesc { get; set; } = string.Empty; + public AvatarItemType AvatarItemType { get; set; } + + public string EquipmentPrefabName { get; set; } = string.Empty; + public string EquipmentModificationGuid { get; set; } = string.Empty; + + public CurrencyType CurrencyType { get; set; } = CurrencyType.RecCenterTokens; + public int Currency { get; set; } = 0; + public int Xp { get; set; } = 0; + + public int Platform { get; set; } = 0; + public int PlatformsToSpawnOn { get; set; } = 0; + + public long FromPlayerId { get; set; } = 0; + + public int? BalanceType { get; set; } = null; + + public RadicadeServer.RecNet.Enums.Store.GiftContext Context { get; set; } = RadicadeServer.RecNet.Enums.Store.GiftContext.Commerce_Purchase; + public Rarity Rarity { get; set; } = Rarity.Common; + + [JsonIgnore] public bool IsFeatured { get; set; } + [JsonIgnore] public ItemAccessibility Accessibility { get; set; } = ItemAccessibility.Purchasable; + + + [System.Text.Json.Serialization.JsonPropertyName("Id")] + [Newtonsoft.Json.JsonProperty("Id")] + [System.ComponentModel.DataAnnotations.Schema.NotMapped] + public long Id { get => GiftDropId; set => GiftDropId = value; } + + [System.Text.Json.Serialization.JsonPropertyName("Message")] + [Newtonsoft.Json.JsonProperty("Message")] + [System.ComponentModel.DataAnnotations.Schema.NotMapped] + public string Message { get => Tooltip; set => Tooltip = value ?? string.Empty; } + + [System.Text.Json.Serialization.JsonPropertyName("GiftRarity")] + [Newtonsoft.Json.JsonProperty("GiftRarity")] + [System.ComponentModel.DataAnnotations.Schema.NotMapped] + public int GiftRarity { get => (int)Rarity; set => Rarity = (Rarity)value; } + + [System.Text.Json.Serialization.JsonPropertyName("GiftContext")] + [Newtonsoft.Json.JsonProperty("GiftContext")] + [System.ComponentModel.DataAnnotations.Schema.NotMapped] + public int GiftContext + { + get => (int)Context; + set => Context = (RadicadeServer.RecNet.Enums.Store.GiftContext)value; + } + + [System.Text.Json.Serialization.JsonPropertyName("GiftPackageId")] + [Newtonsoft.Json.JsonProperty("GiftPackageId")] + [System.ComponentModel.DataAnnotations.Schema.NotMapped] + public long GiftPackageId { get => GiftDropId; set => GiftDropId = value; } + + public Gift Clone() => (Gift)MemberwiseClone(); + + public Gift ForClient() + + { + var copy = (Gift)MemberwiseClone(); + copy.ConsumableItemDesc = ConsumableCatalog.ToClientDesc(ConsumableItemDesc); + + copy.Platform = -1; + copy.PlatformsToSpawnOn = -1; + return copy; + } + } +} diff --git a/Server/Avatars/LockerAccess.cs b/Server/Avatars/LockerAccess.cs new file mode 100644 index 0000000..d84f042 --- /dev/null +++ b/Server/Avatars/LockerAccess.cs @@ -0,0 +1,10 @@ +using RadicadeServer.DB; + +namespace RadicadeServer.RecNet.Avatar; + +public static class LockerAccess +{ + public static bool HasFullInventory(RecDBContext db, long? accountId) => + accountId.HasValue && db.Profiles.FirstOrDefault(p => p.accountId == accountId.Value) + ?.HasRole("unlockedlocker") == true; +} diff --git a/Server/Avatars/Outfit.cs b/Server/Avatars/Outfit.cs new file mode 100644 index 0000000..983a79f --- /dev/null +++ b/Server/Avatars/Outfit.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Avatar +{ + public class Outfit : Avatar + { + public int? Slot { get; set; } + public string PreviewImageName { get; set; } = null!; + public string OutfitSelectionsV2 { get; set; } = null!; + public object[] CustomAvatarItems { get; set; } = null!; + } +} diff --git a/Server/Avatars/OutfitsAPI.cs b/Server/Avatars/OutfitsAPI.cs new file mode 100644 index 0000000..aa37720 --- /dev/null +++ b/Server/Avatars/OutfitsAPI.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.DB; +using RadicadeServer.Extensions; + +namespace RadicadeServer.API.Avatars; + +[Route("outfits")] +[Authorize] +public class OutfitsAPI(RecDBContext db) : Controller +{ + [HttpGet("me")] + public async Task GetCurrentOutfit() + { + if (User.GetUserId() == null) return Unauthorized(); + + var avatarApi = new AvatarV2(db) { ControllerContext = ControllerContext }; + var avatar = await avatarApi.GetLocalAvatar(); + if (avatar == null) return NotFound(); + + return Ok(new CurrentOutfitResponse + { + LegacyData = new LegacyOutfitData + { + SelectionsV1 = avatar.OutfitSelections, + HairColor = avatar.HairColor, + SkinColor = avatar.SkinColor, + FaceFeatures = avatar.FaceFeatures + } + }); + } +} + +public sealed class CurrentOutfitResponse +{ + public long DataVersion { get; init; } + public LegacyOutfitData LegacyData { get; init; } = new(); + public string? CustomizationSettings { get; init; } + public object[] Selections { get; init; } = []; + public int Slot { get; init; } + public string? Name { get; init; } + public int Accessibility { get; init; } + public string? ThumbnailFileName { get; init; } +} + +public sealed class LegacyOutfitData +{ + public string? SelectionsV1 { get; init; } + public string? SelectionsV2 { get; init; } + public string? FaceFeatures { get; init; } + public string? SkinColor { get; init; } + public string? HairColor { get; init; } +} diff --git a/Server/Avatars/PurchasableItem.cs b/Server/Avatars/PurchasableItem.cs new file mode 100644 index 0000000..d9635df --- /dev/null +++ b/Server/Avatars/PurchasableItem.cs @@ -0,0 +1,61 @@ +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Avatar +{ + public class PurchasableItem + { + public enum PurchasableItemType + { + GiftDrop = 0, + SeasonTier = 1, + SeasonEliteUpgrade = 2 + } + + public int PurchasableItemId { get; set; } + + public PurchasableItemType Type { get; set; } = PurchasableItemType.GiftDrop; + + public List Prices { get; set; } = new(); + + public List SubscriberPrices { get; set; } = new(); + + public bool IsFeatured { get; set; } + public DateTime? EndDate { get; set; } + + public DateTime? NewUntil { get; set; } + } + + public class PurchasablePrice + { + public CurrencyType CurrencyType { get; set; } + + public int Price { get; set; } + + public int? PurchaseCurrencyId { get; set; } + + public DateTime? StartDate { get; set; } + + public DateTime? EndDate { get; set; } + } + + public class PurchasableGiftDrop : PurchasableItem + { + public List GiftDrops { get; set; } = new(); + + [JsonPropertyName("GiftDrop")] + public Gift? GiftDrop + { + get => GiftDrops.FirstOrDefault(); + set + { + GiftDrops = value == null ? new List() : new List { value }; + } + } + + public PurchasableGiftDrop() {} + public PurchasableGiftDrop(List giftDrops) { + GiftDrops = giftDrops; + } + } +} diff --git a/Server/Avatars/SavedOutfits.cs b/Server/Avatars/SavedOutfits.cs new file mode 100644 index 0000000..e9dd865 --- /dev/null +++ b/Server/Avatars/SavedOutfits.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts.Local; + +namespace RadicadeServer.RecNet.Avatar; + +public static class SavedOutfits +{ + public static async Task> GetAsync(RecDBContext db, long accountId) + { + var avatar = await db.Avatars.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == accountId); + return Parse(avatar?.Outfits); + } + + public static async Task SaveAsync(RecDBContext db, long accountId, Outfit? outfit) + { + if (outfit?.Slot == null) + return null; + + var avatar = await db.Avatars.FirstOrDefaultAsync(x => x.AccountId == accountId); + if (avatar == null) + { + avatar = new PlayerAvatar + { + AccountId = accountId, + Avatar = DBConfig.DefaultAvatar, + Outfits = "[]" + }; + db.Avatars.Add(avatar); + } + + var outfits = Parse(avatar.Outfits); + + var matchingSlot = outfits.FindIndex(x => x.Slot == outfit.Slot); + if (matchingSlot >= 0) + outfits[matchingSlot] = outfit; + else + outfits.Add(outfit); + + avatar.Outfits = outfits.ToJson(); + + await db.SaveChangesAsync(); + + return outfit; + } + + private static List Parse(string? stored) + { + try + { + var outfits = (stored ?? string.Empty).FromJson>(); + return outfits?.Where(x => x != null).ToList() ?? new List(); + } + catch + { + return new List(); + } + } +} diff --git a/Server/Avatars/starter-avatar-items.json b/Server/Avatars/starter-avatar-items.json new file mode 100644 index 0000000..04c576e --- /dev/null +++ b/Server/Avatars/starter-avatar-items.json @@ -0,0 +1,1226 @@ +[ + { + "AvatarItemType": 0, + "AvatarItemDesc": "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb,,,", + "PlatformMask": -1, + "FriendlyName": "Flowing Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "1d27b674-f9e2-4ffc-9d8c-a58a1be06457,,,", + "PlatformMask": -1, + "FriendlyName": "Afro Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "d84c0ff9-8fbe-4ed8-abf3-7996e81888ab,,,", + "PlatformMask": -1, + "FriendlyName": "Large Afro Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "e5b83dfc-b2e1-4dcb-a4ab-9d3a4c8a34ae,,,", + "PlatformMask": -1, + "FriendlyName": "Long Wavy Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7dd6f7b0-7ba0-429f-a04f-e32d3a79ee61,,,", + "PlatformMask": -1, + "FriendlyName": "Short Wavy Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "eb9611c6-bb50-41a2-93e9-7f959815a846,,,", + "PlatformMask": -1, + "FriendlyName": "Dreads Long Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "1fd69ef8-0b74-4962-af5a-67f0bf0358f2,,,", + "PlatformMask": -1, + "FriendlyName": "Ponytail Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "a12f724f-4a73-4ab8-aad4-6bfc662b4dd6,,,", + "PlatformMask": -1, + "FriendlyName": "Undercut Long Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "0753d7a4-8247-4fca-a6fc-359c26086140,,,", + "PlatformMask": -1, + "FriendlyName": "Fonzie Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "77d3c585-4928-4471-a425-89036efe7299,,,", + "PlatformMask": -1, + "FriendlyName": "Spiky Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "92302d9d-c527-418c-ac5d-1fa869727505,,,", + "PlatformMask": -1, + "FriendlyName": "Part Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "f9dd08f8-16d3-4c39-af4f-89f7bb6e80d3,,,", + "PlatformMask": -1, + "FriendlyName": "Undercut Short Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "b148cb1e-df81-442f-aea6-ab1727aad00e,,,", + "PlatformMask": -1, + "FriendlyName": "Chunky Afro Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "e36bcd98-7e85-43fa-89f8-57e4ec33823a,,,", + "PlatformMask": -1, + "FriendlyName": "Bob with Bangs Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "880a3cc0-7407-4b61-b759-f9dd890fe9e5,,,", + "PlatformMask": -1, + "FriendlyName": "Bob Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "21599b51-c50f-43d8-ac5f-62c30cd02ca5,,,", + "PlatformMask": -1, + "FriendlyName": "Lori Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "193a3bf9-abc0-4d78-8d63-92046908b1c5,,,", + "PlatformMask": -1, + "FriendlyName": "Emo Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "79b90274-6eec-4664-acfb-4a123334661e,,,", + "PlatformMask": -1, + "FriendlyName": "Pig Tails Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "da4e7b34-2095-4a9e-801e-4f409039e0dd,,,", + "PlatformMask": -1, + "FriendlyName": "Buzz Cut Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "9d9fadb6-97eb-480e-a224-4e0179082071,,,", + "PlatformMask": -1, + "FriendlyName": "Meatball Buns Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "d8280c0c-d803-4513-be10-a0ba96d8821e,,,", + "PlatformMask": -1, + "FriendlyName": "Flowhawk Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "e286863c-2967-4d00-b837-b49487b9484a,,,", + "PlatformMask": -1, + "FriendlyName": "Fauxhawk Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2cb4f372-3372-4583-8b57-c4e3988e3c28,,,", + "PlatformMask": -1, + "FriendlyName": "Punky Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,,,", + "PlatformMask": -1, + "FriendlyName": "Cat-Eye Glasses (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,,,", + "PlatformMask": -1, + "FriendlyName": "Glasses (Teal)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "8d10cc78-6b00-45f3-affb-205e9cc5b03f,,,", + "PlatformMask": -1, + "FriendlyName": "Beard (Close)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "cc96f8a5-bc5b-4f89-83b7-ecd53905ada7,,,", + "PlatformMask": -1, + "FriendlyName": "Beard (Thick)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,,,", + "PlatformMask": -1, + "FriendlyName": "Business Tie (Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "4d507dfa-4a99-4ac0-8537-229e9dc0eb4a,,,", + "PlatformMask": -1, + "FriendlyName": "Rec Room Tank Top (Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,,,", + "PlatformMask": -1, + "FriendlyName": "Rec Room Shirt (Crew Neck, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,", + "PlatformMask": -1, + "FriendlyName": "Rec Room T-Shirt (Crew Neck, Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,,,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (Plaid, Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,,,", + "PlatformMask": -1, + "FriendlyName": "T-Shirt", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,", + "PlatformMask": -1, + "FriendlyName": "Rec Room Shirt (V-Neck, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c,", + "PlatformMask": -1, + "FriendlyName": "Rec Room T-Shirt (V-Neck, Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,,,", + "PlatformMask": -1, + "FriendlyName": "Tennis Skirt (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,c5deba2a-6e35-4b13-8e94-8ba5457f39df,b75ef67d-00c3-4ac1-9b72-212032460294,", + "PlatformMask": -1, + "FriendlyName": "Tennis Skirt (Yellow)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,758752bd-db2f-43d2-b580-55b3e1efffd5,b75ef67d-00c3-4ac1-9b72-212032460294,", + "PlatformMask": -1, + "FriendlyName": "Tennis Skirt (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,,,", + "PlatformMask": -1, + "FriendlyName": "Zipper Dress (Yellow)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,,,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,,,", + "PlatformMask": -1, + "FriendlyName": "Wrist Tape (White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,55901f12-d5b5-4fa8-b4c8-e479689ee39d,f600037d-c9c0-43fa-b45b-02f456f9dd5f,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (Denim)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,bf82f2f6-9af8-431e-a296-0890dea48ba7,d015cae7-a905-49e4-8823-6dec069689a6,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (Argyle)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,EfdMcnfHt0mr0PQ_maaYOg,DRJcNhkqvkKFEaZpOguR6w,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (Flowers, Green)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,6d703981-2734-4c45-8983-cdd5f328902f,a0271cd0-e172-4d3f-aa2f-9806f21a82d2,", + "PlatformMask": -1, + "FriendlyName": "Tank Top (Camo)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,9c03f381-7357-4d0f-8cda-8737d4c43d25,", + "PlatformMask": -1, + "FriendlyName": "Tank Top (Rainbow)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "PlatformMask": -1, + "FriendlyName": "Jersey (Orange)", + "Tooltip": "", + "Rarity": 10 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e,", + "PlatformMask": -1, + "FriendlyName": "Tank Top (Dots)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,48abd952-214f-48b2-a8f1-1146f6f69aa2,b78008e8-abbd-4ece-be34-9a911f721fcc,", + "PlatformMask": -1, + "FriendlyName": "Tank Top (Zebra)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Baseball Cap (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Baseball Cap (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Cream)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,018a5c07-e956-457d-a540-a5e2cd68da09,", + "PlatformMask": -1, + "FriendlyName": "Headband (Orange, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,018a5c07-e956-457d-a540-a5e2cd68da09,", + "PlatformMask": -1, + "FriendlyName": "Headband (Pink, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,018a5c07-e956-457d-a540-a5e2cd68da09,", + "PlatformMask": -1, + "FriendlyName": "Headband (Blue, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,6dd95046-acf8-42fe-ab78-80a334096a9d,56a92c8d-af53-413e-929e-4a9a3cfad780,", + "PlatformMask": -1, + "FriendlyName": "Headband (Red, White, Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Pink)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "fcfcaf63-deb4-45f7-b711-c051c9ea45cb,,,", + "PlatformMask": -1, + "FriendlyName": "Top Bun Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,,,", + "PlatformMask": -1, + "FriendlyName": "Blazer (Blue, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,6f2e74bf-1e95-463d-97db-d5d1a53b2c28,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54,", + "PlatformMask": -1, + "FriendlyName": "Blazer (Black, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,9374bf66-2ee5-493b-8439-efce4b201904,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54,", + "PlatformMask": -1, + "FriendlyName": "Blazer (Grey, Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,272fe8eb-5061-4729-a7a8-414ff667a82f,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54,", + "PlatformMask": -1, + "FriendlyName": "Blazer (Grey, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,0ffad843-d6c9-425a-8686-7217009c867e,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54,", + "PlatformMask": -1, + "FriendlyName": "Blazer (Green, Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,e0397982-c2c2-4733-9a40-46e18675b5af,dafa658e-753b-46cb-bd85-85c1de5e6ea7,", + "PlatformMask": -1, + "FriendlyName": "Button Top (Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,,,", + "PlatformMask": -1, + "FriendlyName": "Button Top (Pink)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,49f5864f-9d40-497c-88c8-e87f64d41d74,dafa658e-753b-46cb-bd85-85c1de5e6ea7,", + "PlatformMask": -1, + "FriendlyName": "Button Top (Tan)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,c5deba2a-6e35-4b13-8e94-8ba5457f39df,dafa658e-753b-46cb-bd85-85c1de5e6ea7,", + "PlatformMask": -1, + "FriendlyName": "Button Top (Yellow)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,f750de46-3758-4f7d-9709-0a84b1027009,2c8924aa-68f8-4912-9759-18992f72f08a,", + "PlatformMask": -1, + "FriendlyName": "Collared Dress (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,d66aa400-aa5a-4539-a25d-5f8ce94dc281,2c8924aa-68f8-4912-9759-18992f72f08a,", + "PlatformMask": -1, + "FriendlyName": "Collared Dress (Green)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,2c8924aa-68f8-4912-9759-18992f72f08a,", + "PlatformMask": -1, + "FriendlyName": "Collared Dress (Purple)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,,,", + "PlatformMask": -1, + "FriendlyName": "Collared Dress (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "241506f6-bf88-4b46-b5fe-513a225421f4,,,", + "PlatformMask": -1, + "FriendlyName": "Half Up Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,d6edbc00-3c1d-4f49-8412-3ef8c7c5f4c2,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "PlatformMask": -1, + "FriendlyName": "Long Scarf (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,6d703981-2734-4c45-8983-cdd5f328902f,cfabdefe-0890-436e-b2a3-b5c712e22955,", + "PlatformMask": -1, + "FriendlyName": "Zipper Dress (Green)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,830be2fa-60a5-48cc-931f-34b670eae4bd,cfabdefe-0890-436e-b2a3-b5c712e22955,", + "PlatformMask": -1, + "FriendlyName": "Zipper Dress (Purple)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,bbfa08e3-8e6b-4e0f-b264-1b398d7cd44a,cfabdefe-0890-436e-b2a3-b5c712e22955,", + "PlatformMask": -1, + "FriendlyName": "Zipper Dress (White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Light Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Green)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Gold)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Tank Top (Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Tank Top (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Tank Top (Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Tank Top (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "PlatformMask": -1, + "FriendlyName": "Long Scarf (Purple)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "PlatformMask": -1, + "FriendlyName": "Long Scarf (White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Green)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,,,", + "PlatformMask": -1, + "FriendlyName": "Headband (White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Yellow)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "24a240f4-1574-420b-b898-a7e91f170759,,,", + "PlatformMask": -1, + "FriendlyName": "Back Bun Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "c45ed7b8-99bd-4a4b-a9ff-e16edf5d7a18,,,", + "PlatformMask": -1, + "FriendlyName": "High Pony Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Baseball Cap (Light Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Baseball Cap (Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Baseball Cap (Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,,,", + "PlatformMask": -1, + "FriendlyName": "Baseball Cap (White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Baseball Cap (Yellow)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,,,", + "PlatformMask": -1, + "FriendlyName": "Equestrian Coat (Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d344b8cc-85a8-4ace-9f92-38c84f396e99,", + "PlatformMask": -1, + "FriendlyName": "Equestrian Coat (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,4828b50c-95b6-466a-bb25-514891d78202,d344b8cc-85a8-4ace-9f92-38c84f396e99,", + "PlatformMask": -1, + "FriendlyName": "Equestrian Coat (Grey)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,d6823e01-69f0-4f85-b94a-74894356a2cf,d344b8cc-85a8-4ace-9f92-38c84f396e99,", + "PlatformMask": -1, + "FriendlyName": "Equestrian Coat (Maroon)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "09177621-9ecd-4f6a-b6a5-64490139141d,,,", + "PlatformMask": -1, + "FriendlyName": "Flat Top Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "95ab7a7c-c35d-4da5-9955-0921064470b6,,,", + "PlatformMask": -1, + "FriendlyName": "Gekko Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Grey)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Light Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Headband (Pink)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,018a5c07-e956-457d-a540-a5e2cd68da09,", + "PlatformMask": -1, + "FriendlyName": "Headband (Red, White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,,,", + "PlatformMask": -1, + "FriendlyName": "Slim Blazer (Teal)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,cd5d7285-202d-42d0-b93f-04245875793e,0f36bb97-c61b-4281-929f-ff1d0d11be86,", + "PlatformMask": -1, + "FriendlyName": "Slim Blazer (Green)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,0f36bb97-c61b-4281-929f-ff1d0d11be86,", + "PlatformMask": -1, + "FriendlyName": "Slim Blazer (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "8b9f1413-e786-4a30-946c-9292f207875a,,,", + "PlatformMask": -1, + "FriendlyName": "Pulp Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,,,", + "PlatformMask": -1, + "FriendlyName": "Rec Room Sash", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "1a71064b-794f-40fa-9109-8ad36602b6e1,,,", + "PlatformMask": -1, + "FriendlyName": "Shagg Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,761a3193-60f0-4190-80c7-285b8192e794,91a451c1-b285-4c48-b14d-59ded8cc006f,", + "PlatformMask": -1, + "FriendlyName": "Stoll Dress (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,a819f49b-6c7a-49d3-9e6a-d9d79ef5019f,91a451c1-b285-4c48-b14d-59ded8cc006f,", + "PlatformMask": -1, + "FriendlyName": "Stoll Dress (Green)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,64850553-cdfe-455a-ac00-dafbe63d613e,91a451c1-b285-4c48-b14d-59ded8cc006f,", + "PlatformMask": -1, + "FriendlyName": "Stoll Dress (Orange)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,,,", + "PlatformMask": -1, + "FriendlyName": "Stoll Dress (Pink)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wrist Tape (Black)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wrist Tape (Cream)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Grey)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "PlatformMask": -1, + "FriendlyName": "Wristbands (Cream)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,,,", + "PlatformMask": -1, + "FriendlyName": "Long Scarf (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,0iSsaY-HgkmLaRHCn5vEdw,PioQ0o3yP0a6szPZ4EKs2A,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (Blue)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,jGj28vhq8EGwP2RuM074aQ,PioQ0o3yP0a6szPZ4EKs2A,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (Yellow)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,kmj5zOjcwku_WWKroCeiVQ,PioQ0o3yP0a6szPZ4EKs2A,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (Pink)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,FAviMCQ_EE2Mpt6QPo5OEw,PioQ0o3yP0a6szPZ4EKs2A,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (Red)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,MFrcSQ1DYUm8imvy4ypgvw,PioQ0o3yP0a6szPZ4EKs2A,", + "PlatformMask": -1, + "FriendlyName": "Collared Shirt (White)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,05ac07e1-67f0-486c-abf5-a62866475abb,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54,", + "PlatformMask": -1, + "FriendlyName": "Blazer (Black, Cream)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "0088603e-ec3b-4478-8694-e6fb1989b3f2,,,", + "PlatformMask": -1, + "FriendlyName": "Angled Bob Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "ffea7a65-613f-4835-921e-6dd15f357b7e,,,", + "PlatformMask": -1, + "FriendlyName": "Long Bangs Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "45f5e714-8a5f-4385-a97f-675066167011,,,", + "PlatformMask": -1, + "FriendlyName": "Seventies Stache", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "9bf5d259-7774-4cbe-a90f-7f188cc0dce7,,,", + "PlatformMask": -1, + "FriendlyName": "Thick Goatee", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "a6cbfe76-534a-4655-a8a8-3fed13d001c7,,,", + "PlatformMask": -1, + "FriendlyName": "Bald Top Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "CTcrvbo3OEepIV4oW8bx4w,,,", + "PlatformMask": -1, + "FriendlyName": "Receding Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "-twtjyBdQ02EAdOfBGTiEw,,,", + "PlatformMask": -1, + "FriendlyName": "Van Dyke Beard", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "45eaab67-19c2-4601-8f80-3565a4dceba4,,,", + "PlatformMask": -1, + "FriendlyName": "Pompadour Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "c855dcc3-96cb-470d-b159-d37a025a47d1,,,", + "PlatformMask": -1, + "FriendlyName": "Dutch Braid Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "d7730a9e-78a1-4356-bc09-6b066615850b,,,", + "PlatformMask": -1, + "FriendlyName": "Afro Updo Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "8c35c804-e8d5-49d2-8d5a-ea19fb70bfa6,,,", + "PlatformMask": -1, + "FriendlyName": "Pencil Bun Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "5beeb4c4-f276-4eae-87aa-9302e45b05b7,,,", + "PlatformMask": -1, + "FriendlyName": "Cornrows Hair", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,sxUE0iOSZEmezm54T7xI3Q,tlpa7195x0CkmSjpR1RArQ,", + "PlatformMask": -1, + "FriendlyName": "Rec Room Hoodie - Pride (Rainbow Pride)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,,,", + "PlatformMask": -1, + "FriendlyName": "Winged Hat - Pride (Rainbow Pride)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,D_Xmo0rOzkS-kgq1CYXt3g,tnCJp2eDI0SwjVfJMhk3LQ,", + "PlatformMask": -1, + "FriendlyName": "Rec Room Hoodie - Pride (Trans Pride)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,knXPidb-Rkayfc3kSHfZeQ,1yMyo6oTjU-VAygoeWaohQ,", + "PlatformMask": -1, + "FriendlyName": "Winged Hat - Pride (Trans Pride)", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "88b6ddeb-a455-460d-91d9-a4569ef6903c,,,", + "PlatformMask": -1, + "FriendlyName": "Square Earrings ", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "0abb6b08-20ce-444f-879e-0d1344df096c,,,", + "PlatformMask": -1, + "FriendlyName": "Round Earrings", + "Tooltip": "", + "Rarity": 0 + }, + { + "AvatarItemType": 0, + "AvatarItemDesc": "9b5bde11-7408-4798-9fcb-c7ec175444df,,,", + "PlatformMask": -1, + "FriendlyName": "Hoop Earrings", + "Tooltip": "", + "Rarity": 0 + } +] diff --git a/Server/BanAppeal/AppealAPI.cs b/Server/BanAppeal/AppealAPI.cs new file mode 100644 index 0000000..fb797c8 --- /dev/null +++ b/Server/BanAppeal/AppealAPI.cs @@ -0,0 +1,342 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.Utils; +using RadicadeServer.Discord.Webhooks; + +namespace RadicadeServer.API.Moderation +{ + public class SubmitAppealRequest + { + + public string Code { get; set; } = string.Empty; + + + public string Text { get; set; } = string.Empty; + } + + + + + + + + + [Route("api/Appeal/")] + public class AppealAPI(RecDBContext db) : Controller + { + + + + + + + + + + + + [HttpPost("submit")] + public async Task> Submit([FromBody] SubmitAppealRequest request) + { + var code = request?.Code?.Trim().ToUpperInvariant() ?? string.Empty; + if (code.Length == 0) + return BadRequest(new OkResponse { Success = false, Message = "Missing appeal code." }); + + var text = request?.Text?.Trim() ?? string.Empty; + if (text.Length < 10) + return BadRequest(new OkResponse { Success = false, Message = "Please describe your appeal in at least 10 characters." }); + + var ban = await db.Moderation.FirstOrDefaultAsync(x => x.AppealCode == code); + if (ban == null) + return NotFound(new OkResponse { Success = false, Message = "Unknown appeal code." }); + + + + + if (!ban.IsBan || ban.ExpireDuration < DateTime.UtcNow) + return BadRequest(new OkResponse { Success = false, Message = "Player is not banned!" }); + + if (AccountLock.IsLockBan(ban)) + return BadRequest(new OkResponse { Success = false, Message = AccountLock.NotAppealableMessage }); + + + if (!ModerationPolicy.IssuesAppealCode(ban.BanMethod) || ModerationPolicy.RefusesAppeal(ban.BanMethod)) + return BadRequest(new OkResponse { Success = false, Message = AccountLock.NotAppealableMessage }); + + + if (ban.AppealText != null && ban.AppealResolution == null) + return BadRequest(new OkResponse { Success = false, Message = "Your appeal is already pending review." }); + + if (ban.AppealResolution == "Accepted") + return BadRequest(new OkResponse { Success = false, Message = "Player is not banned!" }); + + + + + + var cooldownError = await PlayerUtils.GetAppealCooldownErrorAsync(db, ban.PlayerId); + if (cooldownError != null) + { + return BadRequest(new OkResponse { Success = false, Message = cooldownError }); + } + + ban.AppealText = text; + ban.AppealSubmittedAt = DateTime.UtcNow; + ban.AppealResolution = null; + ban.AppealResolvedAt = null; + + await db.SaveChangesAsync(); + + + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == ban.PlayerId); + + + + + + + if (profile != null) + { + await Notify.SendNotify(ban.PlayerId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile }); + } + + ReportDiscord.SendAsync(WebhookConfig.BanAppealsUrl, "🎟️ Ban Appeal", + new DiscordPayload + { + Title = "New Ban Appeal", + ThumbnailUrl = WebhookImages.BanAppeal, + Color = 0x5865F2, + Fields = new List + { + new() { Name = "Player", Value = profile?.username ?? ban.PlayerId.ToString(), Inline = true }, + new() { Name = "Player ID", Value = ban.PlayerId.ToString(), Inline = true }, + new() { Name = "Appeal Code", Value = code, Inline = true }, + new() { Name = "Ban Reason", Value = ban.Message ?? "—", Inline = false }, + new() { Name = "Appeal", Value = text, Inline = false } + }, + TimestampUtc = DateTime.UtcNow.ToString("O") + }); + + return Ok(new OkResponse { Success = true, Message = "Appeal submitted. A moderator will review it shortly." }); + } + + + + + + + [HttpGet("status")] + public async Task> Status([FromQuery] string code) + { + var normalized = code?.Trim().ToUpperInvariant() ?? string.Empty; + if (normalized.Length == 0) + return Ok(new { found = false }); + + var ban = await db.Moderation.AsNoTracking().FirstOrDefaultAsync(x => x.AppealCode == normalized); + if (ban == null) + return Ok(new { found = false }); + + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == ban.PlayerId); + + bool banned = ban.IsBan && ban.ExpireDuration > DateTime.UtcNow; + string? cooldownText = null; + DateTime? cooldownUntilUtc = null; + + + if (banned + && (ban.AppealResolution == "Rejected" + || ban.AppealResolution == "Reduced" + || ban.AppealResolution == "Extended" + || ban.AppealResolution == "Reviewed") + && ban.AppealResolvedAt.HasValue) + { + var cooldownEnds = ban.AppealResolvedAt.Value + PlayerUtils.AppealCooldownDuration; + if (cooldownEnds > DateTime.UtcNow) + { + cooldownUntilUtc = cooldownEnds; + cooldownText = $"{PlayerUtils.AppealCooldownPrefix}{PlayerUtils.FormatAppealCooldown(cooldownEnds - DateTime.UtcNow)}"; + } + } + + return Ok(new + { + found = true, + player = profile?.username ?? "unknown", + reason = ban.Message ?? string.Empty, + category = ban.Category.ToString(), + permanent = ban.ExpireDuration >= DateTime.MaxValue.AddYears(-10), + expiresAtUtc = ban.ExpireDuration, + banned, + hasAppeal = ban.AppealText != null, + submittedAt = ban.AppealSubmittedAt, + resolution = ban.AppealResolution, + resolvedAtUtc = ban.AppealResolvedAt, + cooldownUntilUtc, + cooldownText + }); + } + } + + + + + + + + + [Route("api/banappeal/")] + public class BanAppealAPI(RecDBContext db) : Controller + { + + + + + + + + + + + private static object AppealResponse(bool success, string? error, object? value) => new + { + success, + error, + value + }; + + [HttpGet("generateCode"), Authorize] + [HttpPost("generateCode"), Authorize] + public async Task> GenerateCode() + { + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(new { success = false, error = "Not signed in.", value = (object?)null }); + + var ban = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == userId.Value && x.IsBan && x.ExpireDuration > DateTime.UtcNow); + if (ban == null) + return Ok(AppealResponse(false, "Account is not banned.", null)); + + if (AccountLock.IsLockBan(ban)) + return Ok(AppealResponse(false, AccountLock.NotAppealableMessage, null)); + + + if (!ModerationPolicy.IssuesAppealCode(ban.BanMethod) || ModerationPolicy.RefusesAppeal(ban.BanMethod)) + return Ok(AppealResponse(false, AccountLock.NotAppealableMessage, null)); + + var cooldownError = await PlayerUtils.GetAppealCooldownErrorAsync(db, userId.Value); + if (cooldownError != null) + return Ok(AppealResponse(false, cooldownError, null)); + + + + + + if (ban.AppealText != null && ban.AppealResolution == null) + return Ok(AppealResponse(false, "Your appeal is already pending review.", null)); + + ban.AppealCode = PlayerUtils.GenerateAppealCode(); + await db.SaveChangesAsync(); + + var url = $"{PlayerUtils.AppealUrl()}?code={ban.AppealCode}"; + return Ok(AppealResponse(true, null, url)); + } + + private async Task<(ModerationDB? Ban, ActionResult? Stop)> LookUpAppeal(string? code, string? key) + { + if (!key.IsSupportKey()) + return (null, Unauthorized(new { success = false, error = "Invalid secret.", value = (object?)null })); + + var normalized = code?.Trim().ToUpperInvariant() ?? string.Empty; + if (normalized.Length == 0) + return (null, Ok(AppealResponse(false, "Missing appeal code.", null))); + + var ban = await db.Moderation.FirstOrDefaultAsync(x => x.AppealCode == normalized); + if (ban == null) + return (null, Ok(AppealResponse(false, "Unknown appeal code.", null))); + + + if (!ModerationPolicy.IssuesAppealCode(ban.BanMethod) || ModerationPolicy.RefusesAppeal(ban.BanMethod)) + return (null, Ok(AppealResponse(false, AccountLock.NotAppealableMessage, null))); + + return (ban, null); + } + + private string SupportKeyFrom(string? queryKey) + { + return !string.IsNullOrWhiteSpace(queryKey) + ? queryKey + : Request.Headers["X-Support-Key"].ToString().Trim(); + } + + [HttpPost("pending")] + public async Task> Pending([FromQuery] string code, [FromQuery] string key) + { + var (ban, stop) = await LookUpAppeal(code, SupportKeyFrom(key)); + if (stop != null) return stop; + if (ban == null) return Ok(AppealResponse(false, "Unknown appeal code.", null)); + + if (ban.AppealResolution != null) + return Ok(AppealResponse(false, "This appeal has already been reviewed.", ban.AppealResolution)); + + if (ban.AppealText == null) + { + ban.AppealText = PlayerUtils.SupportSiteAppealText; + ban.AppealSubmittedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + return Ok(AppealResponse(true, null, "pending")); + } + + [HttpPost("resolve")] + public async Task> Resolve([FromQuery] string code, [FromQuery] string key, [FromQuery] string? decision = null) + { + var (ban, stop) = await LookUpAppeal(code, SupportKeyFrom(key)); + if (stop != null) return stop; + if (ban == null) return Ok(AppealResponse(false, "Unknown appeal code.", null)); + + if (ban.AppealResolution != null) + return Ok(AppealResponse(true, null, ban.AppealResolution)); + + var verdict = (decision ?? string.Empty).Trim().ToLowerInvariant(); + + if (verdict == "accept") + { + ban.IsBan = false; + ban.AppealResolution = "Accepted"; + } + else if (verdict == "deny") + { + ban.AppealResolution = "Rejected"; + } + else if (verdict == "reviewed" || verdict == "review") + { + ban.AppealResolution = "Reviewed"; + } + else + { + return Ok(AppealResponse(false, "Unknown decision.", null)); + } + + ban.AppealResolvedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + + if (!ban.IsBan) + { + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == ban.PlayerId); + if (profile != null) + await Notify.SendNotify(ban.PlayerId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile }); + } + + return Ok(AppealResponse(true, null, ban.AppealResolution)); + } + } +} diff --git a/Server/Board/BoardAnnouncement.cs b/Server/Board/BoardAnnouncement.cs new file mode 100644 index 0000000..d794f75 --- /dev/null +++ b/Server/Board/BoardAnnouncement.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Board +{ + public class BoardAnnouncement + { + public string Message { get; set; } = null!; + public string MoreInfoUrl { get; set; } = null!; + } +} diff --git a/Server/Board/BoardVideo.cs b/Server/Board/BoardVideo.cs new file mode 100644 index 0000000..b6e0c56 --- /dev/null +++ b/Server/Board/BoardVideo.cs @@ -0,0 +1,11 @@ +namespace RadicadeServer.RecNet.Board +{ + public class BoardVideo + { + public string BlobName { get; set; } = null!; + public string Title { get; set; } = null!; + public string Description { get; set; } = null!; + public string ThumbnailBlobName { get; set; } = null!; + public string SourceUrl { get; set; } = null!; + } +} diff --git a/Server/Board/CommunityBoard.cs b/Server/Board/CommunityBoard.cs new file mode 100644 index 0000000..75acffa --- /dev/null +++ b/Server/Board/CommunityBoard.cs @@ -0,0 +1,11 @@ +namespace RadicadeServer.RecNet.Board +{ + public class CommunityBoard + { + public FeaturedPlayer FeaturedPlayer { get; set; } = null!; + public FeaturedRoomGroup FeaturedRoomGroup { get; set; } = null!; + public BoardAnnouncement CurrentAnnouncement { get; set; } = null!; + public List InstagramImages { get; set; } = null!; + public List Videos { get; set; } = null!; + } +} diff --git a/Server/Board/CommunityBoardBroadcast.cs b/Server/Board/CommunityBoardBroadcast.cs new file mode 100644 index 0000000..1f6cc44 --- /dev/null +++ b/Server/Board/CommunityBoardBroadcast.cs @@ -0,0 +1,41 @@ +using RadicadeServer.API.Notifications; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.RecNet.Board +{ + public static class CommunityBoardBroadcast + { + public const string UpdateNotificationId = "CommunityBoardUpdate"; + + public static IReadOnlyList BuildFrames(CommunityBoard? board) + { + if (board == null) + return Array.Empty(); + + return new List + { + new Notification + { + Id = UpdateNotificationId, + Msg = board + }, + new Notification + { + Id = NotificationType.CommunityBoardAnnouncementUpdate, + Msg = board.CurrentAnnouncement ?? new BoardAnnouncement + { + Message = string.Empty, + MoreInfoUrl = string.Empty + } + } + }; + } + + public static async Task PushAsync(CommunityBoard? board) + { + foreach (var frame in BuildFrames(board)) + await Notify.SendGlobalNotify(frame); + } + } +} diff --git a/Server/Board/CommunityBoardV2.cs b/Server/Board/CommunityBoardV2.cs new file mode 100644 index 0000000..5a644e0 --- /dev/null +++ b/Server/Board/CommunityBoardV2.cs @@ -0,0 +1,94 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Board; + +namespace RadicadeServer.API.Board.V2 +{ + [Route("api/communityboard/v2")] + public class CommunityBoardV2 : Controller + { + private readonly RecDBContext db; + + public CommunityBoardV2(RecDBContext context) + { + db = context; + } + + [HttpGet("current")] + public async Task> GetCurrentCommunityBoard() + { + var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "CommunityBoard"); + var board = config?.Value?.FromJson() ?? CreateDefaultBoard(); + NormalizeBoard(board); + return Ok(board); + } + + private static CommunityBoard CreateDefaultBoard() + { + return new CommunityBoard + { + FeaturedPlayer = new FeaturedPlayer + { + Id = 0, + TitleOverride = string.Empty, + UrlOverride = string.Empty + }, + FeaturedRoomGroup = new FeaturedRoomGroup + { + FeaturedRoomGroupId = 0, + Name = string.Empty, + Rooms = new List() + }, + CurrentAnnouncement = new BoardAnnouncement + { + Message = string.Empty, + MoreInfoUrl = string.Empty + }, + InstagramImages = new List(), + Videos = new List() + }; + } + + private static void NormalizeBoard(CommunityBoard board) + { + board.FeaturedPlayer ??= new FeaturedPlayer + { + Id = 0, + TitleOverride = string.Empty, + UrlOverride = string.Empty + }; + board.FeaturedRoomGroup ??= new FeaturedRoomGroup + { + FeaturedRoomGroupId = 0, + Name = string.Empty, + Rooms = new List() + }; + board.FeaturedRoomGroup.Rooms ??= new List(); + board.CurrentAnnouncement ??= new BoardAnnouncement + { + Message = string.Empty, + MoreInfoUrl = string.Empty + }; + board.InstagramImages ??= new List(); + board.Videos ??= new List(); + + + + + + + foreach (var video in board.Videos) + { + if (string.IsNullOrWhiteSpace(video.SourceUrl) && !string.IsNullOrWhiteSpace(video.BlobName)) + video.SourceUrl = $"{DBConfig.ServerUrl}/cdn/video/{video.BlobName.TrimStart('/')}"; + } + + var playable = board.Videos + .Where(video => !string.IsNullOrWhiteSpace(video.SourceUrl) || !string.IsNullOrWhiteSpace(video.BlobName)) + .ToList(); + board.Videos = playable; + } + } +} diff --git a/Server/Board/Enums/ExternalContentType.cs b/Server/Board/Enums/ExternalContentType.cs new file mode 100644 index 0000000..8e51f74 --- /dev/null +++ b/Server/Board/Enums/ExternalContentType.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Enums.Board +{ + public enum ExternalContentType + { + YouTube + } +} diff --git a/Server/Board/FeaturedPlayer.cs b/Server/Board/FeaturedPlayer.cs new file mode 100644 index 0000000..fdd7cfc --- /dev/null +++ b/Server/Board/FeaturedPlayer.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Board +{ + public class FeaturedPlayer + { + public int Id { get; set; } + public string TitleOverride { get; set; } = null!; + public string UrlOverride { get; set; } = null!; + } +} diff --git a/Server/Board/FeaturedRoom.cs b/Server/Board/FeaturedRoom.cs new file mode 100644 index 0000000..cb380b6 --- /dev/null +++ b/Server/Board/FeaturedRoom.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Board +{ + public class FeaturedRoom + { + public long RoomId { get; set; } + public string RoomName { get; set; } = null!; + public string ImageName { get; set; } = null!; + } +} diff --git a/Server/Board/FeaturedRoomGroup.cs b/Server/Board/FeaturedRoomGroup.cs new file mode 100644 index 0000000..ef9edf0 --- /dev/null +++ b/Server/Board/FeaturedRoomGroup.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Board +{ + public class FeaturedRoomGroup + { + public long FeaturedRoomGroupId { get; set; } + public string Name { get; set; } = null!; + public List Rooms { get; set; } = null!; + } +} diff --git a/Server/Board/InstagramImage.cs b/Server/Board/InstagramImage.cs new file mode 100644 index 0000000..e6640b1 --- /dev/null +++ b/Server/Board/InstagramImage.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Board +{ + public class InstagramImage + { + public string ImageName { get; set; } = null!; + public string ImageUrl { get; set; } = null!; + } +} diff --git a/Server/BugReporting/BugReportingAPI.cs b/Server/BugReporting/BugReportingAPI.cs new file mode 100644 index 0000000..7352e3c --- /dev/null +++ b/Server/BugReporting/BugReportingAPI.cs @@ -0,0 +1,478 @@ +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.Discord.Webhooks; + +namespace RadicadeServer.API.BugReporting; + + + + + + + + + + + +[Route("api/bugreporting/")] +[Route("bugreporting/")] +public class BugReportingAPI : Controller +{ + + private const int MaxLogChars = 3000; + + + private const long MaxAttachmentBytes = 20 * 1024 * 1024; + + + + private const long MaxImageBytes = 8 * 1024 * 1024; + private const int MaxImageAttachments = 10; + + [HttpPost("v2/reportbug")] + [HttpPost("v1/reportbug")] + public async Task ReportBug() + { + + var fields = new List>(); + var logs = new List(); + var images = new List(); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var kv in form) + AddField(fields, kv.Key, kv.Value.ToString()); + foreach (var file in form.Files) + { + var parsed = await ClassifyFileAsync(file); + if (parsed == null) continue; + if (parsed.IsImage) + images.Add(new ReportDiscord.DiscordAttachment(parsed.Filename!, parsed.Data!, parsed.MimeType!)); + else + logs.Add(parsed.Text ?? ""); + } + } + else + { + try + { + using var reader = new StreamReader(Request.Body); + var body = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(body)) + { + var trimmed = body.TrimStart(); + if (trimmed.StartsWith("{", StringComparison.Ordinal)) + { + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in doc.RootElement.EnumerateObject()) + { + AddField(fields, prop.Name, + prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() ?? "" : prop.Value.ToString()); + } + } + } + else if (trimmed.Contains('=')) + { + + foreach (var pair in body.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var eq = pair.IndexOf('='); + if (eq < 0) + { + AddField(fields, Uri.UnescapeDataString(pair), ""); + continue; + } + AddField(fields, Uri.UnescapeDataString(pair.Substring(0, eq)), + Uri.UnescapeDataString(pair.Substring(eq + 1))); + } + } + } + } + catch + { + + + } + } + + + + var description = Get(fields, "Description", "description", "Summary", "summary") ?? ""; + if (string.IsNullOrWhiteSpace(description) && logs.Count > 0) + description = logs[0]; + + + + var buildTimestamp = Get(fields, "BuildTimestamp", "buildTimestamp"); + if (long.TryParse(buildTimestamp, out var ticks) && ticks > 0) + { + try + { + var dt = new DateTime(ticks, DateTimeKind.Utc); + if (dt.Year is > 2000 and < 2100) + AddField(fields, "Build Time", dt.ToString("yyyy-MM-dd HH:mm:ss 'UTC'")); + } + catch + { + + } + } + + var embedFields = new List(); + foreach (var kv in fields) + { + if (string.IsNullOrWhiteSpace(kv.Value)) continue; + embedFields.Add(new DiscordField { Name = kv.Key, Value = kv.Value, Inline = true }); + } + for (int i = 0; i < logs.Count; i++) + embedFields.Add(new DiscordField { Name = $"Log {i + 1}", Value = logs[i], Inline = false }); + + var payload = new DiscordPayload + { + Title = "Bug Report", + Description = Truncate(description, 3900), + ThumbnailUrl = WebhookImages.BugReport, + Color = 0xED4245, + Fields = embedFields.Take(24).ToList(), + TimestampUtc = DateTime.UtcNow.ToString("O") + }; + + + + var imagesToSend = images.Take(MaxImageAttachments).ToList(); + var embedImageUrl = imagesToSend.Count > 0 ? $"attachment://{imagesToSend[0].Filename}" : null; + + ReportDiscord.SendAsync(WebhookConfig.ReportsUrl, "🐞 New Bug Report", payload, imagesToSend, embedImageUrl); + return Ok(new { Success = true }); + } + + + + + + + [HttpPost("ado/bug")] + public async Task ReportAdoBug() + { + string body; + using (var reader = new StreamReader(Request.Body)) + body = await reader.ReadToEndAsync(); + + var summary = body.Length > 0 && body.TrimStart().StartsWith("{", StringComparison.Ordinal) + ? SummarizeJson(body) + : Truncate(body.Trim(), 500); + + ReportDiscord.SendAsync(WebhookConfig.ReportsUrl, "\ud83d\udc1e Crash / bug report", new DiscordPayload + { + Title = "Bug Report (ADO)", + Description = Truncate(string.IsNullOrWhiteSpace(summary) ? "(empty report)" : summary, 3900), + ThumbnailUrl = WebhookImages.BugReport, + Color = 0xED4245, + Fields = new List(), + TimestampUtc = DateTime.UtcNow.ToString("O") + }); + + return Ok(new { Id = 0, Url = string.Empty, Success = true }); + } + + private static string SummarizeJson(string body) + { + try + { + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.ValueKind != JsonValueKind.Object) return Truncate(body, 500); + + var parts = new List(); + foreach (var prop in doc.RootElement.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.Object || prop.Value.ValueKind == JsonValueKind.Array) continue; + var value = prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() ?? "" : prop.Value.ToString(); + if (string.IsNullOrWhiteSpace(value)) continue; + parts.Add($"{prop.Name}={Truncate(value, 200)}"); + if (parts.Count >= 12) break; + } + return string.Join(", ", parts); + } + catch (JsonException) + { + return Truncate(body, 500); + } + } + + private static void AddField(List> fields, string key, string value) + { + if (TryParseJsonObject(value, out var nested)) + { + foreach (var kv in nested) + AddField(fields, kv.Key, kv.Value); + return; + } + + if (fields.Any(kv => string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))) + return; + fields.Add(new KeyValuePair(key, value)); + } + + private static bool TryParseJsonObject(string value, out List> obj) + { + obj = new List>(); + if (string.IsNullOrWhiteSpace(value) || !value.TrimStart().StartsWith("{", StringComparison.Ordinal)) + return false; + try + { + using var doc = JsonDocument.Parse(value); + if (doc.RootElement.ValueKind != JsonValueKind.Object) return false; + foreach (var prop in doc.RootElement.EnumerateObject()) + { + obj.Add(new KeyValuePair(prop.Name, + prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() ?? "" : prop.Value.ToString())); + } + return true; + } + catch + { + return false; + } + } + + private static string? Get(List> fields, params string[] names) + { + foreach (var name in names) + { + var hit = fields.FirstOrDefault(kv => string.Equals(kv.Key, name, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(hit.Value)) return hit.Value; + } + return null; + } + + private static async Task ClassifyFileAsync(IFormFile file) + { + if (file.Length == 0 || file.Length > MaxAttachmentBytes) return null; + try + { + var data = new byte[file.Length]; + await using var stream = file.OpenReadStream(); + var read = 0; + while (read < data.Length) + { + var n = await stream.ReadAsync(data, read, data.Length - read); + if (n <= 0) break; + read += n; + } + + var mime = DetectImageMime(data, read); + if (mime != null) + { + + + if (file.Length > MaxImageBytes) return null; + var ext = MimeToExt(mime); + var name = SanitizeFileName(file.FileName); + if (string.IsNullOrWhiteSpace(name) || !name.Contains('.')) + name = $"screenshot.{ext}"; + return new FileClass(name, data[..read], mime, null, true); + } + + return new FileClass(null, null, null, Truncate(Encoding.UTF8.GetString(data, 0, read), MaxLogChars), false); + } + catch + { + return null; + } + } + + private static string? DetectImageMime(byte[] data, int length) + { + if (length < 8) return null; + if (data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) return "image/png"; + if (data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF) return "image/jpeg"; + if (data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x38) return "image/gif"; + if (data[0] == 0x42 && data[1] == 0x4D) return "image/bmp"; + if (data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 && + data[8] == 0x57 && data[9] == 0x45 && data[10] == 0x42 && data[11] == 0x50) return "image/webp"; + return null; + } + + private static string MimeToExt(string mime) => mime switch + { + "image/png" => "png", + "image/jpeg" => "jpg", + "image/gif" => "gif", + "image/bmp" => "bmp", + "image/webp" => "webp", + _ => "png" + }; + + private static string SanitizeFileName(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) return ""; + var name = Path.GetFileName(fileName).Trim(); + return name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 + ? new string(name.Where(c => !Path.GetInvalidFileNameChars().Contains(c)).ToArray()) + : name; + } + + private static string Truncate(string value, int max) + { + if (string.IsNullOrEmpty(value)) return ""; + if (value.Length <= max) return value; + + + var cut = max; + if (char.IsHighSurrogate(value[cut - 1]) && cut < value.Length && char.IsLowSurrogate(value[cut])) + cut--; + return value[..cut]; + } + + [HttpPost("ado/file")] + public async Task UploadAdoFile() + { + var upload = await ReadUploadAsync(); + if (upload == null) + { + return Ok(new { Id = 0, Url = string.Empty, Success = true }); + } + + var path = await StoreUploadAsync(upload); + + if (upload.Data.Length <= MaxImageBytes) + { + ReportDiscord.SendAsync(WebhookConfig.ReportsUrl, "Crash report attachment", new DiscordPayload + { + Title = "Bug Report Attachment", + Description = $"{upload.Filename} ({upload.Data.Length} byte(s))", + ThumbnailUrl = WebhookImages.BugReport, + Color = 0xED4245, + Fields = new List(), + TimestampUtc = DateTime.UtcNow.ToString("O") + }, new List + { + new(upload.Filename, upload.Data, upload.MimeType) + }); + } + + return Ok(new { Id = 0, Url = string.Empty, Success = true, FileName = upload.Filename, Size = upload.Data.Length }); + } + + private sealed record UploadedFile(string Filename, byte[] Data, string MimeType); + + private static string AdoFileFolder => Path.Combine(DBConfig.PrivateData, "BugReports"); + + private async Task ReadUploadAsync() + { + if (Request.HasFormContentType) + { + try + { + var form = await Request.ReadFormAsync(); + foreach (var file in form.Files) + { + if (file.Length == 0 || file.Length > MaxAttachmentBytes) continue; + var data = await ReadAllAsync(file.OpenReadStream()); + if (data.Length == 0) continue; + return new UploadedFile(UploadName(file.FileName, file.ContentType), data, UploadMime(file.ContentType, file.FileName)); + } + } + catch (Exception) + { + } + } + + var body = await ReadAllAsync(Request.Body); + if (body.Length == 0) return null; + + var name = FirstQuery("fileName", "filename", "name", "file") + ?? DispositionFileName() + ?? "ado-upload"; + return new UploadedFile(UploadName(name, Request.ContentType), body, UploadMime(Request.ContentType, name)); + } + + private static async Task ReadAllAsync(Stream stream) + { + using var buffer = new MemoryStream(); + var chunk = new byte[81920]; + int read; + while ((read = await stream.ReadAsync(chunk)) > 0) + { + buffer.Write(chunk, 0, read); + if (buffer.Length >= MaxAttachmentBytes) break; + } + return buffer.ToArray(); + } + + private string? FirstQuery(params string[] names) + { + foreach (var name in names) + { + var value = Request.Query[name].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(value)) return value!; + } + return null; + } + + private string? DispositionFileName() + { + var header = Request.Headers.ContentDisposition.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(header)) return null; + const string marker = "filename="; + var at = header!.IndexOf(marker, StringComparison.OrdinalIgnoreCase); + if (at < 0) return null; + return header[(at + marker.Length)..].Trim().Trim('"'); + } + + private static string UploadName(string? name, string? contentType) + { + var clean = SanitizeFileName(name ?? ""); + if (string.IsNullOrWhiteSpace(clean)) clean = "ado-upload"; + if (!clean.Contains('.')) clean += ExtensionFor(contentType); + return clean; + } + + private static string UploadMime(string? contentType, string? name) + { + if (!string.IsNullOrWhiteSpace(contentType) && contentType!.Contains('/')) return contentType; + var extension = Path.GetExtension(name ?? "").ToLowerInvariant(); + return extension switch + { + ".png" => "image/png", + ".jpg" or ".jpeg" => "image/jpeg", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".txt" or ".log" => "text/plain", + ".json" => "application/json", + ".zip" => "application/zip", + _ => "application/octet-stream" + }; + } + + private static string ExtensionFor(string? contentType) => (contentType ?? "").Split(';')[0].Trim().ToLowerInvariant() switch + { + "image/png" => ".png", + "image/jpeg" => ".jpg", + "image/gif" => ".gif", + "image/webp" => ".webp", + "text/plain" => ".log", + "application/json" => ".json", + "application/zip" => ".zip", + _ => ".bin" + }; + + private static async Task StoreUploadAsync(UploadedFile upload) + { + Directory.CreateDirectory(AdoFileFolder); + var stamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"); + var path = Path.Combine(AdoFileFolder, $"{stamp}-{upload.Filename}"); + for (int n = 1; System.IO.File.Exists(path); n++) + path = Path.Combine(AdoFileFolder, $"{stamp}-{n}-{upload.Filename}"); + + await System.IO.File.WriteAllBytesAsync(path, upload.Data); + return path; + } + + private sealed record FileClass(string? Filename, byte[]? Data, string? MimeType, string? Text, bool IsImage); +} diff --git a/Server/CDN/CdnCaching.cs b/Server/CDN/CdnCaching.cs new file mode 100644 index 0000000..6df48d1 --- /dev/null +++ b/Server/CDN/CdnCaching.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.CDN +{ + internal static class CdnCaching + { + public const string Immutable = "public, max-age=31536000, immutable"; + + public const string Replaceable = "public, max-age=86400"; + + public static string ForBlob(string name) => + name.EndsWith(".assetbundle", StringComparison.OrdinalIgnoreCase) ? Immutable : Replaceable; + + public static void Apply(Controller controller, string cacheControl) + { + if (controller.ControllerContext?.HttpContext == null) return; + + controller.Response.Headers["Cache-Control"] = cacheControl; + } + } +} diff --git a/Server/CDN/CdnStorageNames.cs b/Server/CDN/CdnStorageNames.cs new file mode 100644 index 0000000..50ccb0e --- /dev/null +++ b/Server/CDN/CdnStorageNames.cs @@ -0,0 +1,47 @@ +using RadicadeServer.Utils; + +namespace RadicadeServer.CDN +{ + /// + /// What may be read out of the CDN storage directory and handed to a client, and which file + /// a requested name actually is. + /// + /// Two rules live here because every route that serves a stored blob needs both, and a route + /// that re-implements them gets one of them wrong: + /// + /// - A request is a single bare file name in that one directory. A name carrying a separator + /// or .. would let a URL read outside it, and a name ending in the admin upload's + /// .uploading suffix is a half-imported bundle: it must answer as if it does not + /// exist rather than serve a truncated file, which the client would then fail to load with + /// no way to tell why. + /// - A bundle is asked for under two spellings. The subroom's UnityAssetId is stored + /// both bare and with the .assetbundle suffix an import appends, so a name resolves + /// to the exact file first and to <name>.assetbundle second. + /// + internal static class CdnStorageNames + { + internal static bool IsServable(string? name) => + !string.IsNullOrEmpty(name) + && name != "." + && !AdminUtils.IsUploadPart(name) + && !name.Contains('/') + && !name.Contains('\\') + && !name.Contains("..") + && name.IndexOf('\0') < 0; + + /// + /// The stored file a requested name is, or null when nothing by either spelling exists. + /// A name this server must never serve answers null too, so a route that only checks the + /// result is still safe. + /// + internal static string? Resolve(string? name) + { + if (!IsServable(name)) return null; + + if (System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, name))) return name; + + var withExtension = name + ".assetbundle"; + return System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, withExtension)) ? withExtension : null; + } + } +} diff --git a/Server/CDN/Config/CDNConfigAPI.cs b/Server/CDN/Config/CDNConfigAPI.cs new file mode 100644 index 0000000..cc6ad12 --- /dev/null +++ b/Server/CDN/Config/CDNConfigAPI.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet; +using RadicadeServer.Utils; + +namespace RadicadeServer.CDN.Config +{ + [Route("cdn/config")] + public class CDNConfigAPI : Controller + { + RecDBContext db; + public CDNConfigAPI(RecDBContext context) + { + this.db = context; + } + + [HttpGet("LoadingScreenTipData")] + public List GetLoadingScreenTipData() + { + var official = LoadingScreenImages.Tips; + var officialNames = new HashSet(LoadingScreenImages.Names, StringComparer.OrdinalIgnoreCase); + + var loadingScreens = db.ServerConfig.FirstOrDefault(x => x.Key == "LoadingScreenTipData"); + var stored = string.IsNullOrWhiteSpace(loadingScreens?.Value) || loadingScreens.Value == "[]" + ? new List() + : loadingScreens.Value.FromJson>() ?? new List(); + + var byImage = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var tip in stored) + { + if (!string.IsNullOrWhiteSpace(tip.ImageName)) + byImage.TryAdd(tip.ImageName, tip); + } + + var adopted = stored.FirstOrDefault(tip => + !officialNames.Contains(tip.ImageName ?? string.Empty) + && !(tip.Title == "radicade" && tip.Message == "yum")); + + var tips = new List(official.Count); + for (var i = 0; i < official.Count; i++) + { + var image = official[i].ImageName; + var text = byImage.TryGetValue(image, out var storedTip) ? storedTip + : i == 0 && adopted != null ? adopted + : official[i]; + + tips.Add(new LoadingScreenTip + { + ImageName = image, + Title = string.IsNullOrWhiteSpace(text.Title) ? official[i].Title : text.Title, + Message = string.IsNullOrWhiteSpace(text.Message) ? official[i].Message : text.Message, + RoomNames = text.RoomNames ?? new List(), + PlatformMask = text.PlatformMask + }); + } + + return tips; + } + } +} diff --git a/Server/CDN/DataCDN.cs b/Server/CDN/DataCDN.cs new file mode 100644 index 0000000..2c18231 --- /dev/null +++ b/Server/CDN/DataCDN.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using SixLabors.ImageSharp; + +namespace RadicadeServer.CDN +{ + [Route("cdn/data/")] + [Route("data/")] + public class DataCDN : Controller + { + + + [HttpGet] + public IActionResult Index() + { + try + { + using var entries = Directory.EnumerateFileSystemEntries(DBConfig.CDNStorage).GetEnumerator(); + _ = entries.MoveNext(); + return Ok(new { service = "data", status = "ok" }); + } + catch + { + return StatusCode(StatusCodes.Status503ServiceUnavailable, + new { service = "data", status = "DATA_STORAGE_UNAVAILABLE" }); + } + } + + [HttpGet("{datablob}")] + public IActionResult GetDataBlob(string datablob) + { + if (!CdnStorageNames.IsServable(datablob)) return BadRequest(); + + // This is the URL the client's bundle downloader ranges over, so the name has to be + // one that may be served at all and it may arrive with or without the .assetbundle + // suffix the import appends. + var name = CdnStorageNames.Resolve(datablob); + if (name == null) return NotFound(); + + CdnCaching.Apply(this, CdnCaching.ForBlob(name)); + + return PhysicalFile(Path.Join(DBConfig.CDNStorage, name), "application/octet-stream", enableRangeProcessing: true); + } + } +} diff --git a/Server/CDN/ImageDB.cs b/Server/CDN/ImageDB.cs new file mode 100644 index 0000000..84baff1 --- /dev/null +++ b/Server/CDN/ImageDB.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.CDN +{ + public class ImageDB + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public string Name { get; set; } = null!; + + public long OwnerId { get; set; } + + public SavedImageType ImageType { get; set; } + public SavedImageAccessibility Accessibility { get; set; } + + public long? RoomId { get; set; } + public long? EventId { get; set; } + + public string PlayerIds { get; set; } = "[]"; + + public DateTime CreatedAt { get; set; } + } +} diff --git a/Server/CDN/ImageNameDTO.cs b/Server/CDN/ImageNameDTO.cs new file mode 100644 index 0000000..2497cac --- /dev/null +++ b/Server/CDN/ImageNameDTO.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.CDN +{ + public class ImageNameDTO + { + public string ImageName { get; set; } = null!; + } +} diff --git a/Server/CDN/SavedImageMetaDTO.cs b/Server/CDN/SavedImageMetaDTO.cs new file mode 100644 index 0000000..2b14083 --- /dev/null +++ b/Server/CDN/SavedImageMetaDTO.cs @@ -0,0 +1,34 @@ +namespace RadicadeServer.RecNet.CDN +{ + public enum SavedImageAccessibility + { + Private = 0, + Public = 1, + FriendsOnly = 2 + } + + public enum SavedImageType + { + None, + ShareCamera, + OutfitThumbnail, + RoomThumbnail, + ProfileThumbnail, + InventionThumbnail, + PlayerEventThumbnail, + RoomLoadScreen + } + + public class SavedImageMetaDTO + { + public List playerIds { get; set; } = new(); + + public SavedImageType savedImageType { get; set; } = SavedImageType.ShareCamera; + + public long? roomId { get; set; } = null; + + public long? playerEventId { get; set; } = null; + + public SavedImageAccessibility accessibility { get; set; } = SavedImageAccessibility.Public; + } +} diff --git a/Server/CDN/SigsCDN.cs b/Server/CDN/SigsCDN.cs new file mode 100644 index 0000000..62b5f20 --- /dev/null +++ b/Server/CDN/SigsCDN.cs @@ -0,0 +1,63 @@ +using System.Text; +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.CDN +{ + [Route("cdn/sigs")] + public class SigsCDN : Controller + { + + private static readonly byte[] PlaceholderSignature = Encoding.ASCII.GetBytes( + "RecRoom.exe:AAAAAAAAAAAAAAAAAAAAAA==:AAAAAAAAAAAAAAAAAAAAAA==\n" + + "RecRoom_Data/globalgamemanagers:AAAAAAAAAAAAAAAAAAAAAA==:AAAAAAAAAAAAAAAAAAAAAA==\n" + + "GameAssembly.dll:AAAAAAAAAAAAAAAAAAAAAA==:AAAAAAAAAAAAAAAAAAAAAA==\n"); + + [HttpGet("{signatureId}")] + public IActionResult GetSignature(string signatureId) + { + if (string.IsNullOrWhiteSpace(signatureId)) return BadRequest(); + + var rangeHeader = ControllerContext.HttpContext?.Request.Headers.Range.ToString(); + if (string.IsNullOrWhiteSpace(rangeHeader) || !rangeHeader.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase)) + return File(PlaceholderSignature, "text/plain"); + + var (start, end) = ParseByteRange(rangeHeader, PlaceholderSignature.Length); + var count = end - start + 1; + var body = new byte[count]; + Array.Copy(PlaceholderSignature, start, body, 0, count); + + Response.Headers.AcceptRanges = "bytes"; + Response.Headers.ContentRange = $"bytes {start}-{end}/{PlaceholderSignature.Length}"; + Response.StatusCode = StatusCodes.Status206PartialContent; + return File(body, "text/plain", enableRangeProcessing: false); + } + + private static (int Start, int End) ParseByteRange(string rangeHeader, int length) + { + var spec = rangeHeader["bytes=".Length..].Trim(); + if (spec.Contains(',')) return (0, length - 1); + + var parts = spec.Split('-', 2); + if (parts.Length != 2) return (0, length - 1); + + if (parts[0].Length == 0) + { + if (!int.TryParse(parts[1], out var suffixLength) || suffixLength <= 0) + return (0, length - 1); + + var start = Math.Max(0, length - suffixLength); + return (start, length - 1); + } + + if (!int.TryParse(parts[0], out var parsedStart)) + return (0, length - 1); + + var startOffset = parsedStart >= length ? 0 : Math.Max(0, parsedStart); + var endOffset = length - 1; + if (parts[1].Length > 0 && int.TryParse(parts[1], out var parsedEnd)) + endOffset = Math.Clamp(parsedEnd, startOffset, length - 1); + + return (startOffset, endOffset); + } + } +} \ No newline at end of file diff --git a/Server/CDN/UnityAssetsCDN.cs b/Server/CDN/UnityAssetsCDN.cs new file mode 100644 index 0000000..d5b9f98 --- /dev/null +++ b/Server/CDN/UnityAssetsCDN.cs @@ -0,0 +1,81 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.API.Studio; + +namespace RadicadeServer.CDN +{ + /// + /// Step one of the client's two-step bundle download. The client asks + /// /rooms/unity_assets/{assetId}/{target}/{version} and is handed a file name, then + /// ranges over that file at /data/{Filename} (and, for a subroom's bundle, under the + /// CDN's own /unity/{Filename} alias). + /// + /// The id arrives in two shapes and both have to be answered. A Studio manifest id is 40 + /// alphanumerics and its baked bundles are named in the manifest's own metadata. A legacy + /// bundle is stored under its file name (or that name minus .assetbundle), and that + /// name is what the subroom's UnityAssetId carries - every imported room, the built-in + /// rooms and every room bound from the admin panel. Answering only the 40-character shape + /// leaves every legacy bundle unresolvable, and the client reports one 400 as + /// "Error downloading room assets" for the whole room. + /// + [Route("rooms/")] + public class UnityAssetsCDN : Controller + { + [HttpGet("unity_assets/{assetId}/{target}/{version}")] + public async Task GetUnityAsset(string assetId, int target, int version) + { + return await ServeMetadataAsync(assetId, target, version); + } + + [HttpGet("unity_assets/{assetId}")] + public async Task GetUnityAsset(string assetId) + { + return await ServeMetadataAsync(assetId, 0, 1); + } + + private async Task ServeMetadataAsync(string assetId, int target, int version) + { + if (!CdnStorageNames.IsServable(assetId) || target < 0 || target > byte.MaxValue || version < 0) + return BadRequest(); + + string? filename = null; + string? rejectedDependencies = null; + + if (StudioUnityAssetRegistry.IsValidAssetId(assetId)) + { + var asset = await StudioUnityAssetRegistry.ReadAsync(assetId, HttpContext.RequestAborted); + var baked = asset?.BakedUnityAssets.FirstOrDefault(x => x.Target == target && x.Version == version); + + if (baked != null) + { + filename = baked.Filename; + rejectedDependencies = baked.RejectedDependenciesFilename; + } + else if (asset != null) + { + // The manifest exists, so the file on disk under this id is that JSON + // manifest and not a bundle. A target/version it does not name is a 404: + // handing the manifest over as the bundle would have the client parse + // metadata as an asset bundle. + return NotFound(); + } + } + + // Reached for every legacy id, and for a 40-character id with no manifest behind it: + // the bundle is the file stored under the name the subroom points at, with or without + // the .assetbundle suffix an import appends. + filename ??= CdnStorageNames.Resolve(assetId); + if (filename == null) return NotFound(); + + var body = JsonSerializer.Serialize(new + { + UnityAssetId = assetId, + Target = target, + Version = version, + Filename = filename, + RejectedDependenciesFilename = rejectedDependencies + }); + return Content(body, "application/json"); + } + } +} diff --git a/Server/CDN/UploadResponse.cs b/Server/CDN/UploadResponse.cs new file mode 100644 index 0000000..b55f228 --- /dev/null +++ b/Server/CDN/UploadResponse.cs @@ -0,0 +1,11 @@ +namespace RadicadeServer.RecNet.CDN +{ + public class UploadResponse + { + public string filename { get; set; } + public UploadResponse(string filename) + { + this.filename = filename; + } + } +} diff --git a/Server/CDN/VideosCDN.cs b/Server/CDN/VideosCDN.cs new file mode 100644 index 0000000..5fb4163 --- /dev/null +++ b/Server/CDN/VideosCDN.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.CDN +{ + [Route("cdn/video/")] + public class VideosCDN : Controller + { + private static readonly StringComparison PathComparison = + OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + + [HttpGet("{*datablob}")] + public IActionResult GetDataBlob(string datablob) + { + + + if (string.IsNullOrEmpty(datablob)) return NotFound(); + + + + + + if (datablob.Contains("..", StringComparison.Ordinal) || + datablob.Contains('\\') || + datablob.Contains(':') || + datablob.StartsWith('/')) + return NotFound(); + + var fullPath = Path.GetFullPath(Path.Join(DBConfig.CDNVideos, datablob)); + var root = DBConfig.CDNVideos.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + + Path.DirectorySeparatorChar; + + if (!fullPath.StartsWith(root, PathComparison)) return NotFound(); + + + + + var candidates = System.IO.File.Exists(fullPath) + ? new[] { fullPath } + : new[] + { + fullPath + ".mp4", + fullPath + ".m4v", + fullPath + ".webm", + fullPath + ".mov", + fullPath + ".ogv" + }; + var existing = candidates.FirstOrDefault(p => System.IO.File.Exists(p)); + + if (existing == null) return NotFound(); + + + + + + var ext = Path.GetExtension(existing).ToLowerInvariant(); + var contentType = ext switch + { + ".mp4" or ".m4v" => "video/mp4", + ".webm" => "video/webm", + ".mov" => "video/quicktime", + ".ogv" => "video/ogg", + _ => "application/octet-stream" + }; + + + Response.Headers["Accept-Ranges"] = "bytes"; + + return PhysicalFile(existing, contentType, enableRangeProcessing: true); + } + } +} \ No newline at end of file diff --git a/Server/Chat/ChatPhotoMessages.cs b/Server/Chat/ChatPhotoMessages.cs new file mode 100644 index 0000000..6b38c17 --- /dev/null +++ b/Server/Chat/ChatPhotoMessages.cs @@ -0,0 +1,39 @@ +using System.Text.RegularExpressions; + +namespace RadicadeServer.API +{ + public static class ChatPhotoMessages + { + public static readonly TimeSpan UploadWindow = TimeSpan.FromMinutes(15); + + private static readonly Regex ImageReference = new(@"]*)>", RegexOptions.Compiled); + + private static readonly Regex PhotoEnvelope = new("\"Type\"\\s*:\\s*2(?![0-9])", RegexOptions.Compiled); + + public static bool IsPhotoEnvelope(string? contents) + => !string.IsNullOrEmpty(contents) && PhotoEnvelope.IsMatch(contents); + + public static bool HasUnresolvedImageReference(string? contents) + { + if (string.IsNullOrEmpty(contents)) + return false; + + foreach (Match match in ImageReference.Matches(contents)) + { + if (!ResolvesToAnImage(match.Groups[1].Value)) + return true; + } + + return false; + } + + public static string ResolveImageReference(string contents, long imageId) + { + return ImageReference.Replace(contents, match => + ResolvesToAnImage(match.Groups[1].Value) ? match.Value : $""); + } + + private static bool ResolvesToAnImage(string rawReference) + => long.TryParse(rawReference, out var imageId) && imageId > 0; + } +} diff --git a/Server/Chat/RecentImageReferences.cs b/Server/Chat/RecentImageReferences.cs new file mode 100644 index 0000000..4799c76 --- /dev/null +++ b/Server/Chat/RecentImageReferences.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; + +namespace RadicadeServer.API +{ + public static class RecentImageReferences + { + public static readonly TimeSpan Window = TimeSpan.FromMinutes(10); + + private sealed record Lookup(long ImageId, DateTime At); + + private static readonly ConcurrentDictionary ByPlayer = new(); + + public static void Remember(long accountId, long imageId) + => Remember(accountId, imageId, DateTime.UtcNow); + + public static void Remember(long accountId, long imageId, DateTime at) + { + if (accountId <= 0 || imageId <= 0) return; + + Prune(at); + ByPlayer[accountId] = new Lookup(imageId, at); + } + + public static bool TryGet(long accountId, out long imageId) + { + imageId = 0; + if (accountId <= 0) return false; + if (!ByPlayer.TryGetValue(accountId, out var lookup)) return false; + if (DateTime.UtcNow - lookup.At > Window) return false; + + imageId = lookup.ImageId; + return true; + } + + public static void Clear() => ByPlayer.Clear(); + + private static void Prune(DateTime now) + { + foreach (var pair in ByPlayer) + { + if (now - pair.Value.At > Window) + ByPlayer.TryRemove(pair.Key, out _); + } + } + } +} diff --git a/Server/Chat/Thread.cs b/Server/Chat/Thread.cs new file mode 100644 index 0000000..3cb6a88 --- /dev/null +++ b/Server/Chat/Thread.cs @@ -0,0 +1,861 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Notifications; +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace RadicadeServer.API +{ + public enum QueryMode + { + Latest = 0, + NewerThan = 1, + OlderThan = 2 + } + + [Route("chat/")] + public class ThreadAPI(RecDBContext _db) : Controller + { + private const int Success = 0; + private const int ThreadNotFound = 2; + private const int MembershipNotFound = 3; + + private const long SystemSenderPlayerId = -5; + + private const int DefaultMessageCount = 50; + private const int MaxMessageCount = 100; + + private const int ChatThreadTypeDefault = 0; + [HttpGet("thread/party"), Authorize] + [HttpPost("thread/party"), Authorize] + public IActionResult GetPartyThread() + { + return Ok(Array.Empty()); + } + + [HttpGet("thread"), Authorize] + public async Task GetThread([FromQuery] int maxCount, [FromQuery] QueryMode mode, [FromQuery] long? messageId = null, [FromQuery] int count = 0) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var threadLimit = count > 0 ? count : maxCount; + var responses = new List<(DateTime LastMessageTime, long ThreadId, Dictionary Response)>(); + + foreach (var thread in await _db.Chats.ToListAsync()) + { + var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value); + if (!playerIds.Contains(userId.Value)) + continue; + + var latest = (thread.GetMessages() ?? new List()) + .OrderByDescending(m => m.chatMessageId) + .FirstOrDefault(); + + responses.Add((latest?.timeSent ?? DateTime.MinValue, thread.chatThreadId, ToListResponse(thread, latest, playerIds, userId.Value))); + } + + var orderedResponses = responses + .OrderByDescending(x => x.LastMessageTime) + .ThenByDescending(x => x.ThreadId) + .Select(x => x.Response); + + if (threadLimit > 0) + orderedResponses = orderedResponses.Take(threadLimit); + + return Ok(orderedResponses.ToList()); + } + + [HttpPost("thread/withmembers"), Authorize] + public async Task CreateThreadWithMembers() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var request = await ReadChatThreadRequest(userId.Value); + var thread = await FindOrCreateThread(request.PlayerIds, request.ThreadName, userId.Value); + + return Ok(ToResponse(thread, userId.Value)); + } + + [HttpPost("thread"), Authorize] + public async Task CreateThread() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var request = await ReadChatThreadRequest(userId.Value); + var thread = await FindOrCreateThread(request.PlayerIds, request.ThreadName, userId.Value); + + var posted = false; + if (!string.IsNullOrWhiteSpace(request.MessageContents)) + { + var message = await AddMessageToThread(thread, userId.Value, request.MessageContents); + await NotifyChatMessage(thread, message); + posted = true; + } + + var chatThreadResponse = ToResponse(thread, userId.Value); + return Ok(new Dictionary + { + ["chatResult"] = posted ? 0 : 1, + ["chatThread"] = chatThreadResponse + }); + } + + [HttpGet("thread/{threadId:long}"), Authorize] + public async Task GetThreadById(long threadId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return NotFound(); + + var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value); + if (!playerIds.Contains(userId.Value)) + return NotFound(); + + var (mode, messageId) = ReadQueryWindow(); + var messages = PageMessages(thread.GetMessages() ?? new List(), mode, messageId, ReadMessageCount(DefaultMessageCount)); + + return Ok(ToResponse(thread, userId.Value, messages, playerIds)); + } + + [HttpGet("thread/{threadId:long}/message"), Authorize] + public async Task GetThreadMessages(long threadId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return NotFound(); + + var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value); + if (!playerIds.Contains(userId.Value)) + return NotFound(); + + var (mode, messageId) = ReadQueryWindow(); + var messages = PageMessages(thread.GetMessages() ?? new List(), mode, messageId, ReadMessageCount(DefaultMessageCount)); + + return Ok(ToMessageResponses(messages)); + } + + [HttpPost("thread/{threadId:long}"), Authorize] + [HttpPost("thread/{threadId:long}/message"), Authorize] + public async Task SendMessageToThread(long threadId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return NotFound(); + + var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value); + if (!playerIds.Contains(userId.Value)) + return NotFound(); + + var request = await ReadChatThreadRequest(userId.Value); + + var posted = !string.IsNullOrWhiteSpace(request.MessageContents); + if (posted) + { + var message = await AddMessageToThread(thread, userId.Value, request.MessageContents); + await NotifyChatMessage(thread, message); + } + + var chatThreadResponse = ToResponse(thread, userId.Value); + return Ok(new Dictionary + { + ["chatResult"] = posted ? 0 : 1, + ["chatThread"] = chatThreadResponse + }); + } + [HttpPost("thread/{threadId:long}/rename"), Authorize] + public async Task RenameThread(long threadId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return NotFound(); + + var body = await ReadJsonBody(); + var newName = GetStringFromBody(body, "chatThreadName", "ChatThreadName", "threadName", "ThreadName", "name", "Name"); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + newName = GetStringFromForm(form, "chatThreadName", "ChatThreadName", "threadName", "ThreadName", "name", "Name") ?? newName; + } + + if (string.IsNullOrWhiteSpace(newName)) + return Ok(ChatResult(4)); + + thread.chatThreadName = newName.Trim(); + await _db.SaveChangesAsync(); + + var chatThreadResponse = ToResponse(thread, userId.Value); + return Ok(new Dictionary + { + ["chatResult"] = 0, + ["chatThread"] = chatThreadResponse + }); + } + + [HttpPost("thread/{threadId:long}/member/{playerId:long}"), Authorize] + public async Task AddThreadMember(long threadId, long playerId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return NotFound(); + if (playerId <= 0) return BadRequest(); + + var playerIds = thread.GetPlayerIds() ?? new List(); + playerIds = playerIds.Where(x => x > 0).Distinct().ToList(); + if (!playerIds.Contains(playerId)) + { + playerIds.Add(playerId); + thread.playerIds = playerIds.OrderBy(x => x).ToJson(); + await _db.SaveChangesAsync(); + } + + var chatThreadResponse = ToResponse(thread, userId.Value); + return Ok(new Dictionary + { + ["chatResult"] = 0, + ["chatThread"] = chatThreadResponse + }); + } + [HttpPut("thread/{threadId:long}/favorite"), Authorize] + [HttpPost("thread/{threadId:long}/favorite"), Authorize] + [HttpDelete("thread/{threadId:long}/favorite"), Authorize] + public async Task FavoriteThread(long threadId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return Ok(ChatResult(2)); + + var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value); + if (!playerIds.Contains(userId.Value)) return Ok(ChatResult(3)); + + var favorite = Request.Method.Equals("DELETE", StringComparison.OrdinalIgnoreCase) + ? false + : await ReadBoolValue("isFavorited", "IsFavorited", "favorite", "Favorite", "value", "Value"); + + var favorited = thread.GetFavoritedBy(); + if (favorite) + { + if (!favorited.Contains(userId.Value)) favorited.Add(userId.Value); + } + else + { + favorited.Remove(userId.Value); + } + + thread.SetFavoritedBy(favorited); + await _db.SaveChangesAsync(); + + return Ok(new Dictionary + { + ["chatResult"] = 0, + ["isFavorited"] = favorite + }); + } + + [HttpPost("thread/{threadId:long}/snooze"), Authorize] + public async Task SnoozeThread(long threadId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return Ok(ChatResult(2)); + + var snooze = await ReadBoolValue("snooze", "Snooze"); + var snoozedUntil = snooze ? DateTime.UtcNow.AddYears(1) : (DateTime?)null; + + return Ok(new Dictionary + { + ["chatResult"] = 0, + ["snoozedUntil"] = snoozedUntil + }); + } + + [HttpPost("thread/{threadId:long}/leave"), Authorize] + public async Task LeaveThread(long threadId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return Ok(ChatResult(2)); + + var playerIds = thread.GetPlayerIds() ?? new List(); + playerIds = playerIds.Where(x => x != userId.Value).Distinct().OrderBy(x => x).ToList(); + thread.playerIds = playerIds.ToJson(); + await _db.SaveChangesAsync(); + + if (playerIds.Count > 0) + await AddMessageToThread(thread, SystemSenderPlayerId, MessageEnvelope($"Player <@U{userId.Value}> left")); + + return Ok(ChatResult(0)); + } + + [HttpPost("thread/{threadId:long}/message/{messageId:long}/read"), Authorize] + public async Task ReadThreadMessage(long threadId, long messageId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return Ok(ChatResult(ThreadNotFound)); + + var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value); + if (!playerIds.Contains(userId.Value)) return Ok(ChatResult(MembershipNotFound)); + + if (messageId > thread.lastReadMessageId) + { + thread.lastReadMessageId = messageId; + await _db.SaveChangesAsync(); + } + + return Ok(ChatResult(Success)); + } + + [HttpPost("thread/{threadId:long}/read"), Authorize] + public async Task ReadThread(long threadId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + if (thread == null) return Ok(ChatResult(2)); + + var messageId = await ReadLongValue("messageId", "MessageId", "lastReadMessageId", "LastReadMessageId"); + if (messageId.HasValue) + { + thread.lastReadMessageId = messageId.Value; + await _db.SaveChangesAsync(); + } + + return Ok(ChatResult(0)); + } + + private async Task ReadChatThreadRequest(long userId) + { + var body = await ReadJsonBody(); + var playerIds = new List { userId }; + + AddIdsFromBody(body, playerIds); + AddIdsFromQuery(Request.Query, playerIds); + + var threadName = GetStringFromBody(body, "chatThreadName", "ChatThreadName", "threadName", "ThreadName", "name", "Name") ?? ""; + var messageContents = GetStringFromBody(body, "messageContents", "MessageContents", "contents", "Contents", "message", "Message") ?? ""; + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + AddIdsFromForm(form, playerIds); + threadName = GetStringFromForm(form, "chatThreadName", "ChatThreadName", "threadName", "ThreadName", "name", "Name") ?? threadName; + messageContents = GetStringFromForm(form, "messageContents", "MessageContents", "contents", "Contents", "message", "Message") ?? messageContents; + } + + var normalizedPlayerIds = playerIds + .Where(x => x > 0) + .Distinct() + .OrderBy(x => x) + .ToList(); + + return new ChatThreadRequest(normalizedPlayerIds, threadName, messageContents); + } + + private async Task FindOrCreateThread(List playerIds, string threadName, long callerId) + { + var thread = await FindThread(playerIds); + if (thread == null) + { + thread = new ChatThread + { + chatThreadName = string.IsNullOrWhiteSpace(threadName) ? "" : threadName, + lastReadMessageId = 0, + messages = "[]", + playerIds = playerIds.ToJson() + }; + + _db.Chats.Add(thread); + await _db.SaveChangesAsync(); + + var other = playerIds.FirstOrDefault(x => x != callerId); + await AddMessageToThread(thread, SystemSenderPlayerId, ChatStartedNotice(other != 0 ? other : callerId)); + } + + return thread; + } + + private static string MessageEnvelope(string text) => JsonSerializer.Serialize(new Dictionary + { + ["Type"] = 0, + ["Version"] = 1, + ["Data"] = text + }, EnvelopeJsonOptions); + + private static readonly JsonSerializerOptions EnvelopeJsonOptions = new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + private static string ChatStartedNotice(long playerId) => MessageEnvelope($"Player <@U{playerId}> started a chat"); + + private async Task AddMessageToThread(ChatThread thread, long userId, string messageContents) + { + messageContents = await ResolvePhotoPlaceholder(userId, messageContents); + + var messages = thread.GetMessages() ?? new List(); + var nextMessageId = messages.Count == 0 ? 1 : messages.Max(x => x.chatMessageId) + 1; + var message = new ChatMessage + { + chatMessageId = nextMessageId, + chatThreadeId = thread.chatThreadId, + senderPlayerId = userId, + timeSent = DateTime.UtcNow, + contents = messageContents + }; + + messages.Add(message); + thread.lastReadMessageId = nextMessageId; + thread.messages = messages.ToJson(); + await _db.SaveChangesAsync(); + + return message; + } + + private async Task ResolvePhotoPlaceholder(long userId, string messageContents) + { + if (userId <= 0) return messageContents; + if (!ChatPhotoMessages.IsPhotoEnvelope(messageContents)) return messageContents; + if (!ChatPhotoMessages.HasUnresolvedImageReference(messageContents)) return messageContents; + + string source; + long? imageId = null; + + if (RecentImageReferences.TryGet(userId, out var referenced)) + { + imageId = referenced; + source = "the image metadata it looked up"; + } + else + { + var cutoff = DateTime.Now - ChatPhotoMessages.UploadWindow; + var recentImage = await _db.Images.AsNoTracking() + .Where(image => image.OwnerId == userId && image.CreatedAt >= cutoff) + .OrderByDescending(image => image.CreatedAt) + .ThenByDescending(image => image.Id) + .FirstOrDefaultAsync(); + + imageId = recentImage?.Id; + source = $"its upload {recentImage?.Name}"; + } + + if (imageId == null) return messageContents; + + var resolved = ChatPhotoMessages.ResolveImageReference(messageContents, imageId.Value); + + return resolved; + } + + private async Task NotifyChatMessage(ChatThread thread, ChatMessage message) + { + var notificationMessage = ToMessageResponse(message); + var playerIds = (thread.GetPlayerIds() ?? new List()) + .Where(x => x > 0) + .Distinct() + .ToList(); + + foreach (var playerId in playerIds) + { + await Notifications.Notify.SendNotify(playerId, new Notification + { + Id = nameof(NotificationType.ChatMessageReceived), + Msg = notificationMessage + }); + } + } + + private async Task ReadJsonBody() + { + if (Request.ContentLength == 0) return null; + if (Request.ContentType?.Contains("json", StringComparison.OrdinalIgnoreCase) != true) return null; + + try + { + return await JsonSerializer.DeserializeAsync(Request.Body); + } + catch + { + return null; + } + } + + private async Task ReadBoolValue(params string[] keys) + { + var body = await ReadJsonBody(); + var bodyValue = GetBoolFromBody(body, keys); + if (bodyValue.HasValue) return bodyValue.Value; + + foreach (var key in keys) + { + if (Request.Query.TryGetValue(key, out var queryValue) && bool.TryParse(queryValue.FirstOrDefault(), out var queryBool)) + return queryBool; + } + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var key in keys) + { + if (form.TryGetValue(key, out var formValue) && bool.TryParse(formValue.FirstOrDefault(), out var formBool)) + return formBool; + } + } + + return false; + } + + private async Task ReadLongValue(params string[] keys) + { + var body = await ReadJsonBody(); + var bodyValue = GetLongFromBody(body, keys); + if (bodyValue.HasValue) return bodyValue.Value; + + foreach (var key in keys) + { + if (Request.Query.TryGetValue(key, out var queryValue) && long.TryParse(queryValue.FirstOrDefault(), out var queryLong)) + return queryLong; + } + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var key in keys) + { + if (form.TryGetValue(key, out var formValue) && long.TryParse(formValue.FirstOrDefault(), out var formLong)) + return formLong; + } + } + + return null; + } + + private async Task FindThread(List playerIds) + { + var threads = await _db.Chats.ToListAsync(); + return threads.FirstOrDefault(thread => + { + var existingPlayerIds = thread.GetPlayerIds() ?? new List(); + return existingPlayerIds + .Where(x => x > 0) + .Distinct() + .OrderBy(x => x) + .SequenceEqual(playerIds); + }); + } + + private static Dictionary ToResponse(ChatThread thread, long userId, int pageSize = DefaultMessageCount) + { + var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId); + var messages = PageMessages(thread.GetMessages() ?? new List(), QueryMode.Latest, null, pageSize); + + return ToResponse(thread, userId, messages, playerIds); + } + + private static Dictionary ToResponse(ChatThread thread, long userId, List messages, List playerIds) + { + playerIds = NormalizePlayerIds(playerIds, userId); + var messageResponses = ToMessageResponses(messages); + + return new Dictionary + { + ["chatThreadId"] = thread.chatThreadId, + ["playerIds"] = playerIds, + ["lastReadMessageId"] = thread.lastReadMessageId, + ["chatThreadName"] = ThreadName(thread), + ["chatThreadType"] = ChatThreadTypeDefault, + ["snoozedUntil"] = null, + ["isFavorited"] = thread.GetFavoritedBy().Contains(userId), + ["messages"] = messageResponses + }; + } + + private static Dictionary ToListResponse(ChatThread thread, ChatMessage? latestMessage, List playerIds, long viewerId) + { + var latest = latestMessage == null ? null : ToMessageResponse(latestMessage); + + return new Dictionary + { + ["latestMessage"] = latest, + ["chatThreadId"] = thread.chatThreadId, + ["playerIds"] = playerIds, + ["lastReadMessageId"] = thread.lastReadMessageId, + ["chatThreadName"] = ThreadName(thread), + ["chatThreadType"] = ChatThreadTypeDefault, + ["snoozedUntil"] = null, + ["isFavorited"] = thread.GetFavoritedBy().Contains(viewerId) + }; + } + + private static string MessageContents(string? contents) + { + var raw = contents ?? ""; + if (raw.Length == 0) return MessageEnvelope(""); + + var trimmed = raw.TrimStart(); + if (trimmed.StartsWith("{", StringComparison.Ordinal)) + { + try + { + using var parsed = System.Text.Json.JsonDocument.Parse(raw); + if (parsed.RootElement.ValueKind == JsonValueKind.Object) return raw; + } + catch (System.Text.Json.JsonException) + { + } + } + + return MessageEnvelope(raw); + } + + private static List NormalizePlayerIds(IEnumerable? playerIds, long userId) + { + var normalized = (playerIds ?? Enumerable.Empty()) + .Where(x => x > 0) + .Distinct() + .OrderBy(x => x) + .ToList(); + + if (normalized.Count == 0) + normalized.Add(userId); + + return normalized; + } + + private static string ThreadName(ChatThread thread) + => string.IsNullOrWhiteSpace(thread.chatThreadName) ? "" : thread.chatThreadName; + + private static List> ToMessageResponses(IEnumerable messages) + { + return messages.Select(ToMessageResponse).ToList(); + } + + private static Dictionary ToMessageResponse(ChatMessage message) + { + var contents = MessageContents(message.contents); + + return new Dictionary + { + ["chatMessageId"] = message.chatMessageId, + ["chatThreadId"] = message.chatThreadeId, + ["senderPlayerId"] = message.senderPlayerId, + ["timeSent"] = message.timeSent, + ["contents"] = contents, + ["moderationState"] = 0 + }; + } + + private static void AddIdsFromBody(JsonElement? body, List playerIds) + { + if (!body.HasValue || body.Value.ValueKind != JsonValueKind.Object) return; + + foreach (var key in new[] { "ids", "Ids", "playerIds", "PlayerIds", "recipientPlayerIds", "RecipientPlayerIds", "memberIds", "MemberIds", "memberPlayerIds", "MemberPlayerIds", "playerId", "PlayerId", "toPlayerId", "ToPlayerId" }) + { + if (body.Value.TryGetProperty(key, out var value)) + AddIdsFromJson(value, playerIds); + } + } + + private static void AddIdsFromJson(JsonElement value, List playerIds) + { + switch (value.ValueKind) + { + case JsonValueKind.Array: + foreach (var item in value.EnumerateArray()) + AddIdsFromJson(item, playerIds); + break; + case JsonValueKind.Number: + if (value.TryGetInt64(out var id) && id > 0) + playerIds.Add(id); + break; + case JsonValueKind.String: + AddIdsFromStrings(new[] { value.GetString() ?? "" }, playerIds); + break; + } + } + + private static void AddIdsFromQuery(IQueryCollection query, List playerIds) + { + foreach (var key in new[] { "ids", "Ids", "playerIds", "PlayerIds", "recipientPlayerIds", "RecipientPlayerIds", "memberIds", "MemberIds", "memberPlayerIds", "MemberPlayerIds", "playerId", "PlayerId", "toPlayerId", "ToPlayerId" }) + { + if (query.TryGetValue(key, out var values)) + AddIdsFromStrings(values, playerIds); + } + } + + private static void AddIdsFromForm(IFormCollection form, List playerIds) + { + foreach (var key in new[] { "ids", "Ids", "playerIds", "PlayerIds", "recipientPlayerIds", "RecipientPlayerIds", "memberIds", "MemberIds", "memberPlayerIds", "MemberPlayerIds", "playerId", "PlayerId", "toPlayerId", "ToPlayerId" }) + { + if (form.TryGetValue(key, out var values)) + AddIdsFromStrings(values, playerIds); + } + } + + private static void AddIdsFromStrings(IEnumerable values, List playerIds) + { + foreach (var value in values) + { + if (string.IsNullOrWhiteSpace(value)) continue; + + foreach (var part in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var clean = part.Trim('[', ']', '"', '\''); + if (long.TryParse(clean, out var id) && id > 0) + playerIds.Add(id); + } + } + } + + private static string? GetStringFromBody(JsonElement? body, params string[] keys) + { + if (!body.HasValue || body.Value.ValueKind != JsonValueKind.Object) return null; + + foreach (var key in keys) + { + if (body.Value.TryGetProperty(key, out var value) && value.ValueKind == JsonValueKind.String) + return value.GetString(); + } + + return null; + } + + private static bool? GetBoolFromBody(JsonElement? body, params string[] keys) + { + if (!body.HasValue || body.Value.ValueKind != JsonValueKind.Object) return null; + + foreach (var key in keys) + { + if (!body.Value.TryGetProperty(key, out var value)) continue; + if (value.ValueKind == JsonValueKind.True) return true; + if (value.ValueKind == JsonValueKind.False) return false; + if (value.ValueKind == JsonValueKind.String && bool.TryParse(value.GetString(), out var parsed)) + return parsed; + } + + return null; + } + + private static long? GetLongFromBody(JsonElement? body, params string[] keys) + { + if (!body.HasValue || body.Value.ValueKind != JsonValueKind.Object) return null; + + foreach (var key in keys) + { + if (!body.Value.TryGetProperty(key, out var value)) continue; + if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var parsedNumber)) + return parsedNumber; + if (value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out var parsedString)) + return parsedString; + } + + return null; + } + + private static string? GetStringFromForm(IFormCollection form, params string[] keys) + { + foreach (var key in keys) + { + if (form.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value.FirstOrDefault())) + return value.FirstOrDefault(); + } + + return null; + } + + private static int ChatResult(int result) => result; + + private int ReadMessageCount(int fallback) + { + foreach (var key in new[] { "MessageCount", "messageCount", "count", "Count" }) + { + if (Request.Query.TryGetValue(key, out var values) && int.TryParse(values.FirstOrDefault(), out var parsed)) + return Math.Clamp(parsed, 1, MaxMessageCount); + } + + return Math.Clamp(fallback, 1, MaxMessageCount); + } + + private (QueryMode Mode, long? MessageId) ReadQueryWindow() + { + var mode = QueryMode.Latest; + foreach (var key in new[] { "Mode", "mode" }) + { + if (Request.Query.TryGetValue(key, out var values) && TryParseQueryMode(values.FirstOrDefault(), out var parsed)) + { + mode = parsed; + break; + } + } + + long? messageId = null; + foreach (var key in new[] { "ReferenceMessageId", "referenceMessageId", "MessageId", "messageId", "LastMessageId", "lastMessageId" }) + { + if (Request.Query.TryGetValue(key, out var values) && long.TryParse(values.FirstOrDefault(), out var parsed)) + { + messageId = parsed; + break; + } + } + + return (mode, messageId); + } + + private static bool TryParseQueryMode(string? raw, out QueryMode mode) + { + mode = QueryMode.Latest; + if (string.IsNullOrWhiteSpace(raw)) return false; + + if (int.TryParse(raw, out var numeric) && Enum.IsDefined(typeof(QueryMode), numeric)) + { + mode = (QueryMode)numeric; + return true; + } + + return Enum.TryParse(raw, ignoreCase: true, out mode); + } + + private static List PageMessages(List stored, QueryMode mode, long? messageId, int pageSize) + { + var window = mode switch + { + QueryMode.NewerThan when messageId.HasValue => stored.Where(m => m.chatMessageId > messageId.Value), + QueryMode.OlderThan when messageId.HasValue => stored.Where(m => m.chatMessageId < messageId.Value), + _ => stored + }; + + return window + .OrderByDescending(m => m.chatMessageId) + .Take(Math.Clamp(pageSize, 1, MaxMessageCount)) + .ToList(); + } + + private sealed record ChatThreadRequest(List PlayerIds, string ThreadName, string MessageContents); + } +} diff --git a/Server/Clubs/AnnouncementAPI.cs b/Server/Clubs/AnnouncementAPI.cs new file mode 100644 index 0000000..a192f85 --- /dev/null +++ b/Server/Clubs/AnnouncementAPI.cs @@ -0,0 +1,82 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.API +{ + [Route("api/announcement")] + public class AnnouncementAPI : Controller + { + RecDBContext db; + public AnnouncementAPI(RecDBContext context) + { + this.db = context; + } + + [HttpGet("v1/get")] + public List? GetAnnouncement() + { + var config = db.ServerConfig.FirstOrDefault(x => x.Key == "Announcement"); + if (config == null || string.IsNullOrWhiteSpace(config.Value) || config.Value == "[]") + return new List(); + + try + { + var announcements = config.Value.FromJson>() ?? new List(); + + + + var raw = System.Text.Json.JsonDocument.Parse(config.Value); + if (raw.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array) + { + var now = DateTime.UtcNow; + var keep = new List(); + int i = 0; + foreach (var element in raw.RootElement.EnumerateArray()) + { + var ann = i < announcements.Count ? announcements[i] : null; + if (ann != null) + { + if (element.TryGetProperty("type", out var typeProp) && typeProp.ValueKind == System.Text.Json.JsonValueKind.String && + string.Equals(typeProp.GetString(), "Game Update", StringComparison.OrdinalIgnoreCase)) + { + i++; + continue; + } + string? expiresAt = null; + if (element.TryGetProperty("expires_at", out var exp) && exp.ValueKind == System.Text.Json.JsonValueKind.String) + expiresAt = exp.GetString(); + + if (string.IsNullOrEmpty(expiresAt) || !DateTime.TryParse(expiresAt, out var expiry) || expiry > now) + keep.Add(ann); + } + i++; + } + announcements = keep; + } + + + announcements = announcements.OrderByDescending(x => x.CreatedAt).ToList(); + foreach (var ann in announcements) + { + ann.IsMostRecent = false; + + + + ann.Platform = Platforms.All; + } + if (announcements.Count > 0) + announcements[0].IsMostRecent = true; + + return announcements; + } + catch + { + return new List(); + } + } + } +} \ No newline at end of file diff --git a/Server/Clubs/ClubAnnouncementsAPI.cs b/Server/Clubs/ClubAnnouncementsAPI.cs new file mode 100644 index 0000000..64f43fe --- /dev/null +++ b/Server/Clubs/ClubAnnouncementsAPI.cs @@ -0,0 +1,280 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.Clubs.Announcements +{ + [Route("clubs/announcements/")] + [Route("api/clubs/v1/announcements/")] + [Route("api/clubs/v2/announcements/")] + public class ClubAnnouncementsAPI : Controller + { + private const string StorageKey = "ClubAnnouncements"; + private static readonly ConcurrentDictionary> Store = new(); + + private sealed class ClubAnnouncementRecord + { + public long AnnouncementId { get; set; } + public long ClubId { get; set; } + public long AccountId { get; set; } + public string Title { get; set; } = string.Empty; + public string Body { get; set; } = string.Empty; + public string ImageName { get; set; } = string.Empty; + public string Meta { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + } + + private readonly RecDBContext? db; + + public ClubAnnouncementsAPI(RecDBContext? db = null) + { + this.db = db; + } + + [HttpGet("v2/subscription/mine/unread")] + [HttpGet("subscription/mine/unread")] + public List GetUnreadSubscribedAnnouncements() + { + return []; + } + [HttpGet("v2/mine/unread")] + [HttpGet("mine/unread")] + public List GetUnreadAnnouncements() + { + return []; + } + + [HttpPost("club/{clubId}")] + [HttpPost("~/announcements/club/{clubId:long}")] + public async Task PostClubAnnouncements(long clubId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var (managedClubId, error) = await ResolveManagedClubAsync(clubId, userId.Value); + if (error != null) return error; + clubId = managedClubId; + + var fields = await ReadBodyFieldsAsync(); + + var announcement = new ClubAnnouncementRecord + { + AnnouncementId = NewAnnouncementId(), + ClubId = clubId, + AccountId = userId.Value, + Title = GetField(fields, "title", "Title") ?? string.Empty, + Body = GetField(fields, "body", "Body") ?? string.Empty, + ImageName = GetField(fields, "imageName", "ImageName", "image_name") ?? string.Empty, + Meta = GetField(fields, "meta", "Meta") ?? string.Empty, + CreatedAt = DateTime.UtcNow + }; + + Add(clubId, announcement); + await PersistAsync(clubId); + + return Ok(new Dictionary + { + ["error"] = string.Empty, + ["success"] = true, + ["value"] = announcement.AnnouncementId + }); + } + + [HttpGet("club/{clubId}")] + [HttpGet("~/announcements/club/{clubId:long}")] + public IActionResult GetClubAnnouncements(long clubId) + { + return Ok(BuildAnnouncementFeed(clubId)); + } + + [HttpDelete("club/{clubId:long}/{announcementId:long}")] + [HttpDelete("~/announcements/club/{clubId:long}/{announcementId:long}")] + public async Task DeleteClubAnnouncement(long clubId, long announcementId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var (managedClubId, error) = await ResolveManagedClubAsync(clubId, userId.Value); + if (error != null) return error; + clubId = managedClubId; + + if (!Store.TryGetValue(clubId, out var announcements)) return NotFound(); + + bool removed; + lock (announcements) + removed = announcements.RemoveAll(item => item.AnnouncementId == announcementId) > 0; + if (!removed) return NotFound(); + + await PersistAsync(clubId); + return Ok(new { error = string.Empty, success = true, value = announcementId }); + } + + private async Task<(long ClubId, IActionResult? Error)> ResolveManagedClubAsync(long clubId, long userId) + { + if (clubId == 0 || PlayerSubscriptions.IsPersonalClubId(clubId)) + { + var personalClubId = PlayerSubscriptions.PersonalClubId(userId); + if (clubId != 0 && clubId != personalClubId) + return (clubId, StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null })); + + return (personalClubId, null); + } + + if (db == null) return (clubId, StatusCode(StatusCodes.Status500InternalServerError)); + + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return (clubId, NotFound()); + if (ClubStore.GetMembershipType(club, userId) < ClubMembershipTypes.Coowner) + return (clubId, StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null })); + + return (clubId, null); + } + + private void Add(long clubId, ClubAnnouncementRecord announcement) + { + var list = Store.GetOrAdd(clubId, _ => new List()); + lock (list) list.Insert(0, announcement); + } + + private static long NewAnnouncementId() + { + return Math.Abs((long)HashCode.Combine(Guid.NewGuid(), Environment.TickCount64)); + } + + [HttpPost("club/{clubId}/announcement/{announcementId}/read")] + [HttpPost("~/announcements/club/{clubId:long}/{announcementId:long}/read")] + [HttpPost("~/clubs/announcements/club/{clubId:long}/{announcementId:long}/read")] + [HttpPost("~/api/clubs/v1/announcements/club/{clubId:long}/{announcementId:long}/read")] + [HttpPost("~/api/clubs/v2/announcements/club/{clubId:long}/{announcementId:long}/read")] + public IActionResult MarkAnnouncementRead(long clubId, long announcementId) + { + if (User.GetUserId() == null) return Unauthorized(); + return Ok(new { error = string.Empty, success = true, value = announcementId }); + } + + private async Task PersistAsync(long clubId) + { + if (db == null) return; + try + { + var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == StorageKey); + if (config == null) + { + config = new ServerConfigVal(StorageKey, "{}"); + await db.ServerConfig.AddAsync(config); + } + var snapshot = Store.ToDictionary( + kv => kv.Key.ToString(), + kv => SerializeAnnouncements(kv.Value)); + config.Value = JsonSerializer.Serialize(snapshot); + await db.SaveChangesAsync(); + } + catch (Exception) + { + } + } + + private static List SerializeAnnouncements(IReadOnlyList announcements) + { + return announcements.Select(announcement => (object)new Dictionary + { + ["AnnouncementId"] = announcement.AnnouncementId, + ["ClubId"] = announcement.ClubId, + ["AccountId"] = announcement.AccountId, + ["Title"] = announcement.Title, + ["Body"] = announcement.Body, + ["ImageName"] = announcement.ImageName, + ["Meta"] = announcement.Meta, + ["CreatedAt"] = announcement.CreatedAt + }).ToList(); + } + + private static object BuildAnnouncementFeed(long clubId) + { + var announcements = Store.TryGetValue(clubId, out var list) + ? SerializeAnnouncements(list) + : new List(); + var lastAnnouncementId = announcements.Count > 0 + ? (long?)((Dictionary)announcements[0])["AnnouncementId"] + : null; + + return new Dictionary + { + ["error"] = string.Empty, + ["success"] = true, + ["value"] = new Dictionary + { + ["Announcements"] = announcements, + ["ClubId"] = clubId, + ["LastAnnouncementId"] = lastAnnouncementId, + ["LastReadAnnouncementId"] = 0L + } + }; + } + + private static string? GetField(Dictionary fields, params string[] names) + { + foreach (var name in names) + { + if (fields.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)) + return value; + } + return null; + } + + private async Task> ReadBodyFieldsAsync() + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var kv in form) + fields[kv.Key] = kv.Value.ToString(); + return fields; + } + + if (Request.ContentLength is not > 0) return fields; + + try + { + using var reader = new StreamReader(Request.Body); + var body = await reader.ReadToEndAsync(); + if (string.IsNullOrWhiteSpace(body)) return fields; + + var trimmed = body.TrimStart(); + if (trimmed.StartsWith("{")) + { + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in doc.RootElement.EnumerateObject()) + { + fields[prop.Name] = prop.Value.ValueKind == JsonValueKind.String + ? prop.Value.GetString() ?? "" + : prop.Value.ToString(); + } + } + } + else + { + foreach (var pair in trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var kv = pair.Split('=', 2); + if (kv.Length < 2) continue; + fields[Uri.UnescapeDataString(kv[0])] = Uri.UnescapeDataString(kv[1].Replace('+', ' ')); + } + } + } + catch + { + } + + return fields; + } + } +} diff --git a/Server/Clubs/ClubStore.cs b/Server/Clubs/ClubStore.cs new file mode 100644 index 0000000..ac9b90a --- /dev/null +++ b/Server/Clubs/ClubStore.cs @@ -0,0 +1,782 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.CDN; +using RadicadeServer.RecNet.Enums; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace RadicadeServer.Clubs; + +public sealed class ClubRecord +{ + public long clubId { get; set; } + public string name { get; set; } = "Radicade Club"; + public string description { get; set; } = ""; + public string primaryTag { get; set; } = "Social"; + public string imageName { get; set; } = "default"; + public string bannerImageName { get; set; } = "default"; + public long ownerPlayerId { get; set; } + public DateTime createdAt { get; set; } = DateTime.UtcNow; + public long? clubhouseRoomId { get; set; } + public int visibility { get; set; } = 1; + public int joinability { get; set; } = 0; + public bool allowJuniors { get; set; } = true; + public int minLevel { get; set; } + public string[] customTags { get; set; } = Array.Empty(); + public string[] additionalImages { get; set; } = Array.Empty(); + public List members { get; set; } = new(); +} + +public sealed class ClubMemberRecord +{ + public long accountId { get; set; } + public int membershipType { get; set; } + public int pendingMembershipType { get; set; } + public long inviterAccountId { get; set; } + public DateTime createdAt { get; set; } = DateTime.UtcNow; +} + +public static class ClubMembershipTypes +{ + public const int Banned = -1; + public const int None = 0; + public const int PendingRequested = 1; + public const int PendingInvited = 2; + public const int PendingDenied = 3; + public const int Member = 10; + public const int Moderator = 20; + public const int Coowner = 30; + public const int Creator = 100; +} + +public sealed record ClubMemberView(long AccountId, int MembershipType, long InviterAccountId, DateTime CreatedAt); + +public static class ClubStore +{ + private const string ConfigKey = "Clubs"; + + public const string DefaultImageName = "DefaultImgPurple"; + private const int ActiveMemberThreshold = ClubMembershipTypes.Member; + + private static readonly string[] CategoryQueryKeys = + { + "category", "Category", "categoryTag", "CategoryTag", "categoryName", "CategoryName", + "primaryTag", "PrimaryTag", "tag", "Tag", "tags", "Tags" + }; + + public static string? CategoryFilter(IQueryCollection query) + { + foreach (var key in CategoryQueryKeys) + { + var value = query[key].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(value)) return value.Trim().TrimStart('#'); + } + + return null; + } + + public static bool MatchesCategory(ClubRecord? club, string? category) + { + if (club == null) return false; + + var wanted = category?.Trim().TrimStart('#'); + if (string.IsNullOrWhiteSpace(wanted)) return true; + + return string.Equals(club.primaryTag?.Trim(), wanted, StringComparison.OrdinalIgnoreCase) + || (club.customTags ?? Array.Empty()) + .Any(tag => string.Equals(tag?.Trim(), wanted, StringComparison.OrdinalIgnoreCase)); + } + + public static List FilterByCategory(List clubs, string? category) + => string.IsNullOrWhiteSpace(category) + ? clubs + : clubs.Where(club => MatchesCategory(club, category)).ToList(); + + public static async Task> LoadAsync(RecDBContext db) + { + var config = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == ConfigKey); + if (config == null || string.IsNullOrWhiteSpace(config.Value)) return new List(); + + try + { + var clubs = config.Value.FromJson>() ?? new List(); + foreach (var club in clubs) + { + club.members ??= new List(); + club.customTags ??= Array.Empty(); + club.additionalImages ??= Array.Empty(); + } + return clubs; + } + catch + { + return new List(); + } + } + + private static readonly string[] HomeClubConfigPrefixes = ["ClubHome:", "ClubHome.", "HomeClub:"]; + + private static string HomeClubConfigKey(long accountId) => $"ClubHome:{accountId}"; + + private static async Task GetHomeClubIdAsync(RecDBContext db, long accountId) + { + var keys = HomeClubConfigPrefixes.Select(prefix => prefix + accountId.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToArray(); + var value = await db.ServerConfig.AsNoTracking() + .Where(config => keys.Contains(config.Key)) + .Select(config => config.Value) + .FirstOrDefaultAsync(); + return long.TryParse(value, out var clubId) && clubId > 0 ? clubId : null; + } + + public static async Task GetHomeClubAsync(RecDBContext db, long accountId) + { + var clubId = await GetHomeClubIdAsync(db, accountId); + if (!clubId.HasValue) return null; + var club = await GetAsync(db, clubId.Value); + return club?.clubhouseRoomId is > 0 ? club : null; + } + + public static async Task SetHomeClubAsync(RecDBContext db, long accountId, long? clubId) + { + var configKey = HomeClubConfigKey(accountId); + var config = await db.ServerConfig.FirstOrDefaultAsync(row => row.Key == configKey); + if (clubId is > 0) + { + if (config == null) + { + config = new ServerConfigVal(configKey, clubId.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + await db.ServerConfig.AddAsync(config); + } + else + { + config.Value = clubId.Value.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + } + else if (config != null) + { + db.ServerConfig.Remove(config); + } + + await db.SaveChangesAsync(); + return clubId is > 0 ? await GetAsync(db, clubId.Value) : null; + } + + public static async Task ClearHomeClubsAsync(RecDBContext db, long clubId) + { + var clubIdValue = clubId.ToString(System.Globalization.CultureInfo.InvariantCulture); + var matching = await db.ServerConfig.Where(config => config.Value == clubIdValue).ToListAsync(); + matching.RemoveAll(config => !HomeClubConfigPrefixes.Any(prefix => config.Key.StartsWith(prefix, StringComparison.Ordinal))); + if (matching.Count == 0) return; + db.ServerConfig.RemoveRange(matching); + await db.SaveChangesAsync(); + } + + public static async Task DeleteAsync(RecDBContext db, long clubId) + { + var clubs = await LoadAsync(db); + if (clubs.RemoveAll(club => club.clubId == clubId) == 0) return false; + await SaveAsync(db, clubs); + await ClearHomeClubsAsync(db, clubId); + return true; + } + + public static async Task UpdateAsync(RecDBContext db, long clubId, JsonObject body) + { + var clubs = await LoadAsync(db); + var club = clubs.FirstOrDefault(item => item.clubId == clubId); + if (club == null) return null; + + if (GetNonEmptyString(body, "name") is { } name) club.name = name.Trim(); + if (GetNonEmptyString(body, "description") is { } description) club.description = description; + if (GetNonEmptyString(body, "category", "primaryTag") is { } category) club.primaryTag = category.Trim(); + if (GetNonEmptyString(body, "mainImageName") is { } imageName) club.imageName = imageName.Trim(); + if (GetNonEmptyString(body, "bannerImageName") is { } bannerName) club.bannerImageName = bannerName.Trim(); + if (GetInt(body, "visibility") is { } visibility) club.visibility = visibility; + if (GetInt(body, "joinability") is { } joinability) club.joinability = joinability; + if (GetInt(body, "minLevel") is { } minLevel && minLevel >= 0) club.minLevel = minLevel; + if (GetBool(body, "allowJuniors") is { } allowJuniors) club.allowJuniors = allowJuniors; + if (GetStringArray(body, "customTags") is { } customTags) club.customTags = NormalizeTags(customTags); + + await SaveAsync(db, clubs); + return club; + } + + public static async Task SetClubhouseAsync(RecDBContext db, long clubId, long? roomId) + { + var clubs = await LoadAsync(db); + var club = clubs.FirstOrDefault(item => item.clubId == clubId); + if (club == null) return null; + club.clubhouseRoomId = roomId; + await SaveAsync(db, clubs); + return club; + } + + public static async Task SetAdditionalImageAsync(RecDBContext db, long clubId, int index, string imageName) + { + var clubs = await LoadAsync(db); + var club = clubs.FirstOrDefault(item => item.clubId == clubId); + if (club == null) return null; + + var images = (club.additionalImages ?? Array.Empty()).ToList(); + if (string.IsNullOrWhiteSpace(imageName)) + { + if (index < images.Count) images.RemoveAt(index); + } + else if (index < images.Count) + { + images[index] = imageName.Trim(); + } + else if (images.Count < 3) + { + images.Add(imageName.Trim()); + } + club.additionalImages = images.ToArray(); + await SaveAsync(db, clubs); + return club; + } + + public static async Task InviteMemberAsync(RecDBContext db, long clubId, long accountId, int membershipType, long inviterAccountId) + { + var clubs = await LoadAsync(db); + var club = clubs.FirstOrDefault(item => item.clubId == clubId); + if (club == null || club.ownerPlayerId == accountId) return null; + + club.members ??= new List(); + var membership = club.members.FirstOrDefault(item => item.accountId == accountId); + if (membership == null) + { + membership = new ClubMemberRecord { accountId = accountId, createdAt = DateTime.UtcNow }; + club.members.Add(membership); + } + + membership.membershipType = ClubMembershipTypes.PendingInvited; + membership.pendingMembershipType = membershipType; + membership.inviterAccountId = inviterAccountId; + await SaveAsync(db, clubs); + return club; + } + + public static async Task AcceptInvitationAsync(RecDBContext db, long clubId, long accountId) + { + var clubs = await LoadAsync(db); + var club = clubs.FirstOrDefault(item => item.clubId == clubId); + var membership = club?.members?.FirstOrDefault(item => item.accountId == accountId); + if (club == null || membership?.membershipType != ClubMembershipTypes.PendingInvited) return null; + + var grantedType = membership.pendingMembershipType is ClubMembershipTypes.Member or ClubMembershipTypes.Moderator or ClubMembershipTypes.Coowner + ? membership.pendingMembershipType + : ClubMembershipTypes.Member; + membership.membershipType = grantedType; + membership.pendingMembershipType = 0; + membership.createdAt = DateTime.UtcNow; + await SaveAsync(db, clubs); + return club; + } + + public static async Task RequestToJoinAsync(RecDBContext db, long clubId, long accountId) + { + var clubs = await LoadAsync(db); + var club = clubs.FirstOrDefault(item => item.clubId == clubId); + if (club == null) return null; + var current = GetMembershipType(club, accountId); + if (current == ClubMembershipTypes.Banned || current >= ClubMembershipTypes.Member || current == ClubMembershipTypes.PendingRequested) + return club; + if (club.joinability == 1) return club; + return await SetMemberTypeAsync(db, clubId, accountId, + club.joinability == 0 ? ClubMembershipTypes.Member : ClubMembershipTypes.PendingRequested, + accountId); + } + + private static string? GetString(JsonObject body, params string[] keys) + { + foreach (var pair in body) + { + if (!keys.Any(key => pair.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) || pair.Value == null) continue; + if (pair.Value is JsonValue jsonValue && jsonValue.TryGetValue(out var value)) return value; + return pair.Value.ToString(); + } + return null; + } + + private static string? GetNonEmptyString(JsonObject body, params string[] keys) + { + var value = GetString(body, keys); + return string.IsNullOrWhiteSpace(value) ? null : value; + } + + private static int? GetInt(JsonObject body, params string[] keys) + { + var raw = GetString(body, keys)?.Trim(); + if (int.TryParse(raw, out var value)) return value; + return raw?.ToLowerInvariant() switch + { + "private" => 0, + "public" => 1, + "open" => 0, + "inviteonly" or "invite_only" => 1, + "asktojoin" or "requesttojoin" or "ask_to_join" => 2, + _ => null + }; + } + + private static bool? GetBool(JsonObject body, params string[] keys) + { + var raw = GetString(body, keys)?.Trim(); + if (bool.TryParse(raw, out var value)) return value; + return raw?.ToLowerInvariant() switch + { + "1" or "yes" => true, + "0" or "no" => false, + _ => null + }; + } + + private static string[]? GetStringArray(JsonObject body, params string[] keys) + { + foreach (var pair in body) + { + if (!keys.Any(key => pair.Key.Equals(key, StringComparison.OrdinalIgnoreCase))) continue; + if (pair.Value is JsonArray array) + return NormalizeTags(array.Select(item => item?.ToString() ?? string.Empty)); + return NormalizeTags(new[] { pair.Value?.ToString() ?? string.Empty }); + } + return null; + } + + private static string[] NormalizeTags(IEnumerable tags) + { + var output = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var raw in tags) + { + var tag = raw.Trim(); + if (tag.Length > 0 && seen.Add(tag)) output.Add(tag); + } + return output.ToArray(); + } + + public static async Task SaveAsync(RecDBContext db, List clubs) + { + var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == ConfigKey); + if (config == null) + { + config = new ServerConfigVal(ConfigKey, "[]"); + await db.ServerConfig.AddAsync(config); + } + + config.Value = clubs.ToJson(); + await db.SaveChangesAsync(); + } + + public static async Task GetAsync(RecDBContext db, long clubId) + { + var clubs = await LoadAsync(db); + return clubs.FirstOrDefault(x => x.clubId == clubId); + } + + public static async Task CreateAsync( + RecDBContext db, + long ownerPlayerId, + string? name, + string? description, + string? primaryTag = null, + int visibility = 1, + int joinability = 0, + bool allowJuniors = true, + int minLevel = 0, + string? mainImageName = null) + { + var clubs = await LoadAsync(db); + var clubName = string.IsNullOrWhiteSpace(name) ? "Radicade Club" : name.Trim(); + var tag = string.IsNullOrWhiteSpace(primaryTag) ? "Social" : primaryTag.Trim(); + var now = DateTime.UtcNow; + var clubId = Math.Abs((long)HashCode.Combine(ownerPlayerId, clubName, now.Ticks)); + + while (clubId == 0 || clubs.Any(x => x.clubId == clubId)) + { + clubId = Math.Abs((long)HashCode.Combine(ownerPlayerId, clubName, Guid.NewGuid())); + } + + var club = new ClubRecord + { + clubId = clubId, + name = clubName, + description = description?.Trim() ?? "", + primaryTag = tag, + imageName = string.IsNullOrWhiteSpace(mainImageName) ? DefaultImageName : mainImageName.Trim(), + visibility = visibility, + joinability = joinability, + allowJuniors = allowJuniors, + minLevel = minLevel, + ownerPlayerId = ownerPlayerId, + createdAt = now + }; + club.members.Add(new ClubMemberRecord + { + accountId = ownerPlayerId, + membershipType = ClubMembershipTypes.Creator, + inviterAccountId = ownerPlayerId, + createdAt = now + }); + + clubs.Add(club); + await SaveAsync(db, clubs); + return club; + } + + public static int GetMembershipType(ClubRecord? club, long accountId) + { + if (club == null || club.clubId <= 0 || accountId <= 0) return ClubMembershipTypes.None; + if (club.ownerPlayerId == accountId) return ClubMembershipTypes.Creator; + return club.members?.FirstOrDefault(x => x.accountId == accountId)?.membershipType ?? ClubMembershipTypes.None; + } + + public static int GetMemberCount(ClubRecord? club) + { + if (club == null || club.clubId <= 0) return 0; + return 1 + (club.members ?? new List()) + .Where(x => x.accountId != club.ownerPlayerId && x.membershipType >= ActiveMemberThreshold) + .Select(x => x.accountId) + .Distinct() + .Count(); + } + + public static IReadOnlyList GetMembers(ClubRecord? club) + { + if (club == null || club.clubId <= 0) return Array.Empty(); + + var members = new List + { + new(club.ownerPlayerId, ClubMembershipTypes.Creator, club.ownerPlayerId, club.createdAt) + }; + members.AddRange((club.members ?? new List()) + .Where(x => x.accountId != club.ownerPlayerId) + .GroupBy(x => x.accountId) + .Select(group => group.Last()) + .Select(x => new ClubMemberView( + x.accountId, + x.membershipType, + x.inviterAccountId, + NormalizeMembershipDate(x.createdAt, club.createdAt)))); + return members; + } + + private static DateTime NormalizeMembershipDate(DateTime memberCreatedAt, DateTime clubCreatedAt) + { + if (memberCreatedAt > DateTime.MinValue) return memberCreatedAt; + if (clubCreatedAt > DateTime.MinValue) return clubCreatedAt; + return DateTime.UnixEpoch; + } + + public static async Task SetMemberTypeAsync( + RecDBContext db, + long clubId, + long accountId, + int membershipType, + long inviterAccountId) + { + var clubs = await LoadAsync(db); + var club = clubs.FirstOrDefault(x => x.clubId == clubId); + if (club == null || club.ownerPlayerId == accountId) return null; + + club.members ??= new List(); + var membership = club.members.FirstOrDefault(x => x.accountId == accountId); + if (membership == null) + { + club.members.Add(new ClubMemberRecord + { + accountId = accountId, + membershipType = membershipType, + inviterAccountId = inviterAccountId, + createdAt = DateTime.UtcNow + }); + } + else + { + if (membership.membershipType < ClubMembershipTypes.Member && membershipType >= ClubMembershipTypes.Member) + membership.createdAt = DateTime.UtcNow; + membership.membershipType = membershipType; + membership.inviterAccountId = inviterAccountId; + } + + await SaveAsync(db, clubs); + return club; + } + + public static async Task RemoveMemberAsync(RecDBContext db, long clubId, long accountId) + { + var clubs = await LoadAsync(db); + var club = clubs.FirstOrDefault(x => x.clubId == clubId); + if (club == null || club.ownerPlayerId == accountId) return null; + + club.members ??= new List(); + club.members.RemoveAll(member => member.accountId == accountId && member.membershipType != ClubMembershipTypes.Banned); + await SaveAsync(db, clubs); + return club; + } + + private static long ToUnix(DateTime value) => new DateTimeOffset(value).ToUnixTimeSeconds(); + + public static object BuildClub(ClubRecord? club, long viewerId = 0) + { + club ??= new ClubRecord { clubId = 0 }; + var memberCount = GetMemberCount(club); + + return new Dictionary + { + ["ClubId"] = club.clubId, + ["Name"] = club.name, + ["Description"] = club.description, + ["MainImageName"] = club.imageName, + ["State"] = 0, + ["CreatorAccountId"] = club.ownerPlayerId, + ["Category"] = club.primaryTag, + ["Visibility"] = club.visibility, + ["Joinability"] = club.joinability, + ["AllowJuniors"] = club.allowJuniors, + ["MinLevel"] = club.minLevel, + ["MemberCount"] = memberCount, + ["IsRRO"] = false, + ["ClubhouseRoomId"] = club.clubhouseRoomId, + ["ClubType"] = 0 + }; + } + + public static async Task BuildClubDetailsAsync(RecDBContext db, ClubRecord? club, long viewerId = 0) + { + club ??= new ClubRecord { clubId = 0 }; + var details = (Dictionary)BuildClubDetails(club, viewerId); + details["AdditionalImages"] = await GetAdditionalImagesAsync(db, club.additionalImages ?? Array.Empty()); + return details; + } + + public static object BuildClubDetails(ClubRecord? club, long viewerId = 0) + { + club ??= new ClubRecord { clubId = 0 }; + return new Dictionary + { + ["Club"] = BuildClub(club, viewerId), + ["ClubId"] = club.clubId, + ["CustomTags"] = club.customTags ?? Array.Empty(), + ["AdditionalImages"] = Array.Empty(), + ["CoownerPermissions"] = BuildPermissions(club.clubId, 0), + ["ModeratorPermissions"] = BuildPermissions(club.clubId, 1), + ["MemberPermissions"] = BuildPermissions(club.clubId, 2), + ["MyMembershipType"] = GetMembershipType(club, viewerId) + }; + } + + private static async Task GetAdditionalImagesAsync(RecDBContext db, string[] imageNames) + { + if (imageNames.Length == 0) return Array.Empty(); + var records = await db.Images.AsNoTracking() + .Where(image => imageNames.Contains(image.Name)) + .ToDictionaryAsync(image => image.Name); + var imageIds = records.Values.Select(image => image.Id).ToList(); + var cheers = await db.ImageInteractions.AsNoTracking() + .Where(interaction => interaction.IsCheered && imageIds.Contains(interaction.SavedImageId)) + .GroupBy(interaction => interaction.SavedImageId) + .Select(group => new { ImageId = group.Key, Count = group.Count() }) + .ToDictionaryAsync(group => group.ImageId, group => group.Count); + + return imageNames.Select(name => + { + if (!records.TryGetValue(name, out var image)) + return (object)new Dictionary + { + ["Id"] = 0L, ["Type"] = 1, + ["Accessibility"] = (int)SavedImageAccessibility.Public, + ["AccessibilityLocked"] = false, ["ImageName"] = name, + ["Description"] = null, ["PlayerId"] = 0L, + ["TaggedPlayerIds"] = Array.Empty(), ["RoomId"] = null, + ["PlayerEventId"] = null, ["CreatedAt"] = DateTime.UnixEpoch, + ["CheerCount"] = 0, ["CommentCount"] = 0 + }; + return (object)new Dictionary + { + ["Id"] = image.Id, ["Type"] = (int)image.ImageType, + ["Accessibility"] = (int)image.Accessibility, ["AccessibilityLocked"] = false, + ["ImageName"] = image.Name, ["Description"] = null, ["PlayerId"] = image.OwnerId, + ["TaggedPlayerIds"] = ParseImagePlayerIds(image.PlayerIds), ["RoomId"] = image.RoomId, + ["PlayerEventId"] = image.EventId, ["CreatedAt"] = image.CreatedAt, + ["CheerCount"] = cheers.GetValueOrDefault(image.Id), ["CommentCount"] = 0 + }; + }).ToArray(); + } + + private static long[] ParseImagePlayerIds(string? playerIds) + { + try { return JsonSerializer.Deserialize(playerIds ?? "[]") ?? Array.Empty(); } + catch { return Array.Empty(); } + } + + public static object BuildPermissions(long clubId, int type) + { + var isCoowner = type == 0; + var isModerator = type == 1; + return new Dictionary + { + ["Type"] = type switch + { + 0 => ClubMembershipTypes.Coowner, + 1 => ClubMembershipTypes.Moderator, + _ => ClubMembershipTypes.Member + }, + ["ClubId"] = clubId, + ["ApproveMember"] = isCoowner || isModerator, + ["BanUnban"] = isCoowner || isModerator, + ["CreateEvent"] = isCoowner, + ["EditDetails"] = isCoowner, + ["EditPermissionSettings"] = isCoowner, + ["PostAnnouncement"] = isCoowner + }; + } + + public static object BuildMemberListItem(long playerId, int role, bool isOwner, long clubId = 0, long created = 0) + { + var type = isOwner ? ClubMembershipTypes.Creator : role; + var createdAt = created > 0 ? DateTimeOffset.FromUnixTimeSeconds(created).UtcDateTime.ToString("O") : null; + return new Dictionary + { + ["ClubMemberId"] = playerId, + ["ClubId"] = clubId, + ["AccountId"] = playerId, + ["MembershipType"] = type, + ["CreatedAt"] = createdAt + }; + } + + public static object BuildMemberListItem(ClubMemberView member, long clubId) => + BuildMemberListItem( + member.AccountId, + member.MembershipType, + member.MembershipType == ClubMembershipTypes.Creator, + clubId, + ToUnix(member.CreatedAt)); + + public static object BuildMember(long playerId, int role, bool isOwner, long clubId = 0, long created = 0) => new Dictionary + { + ["AccountId"] = playerId, + ["ClubId"] = clubId, + ["CreatedAt"] = created > 0 ? DateTimeOffset.FromUnixTimeSeconds(created).UtcDateTime.ToString("O") : null, + ["MembershipType"] = isOwner ? ClubMembershipTypes.Creator : role, + ["membershipRequestId"] = 0L, + ["inviterAccountId"] = 0L, + ["subscriptionId"] = clubId, + ["playerId"] = playerId, + ["username"] = string.Empty, + ["displayName"] = string.Empty, + ["level"] = 0, + ["role"] = isOwner ? ClubMembershipTypes.Creator : role, + ["membershipStatus"] = role >= ActiveMemberThreshold || isOwner ? 1 : 0, + ["isOwner"] = isOwner, + ["created"] = created, + ["createdAt"] = created + }; + + public static object BuildMember(ClubMemberView membership, ClubRecord? club, Profile? profile) + { + var role = membership.MembershipType; + var created = ToUnix(membership.CreatedAt); + var isOwner = role == ClubMembershipTypes.Creator; + return new Dictionary + { + ["AccountId"] = membership.AccountId, + ["ClubId"] = club?.clubId ?? 0L, + ["CreatedAt"] = membership.CreatedAt.ToString("O"), + ["MembershipType"] = role, + ["membershipRequestId"] = 0L, + ["inviterAccountId"] = membership.InviterAccountId, + ["subscriptionId"] = club?.clubId ?? 0L, + ["playerId"] = membership.AccountId, + ["username"] = profile?.username ?? string.Empty, + ["displayName"] = profile?.displayName ?? string.Empty, + ["level"] = 0, + ["role"] = role, + ["membershipStatus"] = role >= ActiveMemberThreshold || isOwner ? 1 : 0, + ["isOwner"] = isOwner, + ["created"] = created, + ["createdAt"] = created + }; + } + + public static object BuildMembership(ClubRecord? club, long viewerId = 0) + { + club ??= new ClubRecord { clubId = 0 }; + var role = GetMembershipType(club, viewerId); + var isMember = role >= ActiveMemberThreshold; + + return new Dictionary + { + ["accountId"] = viewerId, + ["clubId"] = club.clubId, + ["creatorAccountId"] = club.ownerPlayerId, + ["membershipType"] = role, + ["LastAnnouncementId"] = 0L, + ["LastReadAnnouncementId"] = 0L, + ["announcements"] = Array.Empty(), + ["subscriptionId"] = club.clubId + }; + } + + public static object BuildSubscriptionEnvelope(ClubRecord? club, long viewerId = 0) => new Dictionary + { + ["success"] = club != null, + ["error"] = string.Empty, + ["value"] = club == null ? null : BuildMembership(club, viewerId) + }; + + public static object BuildSearchResponse(object[] clubs, int skip, int take) => new Dictionary + { + ["Clubs"] = clubs, + ["TotalClubs"] = clubs.Length, + ["ContinuationToken"] = null + }; + + public static object BuildHome(ClubRecord? homeClub, long viewerId, IReadOnlyList allClubs, IReadOnlyList mine) + { + return BuildClub(homeClub, viewerId); + } + + public static object BuildEmptyHome(long viewerId) + { + return BuildClub(null, viewerId); + } + + public static object BuildModerationBlockDetails() => new Dictionary + { + ["Duration"] = 0L, + ["GameSessionId"] = 0L, + ["PlayerIdReporter"] = 0L, + ["IsBan"] = false, + ["IsHostKick"] = false, + ["Message"] = string.Empty, + ["ReportCategory"] = 0, + ["VoteKickReason"] = string.Empty, + ["bannedUntil"] = string.Empty, + ["displayReason"] = string.Empty, + ["banType"] = string.Empty, + ["banAppealCode"] = string.Empty, + ["banAppealText"] = string.Empty + }; + + public static object BuildEligibility() => new Dictionary + { + ["canCreateClub"] = true, + ["canCreate"] = true, + ["creationAllowed"] = true, + ["isAllowed"] = true, + ["isCreationRestricted"] = false, + ["creationRestricted"] = false, + ["moderationRestricted"] = false, + ["moderationIncidentCount"] = 0, + ["myCreatedClubsCount"] = 0, + ["maxCreatedClubs"] = 100, + ["cooldownSeconds"] = 0, + ["restrictionReason"] = string.Empty + }; +} diff --git a/Server/Clubs/ClubSubscriptionAPI.cs b/Server/Clubs/ClubSubscriptionAPI.cs new file mode 100644 index 0000000..7e0d1ac --- /dev/null +++ b/Server/Clubs/ClubSubscriptionAPI.cs @@ -0,0 +1,183 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using System.Text.Json; + +namespace RadicadeServer.Clubs; + +[Route("clubs/subscription")] +[Route("clubs/subscriptions")] +[Route("api/clubs/v1/subscription")] +[Route("api/clubs/v2/subscription")] +[Route("api/clubs/v1/subscriptions")] +[Route("api/clubs/v2/subscriptions")] +public class ClubSubscriptionAPI(RecDBContext db) : Controller +{ + private static readonly string[] TargetIdKeys = + { + "playerId", "PlayerId", "accountId", "AccountId", "creatorAccountId", "CreatorAccountId", + "subscriptionId", "SubscriptionId", "id", "Id", "targetPlayerId", "TargetPlayerId" + }; + + [HttpGet("mine/member")] + [HttpGet("my/member")] + public IActionResult GetMyMemberships() => Ok(Array.Empty()); + + [HttpGet("my/subscriptions")] + public async Task GetMySubscriptions() + { + var userId = User.GetUserId(); + if (userId == null) return Ok(Array.Empty()); + + return Ok(await PlayerSubscriptions.SubscriptionIdsAsync(db, userId.Value)); + } + + [HttpGet("my/subscribers")] + public async Task GetMySubscribers() + { + var userId = User.GetUserId(); + if (userId == null) return Ok(Array.Empty()); + + return Ok(await PlayerSubscriptions.SubscriberIdsAsync(db, userId.Value)); + } + + [HttpGet("details/{accountId:long}")] + public async Task GetAccountSubscriptionDetails(long accountId) => Ok(new + { + clubId = PlayerSubscriptions.PersonalClubId(accountId), + accountId, + subscriberCount = await PlayerSubscriptions.SubscriberCountAsync(db, accountId) + }); + + [HttpGet("details/{subscription}")] + public IActionResult GetNamedSubscriptionDetails(string subscription) => Ok(new { }); + + [HttpGet("subscriberCount/{accountId:long}")] + public async Task GetSubscriberCount(long accountId) + => Ok(await PlayerSubscriptions.SubscriberCountAsync(db, accountId)); + + [HttpGet("top/creators")] + public async Task GetTopCreators() => Ok(await PlayerSubscriptions.TopCreatorsAsync(db, 100)); + + [HttpGet("top/creators/count")] + public async Task GetTopCreatorCount() + => Ok((await PlayerSubscriptions.TopCreatorsAsync(db, int.MaxValue)).Count); + + [HttpPost("{playerId:long}"), Authorize] + public async Task Subscribe(long playerId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + if (playerId > 0) + await PlayerSubscriptions.SubscribeAsync(db, userId.Value, playerId); + + return Ok(new + { + value = PlayerSubscriptions.PersonalClubId(playerId), + success = true, + errorId = (object?)null, + error = (object?)null + }); + } + + [HttpPost(""), Authorize] + public async Task SubscribeBare() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var playerId = await ReadTargetPlayerIdAsync(); + if (playerId > 0) + await PlayerSubscriptions.SubscribeAsync(db, userId.Value, playerId); + + return Ok(new + { + value = PlayerSubscriptions.PersonalClubId(playerId), + success = true, + errorId = (object?)null, + error = (object?)null + }); + } + + [HttpDelete("{playerId:long}"), Authorize] + [HttpPost("{playerId:long}/unsubscribe"), Authorize] + public async Task Unsubscribe(long playerId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + if (playerId > 0) + await PlayerSubscriptions.UnsubscribeAsync(db, userId.Value, playerId); + + return Ok(new { success = true, errorId = (object?)null, error = (object?)null }); + } + + [HttpDelete(""), Authorize] + public async Task UnsubscribeBare() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var playerId = await ReadTargetPlayerIdAsync(); + if (playerId > 0) + await PlayerSubscriptions.UnsubscribeAsync(db, userId.Value, playerId); + + return Ok(new { success = true, errorId = (object?)null, error = (object?)null }); + } + + private async Task ReadTargetPlayerIdAsync() + { + foreach (var key in TargetIdKeys) + { + var value = Request.Query[key].FirstOrDefault(); + if (long.TryParse(value, out var fromQuery) && fromQuery > 0) return fromQuery; + } + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var key in TargetIdKeys) + { + var value = form[key].FirstOrDefault(); + if (long.TryParse(value, out var fromForm) && fromForm > 0) return fromForm; + } + return 0; + } + + if (Request.ContentLength is not > 0) return 0; + + try + { + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + if (Request.Body.CanSeek) Request.Body.Position = 0; + if (string.IsNullOrWhiteSpace(body)) return 0; + + var trimmed = body.Trim(); + if (trimmed.StartsWith("{")) + { + using var document = JsonDocument.Parse(trimmed); + if (document.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var property in document.RootElement.EnumerateObject()) + { + if (!TargetIdKeys.Any(key => key.Equals(property.Name, StringComparison.OrdinalIgnoreCase))) continue; + if (long.TryParse(property.Value.ToString(), out var fromBody) && fromBody > 0) return fromBody; + } + } + } + else if (long.TryParse(trimmed, out var fromRaw) && fromRaw > 0) + { + return fromRaw; + } + } + catch + { + } + + return 0; + } +} diff --git a/Server/Clubs/ClubsAPI.cs b/Server/Clubs/ClubsAPI.cs new file mode 100644 index 0000000..5dc0898 --- /dev/null +++ b/Server/Clubs/ClubsAPI.cs @@ -0,0 +1,935 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using System.Text.Json.Nodes; + + +namespace RadicadeServer.Clubs; + +[Route("clubs/club")] +[Route("api/clubs/v1/club")] +[Route("api/clubs/v2/club")] +public class ClubsAPI : Controller +{ + private readonly RecDBContext db; + + public ClubsAPI(RecDBContext db) + { + this.db = db; + } + + private long ViewerId => User.GetUserId() ?? 0; + + [HttpGet("home/me"), Authorize] + [HttpGet("~/club/home/me"), Authorize] + [HttpGet("~/api/clubs/v1/home/me"), Authorize] + [HttpGet("~/api/clubs/v2/home/me"), Authorize] + public async Task GetMyClubHome() + { + var homeClub = await ClubStore.GetHomeClubAsync(db, ViewerId); + return homeClub == null + ? NotFound() + : Ok(ClubStore.BuildClub(homeClub, ViewerId)); + } + + [HttpPut("~/clubs/club/home/me"), Authorize] + [HttpPut("~/club/home/me"), Authorize] + [HttpPut("~/api/clubs/v1/home/me"), Authorize] + [HttpPut("~/api/clubs/v2/home/me"), Authorize] + public async Task SetMyClubHome() + { + var body = await ReadBodyObjectAsync(); + var clubId = ReadLong(body, "clubId", "ClubId"); + if (clubId is null or <= 0) return BadRequest(new { error = "Invalid clubId.", success = false, value = (object?)null }); + + var club = await ClubStore.GetAsync(db, clubId.Value); + if (club == null) return NotFound(); + if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Member) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "You are not a member of that club.", success = false, value = (object?)null }); + + var updated = await ClubStore.SetHomeClubAsync(db, ViewerId, clubId.Value); + return Ok(new { error = "", success = true, value = ClubStore.BuildClub(updated, ViewerId) }); + } + + [HttpDelete("~/clubs/club/home/me"), Authorize] + [HttpDelete("~/club/home/me"), Authorize] + [HttpDelete("~/api/clubs/v1/home/me"), Authorize] + [HttpDelete("~/api/clubs/v2/home/me"), Authorize] + public async Task ClearMyClubHome() + { + await ClubStore.SetHomeClubAsync(db, ViewerId, null); + return Ok(new { error = "", success = true, value = (object?)null }); + } + + [HttpGet("{clubId}"), Authorize] + [HttpGet("~/api/clubs/v1/{clubId:long}"), Authorize] + [HttpGet("~/api/clubs/v2/{clubId:long}"), Authorize] + public async Task GetClub(long clubId) + { + return Ok(ClubStore.BuildClub(await ClubStore.GetAsync(db, clubId), ViewerId)); + } + + [HttpGet("{clubId}/details"), Authorize] + [HttpGet("~/club/{clubId:long}/details"), Authorize] + [HttpGet("~/api/clubs/v1/{clubId:long}/details"), Authorize] + [HttpGet("~/api/clubs/v2/{clubId:long}/details"), Authorize] + public async Task GetClubDetails(long clubId) + { + return Ok(await ClubStore.BuildClubDetailsAsync(db, await ClubStore.GetAsync(db, clubId), ViewerId)); + } + + [HttpGet("{clubId:long}/mainimage")] + [HttpGet("~/club/{clubId:long}/mainimage")] + public async Task GetClubMainImage(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + return Ok(new + { + error = string.Empty, + success = true, + value = await ClubStore.BuildClubDetailsAsync(db, club, ViewerId) + }); + } + + [HttpPut("{clubId:long}/mainimage"), Authorize] + [HttpPut("~/club/{clubId:long}/mainimage"), Authorize] + public async Task SetClubMainImage(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var body = await ReadBodyObjectAsync(); + var imageName = ReadString(body, "imageName", "ImageName")?.Trim(); + if (string.IsNullOrWhiteSpace(imageName)) + return BadRequest(new { error = "imageName is required.", success = false, value = (object?)null }); + + var updated = await ClubStore.UpdateAsync(db, clubId, new JsonObject { ["mainImageName"] = imageName }); + if (updated == null) return NotFound(); + return Ok(new + { + error = string.Empty, + success = true, + value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) + }); + } + + [HttpGet("{clubId}/members")] + [HttpGet("~/club/{clubId:long}/members")] + [HttpGet("~/api/clubs/v1/{clubId:long}/members")] + [HttpGet("~/api/clubs/v2/{clubId:long}/members")] + public async Task GetClubMembers(long clubId, [FromQuery] int? membershipType = null, [FromQuery] int? sortBy = null) + { + var club = await ClubStore.GetAsync(db, clubId); + var members = ClubStore.GetMembers(club); + if (membershipType.HasValue) + members = members.Where(member => member.MembershipType == membershipType.Value).ToArray(); + members = sortBy switch + { + 1 => members.OrderBy(member => member.AccountId).ToArray(), + 2 => members.OrderBy(member => member.CreatedAt).ToArray(), + _ => members.OrderByDescending(member => member.MembershipType).ThenBy(member => member.CreatedAt).ToArray() + }; + var values = members + .Select(member => ClubStore.BuildMemberListItem(member, clubId)) + .ToArray(); + + return Ok(new Dictionary + { + ["error"] = string.Empty, + ["success"] = true, + ["value"] = values + }); + } + + [HttpGet("{clubId}/hasDisabledClubChat"), Authorize] + [HttpGet("~/api/clubs/v1/{clubId:long}/hasDisabledClubChat"), Authorize] + [HttpGet("~/api/clubs/v2/{clubId:long}/hasDisabledClubChat"), Authorize] + public IActionResult GetHasDisabledClubChat(long clubId) + { + return Content("false", "application/json"); + } + + [HttpGet("{clubId:long}/members/{playerId:long}"), Authorize] + [HttpGet("~/club/{clubId:long}/members/{playerId:long}"), Authorize] + [HttpGet("~/api/clubs/v1/{clubId:long}/members/{playerId:long}"), Authorize] + [HttpGet("~/api/clubs/v2/{clubId:long}/members/{playerId:long}"), Authorize] + public async Task GetClubMembership(long clubId, long playerId) + { + var club = await ClubStore.GetAsync(db, clubId); + var role = ClubStore.GetMembershipType(club, playerId); + var membership = ClubStore.GetMembers(club).FirstOrDefault(member => member.AccountId == playerId); + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == playerId); + + return Ok(new + { + clubId = clubId, + creatorAccountId = club?.ownerPlayerId ?? 0L, + membershipType = role, + LastAnnouncementId = 0L, + LastReadAnnouncementId = 0L, + announcements = Array.Empty(), + subscriptionId = clubId, + playerId = playerId, + membershipStatus = role >= ClubMembershipTypes.Member ? 1 : 0, + member = membership == null ? null : ClubStore.BuildMember(membership, club, profile), + role = role, + subscribed = role >= ClubMembershipTypes.Member, + created = membership == null ? 0L : new DateTimeOffset(membership.CreatedAt).ToUnixTimeSeconds(), + createdAt = membership == null ? 0L : new DateTimeOffset(membership.CreatedAt).ToUnixTimeSeconds() + }); + } + + [HttpGet("mine/created"), Authorize] + [HttpGet("~/api/clubs/v1/mine/created"), Authorize] + [HttpGet("~/api/clubs/v2/mine/created"), Authorize] + public async Task GetMyCreatedClubs() + { + var clubs = await ClubStore.LoadAsync(db); + return Ok(clubs.Where(x => x.ownerPlayerId == ViewerId).Select(x => ClubStore.BuildClub(x, ViewerId)).ToArray()); + } + + [HttpGet("mine/member"), Authorize] + [HttpGet("~/api/clubs/v1/mine/member"), Authorize] + [HttpGet("~/api/clubs/v2/mine/member"), Authorize] + public async Task GetMyMemberClubs() + { + var clubs = await ClubStore.LoadAsync(db); + return Ok(clubs + .Where(club => ClubStore.GetMembershipType(club, ViewerId) >= ClubMembershipTypes.Member) + .Select(club => ClubStore.BuildClub(club, ViewerId)) + .ToArray()); + } + + [HttpGet("account/{accountId}/created"), Authorize] + [HttpGet("~/clubs/account/{accountId:long}/created"), Authorize] + [HttpGet("~/api/clubs/v1/account/{accountId:long}/created"), Authorize] + [HttpGet("~/api/clubs/v2/account/{accountId:long}/created"), Authorize] + public async Task GetClubsCreatedByAccount(long accountId) + { + var clubs = await ClubStore.LoadAsync(db); + return Ok(clubs.Where(x => x.ownerPlayerId == accountId).Select(x => ClubStore.BuildClub(x, ViewerId)).ToArray()); + } + + [HttpGet("~/clubs/account/{accountId:long}/clubs")] + [HttpGet("~/api/clubs/v1/account/{accountId:long}/clubs")] + [HttpGet("~/api/clubs/v2/account/{accountId:long}/clubs")] + public Task GetClubsForAccount(long accountId) => GetClubsCreatedByAccount(accountId); + + [HttpGet("mostactivetoday"), Authorize] + [HttpGet("~/api/clubs/v1/mostactivetoday"), Authorize] + [HttpGet("~/api/clubs/v2/mostactivetoday"), Authorize] + public async Task GetMostActiveToday() + { + var clubs = await ClubStore.LoadAsync(db); + clubs = ClubStore.FilterByCategory(clubs, ClubStore.CategoryFilter(Request.Query)); + return Ok(clubs.Select(x => ClubStore.BuildClub(x, ViewerId)).ToArray()); + } + + [HttpGet("mostactivenow"), Authorize] + public async Task GetMostActiveNow() + { + var clubs = await ClubStore.LoadAsync(db); + clubs = ClubStore.FilterByCategory(clubs, ClubStore.CategoryFilter(Request.Query)); + return Ok(clubs.Select(x => ClubStore.BuildClub(x, ViewerId)).ToArray()); + } + + [HttpGet("categoryTags"), Authorize] + [HttpGet("~/api/clubs/v1/categoryTags"), Authorize] + [HttpGet("~/api/clubs/v2/categoryTags"), Authorize] + public IActionResult GetCategoryTags() + { + return Ok(new[] { "Social", "Casual", "Competitive", "Creative", "Entertainment", "Educational", "Lifestyle" }); + } + + [HttpGet("search"), Authorize] + [HttpGet("~/api/clubs/v1/search"), Authorize] + [HttpGet("~/api/clubs/v2/search"), Authorize] + public async Task SearchClubs( + [FromQuery] string? query = null, + [FromQuery] int sort = 0, + [FromQuery] string? category = null, + [FromQuery] int skip = 0, + [FromQuery] int count = 30, + [FromQuery] int take = 0) + { + var limit = take > 0 ? take : count; + var clubs = await ClubStore.LoadAsync(db); + if (!string.IsNullOrWhiteSpace(query)) + { + clubs = clubs + .Where(x => x.name.Contains(query, StringComparison.OrdinalIgnoreCase) || + x.description.Contains(query, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + clubs = ClubStore.FilterByCategory(clubs, ClubStore.CategoryFilter(Request.Query) ?? category); + + var items = clubs + .Skip(Math.Max(0, skip)) + .Take(Math.Max(1, limit)) + .Select(x => ClubStore.BuildClub(x, ViewerId)) + .ToArray(); + + return Ok(ClubStore.BuildSearchResponse(items, Math.Max(0, skip), Math.Max(1, limit))); + } + + [HttpGet("cancreate"), Authorize] + [HttpGet("can-create"), Authorize] + [HttpGet("create/eligibility"), Authorize] + [HttpGet("creation/eligibility"), Authorize] + [HttpGet("~/api/clubs/v1/cancreate"), Authorize] + [HttpGet("~/api/clubs/v1/can-create"), Authorize] + [HttpGet("~/api/clubs/v1/create/eligibility"), Authorize] + [HttpGet("~/api/clubs/v1/creation/eligibility"), Authorize] + [HttpGet("~/api/clubs/v2/cancreate"), Authorize] + [HttpGet("~/api/clubs/v2/can-create"), Authorize] + [HttpGet("~/api/clubs/v2/create/eligibility"), Authorize] + [HttpGet("~/api/clubs/v2/creation/eligibility"), Authorize] + public IActionResult CanCreateClub() + { + return Ok(ClubStore.BuildEligibility()); + } + + [HttpPut("{clubId:long}/members/invite"), Authorize] + [HttpPut("{clubId:long}/members/invitemembers"), Authorize] + [HttpPost("{clubId:long}/members/invitemembers"), Authorize] + [HttpPut("~/club/{clubId:long}/members/invite"), Authorize] + [HttpPut("~/club/{clubId:long}/members/invitemembers"), Authorize] + [HttpPost("~/club/{clubId:long}/members/invitemembers"), Authorize] + public async Task InviteMember(long clubId) + { + var inviterId = ViewerId; + if (inviterId <= 0) return Unauthorized(); + + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + + var inviterRole = ClubStore.GetMembershipType(club, inviterId); + if (inviterRole < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var (rawAccountIds, rawMembershipType) = await ReadInviteMemberFieldsAsync(); + if (rawAccountIds.Count == 0) + return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); + + var membershipType = string.IsNullOrWhiteSpace(rawMembershipType) + ? ClubMembershipTypes.Member + : ParseInviteMembershipType(rawMembershipType); + if (membershipType is not (ClubMembershipTypes.Member or ClubMembershipTypes.Moderator or ClubMembershipTypes.Coowner)) + return BadRequest(new { error = "Invalid membershipType.", success = false, value = (object?)null }); + if (membershipType > inviterRole) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var updated = club; + foreach (var rawAccountId in rawAccountIds) + { + if (!long.TryParse(rawAccountId, out var accountId) || accountId <= 0) + return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); + if (accountId == club.ownerPlayerId) + return BadRequest(new { error = "You can’t change the club’s creator.", success = false, value = (object?)null }); + + + + + + if (ClubStore.GetMembershipType(updated, accountId) >= ClubMembershipTypes.Member) + { + updated = await ClubStore.SetMemberTypeAsync(db, clubId, accountId, membershipType, inviterId) ?? updated; + continue; + } + + updated = await ClubStore.InviteMemberAsync(db, clubId, accountId, membershipType, inviterId) ?? updated; + + await Notify.SendMessage(accountId, new Message + { + FromPlayerId = inviterId, + Type = membershipType switch + { + ClubMembershipTypes.Coowner => MessageType.ClubCoownerInvited, + ClubMembershipTypes.Moderator => MessageType.ClubModeratorInvited, + _ => MessageType.ClubMemberInvited + }, + Data = clubId.ToString(System.Globalization.CultureInfo.InvariantCulture), + Details = membershipType.ToString(System.Globalization.CultureInfo.InvariantCulture) + }); + } + + return Ok(new Dictionary + { + ["error"] = string.Empty, + ["success"] = true, + ["value"] = await ClubStore.BuildClubDetailsAsync(db, updated, inviterId) + }); + } + + [HttpPut("{clubId:long}/members/changetype"), Authorize] + [HttpPost("{clubId:long}/members/changetype"), Authorize] + [HttpPut("~/club/{clubId:long}/members/changetype"), Authorize] + [HttpPost("~/club/{clubId:long}/members/changetype"), Authorize] + public async Task ChangeMemberType(long clubId) + { + var actorId = ViewerId; + if (actorId <= 0) return Unauthorized(); + + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + var actorRole = ClubStore.GetMembershipType(club, actorId); + if (actorRole < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var body = await ReadBodyObjectAsync(); + var accountId = ReadLong(body, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId") + ?? ReadLongFromRequest(Request, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId"); + if (accountId is null or <= 0 || accountId == club.ownerPlayerId) + return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); + if (ClubStore.GetMembershipType(club, accountId.Value) < ClubMembershipTypes.Member) + return BadRequest(new { error = "That player is not a member of this club.", success = false, value = (object?)null }); + + var requested = ReadLong(body, "membershipType", "MembershipType", "type", "Type", "role", "Role") + ?? ReadLongFromRequest(Request, "membershipType", "MembershipType", "type", "Type", "role", "Role"); + if (requested is null or < 0) + return BadRequest(new { error = "Invalid membershipType.", success = false, value = (object?)null }); + var membershipType = (int)requested.Value; + if (membershipType is not (ClubMembershipTypes.Member or ClubMembershipTypes.Moderator or ClubMembershipTypes.Coowner)) + return BadRequest(new { error = "Invalid membershipType.", success = false, value = (object?)null }); + if (membershipType > actorRole) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var updated = await ClubStore.SetMemberTypeAsync(db, clubId, accountId.Value, membershipType, actorId); + if (updated == null) return NotFound(); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, actorId) }); + } + + [HttpPost("{clubId:long}/members/acceptinvite"), Authorize] + [HttpPut("{clubId:long}/members/acceptinvite"), Authorize] + [HttpPost("{clubId:long}/members/accept"), Authorize] + [HttpPut("{clubId:long}/members/accept"), Authorize] + [HttpPost("~/club/{clubId:long}/members/acceptinvite"), Authorize] + [HttpPut("~/club/{clubId:long}/members/acceptinvite"), Authorize] + [HttpPost("~/club/{clubId:long}/members/accept"), Authorize] + [HttpPut("~/club/{clubId:long}/members/accept"), Authorize] + public async Task AcceptClubInvitation(long clubId) + { + var inviteeId = ViewerId; + if (inviteeId <= 0) return Unauthorized(); + + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + var pending = ClubStore.GetMembers(club).FirstOrDefault(member => member.AccountId == inviteeId); + if (pending?.MembershipType != ClubMembershipTypes.PendingInvited) + return BadRequest(new { error = "No pending invitation exists.", success = false, value = (object?)null }); + + var updated = await ClubStore.AcceptInvitationAsync(db, clubId, inviteeId); + if (updated == null) return NotFound(); + return Ok(new Dictionary + { + ["error"] = string.Empty, + ["success"] = true, + ["value"] = await ClubStore.BuildClubDetailsAsync(db, updated, inviteeId) + }); + } + + [HttpPost("{clubId:long}/members/declineinvite"), Authorize] + [HttpPut("{clubId:long}/members/declineinvite"), Authorize] + [HttpPost("{clubId:long}/members/decline"), Authorize] + [HttpDelete("{clubId:long}/members/invite"), Authorize] + [HttpPost("~/club/{clubId:long}/members/declineinvite"), Authorize] + [HttpPut("~/club/{clubId:long}/members/declineinvite"), Authorize] + [HttpPost("~/club/{clubId:long}/members/decline"), Authorize] + [HttpDelete("~/club/{clubId:long}/members/invite"), Authorize] + public async Task DeclineClubInvitation(long clubId) + { + var inviteeId = ViewerId; + if (inviteeId <= 0) return Unauthorized(); + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + var pending = ClubStore.GetMembers(club).FirstOrDefault(member => member.AccountId == inviteeId); + if (pending?.MembershipType != ClubMembershipTypes.PendingInvited) + return BadRequest(new { error = "No pending invitation exists.", success = false, value = (object?)null }); + var updated = await ClubStore.RemoveMemberAsync(db, clubId, inviteeId); + return Ok(new Dictionary + { + ["error"] = string.Empty, + ["success"] = true, + ["value"] = await ClubStore.BuildClubDetailsAsync(db, updated, inviteeId) + }); + } + + private static readonly string[] InviteAccountIdKeys = + { + "accountId", "AccountId", "accountIds", "AccountIds", "accountId[]", "AccountId[]", + "playerId", "PlayerId", "playerIds", "PlayerIds", "invitedPlayerId", "InvitedPlayerId", + "invitedPlayerIds", "InvitedPlayerIds", "invitedPlayerIds[]", "InvitedPlayerIds[]", + "targetPlayerIds", "TargetPlayerIds" + }; + + private static int ParseInviteMembershipType(string raw) + { + if (int.TryParse(raw, out var numeric)) return numeric; + + return raw.Trim().ToLowerInvariant() switch + { + "member" or "regular" or "invited" => ClubMembershipTypes.Member, + "moderator" or "mod" => ClubMembershipTypes.Moderator, + "coowner" or "co-owner" or "co_owner" or "co owner" => ClubMembershipTypes.Coowner, + _ => -1 + }; + } + + private async Task<(List AccountIds, string? MembershipType)> ReadInviteMemberFieldsAsync() + { + var accountIds = new List(); + string? membershipType = null; + + void Collect(string key, string? value) + { + if (string.IsNullOrWhiteSpace(value)) return; + if (key.Equals("membershipType", StringComparison.OrdinalIgnoreCase)) + { + membershipType ??= value; + return; + } + if (!InviteAccountIdKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) return; + + foreach (var part in value.Split(new[] { ',', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + if (long.TryParse(part, out _) && !accountIds.Contains(part)) accountIds.Add(part); + } + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var field in form) + foreach (var value in field.Value) + Collect(field.Key, value); + } + else if (Request.ContentLength is > 0) + { + try + { + Request.EnableBuffering(); + if (await JsonNode.ParseAsync(Request.Body) is JsonObject body) + foreach (var pair in body) + { + if (pair.Value is JsonArray array) + foreach (var item in array) Collect(pair.Key, item?.ToString()); + else + Collect(pair.Key, pair.Value?.ToString()); + } + + Request.Body.Position = 0; + } + catch + { + try { Request.Body.Position = 0; } catch { } + } + } + + foreach (var key in InviteAccountIdKeys) + foreach (var value in Request.Query[key]) Collect(key, value); + membershipType ??= Request.Query["membershipType"].FirstOrDefault() + ?? Request.Query["MembershipType"].FirstOrDefault(); + + return (accountIds, membershipType); + } + + [HttpPost("create"), Authorize] + [HttpPost("~/club/create"), Authorize] + [HttpPost("createclub"), Authorize] + [HttpPost("~/api/clubs/v1/create"), Authorize] + [HttpPost("~/api/clubs/v1/createclub"), Authorize] + [HttpPost("~/api/clubs/v2/create"), Authorize] + [HttpPost("~/api/clubs/v2/createclub"), Authorize] + public async Task CreateClub([FromForm] string? name = null, [FromForm] string? description = null) + { + var userId = ViewerId; + if (userId <= 0) return Unauthorized("Missing user ID."); + + var fields = await ReadCreateClubFields(name, description); + var club = await ClubStore.CreateAsync( + db, + userId, + fields.Name, + fields.Description, + fields.Category, + fields.Visibility ?? 1, + fields.Joinability ?? 0, + fields.AllowJuniors ?? true, + fields.MinLevel ?? 0, + fields.MainImageName); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, club, userId) }); + } + + private async Task<(string? Name, string? Description, string? Category, int? Visibility, int? Joinability, bool? AllowJuniors, int? MinLevel, string? MainImageName)> ReadCreateClubFields(string? name, string? description) + { + string? category = null; + string? visibility = null; + string? joinability = null; + string? allowJuniors = null; + string? minLevel = null; + string? mainImageName = null; + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + name ??= FirstFormValue(form, "name", "Name", "clubName", "ClubName"); + description ??= FirstFormValue(form, "description", "Description", "clubDescription", "ClubDescription"); + category = FirstFormValue(form, "category", "Category", "primaryTag", "PrimaryTag"); + visibility = FirstFormValue(form, "visibility", "Visibility"); + joinability = FirstFormValue(form, "joinability", "Joinability"); + allowJuniors = FirstFormValue(form, "allowJuniors", "AllowJuniors"); + minLevel = FirstFormValue(form, "minLevel", "MinLevel"); + mainImageName = FirstFormValue(form, "mainImageName", "MainImageName"); + } + else if (Request.ContentLength is > 0) + { + using var reader = new StreamReader(Request.Body); + var rawBody = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(rawBody) && JsonNode.Parse(rawBody) is JsonObject body) + { + name ??= FirstJsonValue(body, "name", "Name", "clubName", "ClubName"); + description ??= FirstJsonValue(body, "description", "Description", "clubDescription", "ClubDescription"); + category = FirstJsonValue(body, "category", "Category", "primaryTag", "PrimaryTag"); + visibility = FirstJsonValue(body, "visibility", "Visibility"); + joinability = FirstJsonValue(body, "joinability", "Joinability"); + allowJuniors = FirstJsonValue(body, "allowJuniors", "AllowJuniors"); + minLevel = FirstJsonValue(body, "minLevel", "MinLevel"); + mainImageName = FirstJsonValue(body, "mainImageName", "MainImageName"); + } + } + + name ??= Request.Query["name"].FirstOrDefault() ?? Request.Query["Name"].FirstOrDefault() ?? + Request.Query["clubName"].FirstOrDefault() ?? Request.Query["ClubName"].FirstOrDefault(); + description ??= Request.Query["description"].FirstOrDefault() ?? Request.Query["Description"].FirstOrDefault() ?? + Request.Query["clubDescription"].FirstOrDefault() ?? Request.Query["ClubDescription"].FirstOrDefault(); + category ??= Request.Query["category"].FirstOrDefault() ?? Request.Query["Category"].FirstOrDefault() ?? + Request.Query["primaryTag"].FirstOrDefault() ?? Request.Query["PrimaryTag"].FirstOrDefault(); + + return ( + name, + description, + category, + ParseVisibility(visibility), + ParseJoinability(joinability), + ParseFormBool(allowJuniors), + int.TryParse(minLevel, out var parsedLevel) && parsedLevel >= 0 ? parsedLevel : null, + mainImageName); + } + + private static int? ParseVisibility(string? raw) => raw?.Trim().ToLowerInvariant() switch + { + "private" or "0" => 0, + "public" or "1" => 1, + _ => null + }; + + private static int? ParseJoinability(string? raw) => raw?.Trim().ToLowerInvariant().Replace("_", "") switch + { + "open" or "0" => 0, + "inviteonly" or "1" => 1, + "asktojoin" or "requesttojoin" or "2" => 2, + _ => null + }; + + private static bool? ParseFormBool(string? raw) => raw?.Trim().ToLowerInvariant() switch + { + "true" or "1" or "yes" => true, + "false" or "0" or "no" => false, + _ => null + }; + + [HttpPut("{clubId:long}/minlevel"), Authorize] + [HttpPut("~/club/{clubId:long}/minlevel"), Authorize] + public async Task SetMinimumLevel(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var rawMinLevel = await FirstFormValueAsync(Request, "minLevel", "MinLevel"); + if (!int.TryParse(rawMinLevel, out var minLevel) || minLevel < 0) + { + var body = await ReadBodyObjectAsync(); + var parsed = ReadLong(body, "minLevel", "MinLevel"); + if (parsed is null or < 0 or > int.MaxValue) + return BadRequest(new { error = "Invalid minLevel.", success = false, value = (object?)null }); + minLevel = (int)parsed.Value; + } + + var updated = await ClubStore.UpdateAsync(db, clubId, new JsonObject { ["minLevel"] = minLevel }); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); + } + + [HttpPut("{clubId:long}/modifydetails"), Authorize] + [HttpPut("{clubId:long}/modify"), Authorize] + [HttpPut("~/club/{clubId:long}/modifydetails"), Authorize] + [HttpPut("~/club/{clubId:long}/modify"), Authorize] + public async Task ModifyClub(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var body = await ReadBodyObjectAsync(); + if (body == null) return BadRequest(new { error = "Invalid request.", success = false, value = (object?)null }); + var updated = await ClubStore.UpdateAsync(db, clubId, body); + if (updated == null) return NotFound(); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); + } + + [HttpPut("{clubId:long}/clubhouse"), Authorize] + [HttpDelete("{clubId:long}/clubhouse"), Authorize] + [HttpPut("~/club/{clubId:long}/clubhouse"), Authorize] + [HttpDelete("~/club/{clubId:long}/clubhouse"), Authorize] + public async Task SetClubhouse(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + long? roomId = null; + if (!HttpMethods.IsDelete(Request.Method)) + { + var body = await ReadBodyObjectAsync(); + var raw = ReadLong(body, "roomId", "RoomId"); + if (body?.Count > 0 && raw == null) + return BadRequest(new { error = "Invalid roomId.", success = false, value = (object?)null }); + roomId = raw; + } + + var updated = await ClubStore.SetClubhouseAsync(db, clubId, roomId); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); + } + + [HttpPut("{clubId:long}/additionalimage/{index:int}"), Authorize] + [HttpDelete("{clubId:long}/additionalimage/{index:int}"), Authorize] + [HttpPut("~/club/{clubId:long}/additionalimage/{index:int}"), Authorize] + [HttpDelete("~/club/{clubId:long}/additionalimage/{index:int}"), Authorize] + public async Task SetAdditionalImage(long clubId, int index) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + if (index < 0 || index >= 3) + return BadRequest(new { error = "Invalid additional image slot.", success = false, value = (object?)null }); + + var body = HttpMethods.IsDelete(Request.Method) ? null : await ReadBodyObjectAsync(); + var imageName = body == null ? "" : ReadString(body, "imageName", "ImageName") ?? ""; + var updated = await ClubStore.SetAdditionalImageAsync(db, clubId, index, imageName); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); + } + + [HttpDelete("{clubId:long}"), Authorize] + [HttpDelete("~/club/{clubId:long}"), Authorize] + public async Task DeleteClub(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (club.ownerPlayerId != ViewerId) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + await ClubStore.DeleteAsync(db, clubId); + return Ok(new { error = "", success = true, value = (object?)null }); + } + + [HttpPut("{clubId:long}/members/requesttojoin"), Authorize] + [HttpPut("~/club/{clubId:long}/members/requesttojoin"), Authorize] + public async Task RequestToJoinClub(long clubId) + { + var existingClub = await ClubStore.GetAsync(db, clubId); + if (existingClub == null) return NotFound(); + ClubRecord? club; + if (ClubStore.GetMembershipType(existingClub, ViewerId) == ClubMembershipTypes.PendingInvited) + club = await ClubStore.AcceptInvitationAsync(db, clubId, ViewerId); + else + club = await ClubStore.RequestToJoinAsync(db, clubId, ViewerId); + if (club == null) return NotFound(); + var role = ClubStore.GetMembershipType(club, ViewerId); + if (role == ClubMembershipTypes.Banned) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "You are banned from this club.", success = false, value = (object?)null }); + if (club.joinability == 1 && role < ClubMembershipTypes.Member) + return BadRequest(new { error = "This club is invite only.", success = false, value = (object?)null }); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, club, ViewerId) }); + } + + [HttpPost("{clubId:long}/members/leave"), Authorize] + [HttpPost("~/club/{clubId:long}/members/leave"), Authorize] + public async Task LeaveClub(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (club.ownerPlayerId == ViewerId) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "You created this club — delete it instead of leaving.", success = false, value = (object?)null }); + + club = await ClubStore.RemoveMemberAsync(db, clubId, ViewerId) ?? club; + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, club, ViewerId) }); + } + + [HttpPost("{clubId:long}/members/remove"), Authorize] + [HttpDelete("{clubId:long}/members/remove"), Authorize] + [HttpPost("~/club/{clubId:long}/members/remove"), Authorize] + [HttpDelete("~/club/{clubId:long}/members/remove"), Authorize] + public async Task RemoveClubMember(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + var actorRole = ClubStore.GetMembershipType(club, ViewerId); + if (actorRole < ClubMembershipTypes.Moderator) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var body = await ReadBodyObjectAsync(); + var accountId = ReadLong(body, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId") + ?? ReadLongFromRequest(Request, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId"); + if (accountId is null or <= 0 || accountId == club.ownerPlayerId || accountId == ViewerId) + return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); + var targetRole = ClubStore.GetMembershipType(club, accountId.Value); + if (targetRole >= ClubMembershipTypes.Moderator && actorRole < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + + var updated = await ClubStore.RemoveMemberAsync(db, clubId, accountId.Value); + return updated == null + ? NotFound() + : Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); + } + + [HttpPost("{clubId:long}/members/ban"), Authorize] + [HttpPost("~/club/{clubId:long}/members/ban"), Authorize] + public async Task BanMember(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Moderator) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + var body = await ReadBodyObjectAsync(); + var accountId = ReadLong(body, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId"); + if (accountId is null or <= 0 || accountId == club.ownerPlayerId || accountId == ViewerId) + return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); + var targetRole = ClubStore.GetMembershipType(club, accountId.Value); + if (targetRole >= ClubMembershipTypes.Moderator && ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + var updated = await ClubStore.SetMemberTypeAsync(db, clubId, accountId.Value, ClubMembershipTypes.Banned, ViewerId); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); + } + + [HttpPost("{clubId:long}/members/unban"), Authorize] + [HttpPost("~/club/{clubId:long}/members/unban"), Authorize] + public async Task UnbanMember(long clubId) + { + var club = await ClubStore.GetAsync(db, clubId); + if (club == null) return NotFound(); + if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Moderator) + return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); + var body = await ReadBodyObjectAsync(); + var accountId = ReadLong(body, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId"); + if (accountId is null or <= 0) + return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); + var updated = await ClubStore.SetMemberTypeAsync(db, clubId, accountId.Value, ClubMembershipTypes.None, ViewerId); + return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); + } + + private async Task ReadBodyObjectAsync() + { + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + var jsonForm = new JsonObject(); + foreach (var pair in form) + { + if (pair.Value.Count > 1) + { + var values = new JsonArray(); + foreach (var value in pair.Value) values.Add(value); + jsonForm[pair.Key] = values; + } + else + { + jsonForm[pair.Key] = pair.Value.FirstOrDefault(); + } + } + return jsonForm; + } + if (Request.ContentLength is not > 0) return new JsonObject(); + try + { + Request.EnableBuffering(); + var body = await JsonNode.ParseAsync(Request.Body); + Request.Body.Position = 0; + return body as JsonObject; + } + catch + { + try { Request.Body.Position = 0; } catch { } + return null; + } + } + + private static string? ReadString(JsonObject? body, params string[] keys) + { + if (body == null) return null; + foreach (var key in keys) + foreach (var pair in body) + if (pair.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) + return pair.Value?.ToString(); + return null; + } + + private static long? ReadLong(JsonObject? body, params string[] keys) + { + var value = ReadString(body, keys); + return long.TryParse(value, out var parsed) ? parsed : null; + } + + private static long? ReadLongFromRequest(HttpRequest request, params string[] keys) + { + foreach (var key in keys) + if (long.TryParse(request.Query[key].FirstOrDefault(), out var value)) return value; + return null; + } + + private static async Task FirstFormValueAsync(HttpRequest request, params string[] keys) + { + if (!request.HasFormContentType) return null; + var form = await request.ReadFormAsync(); + foreach (var key in keys) + foreach (var pair in form) + if (pair.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) return pair.Value.FirstOrDefault(); + return null; + } + + private static string? FirstFormValue(IFormCollection form, params string[] keys) + { + foreach (var key in keys) + { + var value = form[key].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(value)) return value; + } + + return null; + } + + private static string? FirstJsonValue(JsonObject? json, params string[] keys) + { + if (json == null) return null; + foreach (var key in keys) + { + if (json.TryGetPropertyValue(key, out var value)) + { + var text = value?.ToString(); + if (!string.IsNullOrWhiteSpace(text)) return text; + } + } + + return null; + } +} diff --git a/Server/Clubs/ClubsSearchAPI.cs b/Server/Clubs/ClubsSearchAPI.cs new file mode 100644 index 0000000..c8aa233 --- /dev/null +++ b/Server/Clubs/ClubsSearchAPI.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.Extensions; + +namespace RadicadeServer.Clubs; + +[Route("clubs")] +[Route("api/clubs/v1")] +[Route("api/clubs/v2")] +public class ClubsSearchAPI : Controller +{ + private readonly RecDBContext db; + + public ClubsSearchAPI(RecDBContext db) + { + this.db = db; + } + + private long ViewerId => User.GetUserId() ?? 0; + + [HttpGet("search/mostactivenow"), Authorize] + [HttpGet("mostactivenow"), Authorize] + public async Task GetMostActiveClubs([FromQuery] int skip = 0, [FromQuery] int take = 30) + { + var clubs = await ClubStore.LoadAsync(db); + clubs = ClubStore.FilterByCategory(clubs, ClubStore.CategoryFilter(Request.Query)); + var items = clubs + .Skip(Math.Max(0, skip)) + .Take(Math.Max(1, take)) + .Select(x => ClubStore.BuildClub(x, ViewerId)) + .ToArray(); + + return Ok(items); + } +} diff --git a/Server/Clubs/PlayerSubscriptions.cs b/Server/Clubs/PlayerSubscriptions.cs new file mode 100644 index 0000000..5477220 --- /dev/null +++ b/Server/Clubs/PlayerSubscriptions.cs @@ -0,0 +1,117 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Config; + +namespace RadicadeServer.Clubs; + +public sealed class PlayerSubscriptionRecord +{ + public long subscriberAccountId { get; set; } + public long creatorAccountId { get; set; } + public DateTime createdAt { get; set; } = DateTime.UtcNow; +} + +public static class PlayerSubscriptions +{ + private const string ConfigKey = "PlayerSubscriptions"; + + private const long PersonalClubIdBase = 1_000_000_000_000L; + + public static long PersonalClubId(long accountId) => accountId > 0 ? PersonalClubIdBase + accountId : 0; + + public static bool IsPersonalClubId(long clubId) => clubId >= PersonalClubIdBase; + + public static long? AccountIdForPersonalClub(long clubId) => + IsPersonalClubId(clubId) ? clubId - PersonalClubIdBase : null; + + public static async Task> LoadAsync(RecDBContext db) + { + var config = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == ConfigKey); + if (config == null || string.IsNullOrWhiteSpace(config.Value)) return new List(); + + try + { + return config.Value.FromJson>() ?? new List(); + } + catch + { + return new List(); + } + } + + private static async Task SaveAsync(RecDBContext db, List subscriptions) + { + var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == ConfigKey); + if (config == null) + { + config = new ServerConfigVal(ConfigKey, "[]"); + await db.ServerConfig.AddAsync(config); + } + + config.Value = subscriptions.ToJson(); + await db.SaveChangesAsync(); + } + + public static async Task SubscribeAsync(RecDBContext db, long subscriberId, long creatorId) + { + if (subscriberId <= 0 || creatorId <= 0 || subscriberId == creatorId) return false; + + var subscriptions = await LoadAsync(db); + if (subscriptions.Any(x => x.subscriberAccountId == subscriberId && x.creatorAccountId == creatorId)) + return true; + + subscriptions.Add(new PlayerSubscriptionRecord + { + subscriberAccountId = subscriberId, + creatorAccountId = creatorId + }); + await SaveAsync(db, subscriptions); + return true; + } + + public static async Task UnsubscribeAsync(RecDBContext db, long subscriberId, long creatorId) + { + var subscriptions = await LoadAsync(db); + if (subscriptions.RemoveAll(x => x.subscriberAccountId == subscriberId && x.creatorAccountId == creatorId) == 0) + return false; + + await SaveAsync(db, subscriptions); + return true; + } + + public static async Task> SubscriptionIdsAsync(RecDBContext db, long accountId) => + (await LoadAsync(db)) + .Where(x => x.subscriberAccountId == accountId) + .Select(x => x.creatorAccountId) + .Distinct() + .ToList(); + + public static async Task> SubscriberIdsAsync(RecDBContext db, long accountId) => + (await LoadAsync(db)) + .Where(x => x.creatorAccountId == accountId) + .Select(x => x.subscriberAccountId) + .Distinct() + .ToList(); + + public static async Task SubscriberCountAsync(RecDBContext db, long accountId) => + (await SubscriberIdsAsync(db, accountId)).Count; + + public static async Task> TopCreatorsAsync(RecDBContext db, int limit) + { + var creators = (await LoadAsync(db)) + .GroupBy(x => x.creatorAccountId) + .Select(group => new + { + clubId = PersonalClubId(group.Key), + accountId = group.Key, + subscriberCount = group.Select(x => x.subscriberAccountId).Distinct().Count() + }) + .OrderByDescending(creator => creator.subscriberCount) + .ThenBy(creator => creator.accountId) + .Take(limit) + .ToList(); + + return creators.Cast().ToList(); + } +} diff --git a/Server/Commerce/CampusCard.cs b/Server/Commerce/CampusCard.cs new file mode 100644 index 0000000..257acec --- /dev/null +++ b/Server/Commerce/CampusCard.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Store +{ + public class CampusCard + { + public CampusCardSubscription? subscription { get; set; } + public long? platformAccountSubscribedPlayerId { get; set; } + } +} diff --git a/Server/Commerce/CampusCardAPI.cs b/Server/Commerce/CampusCardAPI.cs new file mode 100644 index 0000000..625489e --- /dev/null +++ b/Server/Commerce/CampusCardAPI.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.RecNet.Store; +using RadicadeServer.Extensions; + +namespace RadicadeServer.API.CampusCard.V1 +{ + [Route("api/CampusCard/v1")] + [Route("econ/api/CampusCard/v1")] + public class CampusCardAPI : Controller + { + [HttpPost("UpdateAndGetSubscription")] + public ActionResult GetSubscription() + { + var playerId = User?.GetUserId() ?? 0; + var now = DateTime.UtcNow; + return Ok(new RecNet.Store.CampusCard + { + subscription = new CampusCardSubscription + { + subscriptionId = 1, + recNetPlayerId = playerId, + platformType = null, + platformId = "", + platformPurchaseId = "", + level = RecNet.Enums.Store.SKU.CampusCardLevel.Gold, + period = RecNet.Enums.Store.SKU.CampusCardPeriod.Year, + expirationDate = now.AddYears(1), + isAutoRenewing = true, + createdAt = now, + modifiedAt = now + }, + platformAccountSubscribedPlayerId = null + }); + } + + [HttpGet("SignUpBonus")] + public IActionResult SignUpBonus() => Ok(new + { + RRPlusSignUpBonusId = 3, + MinFreeItemsPrice = 0, + MaxFreeItemsPrice = 10000 + }); + } +} diff --git a/Server/Commerce/CampusCardSubscription.cs b/Server/Commerce/CampusCardSubscription.cs new file mode 100644 index 0000000..4a6af5c --- /dev/null +++ b/Server/Commerce/CampusCardSubscription.cs @@ -0,0 +1,20 @@ +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Store.SKU; + +namespace RadicadeServer.RecNet.Store +{ + public class CampusCardSubscription + { + public long subscriptionId { get; set; } + public long recNetPlayerId { get; set; } + public Platforms? platformType { get; set; } + public string platformId { get; set; } = null!; + public string platformPurchaseId { get; set; } = null!; + public CampusCardLevel level { get; set; } + public CampusCardPeriod period { get; set; } + public DateTime expirationDate { get; set; } + public bool isAutoRenewing { get; set; } + public DateTime createdAt { get; set; } + public DateTime modifiedAt { get; set; } + } +} diff --git a/Server/Commerce/CommercePlaceholderAPI.cs b/Server/Commerce/CommercePlaceholderAPI.cs new file mode 100644 index 0000000..e24b16a --- /dev/null +++ b/Server/Commerce/CommercePlaceholderAPI.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.Commerce; + +[Route("commerce")] +public class CommercePlaceholderAPI : Controller +{ + [HttpGet("purchase/v1/hasspentmoney")] + public bool HasSpentMoney() + { + return true; + } + + + + + + + + + + + [HttpGet("api/catalog/v1/all")] + public object GetCatalogAll() + { + return Array.Empty(); + } + + + + + + + + + [HttpPost("purchase/v1/cleanuppending")] + public object CleanupPendingTransactions() + { + return new { }; + } +} diff --git a/Server/Compatibility/CommonQueryAPI.cs b/Server/Compatibility/CommonQueryAPI.cs new file mode 100644 index 0000000..5de7e00 --- /dev/null +++ b/Server/Compatibility/CommonQueryAPI.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.API.Compatibility; + +[ApiController] +public sealed class CommonQueryAPI : ControllerBase +{ + [HttpGet("/data/commonQuery/userSegment")] + public IActionResult GetUserSegment() => Ok(new Dictionary + { + + + + + ["User_segment_algo_name"] = "none", + ["User_segment_algo_version"] = "1", + ["User_segment_value"] = "0", + }); +} diff --git a/Server/Compatibility/CompatibilityAPI.cs b/Server/Compatibility/CompatibilityAPI.cs new file mode 100644 index 0000000..8143a4b --- /dev/null +++ b/Server/Compatibility/CompatibilityAPI.cs @@ -0,0 +1,485 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Keepsakes; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Compatibility; + +[ApiController] +public sealed class CompatibilityAPI(RecDBContext db): ControllerBase +{ + [HttpGet("/api/progressionEvents/active")] + public IActionResult ActiveProgressionEvents() + { + if (!KeepsakeFeature.Enabled) return NoActiveEvent(); + + var keepsakeRooms = KeepsakeRoomConfig.Current; + if (!keepsakeRooms.Any || !keepsakeRooms.EventEnabled) return NoActiveEvent(); + + if (string.Equals(Environment.GetEnvironmentVariable("RADICADE_PROGRESSION_ACTIVE"), "record", StringComparison.OrdinalIgnoreCase)) + return Ok(EventRecord(keepsakeRooms)); + return Ok(keepsakeRooms.EventId); + } + + private ContentResult NoActiveEvent() + { + return Content("null", "application/json"); + } + + [HttpGet("/api/progressionEvents/event/{eventId:long}")] + public IActionResult ProgressionEventDefinition(long eventId) => EventById(eventId, "event"); + + [HttpGet("/api/progressionEvents/record/{eventId:long}")] + public IActionResult ProgressionEventRecord(long eventId) => EventById(eventId, "record"); + + [HttpGet("/api/progressionEvents/{eventId:long}")] + public IActionResult ProgressionEvent(long eventId) => EventById(eventId, "event"); + + private IActionResult EventById(long eventId, string route) + { + if (!KeepsakeFeature.Enabled) return NotFound(); + + var keepsakeRooms = KeepsakeRoomConfig.Current; + if (!keepsakeRooms.Any || !keepsakeRooms.EventEnabled || eventId != keepsakeRooms.EventId) return NotFound(); + + return route == "record" + ? Ok(EventRecord(keepsakeRooms, includeDefinitionCopy: true)) + : Ok(EventDefinition(keepsakeRooms)); + } + + private object EventRecord(KeepsakeRoomConfig keepsakeRooms) + => EventRecord(keepsakeRooms, includeDefinitionCopy: false); + + private object EventRecord(KeepsakeRoomConfig keepsakeRooms, bool includeDefinitionCopy) + { + if (!includeDefinitionCopy) return EventDefinition(keepsakeRooms); + + var accountId = HttpContext?.User?.GetUserId() ?? 0; + var categories = db == null ? new List() : db.KeepsakeCollectionRecords + .Where(r => r.AccountId == accountId).Select(r => r.KeepsakeCategoryId).ToList(); + + return new + { + _route = "record", + AccountId = accountId, + Xp = categories.Sum(id => KeepsakeCatalog.Find(id)?.XpValue ?? 0), + RewardsCollected = 0, + BonusRewardsCollected = 0, + GameMinutesToday = 0, + XpBoostLastPurchasedAt = (DateTime?)null, + }; + } + + private object EventDefinition(KeepsakeRoomConfig keepsakeRooms) + { + var now = DateTime.UtcNow; + var end = now.AddYears(1); + + return new + { + _route = "event", + ProgressionEventId = keepsakeRooms.EventId, + Name = keepsakeRooms.EventName, + IsEnabled = true, + StartTime = now.AddDays(-30), + EndTime = end, + CollectionEndTime = end, + Rewards = Array.Empty(), + KeepsakeRoomLists = EventKeepsakeRoomLists(keepsakeRooms), + }; + } + + private static string KeepsakeListMode() + => string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_LISTS"), "empty", StringComparison.OrdinalIgnoreCase) + ? "no" + : "all"; + + private object EventKeepsakeRoomLists(KeepsakeRoomConfig keepsakeRooms) + => string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_LISTS"), "empty", StringComparison.OrdinalIgnoreCase) + ? Array.Empty() + : keepsakeRooms.RoomListEntries(keepsakeRooms.AllRooms && db != null + ? db.Rooms.Select(r => r.RoomId).OrderBy(id => id).ToArray() + : keepsakeRooms.ConfiguredRooms); + + [HttpGet("/commerce/purchasecampaign/allcurrent/v2")] + public IActionResult CurrentPurchaseCampaigns() => Ok(Array.Empty()); + + [HttpGet("/api/apple/musicpromotion/active")] + public IActionResult AppleMusicPromotionActive() => Ok(false); + + [HttpGet("/accounts/emojiConfig/whitelistedEmojis")] + public IActionResult WhitelistedEmojis() => Ok(ProfileEmojis); + + private static readonly string[] ProfileEmojis = + [ + "😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "🙂", + "🙃", "😉", "😍", "🥰", "😘", "😎", "🤓", "🤩", "🥳", "🤔", + "😴", "😭", "😱", "😇", "🤠", "🤖", "👻", "👽", "👾", "💀", + "😺", "😸", "😹", "😻", "🙈", "🙉", "🙊", "👋", "👍", "👏", + "🙌", "💪", "🤝", "🤘", "🤙", "✌️", "❤️", "🧡", "💛", "💚", + "💙", "💜", "🖤", "🤍", "🤎", "💕", "💖", "💯", "✨", "⭐", + "🌟", "🔥", "🌈", "☀️", "🌙", "⚡", "❄️", "🌊", "🍀", "🌻", + "🌸", "🌹", "🌵", "🌲", "🐶", "🐱", "🐭", "🐰", "🦊", "🐻", + "🐼", "🐸", "🐵", "🦁", "🐯", "🐨", "🐷", "🐧", "🐢", "🐙", + "🦋", "🐝", "🦄", "🐲", "🍎", "🍉", "🍓", "🍒", "🍍", "🥑", + "🍕", "🍔", "🍟", "🌮", "🍿", "🍩", "🍪", "🎂", "🍦", "☕", + "⚽", "🏀", "🏈", "⚾", "🎾", "🎯", "🎮", "🎲", "🏆", "🎨", + "🎭", "🎵", "🎸", "🥁", "🎤", "🎧", "🚀", "🛸", "✈️", "🚗", + "🚲", "⛵", "🏠", "🏰", "🏝️", "🎡", "💎", "👑", "🎁", "🎈", + "🎉", "🎊", "💡", "📚", "📷", "🔑", "🧩", "🧸", "🏁", "🏳️‍🌈" + ]; + + [HttpGet("/commerce/reminder/currentTokenBundles/v2")] + public IActionResult CurrentTokenBundles() => Ok(Array.Empty()); + + [HttpPost("/econ/api/freegifts/v1/sendmultiple")] + [HttpPost("/api/freegifts/v1/sendmultiple")] + [Authorize] + public async Task SendFreeGifts() + { + var senderId = User.GetUserId(); + if (senderId is not > 0) return Unauthorized(); + + var request = await ReadFreeGiftRequestAsync(); + var recipients = request.Recipients.Where(id => id > 0).Distinct().ToList(); + + if (recipients.Count == 0) + return FreeGiftResult(false, "No recipients were given."); + if (recipients.Count > FriendotronService.MaxRecipients) + return FreeGiftResult(false, $"A friend box can be sent to {FriendotronService.MaxRecipients} players at a time."); + if (recipients.Contains(senderId.Value) && !await SelfGiftingAllowedAsync()) + return FreeGiftResult(false, "You cannot send a friend box to yourself."); + + var known = await db.Players.AsNoTracking() + .Where(player => recipients.Contains(player.AccountId)) + .Select(player => player.AccountId) + .ToListAsync(); + var missing = recipients.FirstOrDefault(id => !known.Contains(id)); + if (missing != 0) + return FreeGiftResult(false, $"Player {missing} does not exist."); + + var lastSent = await LastFriendBoxAsync(senderId.Value); + if (lastSent != null && DateTime.UtcNow - lastSent.Value < FriendotronService.Cooldown) + return FreeGiftResult(false, + $"You can send another friend box in {FriendotronService.FormatRemaining(DateTime.UtcNow, lastSent.Value)}."); + + var boxes = new List<(long Recipient, Gift Box)>(); + foreach (var recipient in recipients) + { + var item = FriendotronService.RollConsumable(DBConfig.DefaultConsumableItems, out var starTier); + if (item == null) + return FreeGiftResult(false, "No consumable is configured for the friend box."); + + boxes.Add((recipient, FriendotronService.BuildBox(senderId.Value, request.Message, item, starTier))); + } + + + + + await db.Database.CreateExecutionStrategy().ExecuteAsync(async () => + { + await using var transaction = db.Database.IsRelational() ? await db.Database.BeginTransactionAsync() : null; + foreach (var (recipient, box) in boxes) + await StorefrontShared.PersistGiftBoxAsync(db, recipient, box, notify: false); + await StampFriendBoxAsync(senderId.Value); + if (transaction != null) await transaction.CommitAsync(); + }); + + foreach (var (recipient, box) in boxes) + await StorefrontShared.SendGiftPackageNotificationAsync(recipient, box); + + return FreeGiftResult(true, string.Empty); + } + + private static OkObjectResult FreeGiftResult(bool success, string error) => new(new Dictionary + { + ["ALOCDGMHFKA"] = success, + ["FBILGKFKFJE"] = error + }); + + private sealed class FreeGiftRequest + { + public List Recipients { get; } = new(); + public string? Message { get; set; } + } + + private async Task ReadFreeGiftRequestAsync() + { + var request = new FreeGiftRequest(); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds", "players" }) + foreach (var value in form[key]) AddRecipients(request, value); + request.Message = FirstFilled(form["Message"], form["message"]); + return request; + } + + string raw; + using (var reader = new StreamReader(Request.Body)) + raw = await reader.ReadToEndAsync(); + + if (!string.IsNullOrWhiteSpace(raw)) + { + try + { + if (JsonNode.Parse(raw) is JsonObject body) + { + foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds", "players" }) + { + var node = body[key]; + if (node is JsonArray array) + foreach (var entry in array) AddRecipients(request, entry?.ToString()); + else if (node != null) + AddRecipients(request, node.ToString()); + } + + request.Message = FirstFilled(Str(body, "Message"), Str(body, "message")); + } + } + catch (JsonException) + { + + } + } + + foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds" }) + foreach (var value in Request.Query[key]) AddRecipients(request, value); + request.Message ??= FirstFilled(Request.Query["Message"], Request.Query["message"]); + + return request; + } + + private static void AddRecipients(FreeGiftRequest request, string? value) + { + if (string.IsNullOrWhiteSpace(value)) return; + + foreach (var part in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + if (long.TryParse(part, out var id)) + request.Recipients.Add(id); + } + + private static string? FirstFilled(params string?[] values) => + values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)); + + private async Task SelfGiftingAllowedAsync() + { + var row = await db.GameConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "Friendotron_AllowSelfGifting"); + if (row == null || string.IsNullOrWhiteSpace(row.Value)) return true; + return !bool.TryParse(row.Value.Trim(), out var allowed) || allowed; + } + + private async Task LastFriendBoxAsync(long accountId) + { + var key = FriendotronService.CooldownKey(accountId); + var row = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == key); + if (row == null) return null; + + return DateTime.TryParse(row.Value, CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var stamp) + ? stamp + : null; + } + + private async Task StampFriendBoxAsync(long accountId) + { + var key = FriendotronService.CooldownKey(accountId); + var value = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); + + var row = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == key); + if (row == null) db.ServerConfig.Add(new ServerConfigVal(key, value)); + else row.Value = value; + + await db.SaveChangesAsync(); + } + + [HttpPut("/gamelogs/reports/{reportId}/userinfo")] + public IActionResult ReportUserInfo(string reportId) => Ok(); + + [HttpDelete("/auth/cachedlogin/current"), Authorize] + public async Task DeleteCurrentCachedLogin() + { + var accountId = User.GetUserId(); + var platformId = User.GetPlatId(); + var rawPlatform = User.FindFirst("rn.plat")?.Value; + if (accountId is not > 0 || string.IsNullOrWhiteSpace(platformId) || + !int.TryParse(rawPlatform, out var platformValue) || + !Enum.IsDefined(typeof(RadicadeServer.RecNet.Enums.Players.Platforms), platformValue) || + platformValue == (int)RadicadeServer.RecNet.Enums.Players.Platforms.All) + { + return Ok(); + } + + var cachedLogins = await db.CachedLogins + .Where(login => login.accountId == accountId.Value && + login.platform == (RadicadeServer.RecNet.Enums.Players.Platforms)platformValue && + login.platformId == platformId) + .ToListAsync(); + if (cachedLogins.Count > 0) + { + db.CachedLogins.RemoveRange(cachedLogins); + await db.SaveChangesAsync(); + } + + return Ok(); + } + + [HttpPost("/roomcomments/comments/create/{roomId:long}"), Authorize] + public async Task CreateRoomComment(long roomId) + { + var authorId = User.GetUserId(); + if (authorId == null) return Unauthorized(new { success = false, error = "Unauthorized" }); + + var body = await ReadCommentBodyAsync(); + var text = body.Text?.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + return BadRequest(new { success = false, error = "Comment is required" }); + } + + var comment = new RoomComment + { + RoomId = roomId, + CreatorAccountId = authorId.Value, + Comment = text, + CommentType = string.IsNullOrWhiteSpace(body.Type) ? "Feedback" : body.Type!.Trim(), + PositionX = body.X, + PositionY = body.Y, + PositionZ = body.Z, + CreatedAt = DateTime.UtcNow + }; + + await db.RoomComments.AddAsync(comment); + await db.SaveChangesAsync(); + + var roomExists = await db.Rooms.AnyAsync(x => x.RoomId == roomId); + + return Ok(new { success = true, message = string.Empty, comment_id = comment.Id }); + } + + private sealed class CommentBody + { + public string? Text { get; set; } + public string? Type { get; set; } + public float? X { get; set; } + public float? Y { get; set; } + public float? Z { get; set; } + } + + private async Task ReadCommentBodyAsync() + { + var result = new CommentBody(); + + var query = Request.Query; + result.Text = FirstNonEmpty(query["comment"], query["Comment"], query["commentText"], query["CommentText"], + query["text"], query["body"]); + result.Type = FirstNonEmpty(query["commentType"], query["CommentType"], query["type"]); + result.X = TryFloat(query["x"], query["positionX"], query["PositionX"]); + result.Y = TryFloat(query["y"], query["positionY"], query["PositionY"]); + result.Z = TryFloat(query["z"], query["positionZ"], query["PositionZ"]); + + var formKeys = string.Empty; + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + formKeys = string.Join(",", form.Keys); + result.Text = FirstNonEmpty(result.Text, + FirstNonEmpty(form["comment"], form["Comment"], form["commentText"], form["text"], form["body"], + form["message"], form["Message"], form["content"], form["Content"])); + result.Type = FirstNonEmpty(result.Type, + FirstNonEmpty(form["commentType"], form["CommentType"], form["type"])); + result.X ??= TryFloat(FirstNonEmpty(form["x"], form["positionX"], form["PositionX"])); + result.Y ??= TryFloat(FirstNonEmpty(form["y"], form["positionY"], form["PositionY"])); + result.Z ??= TryFloat(FirstNonEmpty(form["z"], form["positionZ"], form["PositionZ"])); + } + + var raw = string.Empty; + if (!Request.HasFormContentType && string.IsNullOrWhiteSpace(result.Text)) + { + using var reader = new StreamReader(Request.Body); + raw = await reader.ReadToEndAsync(); + } + + if (string.IsNullOrWhiteSpace(raw)) return result; + + try + { + var node = JsonNode.Parse(raw); + if (node is JsonValue value && value.TryGetValue(out var bare)) + { + result.Text = FirstNonEmpty(result.Text, bare); + return result; + } + + if (node is not JsonObject obj) return result; + + result.Text = FirstNonEmpty(result.Text, FirstNonEmpty( + Str(obj, "Comment"), Str(obj, "comment"), + Str(obj, "CommentText"), Str(obj, "commentText"), + Str(obj, "Text"), Str(obj, "text"), + Str(obj, "Message"), Str(obj, "message"), + Str(obj, "Content"), Str(obj, "content"))); + + result.Type = FirstNonEmpty(result.Type, + FirstNonEmpty(Str(obj, "CommentType"), Str(obj, "commentType"), Str(obj, "Type"), Str(obj, "type"))); + + var position = obj["Position"] ?? obj["position"]; + if (position is JsonObject pos) + { + result.X ??= TryFloat(Str(pos, "x"), Str(pos, "X")); + result.Y ??= TryFloat(Str(pos, "y"), Str(pos, "Y")); + result.Z ??= TryFloat(Str(pos, "z"), Str(pos, "Z")); + } + else + { + result.X ??= TryFloat(Str(obj, "x"), Str(obj, "positionX")); + result.Y ??= TryFloat(Str(obj, "y"), Str(obj, "positionY")); + result.Z ??= TryFloat(Str(obj, "z"), Str(obj, "positionZ")); + } + } + catch (JsonException) + { + result.Text = raw.Trim(); + } + + return result; + } + + private static string? Str(JsonObject obj, string key) + { + var node = obj[key]; + if (node == null) return null; + try + { + if (node is JsonValue value && value.TryGetValue(out var text)) return text; + return node.ToJsonString().Trim('"'); + } + catch + { + return null; + } + } + + private static string? FirstNonEmpty(params string?[] values) + => values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)); + + private static float? TryFloat(params string?[] values) + { + foreach (var value in values) + if (float.TryParse(value, System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var parsed)) + return parsed; + return null; + } +} diff --git a/Server/Config/AmplitudeConfig.cs b/Server/Config/AmplitudeConfig.cs new file mode 100644 index 0000000..d3e684a --- /dev/null +++ b/Server/Config/AmplitudeConfig.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Config +{ + public class AmplitudeConfig + { + public string AmplitudeKey { get; set; } = ""; + public bool UseRudderStack { get; set; } = false; + public string RudderStackKey { get; set; } = ""; + public bool UseStatSig { get; set; } = false; + public string StatSigKey { get; set; } = ""; + public int StatSigEnvironment { get; set; } = 0; + } +} diff --git a/Server/Config/ConfigAPI.cs b/Server/Config/ConfigAPI.cs new file mode 100644 index 0000000..2d1be1f --- /dev/null +++ b/Server/Config/ConfigAPI.cs @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; + +namespace RadicadeServer.API.Config +{ + [Route("api/config")] + public class ConfigAPI : Controller + { + RecDBContext db; + public ConfigAPI(RecDBContext context) + { + this.db = context; + } + + [HttpGet("v1/amplitude")] + public IActionResult GetAmplitudeConfig() + { + AmplitudeConfig amplitudeConfig = new AmplitudeConfig + { + AmplitudeKey = "e1693a1003671058b6abc356c8ba8d59", + UseRudderStack = true, + RudderStackKey = "23NiJHIgu3koaGNCZIiuYvIQNCu", + UseStatSig = true, + StatSigKey = "client-SBZkOrjD3r1Cat3f3W8K6sBd11WKlXZXIlCWj6l4Aje", + StatSigEnvironment = 0, + }; + return Ok(amplitudeConfig); + } + + [HttpGet("v1/freegiftbutton")] + public IActionResult GetFreeGiftButtonConfig() + { + + + return Ok(true); + } + + [HttpGet("v1/azurespeech")] + public IActionResult GetAzureSpeechConfig() + { + return Ok(new + { + Key = "dce8de5b297747d9b5bddcc7f19e8c5b", + Region = "eastus", + Enabled = false, + }); + } + + [HttpGet("v1/backtrace")] + public IActionResult GetBacktraceConfig() + { + return Ok(new + { + ReportBudget = 125, + FilterType = 0, + SampleRate = 1, + LogLineCount = 50, + CaptureNativeCrashes = 1, + AMRThresholdMS = 0, + MessageCount = 1000, + MessageRegex = "^.*$", + VersionRegex = ".*", + }); + } + + [HttpGet("v2")] + public async Task> GetConfig() + { + MainConfig? config = await db.Config.FirstOrDefaultAsync(); + if (config == null) return StatusCode(500); + + bool didChanges = false; + + if (string.IsNullOrWhiteSpace(config.CdnBaseUri) || config.CdnBaseUri.Contains("localhost", StringComparison.OrdinalIgnoreCase)) + { + config.CdnBaseUri = DBConfig.NameServer.CDN; + didChanges = true; + } + if (string.IsNullOrWhiteSpace(config.ShareBaseUrl) || config.ShareBaseUrl.Contains("localhost", StringComparison.OrdinalIgnoreCase)) + { + config.ShareBaseUrl = DBConfig.NameServer.WWW + "{0}"; + didChanges = true; + } + + if (config.ExpectingReload) { + didChanges = true; + config.SetAsActive(); + config.ExpectingReload = false; + } + + if (config.RefreshConfig()) + didChanges = true; + + if (didChanges) + await db.SaveChangesAsync(); + + return Ok(config?.ToActualConfig()); + } + + [HttpPost("~/statsigUserProperties")] + public IActionResult GetStatsigUserProperties() + { + return Ok(new { success = true }); + } + + [HttpGet("~/voice/config")] + [HttpGet("~/moderation/voice/config")] + public IActionResult GetVoiceConfig() + { + return Ok(new + { + KOLPDOKFMGH = "", + OKHMLNBOBNP = (object?)null, + BEGBMPFDPIE = (object?)null, + BNOIPAAFAMJ = new + { + _AOMMGOMJHPE = "", + _NPEJEBGENBC = "", + PEFLHAPIDIF = "", + ALOMNDOKBLH = "" + }, + PAFBLJGNBLN = new string[0], + HAOCNOKPKDH = new string[0], + MBEGDKGEOMJ = false + }); + } + } +} diff --git a/Server/Config/ConfigVal.cs b/Server/Config/ConfigVal.cs new file mode 100644 index 0000000..2d3414d --- /dev/null +++ b/Server/Config/ConfigVal.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Config +{ + public class ConfigVal + { + public ConfigVal() { } + public ConfigVal(string key, string val) { Key = key; Value = val; } + + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Key { get; set; } = null!; + public string Value { get; set; } = null!; + } +} diff --git a/Server/Config/GameConfigAPI.cs b/Server/Config/GameConfigAPI.cs new file mode 100644 index 0000000..2078fab --- /dev/null +++ b/Server/Config/GameConfigAPI.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Keepsakes; + +namespace RadicadeServer.API.Config +{ + [Route("api/gameconfigs/")] + public class GameConfigAPI : Controller + { + private static readonly Dictionary Local2023Overrides = new() + { + ["RRUI.UseRRUIHomeScreen"] = "true", + ["USE_NEW_HOME_SCREEN"] = "true", + ["new_watch_architecture_enabled"] = "true", + ["watch_layer.new_watch_architecture_enabled"] = "true", + ["HAS_SEEN_HOME_SCREEN_CHOICE"] = "true", + ["AntiHile.QD"] = "false", + ["Radicade.Invites.UseRRUIDialog"] = "true", + ["Radicade.Invites.ShowOnlineFriendsBeforeJoining"] = "true", + ["Lifecycle.PSF.FriendRoomInviteRateLimitingCount"] = "2147483647", + ["AndroidMobile.enforceFileWatcher"] = "0", + ["AndroidMobile.enforceHashCheck"] = "0", + ["AndroidMobile.runFileWatcher"] = "0", + ["AndroidMobile.runHashCheck"] = "0", + ["Desktop_Oculus.enforceFileWatcher"] = "0", + ["Desktop_Oculus.enforceHashCheck"] = "0", + ["Desktop_Oculus.runFileWatcher"] = "0", + ["Desktop_Oculus.runHashCheck"] = "0", + ["Desktop_Steam.enforceFileWatcher"] = "0", + ["Desktop_Steam.enforceHashCheck"] = "0", + ["Desktop_Steam.runFileWatcher"] = "0", + ["Desktop_Steam.runHashCheck"] = "0", + ["OculusQuest.enforceFileWatcher"] = "0", + ["OculusQuest.enforceHashCheck"] = "0", + ["OculusQuest.runFileWatcher"] = "0", + ["OculusQuest.runHashCheck"] = "0", + ["Steam.enforceFileWatcher"] = "0", + ["Steam.enforceHashCheck"] = "0", + ["Steam.runFileWatcher"] = "0", + ["Steam.runHashCheck"] = "0", + }; + + private readonly RecDBContext db; + + public GameConfigAPI(RecDBContext context) + { + db = context; + } + + private static IEnumerable> KeepsakeConfigValues() + { + var enabled = KeepsakeFeature.Enabled; + yield return new("KeepsakeFeatureEnabled", enabled ? "true" : "false"); + yield return new("KeepsakeRoomLimit", enabled ? "100" : "0"); + yield return new("ProgressionEvents.EnableKeepsakeManager", enabled ? "true" : "false"); + } + + [HttpGet("v1/all")] + public IActionResult GetAllGameConfigs() + { + var overrides = new Dictionary(Local2023Overrides); + foreach (var (key, value) in KeepsakeConfigValues()) overrides[key] = value; + + var configs = db.GameConfig + .AsEnumerable() + .Select(x => new ConfigVal( + x.Key, + overrides.TryGetValue(x.Key, out var value) + ? value + : SplitTestConfigs.FilterHardOverridesFor2023Client(x.Key, x.Value))) + .GroupBy(x => x.Key) + .Select(group => + { + if (overrides.TryGetValue(group.Key, out var value)) + return new ConfigVal(group.Key, value); + + return group.Last(); + }) + .ToList(); + + foreach (var (key, value) in overrides) + { + if (!configs.Any(x => x.Key == key)) + configs.Add(new ConfigVal(key, value)); + } + + return Ok(configs); + } + } +} diff --git a/Server/Config/GameConfigSeeder.cs b/Server/Config/GameConfigSeeder.cs new file mode 100644 index 0000000..f742b3a --- /dev/null +++ b/Server/Config/GameConfigSeeder.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; + +namespace RadicadeServer.Utils +{ + public static class GameConfigSeeder + { + public static async Task FindAsync(RecDBContext db, string key) + => db.GameConfig.Local.FirstOrDefault(row => row.Key == key) + ?? await db.GameConfig.FirstOrDefaultAsync(row => row.Key == key); + + public static async Task UpsertAsync(RecDBContext db, string key, string value) + { + var existing = await FindAsync(db, key); + if (existing == null) + { + var added = new ConfigVal(key, value); + await db.GameConfig.AddAsync(added); + return added; + } + + if (string.IsNullOrWhiteSpace(existing.Value)) + existing.Value = value; + + return existing; + } + } +} diff --git a/Server/Config/MainConfig.cs b/Server/Config/MainConfig.cs new file mode 100644 index 0000000..5af0191 --- /dev/null +++ b/Server/Config/MainConfig.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore; +using Rubidium2018.Utils.Objectives; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Maintenance; +using RadicadeServer.RecNet.Progression.Objectives; + +namespace RadicadeServer.RecNet.Config +{ + public class MainConfig + { + [Key] + public string FUCKYOUMICROSOFT { get; set; } = null!; + public string ShareBaseUrl { get; set; } = null!; + public string CdnBaseUri { get; set; } = null!; + public string LevelProgressionMaps { get; set; } = null!; + public string ServerMaintenance { get; set; } = null!; + public string DailyObjectives { get; set; } = null!; + public string AutoMicMutingConfig { get; set; } = null!; + public string StorefrontConfig { get; set; } = null!; + public string RoomKeyConfig { get; set; } = null!; + public string RoomCurrencyConfig { get; set; } = null!; + public string GiftDropStoreItems { get; set; } = null!; + + public bool ExpectingReload { get; set; } + public DateTime LastRefresh_DailyObjectives { get; set; } = DateTime.Now; + + public static MainConfig? ActiveConfig = null; + internal void SetAsActive() + { + ActiveConfig = this; + } + } + + public static class MainConfigExt + { + public static bool RefreshConfig(this MainConfig config, bool force = false) + { + if (config == null) + return false; + + bool changed = false; + + if (DateTime.Now >= config.LastRefresh_DailyObjectives || force) + { + changed = true; + + Objective[][] dailys = + [ + [RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective()], + [RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective()], + [RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective()], + [RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective()], + [RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective()], + [RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective()], + [RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective(), RbPresetDailys.CreateRandomObjective()] + ]; + + config.DailyObjectives = dailys.ToJson(); + config.LastRefresh_DailyObjectives = DateTime.Now.AddDays(7); + } + + return changed; + } + + public static RealMainConfig ToActualConfig(this MainConfig config) + { + return new RealMainConfig + { + ShareBaseUrl = config.ShareBaseUrl ?? string.Empty, + CdnBaseUri = config.CdnBaseUri ?? string.Empty, + StorefrontConfig = config.StorefrontConfig.FromJson() ?? new StorefrontConfig(), + AutoMicMutingConfig = config.AutoMicMutingConfig.FromJson() ?? new MicMuting(), + RoomCurrencyConfig = config.RoomCurrencyConfig.FromJson() ?? new RoomCurrencyConfig(), + RoomKeyConfig = config.RoomKeyConfig.FromJson() ?? new RoomKeyConfig(), + DailyObjectives = config.DailyObjectives.FromJson() ?? Array.Empty(), + LevelProgressionMaps = config.LevelProgressionMaps.FromJson() ?? Array.Empty(), + ServerMaintenance = config.ServerMaintenance.FromJson() ?? new ServerMaintenance { StartsInMinutes = 0 } + }; + } + } +} diff --git a/Server/Config/MicMuting.cs b/Server/Config/MicMuting.cs new file mode 100644 index 0000000..53bd0df --- /dev/null +++ b/Server/Config/MicMuting.cs @@ -0,0 +1,14 @@ +namespace RadicadeServer.RecNet.Config +{ + public class MicMuting + { + public float MicSpamVolumeThreshold { get; set; } + public float MicVolumeSampleInterval { get; set; } + public float MicVolumeSampleRollingWindowLength { get; set; } + public float MicSpamSamplePercentageForWarning { get; set; } + public float MicSpamSamplePercentageForWarningToEnd { get; set; } + public float MicSpamSamplePercentageForForceMute { get; set; } + public float MicSpamSamplePercentageForForceMuteToEnd { get; set; } + public float MicSpamWarningStateVolumeMultiplier { get; set; } + } +} diff --git a/Server/Config/PlayerSettingsAPI.cs b/Server/Config/PlayerSettingsAPI.cs new file mode 100644 index 0000000..cfd06ed --- /dev/null +++ b/Server/Config/PlayerSettingsAPI.cs @@ -0,0 +1,354 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts.Local; + +namespace RadicadeServer.API.Config +{ + [Route("api/settings")] + [Route("playersettings")] + public class PlayerSettingsAPI : Controller + { + private readonly RecDBContext db; + + public PlayerSettingsAPI(RecDBContext context) + { + db = context; + } + + [HttpGet(""), Authorize] + [HttpGet("v2"), Authorize] + [HttpGet("playersettings"), Authorize] + public List GetSettings() + { + var userId = User.GetUserId(); + if (userId == null) + return new List(); + + var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value); + var list = settings?.DeserializeSettings() ?? GetDefaultSettings(); + ApplyDefaultSettings(list); + var player = db.Players.AsNoTracking().FirstOrDefault(x => x.AccountId == userId.Value); + if (player != null && string.IsNullOrWhiteSpace(player.Hashword)) + { + SetOrReplace(list, "Recroom.AccountCreation.HasCreatedPassword", "False"); + SetOrReplace(list, "Recroom.AccountCreation.HasFinished", "False"); + SetOrReplace(list, "Recroom.OOBE", "0"); + } + ApplyResponseMetadata(list, userId.Value); + if (settings == null) + { + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = userId.Value, + Settings = list.ToJson() + }); + db.SaveChanges(); + } + + return list; + } + + private static void SetOrReplace(List list, string key, string value) + { + var existing = list.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)); + if (existing != null) + existing.Value = value; + else + list.Add(new Setting { Key = key, Value = value }); + } + + private static void ApplyDefaultSettings(List list) + { + bool needsOrientation = list.Any(setting => + string.Equals(setting.Key, "Radicade.NeedsOrientation", StringComparison.OrdinalIgnoreCase) && + (setting.Value == "1" || string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase))); + foreach (var setting in GetDefaultSettings()) + { + if (needsOrientation && + (string.Equals(setting.Key, "HAS_COMPLETED_ORIENTATION", StringComparison.OrdinalIgnoreCase) || + string.Equals(setting.Key, "OrientationCompletionTime", StringComparison.OrdinalIgnoreCase))) + continue; + if (list.All(existing => !string.Equals(existing.Key, setting.Key, StringComparison.OrdinalIgnoreCase))) + list.Add(setting); + } + + SetOrReplace(list, "SplitTestAssignedSegments", "1|{}"); + } + + private static void ApplyResponseMetadata(List list, long accountId) + { + for (var i = 0; i < list.Count; i++) + { + list[i].Id = i + 1; + list[i].AccountId = accountId; + } + } + + public const int InviteLinkLifetimeInMinutes = 60; + + [HttpGet("partyinvite")] + [HttpGet("~/chat/settings/partyinvite")] + public IActionResult GetPartyInviteSettings() => Ok(new { InviteLinkLifetimeInMinutes }); + + [HttpPost("v2/set"), Authorize] + [HttpPost(""), Authorize] + [HttpPost("playersettings/set"), Authorize] + public async Task SetSettingRequest() + { + if (User.GetUserId() == null) return Unauthorized(); + var body = await ReadJsonBodyAsync(); + var updates = body.HasValue ? ReadSettings(body.Value) : new List(); + if (updates.Count != 1) return BadRequest("Expected one setting with Key and Value."); + return await SetSetting(updates[0]); + } + + [NonAction] + public async Task SetSetting(Setting val) + { + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(); + if (val == null || string.IsNullOrWhiteSpace(val.Key) || val.Value == null) + return BadRequest("Expected a setting with Key and Value."); + + var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value); + if (settings == null) + { + settings = new PlayerSettings + { + AccountId = userId.Value, + Settings = DBConfig.DefaultSettings2023 + }; + await db.PlayerSettings.AddAsync(settings); + } + + var actualSettings = settings.DeserializeSettings(); + if (!await CanSaveOnboardingMarkerAsync(userId.Value, val, actualSettings, new[] { val })) + return Ok(Array.Empty()); + + SetOrReplace(actualSettings, val.Key, val.Value); + if (IsOnboardingFinishedMarker(val)) + SetOrReplace(actualSettings, "Radicade.NeedsOrientation", "1"); + + settings.Settings = actualSettings.ToJson(); + await db.SaveChangesAsync(); + + await RecordCodeOfConductAsync(userId.Value, new[] { val }, actualSettings); + + return Ok(Array.Empty()); + } + + [HttpPut(""), Authorize] + [HttpPut("v2"), Authorize] + [HttpPut("playersettings"), Authorize] + public async Task PutSettings() + { + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(); + + var body = await ReadJsonBodyAsync(); + var updates = body.HasValue ? ReadSettings(body.Value) : new List(); + if (updates.Count == 0) + return BadRequest("No valid settings were supplied."); + + var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value); + if (settings == null) + { + settings = new PlayerSettings + { + AccountId = userId.Value, + Settings = DBConfig.DefaultSettings2023 + }; + await db.PlayerSettings.AddAsync(settings); + } + + var actualSettings = settings.DeserializeSettings(); + foreach (var update in updates) + { + if (string.IsNullOrWhiteSpace(update.Key) || update.Value == null || + !await CanSaveOnboardingMarkerAsync(userId.Value, update, actualSettings, updates)) + continue; + + SetOrReplace(actualSettings, update.Key, update.Value); + if (IsOnboardingFinishedMarker(update)) + SetOrReplace(actualSettings, "Radicade.NeedsOrientation", "1"); + } + + settings.Settings = actualSettings.ToJson(); + await db.SaveChangesAsync(); + await RecordCodeOfConductAsync(userId.Value, updates, actualSettings); + ApplyResponseMetadata(actualSettings, userId.Value); + + return Ok(actualSettings); + } + + private async Task RecordCodeOfConductAsync( + long accountId, + IEnumerable incomingSettings, + IEnumerable savedSettings) + { + bool accepted = incomingSettings.Concat(savedSettings).Any(setting => + string.Equals(setting.Key, "Recroom.AccountCreation.AcceptedCodeOfConduct", StringComparison.OrdinalIgnoreCase) && + string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); + if (!accepted) return; + + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == accountId); + if (player == null || player.CodeOfConductAccepted) return; + + player.CodeOfConductAccepted = true; + await db.SaveChangesAsync(); + } + + private static bool IsOnboardingFinishedMarker(Setting setting) => + (string.Equals(setting.Key, "Recroom.OOBE", StringComparison.OrdinalIgnoreCase) && setting.Value == "100") || + (string.Equals(setting.Key, "Recroom.AccountCreation.HasFinished", StringComparison.OrdinalIgnoreCase) && + string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); + + private async Task CanSaveOnboardingMarkerAsync( + long accountId, + Setting update, + IReadOnlyCollection savedSettings, + IReadOnlyCollection incomingSettings) + { + bool marksPasswordCreated = + string.Equals(update.Key, "Recroom.AccountCreation.HasCreatedPassword", StringComparison.OrdinalIgnoreCase) && + string.Equals(update.Value, "true", StringComparison.OrdinalIgnoreCase); + if (!marksPasswordCreated && !IsOnboardingFinishedMarker(update)) + return true; + + var player = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == accountId); + if (player == null || string.IsNullOrWhiteSpace(player.Hashword)) + { + return false; + } + if (marksPasswordCreated) + return true; + + var allSettings = incomingSettings.Concat(savedSettings).ToList(); + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId); + bool hasBirthday = player.Birthday.HasValue && player.Birthday.Value.Year > 1900; + bool hasChosenUsername = allSettings.Any(setting => + string.Equals(setting.Key, "Recroom.AccountCreation.HasChosenUsername", StringComparison.OrdinalIgnoreCase) && + string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); + bool hasAcceptedCodeOfConduct = player.CodeOfConductAccepted || allSettings.Any(setting => + string.Equals(setting.Key, "Recroom.AccountCreation.AcceptedCodeOfConduct", StringComparison.OrdinalIgnoreCase) && + string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); + + if (profile == null || string.IsNullOrWhiteSpace(profile.username)) + { + return false; + } + if (!hasChosenUsername) + { + return false; + } + if (!hasAcceptedCodeOfConduct) + { + return false; + } + return true; + } + + private static List GetDefaultSettings() + { + try + { + return DBConfig.DefaultSettings2023.FromJson>() ?? new List(); + } + catch (Exception) + { + return new List(); + } + } + + private async Task ReadJsonBodyAsync() + { + if (Request.Body == null) + return null; + + if (Request.ContentLength == 0) + return null; + + try + { + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + return JsonSerializer.SerializeToElement(form.ToDictionary(pair => pair.Key, pair => pair.Value.ToString())); + } + using var doc = await JsonDocument.ParseAsync(Request.Body); + return doc.RootElement.Clone(); + } + catch (JsonException) + { + return null; + } + } + + private static List ReadSettings(JsonElement body) + { + if (body.ValueKind == JsonValueKind.Array) + return ReadSettingsArray(body); + + if (body.ValueKind != JsonValueKind.Object) + return new List(); + + foreach (var key in new[] { "settings", "Settings", "playerSettings", "PlayerSettings" }) + { + if (body.TryGetProperty(key, out var nested) && nested.ValueKind == JsonValueKind.Array) + return ReadSettingsArray(nested); + } + + var setting = ReadSettingObject(body); + return setting == null ? new List() : new List { setting }; + } + + private static List ReadSettingsArray(JsonElement array) + { + var settings = new List(); + foreach (var item in array.EnumerateArray()) + { + var setting = ReadSettingObject(item); + if (setting != null) + settings.Add(setting); + } + return settings; + } + + private static Setting? ReadSettingObject(JsonElement item) + { + if (item.ValueKind != JsonValueKind.Object) + return null; + + var key = ReadString(item, "Key", "key", "Name", "name"); + var value = ReadString(item, "Value", "value"); + if (string.IsNullOrWhiteSpace(key) || value == null) + return null; + + return new Setting { Key = key, Value = value }; + } + + private static string? ReadString(JsonElement item, params string[] names) + { + foreach (var name in names) + { + if (!item.TryGetProperty(name, out var value)) + continue; + + return value.ValueKind switch + { + JsonValueKind.String => value.GetString(), + JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => value.ToString(), + _ => null, + }; + } + + return null; + } + } +} diff --git a/Server/Config/RealMainConfig.cs b/Server/Config/RealMainConfig.cs new file mode 100644 index 0000000..d7b9630 --- /dev/null +++ b/Server/Config/RealMainConfig.cs @@ -0,0 +1,19 @@ +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Maintenance; +using RadicadeServer.RecNet.Progression.Objectives; + +namespace RadicadeServer.RecNet.Config +{ + public class RealMainConfig + { + public string ShareBaseUrl { get; set; } = null!; + public string CdnBaseUri { get; set; } = null!; + public LevelProgressionMap[] LevelProgressionMaps { get; set; } = null!; + public ServerMaintenance ServerMaintenance { get; set; } = null!; + public Objective[][] DailyObjectives { get; set; } = null!; + public MicMuting AutoMicMutingConfig { get; set; } = null!; + public StorefrontConfig StorefrontConfig { get; set; } = null!; + public RoomKeyConfig RoomKeyConfig { get; set; } = null!; + public RoomCurrencyConfig RoomCurrencyConfig { get; set; } = null!; + } +} diff --git a/Server/Config/RoomCurrencyConfig.cs b/Server/Config/RoomCurrencyConfig.cs new file mode 100644 index 0000000..22c68d6 --- /dev/null +++ b/Server/Config/RoomCurrencyConfig.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Config +{ + public class RoomCurrencyConfig + { + public float AwardCurrencyCooldownSeconds { get; set; } + } +} diff --git a/Server/Config/RoomKeyConfig.cs b/Server/Config/RoomKeyConfig.cs new file mode 100644 index 0000000..0c5d68c --- /dev/null +++ b/Server/Config/RoomKeyConfig.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Config +{ + public class RoomKeyConfig + { + public int MaxKeysPerRoom { get; set; } + } +} diff --git a/Server/Config/ServerConfigVal.cs b/Server/Config/ServerConfigVal.cs new file mode 100644 index 0000000..dab5c23 --- /dev/null +++ b/Server/Config/ServerConfigVal.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Config +{ + public class ServerConfigVal + { + public ServerConfigVal() { } + public ServerConfigVal(string key, string val) { Key = key; Value = val; } + + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Key { get; set; } = null!; + public string Value { get; set; } = null!; + } +} diff --git a/Server/Config/SplitTestConfigs.cs b/Server/Config/SplitTestConfigs.cs new file mode 100644 index 0000000..5fba55a --- /dev/null +++ b/Server/Config/SplitTestConfigs.cs @@ -0,0 +1,54 @@ +namespace RadicadeServer.RecNet.Config +{ + public static class SplitTestConfigs + { + public const string HardOverrides = + "IOSAppPermissions_2019_10_02=Immediate;GottaGoFastNUX_2020_09_04=Off;RecNetImageCacheRefCount_2020_09_18=On;Curated_Rooms_2020_08_06=On;PlayMenuRRUI_2021_01_11=On;PlayMenuRRUI_HotRoomsCarousel_2021_02_09=On;PlayMenuRRUI_CarouselOrdering_2021_02_09=PlayMenuCarouselOrdering_1;CreationCommands_2021_02_16=Off;RecRoomPlus_DormMirrorButton_2021_03_09=ButtonOn;CurrentContestWinnersCarouselVisibility_2021_04_15=Off;MutualFriends_2021_04_22=On;PlayMenuRRUI_DefaultTab_2021_05_13=Highlight;DefaultNewPlayersToPlayMenu_2021_05_24=On;ShowNewRRPlusIcon_2021_06_01=NewIcon;ParallelNetworkingInitialRoomLoad_2021_06_28=ParallelLoad;ShowNewRRPlusBenefitsPage_2021_06_14=OldPage;WatchStoreHomepageLayout_2021_06_14=OldStoreLayout;NotificationPermissionsExplanationText_2021_06_25=Control;PlayMenuRRUI_TopCreatorsCarousel_2021_09_01=Off;UseSets_2021_08_06=UseSets;RoomSimilarities_2021_10_29=On;ProfileOnClick_2021_04_30=On;RoomRecommendations_2020_05_06=On;FriendImportLogin_2021_09_23=On;RudderStackAnalyticsDestinations_2022_02_10=RudderStackOnly;ObjectModel_EnableForRoom_2022_07_01=Disallowed;PlayMenuCarouselOrdering_2021_11_01=PlayMenuCarouselOrdering_1;PlayMenuBanner_2021_11_01=Off;"; + + public const string TestsAvailable = + "RoomRecommendations_2020_05_06=On,Off;Curated_Rooms_2020_08_06=Off,On;RecNetImageCacheRefCount_2020_09_18=On,Off;GottaGoFastNUX_2020_09_04=Off,On;Curated_Rooms_Tab_Placement_2020_09_22=First,Second;PlayMenuRRUI_2021_01_11=Off,On;PlayMenuRRUI_HotRoomsCarousel_2021_02_09=Off,On;PlayMenuRRUI_CarouselOrdering_2021_02_09=PlayMenuCarouselOrdering_1,PlayMenuCarouselOrdering_2,PlayMenuCarouselOrdering_3;CreationCommands_2021_02_16=Off,On;RecRoomPlus_DormMirrorButton_2021_03_09=ButtonOn,ButtonOff;CurrentContestWinnersCarouselVisibility_2021_04_15=Off,On;MutualFriends_2021_04_22=Off,On;ProfileOnClick_2021_04_30=Off,On;PlayMenuRRUI_DefaultTab_2021_05_13=Highlight,RRO,Hot;DefaultNewPlayersToPlayMenu_2021_05_24=Off,On;ShowNewRRPlusIcon_2021_06_01=OldIcon,NewIcon;ShowNewRRPlusBenefitsPage_2021_06_14=OldPage,NewPage;WatchStoreHomepageLayout_2021_06_14=OldStoreLayout,NewStoreLayout;NotificationPermissionsExplanationText_2021_06_25=Control,SpecificValueText;ParallelNetworkingInitialRoomLoad_2021_06_28=SequentialLoad,ParallelLoad;UseSets_2021_08_06=IgnoreSets,UseSets;PlayMenuRRUI_TopCreatorsCarousel_2021_09_01=Off,On;FriendImportLogin_2021_09_23=Off,On;Purchase_Campaigns_AB_2021_09_13=Purchase_Campaign_AB_A,Purchase_Campaign_AB_B;Purchase_Campaigns_ABCD_2021_09_13=Purchase_Campaign_ABCD_A,Purchase_Campaign_ABCD_B,Purchase_Campaign_ABCD_C,Purchase_Campaign_ABCD_D;RoomSimilarities_2021_10_29=On,Off;PlayMenuBanner_2021_11_01=Off,On;PlayMenuCarouselOrdering_2021_11_01=PlayMenuCarouselOrdering_1,PlayMenuCarouselOrdering_2,PlayMenuCarouselOrdering_3;RudderStackAnalyticsDestinations_2022_02_10=Disabled,RudderStackOnly,Both;ObjectModel_EnableForRoom_2022_07_01=Disallowed,Allowed;"; + + public const string SegmentProbabilities = + "{\"RoomRecommendations_2020_05_06\":[{\"SegmentName\":\"On\",\"Probability\":0.25},{\"SegmentName\":\"Off\",\"Probability\":0.75}],\"CurrentContestWinnersCarouselVisibility_2021_04_15\":[{\"SegmentName\":\"Off\",\"Probability\":1.0},{\"SegmentName\":\"On\",\"Probability\":0.0}],\"PlayMenuBanner_2021_11_01\":[{\"SegmentName\":\"Off\",\"Probability\":0.5},{\"SegmentName\":\"On\",\"Probability\":0.5}],\"FriendImportLogin_2021_09_23\":[{\"SegmentName\":\"Off\",\"Probability\":0.5},{\"SegmentName\":\"On\",\"Probability\":0.5}],\"PlayMenuCarouselOrdering_2021_11_01\":[{\"SegmentName\":\"PlayMenuCarouselOrdering_1\",\"Probability\":0.33},{\"SegmentName\":\"PlayMenuCarouselOrdering_2\",\"Probability\":0.33},{\"SegmentName\":\"PlayMenuCarouselOrdering_3\",\"Probability\":0.34}]}"; + + public static readonly IReadOnlyDictionary> Client2023Catalog = + new Dictionary>(StringComparer.Ordinal) + { + ["Example_Test_2020_06_06"] = new[] { "On", "Off" }, + ["RoomRecommendations_2020_05_06"] = new[] { "On", "Off" }, + ["RoomRecommendationsType_2020_08_14"] = new[] { "June22", "Aug14JuniorSpecificModel", "Aug14JuniorAccessibleModelForAll", "Aug14MinVisitors350", "Aug14MinVisitors3500", "Aug14MinVisitors35000" }, + ["GottaGoFastNUX_2020_09_04"] = new[] { "Off", "On" }, + ["PlayMenuRRUI_HotRoomsCarousel_2021_02_09"] = new[] { "Off", "On" }, + ["PlayMenuRRUI_CarouselOrdering_2021_02_09"] = new[] { "PlayMenuCarouselOrdering_1", "PlayMenuCarouselOrdering_2", "PlayMenuCarouselOrdering_3" }, + ["MutualFriends_2021_04_22"] = new[] { "Off", "On" }, + ["CurrentContestWinnersCarouselVisibility_2021_04_15"] = new[] { "Off", "On" }, + ["PlayMenuRRUI_DefaultTab_2021_05_13"] = new[] { "Highlight", "RRO", "Hot" }, + ["ProfileOnClick_2021_04_30"] = new[] { "Off", "On" }, + ["DefaultNewPlayersToPlayMenu_2021_05_24"] = new[] { "Off", "On" }, + ["ParallelNetworkingInitialRoomLoad_2021_06_28"] = new[] { "SequentialLoad", "ParallelLoad" }, + ["PlayMenuRRUI_TopCreatorsCarousel_2021_09_01"] = new[] { "Off", "On" }, + ["FriendImportLogin_2021_09_23"] = new[] { "Off", "On" }, + ["RoomSimilarities_2021_10_29"] = new[] { "On", "Off" }, + ["PlayMenuBanner_2021_11_01"] = new[] { "Off", "On" }, + ["PlayMenuCarouselOrdering_2021_11_01"] = new[] { "PlayMenuCarouselOrdering_1", "PlayMenuCarouselOrdering_2", "PlayMenuCarouselOrdering_3" }, + ["RudderStackAnalyticsDestinations_2022_02_10"] = new[] { "Disabled", "RudderStackOnly", "Both" }, + ["ObjectModel_EnableForRoom_2022_07_01"] = new[] { "Disallowed", "Allowed" }, + }; + + public static string FilterHardOverridesFor2023Client(string key, string value) + { + if (key != "splitTestHardOverrides" || string.IsNullOrWhiteSpace(value)) + return value; + + var kept = value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(entry => entry.Split('=', 2)) + .Where(pair => pair.Length == 2 + && Client2023Catalog.TryGetValue(pair[0], out var segments) + && segments.Contains(pair[1], StringComparer.Ordinal)) + .Select(pair => pair[0] + "=" + pair[1]); + + var keptList = kept.ToList(); + return keptList.Count > 0 ? string.Join(";", keptList) + ";" : string.Empty; + } + } +} diff --git a/Server/Config/StorefrontConfig.cs b/Server/Config/StorefrontConfig.cs new file mode 100644 index 0000000..761bb11 --- /dev/null +++ b/Server/Config/StorefrontConfig.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Config +{ + public class StorefrontConfig + { + public int MinPlayerLevelForGifting { get; set; } + } +} diff --git a/Server/Config/VoiceChatRestrictionAPI.cs b/Server/Config/VoiceChatRestrictionAPI.cs new file mode 100644 index 0000000..fdc6170 --- /dev/null +++ b/Server/Config/VoiceChatRestrictionAPI.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Config +{ + [Route("api/voicechat")] + public class VoiceChatRestrictionAPI(RecDBContext db) : Controller + { + [HttpGet("restriction"), Authorize] + public async Task GetRestriction() + { + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(new { restricted = false }); + + var restriction = await VoiceChatRestrictions.ActiveAsync(db, userId.Value); + return Ok(VoiceChatRestrictions.Payload(restriction)); + } + } +} \ No newline at end of file diff --git a/Server/CustomAvatarItems/CustomAvatarItemDB.cs b/Server/CustomAvatarItems/CustomAvatarItemDB.cs new file mode 100644 index 0000000..55edc72 --- /dev/null +++ b/Server/CustomAvatarItems/CustomAvatarItemDB.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Avatar +{ + public class CustomAvatarItemDB + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public Guid CustomAvatarItemId { get; set; } + + public long CreatorAccountId { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } + + public int Price { get; set; } + + public int BaseAvatarItemId { get; set; } + + public string? BaseAvatarItemColor { get; set; } + + public int Accessibility { get; set; } + + public bool IsPublished { get; set; } + + public string? ImageName { get; set; } + + public string? DesignFileName { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime ModifiedAt { get; set; } + } +} diff --git a/Server/CustomAvatarItems/CustomAvatarItemsAPI.cs b/Server/CustomAvatarItems/CustomAvatarItemsAPI.cs new file mode 100644 index 0000000..64f450d --- /dev/null +++ b/Server/CustomAvatarItems/CustomAvatarItemsAPI.cs @@ -0,0 +1,491 @@ +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Avatars +{ + [Route("api/customAvatarItems")] + [Route("econ/customAvatarItems")] + [Route("econ/econ/customAvatarItems")] + public class CustomAvatarItemsAPI : Controller + { + readonly RecDBContext db; + + public CustomAvatarItemsAPI(RecDBContext context) + { + this.db = context; + } + + private static Task AnnounceCustomItemChange(long ownerId) => Notify.SendAccountUpdate(ownerId); + + /// + /// Whether this account may draw a custom item at all. The client reads the answer off the + /// two obfuscated members this route's own response type carries, so a PascalCase "success" + /// body left every option on the drawing screen disabled - creating and recolouring both + /// greyed out - even though nothing on the account was wrong. + /// + [HttpGet("v1/isCreationAllowedForAccount")] + public IActionResult IsCreationAllowedForAccount() + { + return Ok(ClientAction.Ok()); + } + + [HttpGet("v1/isCreationEnabled")] + [HttpGet("v1/isRenderingEnabled")] + public IActionResult IsEnabled() + { + return Ok(true); + } + + [HttpGet("v1/minPriceForPublicItem")] + public IActionResult MinPriceForPublicItem() + { + return Ok(0); + } + + [HttpGet("v1/itemOwnershipLimit")] + public IActionResult ItemOwnershipLimit() + { + return Ok(100); + } + + + private static object ToItem(CustomAvatarItemDB item) => new + { + CustomAvatarItemId = item.CustomAvatarItemId.ToString(), + CreatorAccountId = item.CreatorAccountId, + Name = item.Name, + Description = item.Description, + Price = item.Price, + BaseAvatarItemId = item.BaseAvatarItemId, + BaseAvatarItemColor = item.BaseAvatarItemColor, + Accessibility = item.Accessibility, + IsFeatured = false, + Permission = 0, + ThumbnailImageFilename = item.ImageName, + ImageName = item.ImageName, + DesignFilename = item.DesignFileName, + CreatedAt = item.CreatedAt, + ModifiedAt = item.ModifiedAt, + }; + + private static bool Visible(CustomAvatarItemDB item, long? viewer) => + item.IsPublished || (viewer != null && item.CreatorAccountId == viewer); + + + [HttpGet("v1/featured")] + [HttpGet("v1/hot")] + public async Task Featured() + { + var viewer = User.GetUserId() ?? 0; + var rows = await db.CustomAvatarItems + .Where(x => x.IsPublished || x.CreatorAccountId == viewer) + .OrderByDescending(x => x.CreatedAt) + .Take(50) + .ToListAsync(); + + return Ok(rows.Select(ToItem).ToList()); + } + + [HttpGet("v1/owned")] + public async Task Owned([FromQuery] int skip = 0, [FromQuery] int take = 100) + { + return await Page(x => x.CreatorAccountId == User.GetUserId(), skip, take); + } + + [HttpGet("v1")] + [HttpGet("v1/me")] + public async Task Items([FromQuery] int skip = 0, [FromQuery] int take = 100) + { + return await Page(x => x.CreatorAccountId == User.GetUserId(), skip, take); + } + + private static object ToDesign(CustomAvatarItemDB item) => new + { + CustomAvatarItemId = item.CustomAvatarItemId.ToString(), + CustomAvatarItemColor = item.BaseAvatarItemColor, + DesignFilename = item.DesignFileName, + ImageName = item.ImageName, + }; + + [HttpGet("v1/design")] + public async Task Design() + { + var viewer = User.GetUserId() ?? 0; + var itemId = Guid.TryParse(Request.Query["customAvatarItemId"], out var parsed) ? parsed : (Guid?)null; + var color = Request.Query["customAvatarItemColor"].ToString(); + + var query = db.CustomAvatarItems.Where(x => x.DesignFileName != null && (x.IsPublished || x.CreatorAccountId == viewer)); + if (itemId != null) + query = query.Where(x => x.CustomAvatarItemId == itemId); + if (!string.IsNullOrEmpty(color)) + query = query.Where(x => x.BaseAvatarItemColor == color); + + var rows = await query.OrderByDescending(x => x.CreatedAt).ToListAsync(); + + return Ok(rows.Select(ToDesign).ToList()); + } + + [HttpPost("v1/design")] + public async Task SaveDesign() + { + var ownerId = User.GetUserId(); + if (ownerId == null) + { + return Unauthorized(); + } + + IFormFile? uploaded = null; + string? itemIdRaw = Request.Query["customAvatarItemId"].ToString(); + string? color = Request.Query["customAvatarItemColor"].ToString(); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + uploaded = form.Files.FirstOrDefault(f => f.Length > 0); + if (string.IsNullOrEmpty(itemIdRaw)) + itemIdRaw = form["customAvatarItemId"].ToString(); + if (string.IsNullOrEmpty(color)) + color = form["customAvatarItemColor"].ToString(); + } + + var item = Guid.TryParse(itemIdRaw, out var itemId) + ? await db.CustomAvatarItems.FirstOrDefaultAsync(x => x.CustomAvatarItemId == itemId) + : null; + + item ??= await db.CustomAvatarItems + .Where(x => x.CreatorAccountId == ownerId) + .OrderByDescending(x => x.CreatedAt) + .FirstOrDefaultAsync(); + + if (item == null) + { + return NotFound(); + } + + if (uploaded != null) + item.DesignFileName = await SaveUpload(uploaded, DBConfig.CDNImages, ".png"); + if (!string.IsNullOrEmpty(color)) + item.BaseAvatarItemColor = color; + + item.ModifiedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + + await AnnounceCustomItemChange(ownerId.Value); + return Ok(ToDesign(item)); + } + + [HttpGet("v2/fromCreator/{accountId:long}")] + public async Task FromCreator(long accountId, [FromQuery] int skip = 0, [FromQuery] int take = 100) + { + return await Page(x => x.CreatorAccountId == accountId, skip, take); + } + + [HttpGet("v1/{customAvatarItemId:guid}")] + public async Task Item(Guid customAvatarItemId) + { + var item = await db.CustomAvatarItems.FirstOrDefaultAsync(x => x.CustomAvatarItemId == customAvatarItemId); + if (item == null || !Visible(item, User.GetUserId())) + return NotFound(); + + return Ok(ToItem(item)); + } + + [HttpPut("v1/{customAvatarItemId:guid}")] + public async Task Update(Guid customAvatarItemId) + { + var ownerId = User.GetUserId(); + if (ownerId == null) return Unauthorized(); + + var item = await db.CustomAvatarItems.FirstOrDefaultAsync(x => x.CustomAvatarItemId == customAvatarItemId); + if (item == null) return NotFound(); + if (item.CreatorAccountId != ownerId.Value) + { + return StatusCode(StatusCodes.Status403Forbidden); + } + + var meta = await ReadMetadataAsync(); + if (ReadString(meta, "Name") is { } name && !string.IsNullOrWhiteSpace(name)) + item.Name = name; + if (ReadString(meta, "Description") is { } description) + item.Description = description; + if (TryReadInt(meta, "Price", out var price)) + item.Price = price; + if (TryReadInt(meta, "BaseAvatarItemId", out var baseItemId)) + item.BaseAvatarItemId = baseItemId; + if (ReadString(meta, "BaseAvatarItemColor") is { } color) + item.BaseAvatarItemColor = color; + if (TryReadInt(meta, "Accessibility", out var accessibility)) + { + item.Accessibility = accessibility; + item.IsPublished = accessibility != 0; + } + + item.ModifiedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + + await AnnounceCustomItemChange(ownerId.Value); + + // The route answers with the two obfuscated members, not the item: the client refetches + // what it needs from the account update above, and an item body here read as a failed save. + return Ok(ClientAction.Ok()); + } + + [HttpDelete("v1/{customAvatarItemId:guid}")] + public async Task Delete(Guid customAvatarItemId) + { + var ownerId = User.GetUserId(); + if (ownerId == null) return Unauthorized(); + + var item = await db.CustomAvatarItems.FirstOrDefaultAsync(x => x.CustomAvatarItemId == customAvatarItemId); + if (item == null) return NotFound(); + if (item.CreatorAccountId != ownerId.Value) + { + return StatusCode(StatusCodes.Status403Forbidden); + } + + db.CustomAvatarItems.Remove(item); + await db.SaveChangesAsync(); + + await AnnounceCustomItemChange(ownerId.Value); + return Ok(ClientAction.Ok()); + } + + [HttpGet("v1/search")] + public async Task Search( + [FromQuery] string? query = null, + [FromQuery] string? searchTerm = null, + [FromQuery] int skip = 0, + [FromQuery] int take = 100) + { + var viewer = User.GetUserId() ?? 0; + var term = (query ?? searchTerm ?? string.Empty).Trim(); + return await Page(x => (x.IsPublished || x.CreatorAccountId == viewer) + && (term.Length == 0 || (x.Name ?? string.Empty).Contains(term) || (x.Description ?? string.Empty).Contains(term)), skip, take); + } + + [HttpPost("v1/bulk")] + public async Task Bulk() + { + var ids = new List(); + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var value in form["customAvatarItemIds"]) + if (Guid.TryParse(value, out var id)) + ids.Add(id); + } + + var viewer = User.GetUserId(); + var rows = await db.CustomAvatarItems.Where(x => ids.Contains(x.CustomAvatarItemId)).ToListAsync(); + + return Ok(rows.Where(x => Visible(x, viewer)).Select(ToItem).ToList()); + } + + [HttpPost("v1")] + [HttpPost("v1/me")] + public async Task Create() + { + var ownerId = User.GetUserId(); + if (ownerId == null) + { + return Unauthorized(); + } + + string? rawMeta = null; + var files = new List(); + IFormFile? thumbnail = null; + IFormFile? design = null; + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + rawMeta = form["metadata"].ToString(); + + foreach (var file in form.Files) + { + files.Add($"{file.Name}:{file.ContentType}:{file.Length}"); + if (file.Length <= 0) + continue; + + var isThumbnail = file.Name.Contains("thumbnail", StringComparison.OrdinalIgnoreCase); + var isDesign = file.Name.Contains("design", StringComparison.OrdinalIgnoreCase); + + if (isThumbnail && thumbnail == null) + thumbnail = file; + else if (isDesign && design == null) + design = file; + else if (thumbnail == null) + thumbnail = file; + else if (design == null) + design = file; + } + } + else + { + Request.EnableBuffering(); + Request.Body.Position = 0; + var buffer = new byte[1024]; + var read = await Request.Body.ReadAsync(buffer, 0, buffer.Length); + Request.Body.Position = 0; + rawMeta = Encoding.UTF8.GetString(buffer, 0, read); + } + + using var meta = ParseMetadata(rawMeta); + + var now = DateTime.UtcNow; + var item = new CustomAvatarItemDB + { + CustomAvatarItemId = Guid.NewGuid(), + CreatorAccountId = (long)ownerId, + Name = ReadString(meta, "Name") ?? "Custom Avatar Item", + Description = ReadString(meta, "Description") ?? string.Empty, + Price = ReadInt(meta, "Price"), + BaseAvatarItemId = ReadInt(meta, "BaseAvatarItemId"), + BaseAvatarItemColor = ReadString(meta, "BaseAvatarItemColor"), + Accessibility = ReadInt(meta, "Accessibility"), + CreatedAt = now, + ModifiedAt = now, + }; + + item.IsPublished = item.Accessibility != 0; + + if (thumbnail != null) + item.ImageName = await SaveUpload(thumbnail, DBConfig.CDNImages, ".png"); + if (design != null) + item.DesignFileName = await SaveUpload(design, DBConfig.CDNImages, ".png"); + + db.CustomAvatarItems.Add(item); + await db.SaveChangesAsync(); + + await AnnounceCustomItemChange(ownerId.Value); + var body = ToItem(item); + + return Ok(body); + } + + [HttpPost("GetCustomAvatarItemCurrentSavesForLegacyAvatarItems")] + public IActionResult CurrentSavesForLegacyAvatarItems() + { + return Ok(new { customAvatarItemSavesByAvatarItemDesc = new Dictionary() }); + } + + + private async Task Page(System.Linq.Expressions.Expression> filter, int skip, int take) + { + take = take <= 0 ? 100 : Math.Min(take, 100); + skip = Math.Max(skip, 0); + + var query = db.CustomAvatarItems.Where(filter); + var total = await query.CountAsync(); + var rows = await query + .OrderByDescending(x => x.CreatedAt) + .Skip(skip) + .Take(take) + .ToListAsync(); + + return Ok(new { Results = rows.Select(ToItem).ToList(), TotalResults = total }); + } + + private async Task ReadMetadataAsync() + { + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + return ParseMetadata(form["metadata"].ToString()); + } + + if (Request.ContentLength is not > 0) + return ParseMetadata("{}"); + + Request.EnableBuffering(); + Request.Body.Position = 0; + string raw; + using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) + raw = await reader.ReadToEndAsync(); + Request.Body.Position = 0; + return ParseMetadata(raw); + } + + private static bool TryReadInt(JsonDocument meta, string key, out int value) + { + value = 0; + if (!TryGet(meta, key, out var element)) return false; + if (element.ValueKind == JsonValueKind.Number && element.TryGetInt32(out value)) return true; + return int.TryParse(element.ToString(), out value); + } + + private static JsonDocument ParseMetadata(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) + return JsonDocument.Parse("{}"); + + try + { + return JsonDocument.Parse(raw); + } + catch (JsonException) + { + return JsonDocument.Parse("{}"); + } + } + + private static string? ReadString(JsonDocument meta, string key) + { + if (!TryGet(meta, key, out var value)) + return null; + + return value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString(); + } + + private static int ReadInt(JsonDocument meta, string key) + { + if (!TryGet(meta, key, out var value) || value.ValueKind != JsonValueKind.Number) + return 0; + + return value.GetInt32(); + } + + private static bool TryGet(JsonDocument meta, string key, out JsonElement value) + { + value = default; + if (meta == null) + return false; + + if (meta.RootElement.TryGetProperty(key, out value)) + return true; + + foreach (var property in meta.RootElement.EnumerateObject()) + { + if (string.Equals(property.Name, key, StringComparison.OrdinalIgnoreCase)) + { + value = property.Value; + return true; + } + } + + return false; + } + + private static async Task SaveUpload(IFormFile file, string directory, string extension) + { + Directory.CreateDirectory(directory); + + var name = Guid.NewGuid().ToString() + extension; + var path = Path.Join(directory, name); + + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return name; + } + } +} diff --git a/Server/DB/DBConfig.cs b/Server/DB/DBConfig.cs new file mode 100644 index 0000000..e91551c --- /dev/null +++ b/Server/DB/DBConfig.cs @@ -0,0 +1,585 @@ +using Azure; +using RadicadeServer.RecNet; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts.Local; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace RadicadeServer +{ + public class DBConfig + { + public static string ServerUrl = "https://ns.radicade.app"; + + // Every service the nameserver hands the client is its own hostname, and the path this + // server actually serves that service under is normally the host's label: the client + // builds each call as the advertised base plus its own path literal, and the desktop + // patch maps the host back onto that prefix before it connects (2023Patch/ + // RadicadeEacBypass/NameserverRedirect.cs lists the same names). The two lists are one + // mapping kept in two places: a name added here with no redirect entry there leaves that + // service unreachable, silently - the client resolves the host itself and gets nothing + // back. ServiceRouteOverrides holds the one deliberate exception to label == route. + public const string ServiceDomain = "radicade.app"; + + // Advertised host label -> the route this server serves that service at, for the names + // that are deliberately not their own prefix. There is one: the notify service is + // advertised under its full name, "notifications", while its route stays /notify - the + // hub socket's address (NotifyHubRoutes) and the root the health API answers at - because + // /notify is the path every client that has ever talked to this server already carries. + // The client's half of this is the one map entry in NameserverRedirect.cs that does not + // spell its own label. api is the other advertised name that is not simply its own + // prefix, but it is a bare base rather than a renamed route, so it has no entry here. + // ServerTests/HubRealtimeTests.cs reads this table, the advertised names and the server's + // own route list together, so the two sides of the mapping cannot drift apart. + public static readonly IReadOnlyDictionary ServiceRouteOverrides = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["notifications"] = "notify", + }; + + public static string ServiceRoute(string service) => + ServiceRouteOverrides.TryGetValue(service, out var route) ? route : service; + + public static string ServiceUrl(string service) => $"https://{service}.{ServiceDomain}"; + + public static NameServerResponse NameServer => new NameServerResponse + { + API = ServiceUrl("api"), + AI = ServiceUrl("ai"), + Auth = ServiceUrl("auth"), + WWW = ServiceUrl("www"), + Cards = ServiceUrl("cards"), + Chat = ServiceUrl("chat"), + Notifications = ServiceUrl("notifications"), + Matchmaking = ServiceUrl("matchmaking"), + Images = ServiceUrl("images"), + CDN = ServiceUrl("cdn"), + Link = ServiceUrl("link"), + Commerce = ServiceUrl("commerce"), + Data = ServiceUrl("data"), + DataCollection = ServiceUrl("datacollection"), + Discovery = ServiceUrl("discovery"), + Storage = ServiceUrl("storage"), + Geo = ServiceUrl("geo"), + Locker = ServiceUrl("locker"), + Moderation = ServiceUrl("moderation"), + Clubs = ServiceUrl("clubs"), + Rooms = ServiceUrl("rooms"), + RoomComments = ServiceUrl("roomcomments"), + RoomieIntegrations = ServiceUrl("roomieintegrations"), + Accounts = ServiceUrl("accounts"), + Leaderboard = ServiceUrl("leaderboard"), + Econ = ServiceUrl("econ"), + BugReporting = ServiceUrl("bugreporting"), + CMS = ServiceUrl("cms"), + GameLogs = ServiceUrl("gamelogs"), + PlatformNotifications = ServiceUrl("platformnotifications"), + Lists = ServiceUrl("lists"), + PlayerSettings = ServiceUrl("playersettings"), + Strings = ServiceUrl("strings"), + StringsCDN = ServiceUrl("stringscdn"), + Studio = ServiceUrl("studio"), + StudioCDN = ServiceUrl("studiocdn"), + Thorn = ServiceUrl("thorn"), + Videos = ServiceUrl("videos"), + }; + + public static string PublicData = Path.GetFullPath("./RadicadeServer-Data/"); + public static string PrivateData = Path.GetFullPath("./RadicadeServer-Config/"); + public static string PersistentLogs = Path.Join(PublicData, "Persistent", "Logs"); + public static string CDNImages = Path.Join(PublicData, "CDN", "Images"); + public static string CDNVideos = Path.Join(PublicData, "CDN", "Videos"); + public static string CDNStorage = Path.Join(PublicData, "CDN", "Storage"); + public static string CDNCustom = Path.Join(PublicData, "CDN", "Custom"); + public static string DefaultImagesFile = Path.GetFullPath("./images.txt"); + public static string DefaultProfileImage = "DefaultProfileImage"; + public static string DefaultRoomImage = "DefaultRoomImage"; + public const bool EnforceBarrel = false; + public const string GameVersion = "20230825"; + public static readonly IReadOnlySet SupportedGameVersions = new HashSet(StringComparer.Ordinal) + { + GameVersion, + "20230831", + "20230301", + "20230302", + "20230303", + "20230414", + "20230616", + "20230815", + "20231207", + "20250424.01", + "20250718.01", + }; + + public static bool IsSupportedGameVersion(string? version) + { + return !string.IsNullOrWhiteSpace(version) && SupportedGameVersions.Contains(version); + } + + + + + public static string AdminApiKey = ""; + public static string SupportApiKey = ""; + public static string DebugKey = ""; + public static string JwtKey = ""; + public static string SteamApiKey = ""; + + public static bool TrustUnverifiedSteamIdentity = false; + + public static string PhotonRealtimeAppId = ""; + public static string PhotonVoiceAppId = ""; + public static string PhotonChatAppId = ""; + public static string PhotonRegion = "us"; + public static string MailApiKey = ""; + public static string MailFrom = "support@radicade.app"; + public static string MailFromName = "Radicade Support"; + public static string MailApiUrl = "https://api.resend.com/emails"; + public static string AdminPanelUrl = "https://admin.radicade.app/"; + + public sealed record AdminAccount(string Username, string Password); + + public static List AdminAccounts = new(); + public static List TestInfluencerAccountIds = new() { 1, 2 }; + + public static List TestCoOwnerAccountIds = new(); + public static string TokenIssuer => ServerUrl; + public const string DefaultAvatar = "{\"OutfitSelections\":\"\",\"FaceFeatures\":\"{\\\"ver\\\":3,\\\"eyeId\\\":\\\"AjGMoJhEcEehacRZjUMuDg\\\",\\\"eyePos\\\":{\\\"x\\\":0.0,\\\"y\\\":0.0},\\\"eyeScl\\\":0.0,\\\"mouthId\\\":\\\"FrZBRanXEEK29yKJ4jiMjg\\\",\\\"mouthPos\\\":{\\\"x\\\":0.0,\\\"y\\\":0.0},\\\"mouthScl\\\":0.0,\\\"beardColorId\\\":\\\"befcc00a-a2e6-48e4-864c-593d57bbbb5b\\\"}\",\"SkinColor\":\"85343b16-d58a-4091-96d8-083a81fb03ae\",\"HairColor\":\"fT0t-J1A-0i_tdoogFEz-g\"}"; + public const string DefaultSettings2023 = """ +[ + { + "Key": "TUTORIAL_COMPLETE_MASK", + "Value": "109" + }, + { + "Key": "TUTORIAL_COMPLETED_MASK", + "Value": "109" + }, + { + "Key": "PlayerSessionCount", + "Value": "46" + }, + { + "Key": "LastSelectedLocale", + "Value": "ja-JP" + }, + { + "Key": "HAS_RECEIVED_WATCH", + "Value": "True" + }, + { + "Key": "HAS_COMPLETED_ORIENTATION", + "Value": "True" + }, + { + "Key": "OrientationCompletionTime", + "Value": "2026-07-06T03:28:05.4421306Z" + }, + { + "Key": "BACKPACK_FAVORITE_TOOL", + "Value": "-1" + }, + { + "Key": "MakerPen_GridSnap_V2", + "Value": "0.5" + }, + { + "Key": "MakerPen_AngleSnap_V2", + "Value": "45" + }, + { + "Key": "MakerPen_CurveThickness_V2", + "Value": "0.0799999982" + }, + { + "Key": "HasCheckedForPlatformReferrers", + "Value": "True" + }, + { + "Key": "HAS_OPENED_WATCH_MENU_BEFORE", + "Value": "True" + }, + { + "Key": "VoiceChat", + "Value": "0" + }, + { + "Key": "MakerPenSessionCount", + "Value": "9" + }, + { + "Key": "Growth.YIR.TimeSpentWithFriends", + "Value": "0" + }, + { + "Key": "Growth.YIR.DistanceTraveled", + "Value": "98.1108322" + }, + { + "Key": "SCREENS_HEAD_BOB", + "Value": "1" + }, + { + "Key": "AutosaveInterval", + "Value": "300" + }, + { + "Key": "SCREEN_MKB_CONTROL_SCHEME_PREF", + "Value": "LEGACY" + }, + { + "Key": "LastViewedNotificationsTimestamp_General_Activities", + "Value": "2026-07-12T20:59:17.6763730Z" + }, + { + "Key": "HasSeenFullPlayMenuPrompt", + "Value": "True" + }, + { + "Key": "LastViewedNotificationsTimestamp_Official", + "Value": "2026-07-12T20:59:21.4922589Z" + }, + { + "Key": "Growth.LastEmailPromptTime", + "Value": "2026-08-09T01:32:33.0015683Z" + }, + { + "Key": "DebugGuiEnabled", + "Value": "True" + }, + { + "Key": "MakerPen_TubeHeightScale_V2", + "Value": "1" + }, + { + "Key": "MakerPen_TubeRingPoints_V2", + "Value": "10" + }, + { + "Key": "MakerPenIntroDismissed", + "Value": "True" + } +] +"""; + public static string NewAccountSettings2023 => + BuildAccountCreationSettings(started: false, usernameChosen: false, passwordCreated: false); + + public static string AccountCreationInProgressSettings2023(bool usernameChosen, bool passwordCreated) => + BuildAccountCreationSettings(started: true, usernameChosen: usernameChosen, passwordCreated: passwordCreated); + + private static string BuildAccountCreationSettings(bool started, bool usernameChosen, bool passwordCreated) + { + var settings = JsonSerializer.Deserialize>(DefaultSettings2023) ?? new List(); + SetSetting(settings, "Recroom.OOBE", "0"); + SetSetting(settings, "Recroom.AccountCreation.HasStarted", started ? "True" : "False"); + SetSetting(settings, "Recroom.AccountCreation.HasChosenUsername", usernameChosen ? "True" : "False"); + SetSetting(settings, "Recroom.AccountCreation.HasCreatedPassword", passwordCreated ? "True" : "False"); + SetSetting(settings, "Recroom.AccountCreation.HasFinished", "False"); + SetSetting(settings, "Recroom.AccountCreation.FinishedAvatarCustomization", "False"); + SetSetting(settings, "Recroom.AccountCreation.AcceptedCodeOfConduct", "False"); + SetSetting(settings, "Recroom.AccountCreation.HasImportedFriends", "False"); + SetSetting(settings, "Radicade.NeedsOrientation", "1"); + settings.RemoveAll(setting => + string.Equals(setting.Key, "HAS_COMPLETED_ORIENTATION", StringComparison.OrdinalIgnoreCase) || + string.Equals(setting.Key, "OrientationCompletionTime", StringComparison.OrdinalIgnoreCase)); + return JsonSerializer.Serialize(settings); + } + + private static void SetSetting(List settings, string key, string value) + { + var setting = settings.FirstOrDefault(existing => + string.Equals(existing.Key, key, StringComparison.OrdinalIgnoreCase)); + if (setting == null) + settings.Add(new Setting { Key = key, Value = value }); + else + setting.Value = value; + } + public const string DefaultSettings = "[{\"Key\":\"MOD_BLOCKED_TIME\",\"Value\":\"0\"},{\"Key\":\"MOD_BLOCKED_DURATION\",\"Value\":\"0\"},{\"Key\":\"PlayerSessionCount\",\"Value\":\"138\"},{\"Key\":\"ShowRoomCenter\",\"Value\":\"1\"},{\"Key\":\"QualitySettings\",\"Value\":\"3\"},{\"Key\":\"Recroom.OOBE\",\"Value\":\"0\"},{\"Key\":\"Radicade.NeedsOrientation\",\"Value\":\"1\"},{\"Key\":\"Radicade.NeedsOrientation\",\"Value\":\"1\"},{\"Key\":\"VoiceFilter\",\"Value\":\"0\"},{\"Key\":\"VIGNETTED_TELEPORT_ENABLED\",\"Value\":\"1\"},{\"Key\":\"CONTINUOUS_ROTATION_MODE\",\"Value\":\"1\"},{\"Key\":\"ROTATION_INCREMENT\",\"Value\":\"0\"},{\"Key\":\"ROTATE_IN_PLACE_ENABLED\",\"Value\":\"0\"},{\"Key\":\"TeleportBuffer\",\"Value\":\"0\"},{\"Key\":\"VoiceChat\",\"Value\":\"0\"},{\"Key\":\"PersonalBubble\",\"Value\":\"0\"},{\"Key\":\"ShowNames\",\"Value\":\"1\"},{\"Key\":\"H.264 plugin\",\"Value\":\"0\"},{\"Key\":\"google_analytics_clientid_pref_key\",\"Value\":\"YPSBPEGEf8ed48c71d2e0ddd40bebb2d09d0dc1ac53377ca\"},{\"Key\":\"USER_TRACKING\",\"Value\":\"53\"},{\"Key\":\"IgnoreBuffer\",\"Value\":\"0\"},{\"Key\":\"FIRST_TIME_IN_FLAGS\",\"Value\":\"0\"},{\"Key\":\"WALKING_MODE\",\"Value\":\"1\"},{\"Key\":\"COMFORT_FLY\",\"Value\":\"0\"},{\"Key\":\"COMFORT_SMOOTH_ROTATE\",\"Value\":\"0\"},{\"Key\":\"COMFORT_SWIPE_ROTATE\",\"Value\":\"0\"},{\"Key\":\"COMFORT_TELEPORT\",\"Value\":\"0\"},{\"Key\":\"COMFORT_WALK\",\"Value\":\"0\"},{\"Key\":\"COMFORT_SPRINT\",\"Value\":\"0\"},{\"Key\":\"COMFORT_VEHICLES\",\"Value\":\"0\"},{\"Key\":\"DAILY_LOGIN_DATE\",\"Value\":\"2\"},{\"Key\":\"OBJECTIVE_DATE\",\"Value\":\"2\"},{\"Key\":\"OBJECTIVE_PROGRESS0\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_COMPLETED0\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_PROGRESS1\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_COMPLETED1\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_PROGRESS2\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_COMPLETED2\",\"Value\":\"0\"},{\"Key\":\"SFX_VOLUME_PERCENT_PREF\",\"Value\":\"0.8265823\"},{\"Key\":\"SCREENS_INVERT_MOUSE\",\"Value\":\"0\"},{\"Key\":\"SCREENS_MOUSE_SENSITIVITY_X\",\"Value\":\"1.187885\"},{\"Key\":\"SCREENS_MOUSE_SENSITIVITY_Y\",\"Value\":\"1.100286\"},{\"Key\":\"MOTION_TELEPORT_ENABLED\",\"Value\":\"0\"},{\"Key\":\"BACKPACK_FAVORITE_TOOL\",\"Value\":\"-1\"},{\"Key\":\"SplitTestAssignedSegments\",\"Value\":\"1|{\\\"SplitTesting+NUX_HoloHelpers_2019_11_15\\\":\\\"On\\\"}\"},{\"Key\":\"SCREENS_HEAD_BOB\",\"Value\":\"0\"},{\"Key\":\"MUSIC_VOLUME_PERCENT_PREF\",\"Value\":\"1\"},{\"Key\":\"SCREENS_CAMERA_RELATIVE_DANCE_CONTROLS\",\"Value\":\"0\"},{\"Key\":\"AMBIENCE_VOLUME_PERCENT_PREF\",\"Value\":\"1\"}]"; + + public const string SafeDefaultAvatarUnlocks2023 = """ +[ + { + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,", + "UnlockedLevel": 0, + "PlatformMask": -1, + "FriendlyName": "Coach Shirt", + "Tooltip": "", + "Rarity": 0 + } +] +"""; + + public static string DefaultAvatarUnlocks = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.LoadStarterJson(); + public static string DefaultConsumableUnlocks = "[]"; + public static string DefaultEquipmentUnlocks = "[]"; + public static string DefaultAvatarCatalog = SafeDefaultAvatarUnlocks2023; + public static string DefaultEquipmentItems = "[]"; + public static string DefaultEquipmentSkinItems = "[]"; + public static string DefaultConsumableItems = "[]"; + public const string PlaceholderConfig = "[{\"Key\":\"Screens.ForceVerification\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Screens.ForceWaitlist\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"splitTestSoftOverrides\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Creative.Query\",\"Value\":\"^CreativeCampus | ^RecRoomGallery\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Creative.Title\",\"Value\":\"Creative Campus\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Featured.Query\",\"Value\":\"#featured\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Featured.Title\",\"Value\":\"Featured\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Quests.Query\",\"Value\":\"#quest #rro\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Quests.Title\",\"Value\":\"QUESTS\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Shooters.Query\",\"Value\":\"#import\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Shooters.Title\",\"Value\":\"Import Rooms!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Sports.Query\",\"Value\":\"#sport #rro | #pvp #rro\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Sports.Title\",\"Value\":\"SPORTS & PVP\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Screens.ForceRegistrationPS4\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"splitTestHardOverrides\",\"Value\":\"IOSAppPermissions_2019_10_02=Immediate;GottaGoFastNUX_2020_09_04=Off;RecNetImageCacheRefCount_2020_09_18=On;Curated_Rooms_2020_08_06=On;PlayMenuRRUI_2021_01_11=On;PlayMenuRRUI_HotRoomsCarousel_2021_02_09=On;PlayMenuRRUI_CarouselOrdering_2021_02_09=PlayMenuCarouselOrdering_1;CreationCommands_2021_02_16=Off;RecRoomPlus_DormMirrorButton_2021_03_09=ButtonOn;CurrentContestWinnersCarouselVisibility_2021_04_15=Off;MutualFriends_2021_04_22=On;PlayMenuRRUI_DefaultTab_2021_05_13=Highlight;DefaultNewPlayersToPlayMenu_2021_05_24=On;ShowNewRRPlusIcon_2021_06_01=NewIcon;ParallelNetworkingInitialRoomLoad_2021_06_28=ParallelLoad;ShowNewRRPlusBenefitsPage_2021_06_14=OldPage;WatchStoreHomepageLayout_2021_06_14=OldStoreLayout;NotificationPermissionsExplanationText_2021_06_25=Control;PlayMenuRRUI_TopCreatorsCarousel_2021_09_01=Off;UseSets_2021_08_06=UseSets;RoomSimilarities_2021_10_29=On;ProfileOnClick_2021_04_30=On;RoomRecommendations_2020_05_06=On;FriendImportLogin_2021_09_23=On;RudderStackAnalyticsDestinations_2022_02_10=RudderStackOnly;ObjectModel_EnableForRoom_2022_07_01=Disallowed;PlayMenuCarouselOrdering_2021_11_01=PlayMenuCarouselOrdering_1;PlayMenuBanner_2021_11_01=Off;\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GASamplingRatio\",\"Value\":\"0.02\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"splitTestsAvailable\",\"Value\":\"RoomRecommendations_2020_05_06=On,Off;Curated_Rooms_2020_08_06=Off,On;RecNetImageCacheRefCount_2020_09_18=On,Off;GottaGoFastNUX_2020_09_04=Off,On;Curated_Rooms_Tab_Placement_2020_09_22=First,Second;PlayMenuRRUI_2021_01_11=Off,On;PlayMenuRRUI_HotRoomsCarousel_2021_02_09=Off,On;PlayMenuRRUI_CarouselOrdering_2021_02_09=PlayMenuCarouselOrdering_1,PlayMenuCarouselOrdering_2,PlayMenuCarouselOrdering_3;CreationCommands_2021_02_16=Off,On;RecRoomPlus_DormMirrorButton_2021_03_09=ButtonOn,ButtonOff;CurrentContestWinnersCarouselVisibility_2021_04_15=Off,On;MutualFriends_2021_04_22=Off,On;ProfileOnClick_2021_04_30=Off,On;PlayMenuRRUI_DefaultTab_2021_05_13=Highlight,RRO,Hot;DefaultNewPlayersToPlayMenu_2021_05_24=Off,On;ShowNewRRPlusIcon_2021_06_01=OldIcon,NewIcon;ShowNewRRPlusBenefitsPage_2021_06_14=OldPage,NewPage;WatchStoreHomepageLayout_2021_06_14=OldStoreLayout,NewStoreLayout;NotificationPermissionsExplanationText_2021_06_25=Control,SpecificValueText;ParallelNetworkingInitialRoomLoad_2021_06_28=SequentialLoad,ParallelLoad;UseSets_2021_08_06=IgnoreSets,UseSets;PlayMenuRRUI_TopCreatorsCarousel_2021_09_01=Off,On;FriendImportLogin_2021_09_23=Off,On;Purchase_Campaigns_AB_2021_09_13=Purchase_Campaign_AB_A,Purchase_Campaign_AB_B;Purchase_Campaigns_ABCD_2021_09_13=Purchase_Campaign_ABCD_A,Purchase_Campaign_ABCD_B,Purchase_Campaign_ABCD_C,Purchase_Campaign_ABCD_D;RoomSimilarities_2021_10_29=On,Off;PlayMenuBanner_2021_11_01=Off,On;PlayMenuCarouselOrdering_2021_11_01=PlayMenuCarouselOrdering_1,PlayMenuCarouselOrdering_2,PlayMenuCarouselOrdering_3;RudderStackAnalyticsDestinations_2022_02_10=Disabled,RudderStackOnly,Both;ObjectModel_EnableForRoom_2022_07_01=Disallowed,Allowed;\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"splitTestSegmentProbabilities\",\"Value\":\"{\\\"RoomRecommendations_2020_05_06\\\":[{\\\"SegmentName\\\":\\\"On\\\",\\\"Probability\\\":0.25},{\\\"SegmentName\\\":\\\"Off\\\",\\\"Probability\\\":0.75}],\\\"CurrentContestWinnersCarouselVisibility_2021_04_15\\\":[{\\\"SegmentName\\\":\\\"Off\\\",\\\"Probability\\\":1.0},{\\\"SegmentName\\\":\\\"On\\\",\\\"Probability\\\":0.0}],\\\"PlayMenuBanner_2021_11_01\\\":[{\\\"SegmentName\\\":\\\"Off\\\",\\\"Probability\\\":0.5},{\\\"SegmentName\\\":\\\"On\\\",\\\"Probability\\\":0.5}],\\\"FriendImportLogin_2021_09_23\\\":[{\\\"SegmentName\\\":\\\"Off\\\",\\\"Probability\\\":0.5},{\\\"SegmentName\\\":\\\"On\\\",\\\"Probability\\\":0.5}],\\\"PlayMenuCarouselOrdering_2021_11_01\\\":[{\\\"SegmentName\\\":\\\"PlayMenuCarouselOrdering_1\\\",\\\"Probability\\\":0.33},{\\\"SegmentName\\\":\\\"PlayMenuCarouselOrdering_2\\\",\\\"Probability\\\":0.33},{\\\"SegmentName\\\":\\\"PlayMenuCarouselOrdering_3\\\",\\\"Probability\\\":0.34}]}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Friends.PostGamePromptUnderFriendCount\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Friends.SuggestFriendCodeOnFriendsScreenCount\",\"Value\":\"25\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"runningNetworkTimeout\",\"Value\":\"30\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"loadingNetworkTimeout\",\"Value\":\"180\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"SynchronizedField.RemoveDefaultEntries\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Rewards.SelectionTimeout\",\"Value\":\"1800\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Rewards.UseRewardSelection\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomDetails.PhotoRollEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Rendering.DisableSrpBatcher\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Backtrace.config\",\"Value\":\"0|0.05|0|20|50|1|0|0|1000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Backtrace.enabledLikelihood\",\"Value\":\"1.0|1.0|1.0|1.0|1.0|1.0|1.0|1.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomScaleMotionCheatDetector.MinHmdSpeedAboveTriggerCount\",\"Value\":\"6\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomScaleMotionCheatDetector.ShouldBootToDormWhenDetected.PlayStation\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomScaleMotionCheatDetector.ShouldBootToDormWhenDetected.Steam\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomScaleMotionCheatDetector.ShouldBootToDormWhenDetected.Oculus\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayMenuCarouselOrdering_3\",\"Value\":\"hotrooms=14;new=13;recroomoriginals=12;continueplaying=11;myfavorites=10;friendsplaying=9;recommended=8;mysubscriptions=7;staffcurated=6;featured=5;myclubhouses=4;myevents=3;topcreators=2;discoverability=1;hiddenworldscontest=0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayMenuCarouselOrdering_2\",\"Value\":\"continueplaying=14;hotrooms=13;new=12;recroomoriginals=11;myfavorites=10;friendsplaying=9;recommended=8;mysubscriptions=7;staffcurated=6;featured=5;myclubhouses=4;myevents=3;topcreators=2;discoverability=1;hiddenworldscontest=0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayMenuCarouselOrdering_1\",\"Value\":\"foryou=25;hotrooms=24;new=23;recroomoriginals=22;continueplaying=21;rising=20;myfavorites=19;friendsplaying=18;topearning=17;mysubscriptions=16;earnroomcurrency_algo=14;roleplay_algoendpoint=13;casual_algoendpoint=12;horror_algoendpoint=11;battle_algoendpoint=10;explore_algoendpoint=9;quests_algoendpoint=8;hangout_algoendpoint=7;inkshowcase=6;featured=5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Backtrace.versionRegex\",\"Value\":\".*\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Backtrace.messageRegex\",\"Value\":\"^Cannot set the parent of the GameObject .* while its new parent|^\\\\>\\\\x2010x\\\\:\\\\x20|\\\\'LabelTheme\\\\' contains missing PaletteTheme reference on\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomScaleMotionCheatDetector.MaxAllowedAverageHmdSpeed\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomScaleMotionCheatDetector.MeasurementPeriodInSeconds\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomScaleMotionCheatDetector.TriggerHmdSpeed\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GDK_Enable_RRPlus\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.StuntRunner.Title\",\"Value\":\"COMMUNITY\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.StuntRunner.Query\",\"Value\":\"#stuntrunner #community\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.EnableRRUIConfigMenu\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.EnableProfilePhoneButton\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.FriendFestPromosActive\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Backtrace.stopTimeUTC\",\"Value\":\"9999-09-28 23:55\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"OldStoreLayout\",\"Value\":\"banner=12;storecampaigns=11;tokenpack=10;allskus=9;hotitem=8;hotinvention=7;newitem=6;mysubscriptioninvention=5;rrplusitem=4;fivestaritem=3;skinsitem=2;hairdyeitem=1;\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"NewStoreLayout\",\"Value\":\"banner=9;hotitem=7;hotinvention=8;newitem=6;mysubscriptioninvention=5;rrplusitem=4;fivestaritem=3;skinsitem=2;hairdyeitem=1;\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ForbiddenSavedSpawnableTools\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MobileNotifications.UseFirebasePushNotifications\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MaxChipsVisible\",\"Value\":\"5000\",\"ActiveExperiments\":null,\"StartTime\":\"2021-07-21T23:50:00Z\",\"EndTime\":null},{\"Key\":\"UGC.MaxChipsUnculled\",\"Value\":\"200\",\"ActiveExperiments\":null,\"StartTime\":\"2021-07-21T23:51:23Z\",\"EndTime\":null},{\"Key\":\"MagicDoor.NewDestinationText\",\"Value\":\"Magic Door\",\"ActiveExperiments\":null,\"StartTime\":\"2021-07-21T23:58:09Z\",\"EndTime\":null},{\"Key\":\"UGC.Persistence.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":\"2021-07-22T00:11:16Z\",\"EndTime\":null},{\"Key\":\"UGC.Persistence.AutosaveIntervalSeconds\",\"Value\":\"300\",\"ActiveExperiments\":null,\"StartTime\":\"2021-07-22T00:13:54Z\",\"EndTime\":null},{\"Key\":\"MagicDoor.DestinationRefreshingText\",\"Value\":\"New Room in\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CannedChatAnalyticsSampleRate\",\"Value\":\".25\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CV2.CooldownBeforeSubscriptionCheckSeconds\",\"Value\":\"240\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RRUI.UseRRUIHomeScreen\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"SafePlaySFXAudioSourceSounds\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ScreenSharing.EnabledForMultiInstanceEvents\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ScreenSharing.CompressionQualityMultiplier\",\"Value\":\"1.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ScreenSharing.EnableZipCompression\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ScreenSharing.MaxFragmentBytes\",\"Value\":\"15360\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ScreenSharing.RefreshHzMultiplier\",\"Value\":\"30.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ScreenSharing.ResolutionMultiplier\",\"Value\":\"1.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BroadcastManager.VoiceRolloffMaxDistance\",\"Value\":\"500\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BroadcastManager.IsRoomChatBroadcastSupported\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BroadcastManager.RoomChatMaxMessagesPerTrackingPeriod\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.EnableInfluencerProgram\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.EnableSMSInvites\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MobileNotifications.AndroidPushNotificationsEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlatformNotifications.AndroidPushNotificationsEnabled\\t\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlatformNotifications.UseFirebasePushNotifications\\t\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlatformNotifications.ActiveHoursStartLocalTime\",\"Value\":\"9\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AndroidMobile.runFileWatcher\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AndroidMobile.runHashCheck\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"OculusQuest.runHashCheck\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ToxMod.Enabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Chips\",\"Value\":\"50000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.CloudValues\",\"Value\":\"500\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.CloudValuesAutosaveSeconds\",\"Value\":\"20\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute\",\"Value\":\"8000000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.InkPerChip\",\"Value\":\"8\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Network\",\"Value\":\"[ { \\\"NumPlayersForActivation\\\": 0, \\\"Bytes\\\": 3072, \\\"Rpcs\\\": 15 }, { \\\"NumPlayersForActivation\\\": 13, \\\"Bytes\\\": 2048, \\\"Rpcs\\\": 10 }, { \\\"NumPlayersForActivation\\\": 25, \\\"Bytes\\\": 1024, \\\"Rpcs\\\": 5 } ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.UnculledChips\",\"Value\":\"500\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.VisibleChips\",\"Value\":\"40000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.Mic_SCO_AccountCreationCutOff\",\"Value\":\"2021-11-10 00:00\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AndroidMobile.enforceFileWatcher\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AndroidMobile.enforceHashCheck\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"OculusQuest.enforceFileWatcher\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"OculusQuest.enforceHashCheck\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RRUIRouteChangeSampleRate\",\"Value\":\"0.01\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlatformNotifications.AndroidPushNotificationsEnabled\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlatformNotifications.UseFirebasePushNotifications\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.DeepLinks\",\"Value\":\"[ { \\\"Link\\\": \\\"recroom://testlink\\\", \\\"Action\\\": \\\"GoToRoom\\\", \\\"Data\\\": { \\\"RoomName\\\": \\\"Festival\\\" } }, { \\\"Link\\\": \\\"recroom://solus\\\", \\\"Action\\\": \\\"GoToRoom\\\", \\\"Data\\\": { \\\"RoomName\\\": \\\"TestRoom\\\" } }, { \\\"Link\\\": \\\"recroom://winterwonderland\\\", \\\"Action\\\": \\\"GoToRoom\\\", \\\"Data\\\": { \\\"RoomName\\\": \\\"RecHolidayMarket\\\" } }, { \\\"Link\\\": \\\"recroom://reccenter\\\", \\\"Action\\\": \\\"GoToRoom\\\", \\\"Data\\\": { \\\"RoomName\\\": \\\"RecCenter\\\" } } ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.RoomReloadTimeout\",\"Value\":\"30\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.RoomJackpotAccountCreationCutoff\",\"Value\":\"2022-05-27 00:00\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.ShowShuffleChip\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.SCO_HasMicTestDependency\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Friendotron_AllowSelfGifting\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Friendotron_MaxUsersPerSession\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.CorruptRoomDetectionProbability\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.CorruptRoomDetectionTimeSeconds\",\"Value\":\"900\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LogRoomLoadSnapshots\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RRUI.MaxActiveHiddenPages\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Mitigation.IgnoreSendingRoomUpdatesOnAppQuit\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Mitigation.DisconnectPhotonOnAppQuit\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ToxMod.Rooms\",\"Value\":\"^PrideMonth,^RecRoomGallery\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Mitigation.FixBrokenLobbies\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.AdditionalLogTraceSizeForLoading\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.AdditionalTraceFlags\",\"Value\":\"None\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.AdditionalTraceFlagsForLoading\",\"Value\":\"None\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.LoadBacktraceProbability\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.LoadBacktraceTimeoutSeconds\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.LoadLogTimesSeconds\",\"Value\":\"[40, 60, 120, 180, 240]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.LoadTimeoutSeconds\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.LogLoadComplete\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TerminateLoadForMasterTransition\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.AllowBetaInvertedTubeCreation\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.AllowNonBetaInvertedTubeCreation\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MobileHome.ChatEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UseRRUICreateScreen\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.RoomCategorySelectAccountCreationCutoff\",\"Value\":\"2022-06-28 00:00\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.IsEmailMandatory_Jr\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.IsEmailMandatory_NonJr\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.IsPhoneMandatory_NonJr\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.EmailPromptFrequencyHours\",\"Value\":\"120\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.TraceProbability\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.UseLegacyUnityAudioOut\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Statsig.Enabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UseRRUIEventsScreen\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Shapes.OptimizedCollidersDefaultForExisting\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LeagueOfHeroesEnabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.DisablePlayerBoardRooms\",\"Value\":\"[]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerPen_CreationMode.Default.Screen\",\"Value\":\"Scale\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RRUI.UseNewProfile\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerRelationships.NumFriendsWarningThreshold\",\"Value\":\"3000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.RoomCategoriesJson\",\"Value\":\"[{ \\\"Tag\\\": \\\"PVP\\\", \\\"ImageName\\\": \\\"roomCatSelect_pvp.jpg\\\" }, { \\\"Tag\\\": \\\"Quest\\\", \\\"ImageName\\\": \\\"roomCatSelect_quest.jpg\\\" }, { \\\"Tag\\\": \\\"Horror\\\", \\\"ImageName\\\": \\\"roomCatSelect_horror.jpg\\\" }, { \\\"Tag\\\": \\\"Hangout\\\", \\\"ImageName\\\": \\\"roomCatSelect_hangout.jpg\\\" }, { \\\"Tag\\\": \\\"Art\\\", \\\"ImageName\\\": \\\"roomCatSelect_art.jpg\\\" }]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.RoomCategorySelect_StraightToRandomRoom_TakeFirstRoom\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.ShowRoomCategorySelectChip\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecNet.JsonParserType\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"NewPlayerChallenges_InventionTower_Screen\",\"Value\":\"InkInc\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"NewPlayerChallenges_InventionTower_VR\",\"Value\":\"InkInc\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"NewPlayerChallenges_LaunchDateTime\",\"Value\":\"03/24/1922 12:00:00 \",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"NewPlayerChallenges_MaxDisplayedFamilies\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.QuickOrientationEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AppleMusicPromo.Description\",\"Value\":\"<align="center">Access over 90 million songs, over 30,000 playlists, and original content from your favorite artists all ad-free with Apple Music.</align>\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AppleMusicPromo.Title\",\"Value\":\"Get up to 2 months of Apple Music free! \",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AppleMusicPromo.TOC\",\"Value\":\"<size=70%>Code expires on June 30, 2022.<br>This is a promotional code and is not for resale, has no cash value, and will not be replaced if lost or stolen. Valid only for Apple Music in the United States. Requires Apple ID with payment method on file. After promotional period ends, subscription will auto-renew at $9.99/month until cancelled. Payment will be billed to the payment method on file and can be cancelled anytime at least a day before each renewal date at Settings > Apple ID. Terms and Apple Privacy Policy apply; see the applicable terms for your country at https://www.apple.com/legal/internet-services/itunes/ww. Cannot be combined with other offers providing access to the same service. Must be of the minimum age set forth in the subscription registration process for your country and in the country that matches the store front on which you are redeeming the code. Compatible products and services required.<br>Apple Music is a registered trademark of Apple Inc. Apple is not a sponsor of this promotion.</size>\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RRUI.EnableUIAnchors\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.MakerPenIntroCreationCutoff\",\"Value\":\"9999-05-11 00:00\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.MakerPenIntroDormOnly\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Social.ClubChatEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.RoomSavingEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Video.MaxVideoDesyncSeconds\",\"Value\":\"3.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Video.PS5IsPlaybackSupported\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Video.PlaystationIsRemotePlaybackSupported\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Video.IOSSeekCooldownSeconds\",\"Value\":\"2.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"NewPlayerChallenges_MaxPlayerPromptCount\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Social.TimedFlowSampleRate\",\"Value\":\"0.05\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Photon.ConnectionFailure.Exception.SampleRate\",\"Value\":\"0.01\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AutoClickerCheatDetector.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AutoClickerCheatDetector.NumberOfActionsAllowed_ScreenPress\",\"Value\":\"3000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AutoClickerCheatDetector.NumberOfActionsAllowed_TriggerPress\",\"Value\":\"3000\",\"ActiveExperiments\":null,\"StartTime\":\"2022-05-18T15:31:33Z\",\"EndTime\":null},{\"Key\":\"RoomJoinV2.Detailed\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PhotonClient.Detailed\",\"Value\":\"0.05\",\"ActiveExperiments\":null,\"StartTime\":\"2022-05-26T16:37:57Z\",\"EndTime\":null},{\"Key\":\"RoomJoinV2.PathDepth\",\"Value\":\"999\",\"ActiveExperiments\":null,\"StartTime\":\"2023-06-01T16:53:00Z\",\"EndTime\":null},{\"Key\":\"Growth.RoomJackpotPlaylists_Screens\",\"Value\":\"[ { \\\"PlaylistType\\\": \\\"HotRooms\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 20 }, { \\\"PlaylistType\\\": \\\"RRO\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 5 }, { \\\"PlaylistType\\\": \\\"Featured\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 5 } ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.RoomJackpotPlaylists_Touch\",\"Value\":\"[ { \\\"PlaylistType\\\": \\\"HotRooms\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 20 }, { \\\"PlaylistType\\\": \\\"RRO\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 5 }, { \\\"PlaylistType\\\": \\\"Featured\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 5 } ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.RoomJackpotPlaylists_VR\",\"Value\":\"[ { \\\"PlaylistType\\\": \\\"HotRooms\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 20 }, { \\\"PlaylistType\\\": \\\"RRO\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 5 }, { \\\"PlaylistType\\\": \\\"Featured\\\", \\\"DisplayType\\\": \\\"Always\\\", \\\"ShuffleType\\\": \\\"MixWithOthersAndShuffle\\\", \\\"MaxTopRoomsCountLimit\\\": 5 } ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.ForceQuickOrientationCreationCutoff\",\"Value\":\"9999-06-17 00:00\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.DetailedRoomLoadLogs\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.BufferedEventTimeoutSeconds\",\"Value\":\"180\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.PostDownloadTimeoutSeconds\",\"Value\":\"240\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.DetailedRoomLoadLogsForDevs\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomAvatarItems.CustomShirtId\",\"Value\":\"2184\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LoadingScreenProgressBar\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.SceneSpawnManager.TotalPlayerSpawnTimeoutSeconds\",\"Value\":\"150\",\"ActiveExperiments\":null,\"StartTime\":\"2022-07-01T16:54:59Z\",\"EndTime\":null},{\"Key\":\"Experience.GameLogTool.HideForJunior\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.GameLogTool.HideTool\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.GameLogTool.UseBugWording\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Social.DisablePhotoRollPrintingWhenMakerPenDisabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.BlockedRoomNamesOnLowMemoryDevices\",\"Value\":\"0Project29,Showdown\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.SnapshotThresholdBytes\",\"Value\":\"204000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.DownloadAssetBundleTimeoutSeconds\",\"Value\":\"900\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.DownloadTimeoutSeconds\",\"Value\":\"900\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.SupportInfluencerConfirmMessage\",\"Value\":\"When you support a player, you show that you like them.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ToxMod.EnableUGCRoomVoiceModeration\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomJoinV2.AlwaysLogPaths\",\"Value\":\"StackTimer;StackTimer.ConnectToRoomAndRunLoadLogic;StackTimer.ConnectToRoomAndRunLoadLogic.RunRoomLoadLogic;StackTimer.ConnectToRoomAndRunLoadLogic.RunRoomLoadLogic.InitialRoomLoad;StackTimer.ConnectToRoomAndRunLoadLogic.RunRoomLoadLogic.InitialRoomLoad.LoadRoomLocal;StackTimer.ConnectToRoomAndRunLoadLogic.RunRoomLoadLogic.InitialRoomLoad.LoadRoomLocal.Downloading Data;StackTimer.ConnectToRoomAndRunLoadLogic.RunRoomLoadLogic.InitialRoomLoad.LoadRoomLocal.Downloading Data.AssetBundle_Download\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayMenuCuratedCarouselsMap\",\"Value\":\"inkincroomhighlights=73682475;topearning=65380152;inkshowcase=52548973;quest=17859348;battle=17859349;horror=17859352;casualplay=17859347;roleplay=17859346;hangout=17859350;explore=17859351\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomJoinV2.TimeClampMax\",\"Value\":\"900\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RRUI.PlayHittingEndsSFX\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MetaHardware.ApplyJuniorRestrictions\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Growth.ForcedPostOrientationCategorySelectDuration\",\"Value\":\"1440\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.EnableTryingBundleItems\",\"Value\":\"true\",\"ActiveExperiments\":\"econ_try_bundle_items\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.HoursBeforeShowingBackupTutorial\",\"Value\":\"72\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.HubRoomName\",\"Value\":\"TheScienceDepartment\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.ProgressionEventCurrencyName\",\"Value\":\"Credit\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialFirstIntro_PressFeaturedRoomButton_NewWatch\",\"Value\":\"We need help... Collect the Core in ^TheScienceDepartment for more information!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.RoomCarousel_RegularRoomsHeader\",\"Value\":\"Restoration Rooms: Science Cores located here!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.InventionSpawningEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AntiHile.DC\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.GunHandle.ShowReloadOnFullAmmo\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.ShowPartiallyOwnedBundles\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CCT.RRCA.CampusRoomId\",\"Value\":\"77471693\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CCT.RRCA.TutorialRooms.Building\",\"Value\":\"688282368967907215,399286008370399908,2767464404937978317\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CCT.RRCA.TutorialRooms.Circuits\",\"Value\":\"5533753109055429437,367323597097954194,6805637954817370678\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CCT.RRCA.TutorialRooms.Creation\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Roles.SetPlayerRolesAnalyticsThresholdMs\",\"Value\":\"-1.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CCT.RRCA.TutorialRooms.LearnToCreate\",\"Value\":\"3406864552519591337,688282368967907215,5533753109055429437,5894707241498355365,2331615644215560745\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenuNegativeTestsCutoff\",\"Value\":\"2018-01-01 00:00\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.EnableLocText\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.PreDownloadTimeoutSeconds\",\"Value\":\"120\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Analytics.HeartbeatInterval\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.OneTimeData.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Backtrace.deobfuscationRate\",\"Value\":\"1.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.GiftDropIdToRoomIdMapping\",\"Value\":\"4953=76890764\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.RoomCarousel_RegularRoomsDesc\",\"Value\":\"Collect Science Cores in these Rooms!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenuCardSizesAccountCreationCutoff\",\"Value\":\"2017-01-01 00:01\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ShowPurchasePromptCV2.RateLimitMaxInvocations\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ShowPurchasePromptCV2.RateLimitTimeInverval\\t\",\"Value\":\"60\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CameraFade.TimeoutSeconds\",\"Value\":\"60\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CameraFade.TimeoutTrackingEnabled\\t\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.iOSQueuedBgTaskDelay\",\"Value\":\"-1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AntiHile.LPD\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Gamesight.PauseGamesightEvents\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenuCarouselHighlighting\",\"Value\":\"all=Favorites:highlight_scale_75\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenuCarouselHighlightingCutoff\",\"Value\":\"2017-01-01 00:01\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.BlockedRooms\",\"Value\":\"[170126]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.EligibleRooms\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.FailedStateContinuePlayingButtonText\",\"Value\":\"Continue Playing\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.FailedStateInviteFriendsButtonText\",\"Value\":\"Invite Friends\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.FailedStateSubtitle\",\"Value\":\"Keep playing while we continue searching in the background...\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.FailedStateTitle\",\"Value\":\"This is taking longer than expected!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.MinRoomSize\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.ShowIfEveryoneLeaves\",\"Value\":\"true\",\"ActiveExperiments\":\"lifecycle_2023q1_matchmaking_toast\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.SucceededStateAutoDismissTimeout\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.SucceededStateTitle\",\"Value\":\"Another player has joined you!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.WaitingStateCountdown\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.WaitingStateSubtitle\",\"Value\":\"Scouting the Room-i-verse...\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.WaitingStateTitle\",\"Value\":\"Finding Players To Join You\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.HideWhileInParty\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MaxInkResources\",\"Value\":\"900000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.InRoomChat.AccountCreationCutoff\",\"Value\":\"2023-06-06 07:10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.NoOrientation.Platforms\",\"Value\":\"mobile\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.NoOrientation.DestinationRoomId\",\"Value\":\"170126\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.NoOrientation.AccountCreationCutoff\",\"Value\":\"9999-06-06 15:30\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayMenuRoomBanner\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":\"2023-08-17T18:00:00Z\",\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.daily_cap\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.hide_skip_button_when_rating_selected\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.negative_canned_options\",\"Value\":\"[ \\\"Negative Players\\\", \\\"Bugs/Technical Issues\\\", \\\"Poor Experience\\\", \\\"No Other Players\\\", \\\"Cheating\\\", \\\"Inappropriate Content\\\" ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.negative_canned_question_text\",\"Value\":\"Select one or more issues (Up to 3)\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.negative_rating_text\",\"Value\":\"Dislike\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.neutral_canned_options\",\"Value\":\"[ \\\"Negative Players\\\", \\\"Bugs/Technical Issues\\\", \\\"Poor Experience\\\", \\\"No Other Players\\\", \\\"Cheating\\\", \\\"Inappropriate Content\\\" ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.neutral_canned_question_text\",\"Value\":\"Select one or more issues (Up to 3)\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.neutral_rating_text\",\"Value\":\"Like\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.number_of_ratings\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.playlists\",\"Value\":\"{17859340 : 0}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.positive_canned_options\",\"Value\":\"[ \\\"Fun With Friends\\\", \\\"Awesome Visuals\\\", \\\"Immersive Experience\\\", \\\"Exciting Events\\\", \\\"Amazing Gameplay\\\", \\\"Very Challenging\\\", \\\"Cool Soundtrack\\\", \\\"Unique Adventure\\\" ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.positive_canned_question_text\",\"Value\":\"What did you LOVE about it? (Up to 3)\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.positive_rating_text\",\"Value\":\"Love\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.reverse_ratings\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.show_skip_button_on_thank_you_page\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.survey_question\",\"Value\":\"What do you think of the quality of ^{0}?\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.title_first_line_text\",\"Value\":\"👋 We're listening!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.UI.Triangles.Max\",\"Value\":\"500000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomJoinV2.SuccessTiming\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomJoinV2.FailureTracking\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MobileHome.DiscoverySections\",\"Value\":\"[{\\\"title\\\":\\\"Friends\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"friends\\\"},{\\\"title\\\":\\\"Continue Playing\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"recent\\\"},{\\\"title\\\":\\\"Rec Room Originals\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"hotList\\\",\\\"sourceMetadata\\\":\\\"rro\\\"},{\\\"title\\\":\\\"Hot List\\\",\\\"isHorizontal\\\":false,\\\"source\\\":\\\"hotList\\\"}]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UseNewPhotonAuthToken\",\"Value\":\"true\",\"ActiveExperiments\":\"server_use_new_photon_auth_token\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Player.ShouldWaitForCameraFirstSpawn\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Player.WaitForLoadingScreen\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.RoomLoadCameraTimeoutSeconds\",\"Value\":\"45\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenu.RoomCollectionsCarousel.AccountCreationCutoff\",\"Value\":\"2017-01-01 00:00\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenu.RoomCollectionsCarousel.CardCountLimit\",\"Value\":\"0\",\"ActiveExperiments\":\"lifecycle_2023q2_play_menu_categories_3_1_v2\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenu.RoomCollectionsCarousel.Enabled\",\"Value\":\"false\",\"ActiveExperiments\":\"lifecycle_2023q2_play_menu_categories_3_1_v2\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenu.RoomCollectionsCarousel.Theme\",\"Value\":\"#recroomoriginal=bg#EF6825,txt#FFFFFF;#action=bg#DE8C90,txt#FFFFFF;#art=bg#75B0BD,txt#FFFFFF;#backrooms=bg#5F4949,txt#FFFFFF;#beta=bg#8689A7,txt#FFFFFF;#fnaf=bg#C1BF6C,txt#FFFFFF;#fun=bg#DE9812,txt#FFFFFF;#game=bg#4191A7,txt#FFFFFF;#hangout=bg#8F9953,txt#FFFFFF;#hiddenworlds=bg#B4A1D2,txt#FFFFFF;#horror=bg#7B4257,txt#FFFFFF;#military=bg#5C5F49,txt#FFFFFF;#obby=bg#CF7293,txt#FFFFFF;#parkour=bg#EB7B05,txt#FFFFFF;#playtime=bg#EF9C73,txt#FFFFFF;#playtime_friend=bg#579255,txt#FFFFFF;#playtime_visit=bg#895129,txt#FFFFFF;#prettycolors=bg#BD6060,txt#FFFFFF;#puzzle=bg#526FBA,txt#FFFFFF;#pvp=bg#DA9BC4,txt#FFFFFF;#quest=bg#C77337,txt#FFFFFF;#roleplay=bg#89AEDA,txt#FFFFFF;#rp=bg#A78480,txt#FFFFFF;#rrstudio=bg#4E548C,txt#FFFFFF;#scary=bg#786C9B,txt#FFFFFF;#simulator=bg#899AC5,txt#FFFFFF;#story=bg#75BDAB,txt#FFFFFF;#treasurehunt=bg#EBB64E,txt#FFFFFF;#tutorial=bg#7F96A4,txt#FFFFFF;#wonderland=bg#B0608C,txt#FFFFFF;#3dcharades=bg#BE86DB,txt#FFFFFF;#janrrmonthly=bg#469068,txt#FFFFFF;#febrrmonthly=bg#469068,txt#FFFFFF;#marrrmonthly=bg#469068,txt#FFFFFF;#aprrrmonthly=bg#469068,txt#FFFFFF;#mayrrmonthly=bg#469068,txt#FFFFFF;#junrrmonthly=bg#469068,txt#FFFFFF;#julrrmonthly=bg#469068,txt#FFFFFF;#augrrmonthly=bg#469068,txt#FFFFFF;#seprrmonthly=bg#469068,txt#FFFFFF;#octrrmonthly=bg#469068,txt#FFFFFF;#novrrmonthly=bg#469068,txt#FFFFFF;#decrrmonthly=bg#469068,txt#FFFFFF;#januaryrrmonthly=bg#469068,txt#FFFFFF;#februaryrrmonthly=bg#469068,txt#FFFFFF;#marchrrmonthly=bg#469068,txt#FFFFFF;#aprilrrmonthly=bg#469068,txt#FFFFFF;#junerrmonthly=bg#469068,txt#FFFFFF;#julyrrmonthly=bg#469068,txt#FFFFFF;#augustrrmonthly=bg#469068,txt#FFFFFF;#septemberrrmonthly=bg#469068,txt#FFFFFF;#octoberrrmonthly=bg#469068,txt#FFFFFF;#novemberrrmonthly=bg#469068,txt#FFFFFF;#decemberrrmonthly=bg#469068,txt#FFFFFF;#mayrrrmonthly=bg#469068,txt#FFFFFF;#septrrmonthly=bg#469068,txt#FFFFFF\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PlayMenu.TrendingTags.WrappableNames\",\"Value\":\"#recroomoriginal = #Rec[]Room[]Original\\r\\n#beta = #Beta\\r\\n#hangout = #hang[]out\\r\\n#hiddenworlds = #hidden[]worlds\\r\\n#3dcharades = #3d[]charades\\r\\n#prettycolors = #pretty[]colors\\r\\n#treasurehunt = #treasure[]​hunt\\r\\n#wonderland = #wonder[]land\\r\\n#roleplay = #role[]play\\r\\n#playtime = #play[]time\\r\\n#rrstudio = #RR[]studio\\r\\n#hangout = #hang[]out\\r\\n#parkour = #parkour\\r\\n#military = #mili[]tary\\r\\n#playtime_visit = #play[]time_[]visit\\r\\n#playtime_friend = #play[]time_[]friend\\r\\n#backrooms = #back[]rooms\\r\\n#simulator = #simu[]lator\\r\\n#janrrmonthly = #jan[]RR[]monthly\\r\\n#febrrmonthly = #feb[]RR[]monthly\\r\\n#marrrmonthly = #mar[]RR[]monthly\\r\\n#aprrrmonthly = #apr[]RR[]monthly\\r\\n#mayrrmonthly = #may[]RR[]monthly\\r\\n#junrrmonthly = #jun[]RR[]monthly\\r\\n#julrrmonthly = #jul[]RR[]monthly\\r\\n#augrrmonthly = #aug[]RR[]monthly\\r\\n#seprrmonthly = #sep[]RR[]monthly\\r\\n#octrrmonthly = #oct[]RR[]monthly\\r\\n#novrrmonthly = #nov[]RR[]monthly\\r\\n#decrrmonthly = #dec[]RR[]monthly\\r\\n#januaryrrmonthly = #jan[]uary[]RR[]monthly\\r\\n#februaryrrmonthly= #feb[]ruary[]RR[]monthly\\r\\n#marchrrmonthly = #march[]RR[]monthly\\r\\n#aprilrrmonthly = #april[]RR[]monthly\\r\\n#junerrmonthly = #june[]RR[]monthly\\r\\n#julyrrmonthly = #july[]RR[]monthly\\r\\n#augustrrmonthly = #august[]RR[]monthly\\r\\n#septemberrrmonthly = #september[]RR[]monthly\\r\\n#octoberrrmonthly = #oct[]ober[]RR[]monthly\\r\\n#novemberrrmonthly = #november[]RR[]monthly\\r\\n#decemberrrmonthly = #december[]RR[]monthly\\r\\n#mayrrrmonthly = #may[]RR[]monthly\\r\\n#septrrmonthly = #sept[]RR[]monthly\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Analytics.CreationSamplingMod\",\"Value\":\".1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.Playmenu.CarouselEndpoint\",\"Value\":\"foryou:Recommended For You,rising:Rising Rooms,earnroomcurrency_algo:Earn Room Currency,roleplay_algoendpoint:Roleplay,casual_algoendpoint:Casual,horror_algoendpoint:Horror,battle_algoendpoint:Battle,explore_algoendpoint:Explore,quests_algoendpoint:Quests,hangout_algoendpoint:Hangout\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"WatchHomeCarouselOrdering\",\"Value\":\"rooms_continueplaying=23;rooms_friendsplaying=22;foryou=21;rooms_hot=20;rooms_rro=19;items_featured=8;items_featuredcustom=7;inventions_featured=6;inventions_featureddormskins=5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.Homemenu.CarouselEndpoint\",\"Value\":\"foryou:Recommended For You\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Dormroom.Query\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.Dormroom.Title\",\"Value\":\"EXIT\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.Playmenu.DedupingExceptions\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.FriendRoomInviteRateLimitingCount\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ProUXFeatureFlag\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.UI.Ink.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.UI.Triangles.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.hot_rooms_probability\",\"Value\":\"0.06\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.new_rooms_probability\",\"Value\":\"0.20\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.StoreClothingOrderingExpOverride\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.StoreFeaturedOrderingExpOverride\",\"Value\":\"store_featured_carousel_ordering_v2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MobileHome.Discovery.Sections\",\"Value\":\"[{\\\"title\\\":\\\"Friends\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"friends\\\"}, {\\\"title\\\":\\\"Featured\\\", \\\"source\\\":\\\"featuredRoom\\\", \\\"sourceMetadata\\\":\\\"PlayMenuRoomBanner\\\"}, {\\\"title\\\":\\\"Continue Playing\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"recent\\\"}, {\\\"title\\\":\\\"Rec Room Originals\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"hotList\\\",\\\"sourceMetadata\\\":\\\"rro\\\"}, {\\\"title\\\":\\\"Hot Rooms\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"hotList\\\"}, {\\\"title\\\":\\\"New Rooms\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"hotList\\\",\\\"sourceMetadata\\\":\\\"new\\\"}, {\\\"title\\\":\\\"Battle\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"carousel\\\",\\\"sourceMetadata\\\":\\\"battle_algoendpoint\\\"}, {\\\"title\\\":\\\"Horror\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"carousel\\\",\\\"sourceMetadata\\\":\\\"horror_algoendpoint\\\"}, {\\\"title\\\":\\\"Quests\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"carousel\\\",\\\"sourceMetadata\\\":\\\"quests_algoendpoint\\\"}, {\\\"title\\\":\\\"Casual\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"carousel\\\",\\\"sourceMetadata\\\":\\\"casual_algoendpoint\\\"}, {\\\"title\\\":\\\"Roleplay\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"carousel\\\",\\\"sourceMetadata\\\":\\\"roleplay_algoendpoint\\\"}, {\\\"title\\\":\\\"Hangout\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"carousel\\\",\\\"sourceMetadata\\\":\\\"hangout_algoendpoint\\\"}, {\\\"title\\\":\\\"Explore\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"carousel\\\",\\\"sourceMetadata\\\":\\\"explore_algoendpoint\\\"}]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MatchmakingUI.AllowedLoadSources\",\"Value\":\"[\\\"CAROUSEL_ENDPOINT_WATCH\\\",\\\"HOT_ROOMS_WATCH\\\",\\\"RRO_ROOMS_WATCH\\\",\\\"NEW_ROOMS_WATCH\\\",\\\"CURATED_PLAYLISTS_WATCH\\\",\\\"RECENT_ROOMS_WATCH\\\",\\\"FRIEND_PRESENCE_ROOMS_WATCH\\\",\\\"FEATURED_ROOMS_WATCH\\\"]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.RoomDiscovery.CCUVariants.AccountCreationCutoff\",\"Value\":\"2017-01-01 00:00\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.RoomDiscovery.CCUVariants.LoggingEnabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.RoomDiscovery.CCUVariants.RoomCardInfo\",\"Value\":\"[{ \\\"CCUCountRange\\\": { \\\"Min\\\": 0, \\\"Max\\\": -1 }, \\\"RoomCardInfoElementIDs\\\": [\\\"cheersIcon\\\", \\\"cheersText\\\"] }]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.RoomDiscovery.CCUVariants.UIEnabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.RoomDiscovery.Cheers.LoggingEnabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.PartyChatEnabled\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.PartyChatMaxAutoRejoinMinutes\",\"Value\":\"60\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.PartyChatUpdatePeriod\",\"Value\":\"60\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.HierarchyViewVisible\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.CorruptDataRequest.RoomIdToDeviceModelsBlacklist\",\"Value\":\"{ 7740359740005459806: [ \\\"iPhone10,2\\\", \\\"iPhone10,3\\\", \\\"iPhone10,5\\\", \\\"iPhone10,6\\\", \\\"iPhone11,8\\\", \\\"iPhone12,1\\\", \\\"iPhone12,5\\\", \\\"iPhone12,8\\\", \\\"iPhone13,1\\\", \\\"iPhone13,2\\\", \\\"iPhone14,5\\\", \\\"iPhone14,6\\\", \\\"iPad3,1\\\", \\\"iPad3,2\\\", \\\"iPad3,3\\\", \\\"iPad7,11\\\", \\\"iPad7,12\\\", \\\"iPad11,1\\\", \\\"iPad11,2\\\", \\\"iPad11,3\\\", \\\"iPad11,4\\\", \\\"iPad11,6\\\", \\\"iPad11,7\\\", \\\"iPad12,1\\\", \\\"iPad12,2\\\", \\\"SM-X200\\\", \\\"SM-X205\\\" ] }\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Chips.7740359740005459806\",\"Value\":\"5750\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"FullBodyAvatars.MinLevelToUseFBAs\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Door.OrientationExit.Query\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.LogCV2ErrorsToAnalyticsInRooms\",\"Value\":\"[7740359740005459806, 52815862, 558011494282369883]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.VR.UIRaycastIgnoresClampedHeldTools\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.STN.ShowBonkyHandsWhenUsingUI\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.InventionLoading\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.RoomSaving\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.RoomPublishing\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AntiHile.QD\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CommunityBoard.HowToBuildLink\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.RoomUpgrade\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.UpgradeEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BadgeNotifications.StartEndTimesJson\",\"Value\":\"[\\r\\n\\t\\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-13_10-20\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-10-13T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-10-15T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-13_10-20\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-10-15T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-10-18T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-13_10-20\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-10-18T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-10-20T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n\\t\\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-20_10-27\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-10-20T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-10-22T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-20_10-27\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-10-22T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-10-25T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-20_10-27\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-10-25T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-10-27T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-27_11-03\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-10-27T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-10-29T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-27_11-03\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-10-29T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-01T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_10-27_11-03\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-01T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-03T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-03_11-10\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-03T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-05T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-03_11-10\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-05T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-08T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-03_11-10\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-08T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-10T10:59:59\\\\\\\"}\\\",\\r\\n \\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-10_11-17\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-10T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-12T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-10_11-17\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-12T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-15T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-10_11-17\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-15T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-17T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-17_11-24\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-17T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-19T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-17_11-24\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-19T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-22T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-17_11-24\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-22T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-24T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-24_12-01\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-24T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-26T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-24_12-01\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-26T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-11-29T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_11-24_12-01\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-11-29T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-01T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-01_12-08\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-01T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-03T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-01_12-08\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-03T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-06T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-01_12-08\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-06T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-08T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-08_12-15\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-08T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-10T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-08_12-15\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-10T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-13T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-08_12-15\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-13T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-15T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-15_12-22\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-15T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-17T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-15_12-22\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-17T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-20T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-15_12-22\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-20T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-22T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-22_12-29\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-22T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-24T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-22_12-29\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-24T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-27T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-22_12-29\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-27T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-29T10:59:59\\\\\\\"}\\\",\\r\\n\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-29_01-05\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-29T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2023-12-31T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-29_01-05\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2023-12-31T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2024-01-03T10:59:59\\\\\\\"}\\\",\\r\\n \\\"{\\\\\\\"title\\\\\\\":\\\\\\\"RCStorefront_12-29_01-05\\\\\\\",\\\\\\\"start_datetime\\\\\\\":\\\\\\\"2024-01-03T11:00:00\\\\\\\",\\\\\\\"end_datetime\\\\\\\":\\\\\\\"2024-01-05T10:59:59\\\\\\\"}\\\"\\r\\n]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"FullBodyAvatars.FeatureEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.LogEnteredStorePageSwaps\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.AllowedInRoomV1\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.CannedChatOptions.CustomA\",\"Value\":\"Yes,\\r\\nYay,\\r\\nAmazing!,\\r\\nI love this,\\r\\nYou look great!,\\r\\nYou look..., \\r\\nTIL,\\r\\nToo funny,\\r\\nLol,\\r\\nWhat!?,\\r\\nGood luck!,\\r\\nYou got this!,\\r\\nReally?,\\r\\nApplause,\\r\\nPerfect,\\r\\nDo it again!,\\r\\nLet's keep playing,\\r\\ngg\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.CannedChatOptions.CustomB\",\"Value\":\"No,\\r\\nSorry,\\r\\nMy bad,\\r\\nUgh,\\r\\nBoring,\\r\\nMeh,\\r\\nBoo,\\r\\nWhat the coach?,\\r\\nI'm out,\\r\\nStop,\\r\\nLeave me alone,\\r\\nWhy?,\\r\\nWhy not?,\\r\\nOh geez,\\r\\nI'm confused,\\r\\nNo comment\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.CannedChatOptions.RecRoom\",\"Value\":\"Hi!,\\r\\nHey there,\\r\\nBye! ,\\r\\nHow's it going? ,\\r\\nGood,\\r\\nOK,\\r\\nBad,\\r\\nYes,\\r\\nNo,\\r\\nThanks!,\\r\\nNo thanks!,\\r\\nWanna play?,\\r\\nCome with me,\\r\\nReady?,\\r\\nI gotta go,\\r\\nBe right back,\\r\\nLet's party up,\\r\\nLet's be friends!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.CannedChatOptions.Room\",\"Value\":\"Cool room!,\\r\\nShow me the room?,\\r\\nWatch out!,\\r\\nHelp!,\\r\\nWhere did you get that?,\\r\\nRight here,\\r\\nOver there,\\r\\nI'll take you there,\\r\\nOf course,\\r\\nIt's a no from me,\\r\\nHow did you do that?,\\r\\nWanna see something cool?,\\r\\nI can't even,\\r\\nIt's giving...,\\r\\nI'm done... new room?\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.ChipEvaluations\",\"Value\":\"400\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.Physics\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.Copy\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.Clone\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.Undo\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.UndoCreation\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.WatchHome.SegmentConfigMap\",\"Value\":\"CreatorSegment_EliteCreator:EliteCreatorCarouselList,CreatorSegment_AdvancedCreator:AdvancedCreatorCarouselList,CreatorSegment_MidCreator:MidCreatorCarouselList,CreatorSegment_BeginnerCreator:BeginnerCreatorCarouselList\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"EliteCreatorCarouselList\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AdvancedCreatorCarouselList\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MidCreatorCarouselList\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BeginnerCreatorCarouselList\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MobileHome.Discovery.Sections.v2\",\"Value\":\"[{\\\"title\\\":\\\"Friends\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"friends\\\"}, {\\\"title\\\":\\\"Featured\\\",\\\"source\\\":\\\"featuredRoom\\\",\\\"sourceMetadata\\\":\\\"PlayMenuRoomBanner\\\"}, {\\\"title\\\":\\\"Continue Playing\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"recent\\\"}, {\\\"title\\\":\\\"Rec Room Originals\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"hotList\\\",\\\"sourceMetadata\\\":\\\"rro\\\"}, {\\\"title\\\":\\\"Recommended For You\\\",\\\"isHorizontal\\\":true, \\\"hideIfNoContent\\\":true, \\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"foryou\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}, {\\\"title\\\":\\\"Hot Rooms\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"HotList\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}, {\\\"title\\\":\\\"New Rooms\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"new\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}, {\\\"title\\\":\\\"Battle\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"battle_algoendpoint\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}, {\\\"title\\\":\\\"Horror\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"horror_algoendpoint\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}, {\\\"title\\\":\\\"Casual\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"casual_algoendpoint\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}, {\\\"title\\\":\\\"Roleplay\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"roleplay_algoendpoint\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}, {\\\"title\\\":\\\"Hangout\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"hangout_algoendpoint\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}, {\\\"title\\\":\\\"Explore\\\",\\\"isHorizontal\\\":true,\\\"source\\\":\\\"algorithmicList\\\",\\\"sourceMetadata\\\":\\\"{\\\\\\\"endpoint\\\\\\\":\\\\\\\"explore_algoendpoint\\\\\\\",\\\\\\\"entityType\\\\\\\":1}\\\"}]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.StorefrontCarouselsToUseListsService\",\"Value\":\"(StoreClothing)/HatsItems,(StoreClothing)/TorsoItems,(StoreClothing)/AccessoriesItems,(StoreClothing)/GlovesItems,(StoreClothing)/HairItems\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MaxInkResourcesDefault\",\"Value\":\"40000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.SendHeartbeatAtEmptyScene\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.UseHeartbeatCoroutineDelays\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomInventory_SecondsBetweenFiringUseEvents\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InfoText\",\"Value\":\"Rooms 2.0 enables you to create higher quality rooms than ever before. However, there are a few things you should know before creating one.\\r\\n • This is an ALPHA release, so you may lose content, experience weird behaviors, etc.\\r\\n • Not every feature in Rooms 1.0 will be available in Rooms 2.0\\r\\n • Inventions are temporarily disabled\\r\\n • Rooms 2.0 introduces powerful new features like:\\r\\n • Compound Objects\\r\\n • MUCH higher creation limit\\r\\n • Player Properties (replaces Roles)\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.Progression.DailyDesc\",\"Value\":\"Earn up to 150 XP every 24 hours\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.Progression.RewardMap\",\"Value\":\"{\\\"3288\\\":\\\"153actlojyx0ktpl4dmlyrhcz.png\\\", \\\"3293\\\":\\\"153actlojyx0ktpl4dmlyrhcz.png\\\", \\\"3292\\\":\\\"5pawwzw4hvcrxhji1qo5iszfr.png\\\", \\\"3291\\\":\\\"0dydhae740mw5sj76vmfqkk1d.png\\\", \\\"3290\\\":\\\"erac8ves2ra4fhsuushotez9h.png\\\"}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.Progression.UnknownRewardImage\",\"Value\":\"aaitfknii3f6qdjkvhgb7vbhe.png\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.RestrictBlockedUnicodeStringsInPublicChat\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.RestrictUnicodeSupportInClubChat\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.RestrictUnicodeSupportInPartyChat\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.RestrictUnicodeSupportInRoomChat\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.HideRoomOffers\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.SerializationV4\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Friends.FriendRequestCooldownSeconds\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.EnableAllChatVoiceCalls\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.EnablePartyVoiceCall\",\"Value\":\"False\",\"ActiveExperiments\":\"retention_2024q3_cross_room_voice_chat\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.MaxBatchDelaySeconds\",\"Value\":\"15\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.MaxBatchSize\",\"Value\":\"131072\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle_announcement_min_view_time\",\"Value\":\"0.25\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle_announcement_track_shown\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.NetworkSnapshot\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2PublishingEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"HomeMenu.Carousels.ViewMoreButtonText\",\"Value\":\"Similar Content\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Analytics.CreationSamplingMod_Room\",\"Value\":\".2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayMenuDedupeSources\",\"Value\":\"Hot,CarouselEndpoint\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"FullBodyAvatars.UberMaterialCollapsingEnabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.MaxTimePostOrientationForFullPlayMenuPrompt\",\"Value\":\"30\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.StorefrontUGCCarouselEndpointFriendlyNames\",\"Value\":\"New:New Custom Shirts||Trending:Trending Custom Shirts||\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionsEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"HomeMenu.Carousels.MultiRowConfig\",\"Value\":\"{\\\"CarouselId\\\":\\\"Recommended For You\\\",\\\"MaxRows\\\":1,\\\"BackgroundColor\\\":\\\"#F3DDCE\\\",\\\"Enabled\\\":true}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayMenu.Carousels.MultiRowConfig\",\"Value\":\"{\\\"CarouselId\\\":\\\"Recommended For You\\\",\\\"MaxRows\\\":1,\\\"BackgroundColor\\\":\\\"#F3DDCE\\\",\\\"Enabled\\\":true}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.BulkPurchaseCap\",\"Value\":\"200\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.UseNewDiscoveryServerAPI\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.DiscoveryPageContent.PlayHighlight\",\"Value\":\"{\\\"pageSource\\\":1,\\\"sections\\\":[{\\\"Id\\\":\\\"Rooms_ForYou\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"foryou\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Recommended For You\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":2,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"#F3DDCE\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_HotRooms\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Hot\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Hot Rooms\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1}},{\\\"Id\\\":\\\"Rooms_New\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Hot\\\",\\\"SourceMetadata\\\":\\\"new\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"New\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_RRO\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Hot\\\",\\\"SourceMetadata\\\":\\\"rro\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Rec Room Originals\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_ContinuePlaying\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Recent\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Continue Playing\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Rising\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"rising\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Rising Rooms\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_MyFavorites\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"MyFavorites\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"My Favorites\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"all=highlight_scale_150\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_FriendsPlaying\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"FriendPresence\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Friends are Playing\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_TopEarning\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"PlaylistById\\\",\\\"SourceMetadata\\\":\\\"65380152\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Top Earning\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_MySubscriptions\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"MySubscriptions\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"My Subscriptions\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_EarnRoomCurrency_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"earnroomcurrency_algo\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Earn Room Currency\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Roleplay_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"roleplay_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Roleplay\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Casual_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"casual_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Casual\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Horror_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"horror_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Horror\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Battle_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"battle_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Battle\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Explore_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"explore_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Explore\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Quests_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"quests_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Quests\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Hangout_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"hangout_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Hangout\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_InkShowcase\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"PlaylistById\\\",\\\"SourceMetadata\\\":\\\"52548973\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Ink Showcase\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Featured\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Featured\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Featured\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}}]}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.DiscoveryPageContent.CommunityBoard\",\"Value\":\"{\\\"pageSource\\\":2,\\\"sections\\\":[{\\\"Id\\\":\\\"Rooms_ForYou\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"foryou\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Recommended For You\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":2,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"#F3DDCE\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_HotRooms\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Hot\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Hot Rooms\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1}},{\\\"Id\\\":\\\"Rooms_New\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Hot\\\",\\\"SourceMetadata\\\":\\\"new\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"New\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_RRO\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Hot\\\",\\\"SourceMetadata\\\":\\\"rro\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Rec Room Originals\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_ContinuePlaying\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Recent\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Continue Playing\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Rising\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"rising\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Rising Rooms\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_MyFavorites\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"MyFavorites\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"My Favorites\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"all=highlight_scale_150\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_FriendsPlaying\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"FriendPresence\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Friends are Playing\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_TopEarning\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"PlaylistById\\\",\\\"SourceMetadata\\\":\\\"65380152\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Top Earning\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_MySubscriptions\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"MySubscriptions\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"My Subscriptions\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_EarnRoomCurrency_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"earnroomcurrency_algo\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Earn Room Currency\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Roleplay_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"roleplay_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Roleplay\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Casual_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"casual_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Casual\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Horror_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"horror_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Horror\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Battle_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"battle_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Battle\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Explore_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"explore_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Explore\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Quests_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"quests_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Quests\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Hangout_AlgoEndpoint\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"CarouselEndpoint\\\",\\\"SourceMetadata\\\":\\\"hangout_algoendpoint\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Hangout\\\",\\\"supportsDedupe\\\":true,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_InkShowcase\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"PlaylistById\\\",\\\"SourceMetadata\\\":\\\"52548973\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Ink Showcase\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_Featured\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Featured\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Featured\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}}]}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.DiscoveryPageContent.WatchHome\",\"Value\":\"{\\\"pageSource\\\":0,\\\"sections\\\":[{\\\"Id\\\":\\\"Rooms_ContinuePlaying\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Recent\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Continue Playing\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_FriendsPlaying\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"FriendPresence\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Friends are Playing\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_HotRooms\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Hot\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Hot Rooms\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":2,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"#F3DDCE\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Rooms_RRO\\\",\\\"DiscoveryEntityType\\\":0,\\\"Source\\\":\\\"Hot\\\",\\\"SourceMetadata\\\":\\\"rro\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"Rec Room Originals\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Items_Featured\\\",\\\"DiscoveryEntityType\\\":4,\\\"Source\\\":\\\"Store\\\",\\\"SourceMetadata\\\":\\\"FeaturedItems\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Items_FeaturedCustom\\\",\\\"DiscoveryEntityType\\\":8,\\\"Source\\\":\\\"Featured\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Inventions_Featured\\\",\\\"DiscoveryEntityType\\\":2,\\\"Source\\\":\\\"FeaturedInventions\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Inventions_FeaturedDormSkins\\\",\\\"DiscoveryEntityType\\\":2,\\\"Source\\\":\\\"FeaturedDormSkins\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Events_My\\\",\\\"DiscoveryEntityType\\\":5,\\\"Source\\\":\\\"MyEvents\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Clubs_My\\\",\\\"DiscoveryEntityType\\\":3,\\\"Source\\\":\\\"ClubsIBelongTo\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}},{\\\"Id\\\":\\\"Top5\\\",\\\"DiscoveryEntityType\\\":7,\\\"Source\\\":\\\"\\\",\\\"SourceMetadata\\\":\\\"\\\",\\\"DisplayMetadata\\\":{\\\"DisplayTitle\\\":\\\"\\\",\\\"supportsDedupe\\\":false,\\\"numRows\\\":1,\\\"sizePerPlatform\\\":\\\"\\\",\\\"backgroundColor\\\":\\\"\\\",\\\"horizontalScrollEnabled\\\":false}}]}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionConversionEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.RoomSearch.CustomCarouselData\",\"Value\":\"\\t{ \\\"SourceText\\\": \\\"CarouselEndpoint\\\", \\\"SourceMetadata\\\": \\\"HotList\\\", \\\"Title\\\": \\\"Visit our hot rooms instead?\\\" }\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.RoomSearch.SearchTips\",\"Value\":\"1. Try other keywords\\r\\n2. Use more generic search terms\\r\\n3. Check your search for typos or spelling errors\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.RoomSearch.SimilarRoomsCarouselTitle\",\"Value\":\"Enjoying ^{0}? You might also like\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.LogFriendIdsWithSocialHeartbeat\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.CalculatingDelay\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.MaxPrompts\",\"Value\":\"23\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_animals\",\"Value\":\"[\\\"Rhinos\\\",\\\"Giraffes\\\",\\\"Cheetahs\\\",\\\"Rats\\\",\\\"Roaches\\\",\\\"Dogs\\\",\\\"Chimpanzees\\\",\\\"Mooses\\\",\\\"Wooly Mammoths\\\",\\\"Hyenas\\\",\\\"Cats\\\",\\\"Platypus\\\",\\\"Kangaroos\\\",\\\"Flamingos\\\",\\\"Hippopotamuses\\\",\\\"Squirrels\\\",\\\"Sloths\\\",\\\"Monkeys\\\",\\\"Gorillas\\\",\\\"Mandrill\\\",\\\"Lizards\\\",\\\"Platypuses\\\",\\\"Gators\\\",\\\"Echidnas\\\",\\\"Wallabies\\\",\\\"Shoebills\\\",\\\"Cats\\\",\\\"Parrots\\\",\\\"T-Rexes\\\"]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_condiments\",\"Value\":\"[\\\"Ketchup\\\",\\\"Mayonnaise\\\",\\\"BBQ Sauce\\\",\\\"Salt\\\",\\\"Mustard\\\",\\\"Soy Sauce\\\",\\\"Hot Sauce\\\",\\\"Pepper\\\"]\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_food\",\"Value\":\"[\\\"Ketchup\\\",\\\"Mayonnaise\\\",\\\"Buffalo Chicken\\\",\\\"Pepperoni\\\",\\\"Hot Sauce\\\",\\\"Soy Sauce\\\",\\\"Cheese\\\",\\\"Anchovies\\\",\\\"Pineapples\\\",\\\"Mac and Cheese\\\",\\\"Apples\\\",\\\"Bananas\\\",\\\"Durians\\\",\\\"Blueberries\\\",\\\"Apricots\\\",\\\"Peaches\\\",\\\"Pancakes\\\",\\\"Cherries\\\",\\\"Lemons\\\",\\\"Oranges\\\",\\\"Avocados\\\",\\\"Coconut\\\",\\\"Blackberries\\\",\\\"Waffles\\\",\\\"Bacon\\\",\\\"Sushi\\\",\\\"Donuts\\\",\\\"Hot dogs\\\",\\\"Pizza\\\"]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_genre\",\"Value\":\"[\\\"Elves\\\",\\\"Dragons\\\",\\\"Swords\\\",\\\"Magic\\\",\\\"Robots\\\",\\\"Lasers\\\",\\\"Aliens\\\",\\\"Spaceships\\\",\\\"Kisses\\\",\\\"Smoochies\\\",\\\"Meet-Cutes\\\",\\\"Pratfalls\\\",\\\"Fart sounds\\\",\\\"Explosions\\\",\\\"Car chases\\\",\\\"Martial arts\\\",\\\"Secret gadgets\\\",\\\"Superheroes\\\",\\\"Supervillains\\\",\\\"Secret identities\\\",\\\"Crime\\\",\\\"Betrayal\\\",\\\"Heists\\\",\\\"Original songs\\\",\\\"Giant monsters\\\",\\\"Easter eggs\\\",\\\"Post-credit scenes\\\",\\\"Death traps\\\",\\\"Talking animals\\\"]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_pizzatoppings\",\"Value\":\"[\\\"Cheese\\\",\\\"Anchovy\\\",\\\"Buffalo Chicken\\\",\\\"Thin Crust\\\",\\\"Pepperoni\\\",\\\"Pineapple\\\",\\\"Mac and Cheese\\\",\\\"Deep dish\\\"]\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_place\",\"Value\":\"[\\\"Mountain\\\",\\\"Ocean\\\",\\\"Desert\\\",\\\"Prairie\\\",\\\"City\\\",\\\"Mansion\\\",\\\"Yurt\\\",\\\"Forest\\\",\\\"Tundra\\\",\\\"Rainforest\\\",\\\"Bathroom\\\",\\\"Treehouse\\\",\\\"Stonehenge\\\",\\\"The Sphynx\\\",\\\"Eiffel Tower\\\",\\\"Great Wall\\\",\\\"Mariana Trench\\\",\\\"North Pole\\\",\\\"South Pole\\\",\\\"Sahara desert\\\",\\\"Beach\\\",\\\"Park\\\",\\\"Dorm room\\\",\\\"Dungeon\\\",\\\"Hole\\\",\\\"Mall\\\",\\\"Train station\\\",\\\"Rec Center\\\",\\\"Airport\\\"]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_powers\",\"Value\":\"[\\\"Flight\\\",\\\"Laser eyes\\\",\\\"Invincibility\\\",\\\"Super speed\\\",\\\"Grow gigantic\\\",\\\"Teleportation\\\",\\\"Talk to fish\\\",\\\"Invisibility\\\",\\\"Freeze breath\\\",\\\"Super strength\\\",\\\"Strechy body\\\",\\\"Shrink tiny\\\",\\\"Weather control\\\",\\\"Talk to squirrels\\\",\\\"Telepathy\\\",\\\"Telekinesis\\\",\\\"Duplicate anything\\\",\\\"Animal transformation\\\",\\\"Magnetism\\\",\\\"Control gravity\\\",\\\"Eat anything\\\",\\\"Instant parties\\\",\\\"Always have discounts\\\",\\\"Raise the dead\\\",\\\"Infinite chocolate\\\",\\\"Infinite soup\\\",\\\"Night vision\\\",\\\"Robot arms\\\",\\\"Shapeshifting\\\"]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_smell\",\"Value\":\"[\\\"New cars\\\",\\\"After it rains\\\",\\\"Toasty cookies\\\",\\\"Lavender\\\",\\\"Fresh coffee\\\",\\\"Mint\\\",\\\"Popcorn\\\",\\\"Shampoo\\\",\\\"Mowed grass\\\",\\\"Old books\\\",\\\"Fresh bread\\\",\\\"Christmas trees\\\",\\\"Lilac\\\",\\\"Wood fire\\\",\\\"Stink bugs\\\",\\\"Gasoline\\\",\\\"Feet\\\",\\\"Armpits\\\",\\\"Wet dogs\\\",\\\"Cow farms\\\",\\\"Skunks\\\",\\\"Sewers\\\",\\\"Litterboxes \\\",\\\"Goo\\\",\\\"Locker rooms\\\",\\\"Toots\\\",\\\"Poop\\\",\\\"Diapers\\\",\\\"Rotten eggs\\\"]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Pool_things\",\"Value\":\"[\\\"Anime\\\",\\\"Video games\\\",\\\"Pencils\\\",\\\"Gel Pens\\\",\\\"Riches\\\",\\\"Movies\\\",\\\"Breakfast for dinner\\\",\\\"VR Headsets\\\",\\\"Jobs\\\",\\\"Rubber duckies\\\",\\\"Books\\\",\\\"Fame\\\",\\\"Sleep\\\",\\\"Computers\\\",\\\"Pirates\\\",\\\"Ninjas\\\",\\\"Ice cream\\\",\\\"Dandruff\\\",\\\"Monkeys\\\",\\\"Robots\\\",\\\"Gold\\\",\\\"Cryptocurrencies\\\",\\\"Goo\\\",\\\"Loot boxes\\\",\\\"Coffee\\\",\\\"Snacks\\\",\\\"Underwear\\\",\\\"Smartphones\\\",\\\"Parties\\\"]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.PreGameDelay\",\"Value\":\"0.5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_0\",\"Value\":\"{\\\"Prompt\\\":\\\"Best animal?\\\",\\\"Pool\\\":\\\"animals\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_1\",\"Value\":\"{\\\"Prompt\\\":\\\"Best Condiment?\\\",\\\"Pool\\\":\\\"condiments\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_10\",\"Value\":\"{\\\"Prompt\\\":\\\"I want to drink this as a smoothie...\\\",\\\"Pool\\\":\\\"food\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_11\",\"Value\":\"{\\\"Prompt\\\":\\\"My new healthy diet is only eating...\\\",\\\"Pool\\\":\\\"food\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_12\",\"Value\":\"{\\\"Prompt\\\":\\\"The coolest superpower would be...\\\",\\\"Pool\\\":\\\"powers\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_13\",\"Value\":\"{\\\"Prompt\\\":\\\"The worst superpower to have is...\\\",\\\"Pool\\\":\\\"powers\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_14\",\"Value\":\"{\\\"Prompt\\\":\\\"The best place to live is...\\\",\\\"Pool\\\":\\\"place\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_15\",\"Value\":\"{\\\"Prompt\\\":\\\"I'd love to vacation in...\\\",\\\"Pool\\\":\\\"place\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_16\",\"Value\":\"{\\\"Prompt\\\":\\\"Coach's secret hideout is located in...\\\",\\\"Pool\\\":\\\"place\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Image\",\"Value\":\"e8ia6kydhl9my3r6d76wie3en.png\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_17\",\"Value\":\"{\\\"Prompt\\\":\\\"I'd rather have...\\\",\\\"Pool\\\":\\\"things\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_18\",\"Value\":\"{\\\"Prompt\\\":\\\"If I had $1,000,000, I'd spend it all on...\\\",\\\"Pool\\\":\\\"things\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_19\",\"Value\":\"{\\\"Prompt\\\":\\\"Like my perfume? It's Eau de...\\\",\\\"Pool\\\":\\\"smell\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_2\",\"Value\":\"{\\\"Prompt\\\":\\\"Best Pizza Topping?\\\",\\\"Pool\\\":\\\"pizzatoppings\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_20\",\"Value\":\"{\\\"Prompt\\\":\\\"I wish everything smelled like...\\\",\\\"Pool\\\":\\\"smell\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_21\",\"Value\":\"{\\\"Prompt\\\":\\\"All my favorite books have this in common!\\\",\\\"Pool\\\":\\\"genre\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_22\",\"Value\":\"{\\\"Prompt\\\":\\\"All my favorite movies gotta include:\\\",\\\"Pool\\\":\\\"genre\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_3\",\"Value\":\"{\\\"Prompt\\\":\\\"The coolest animal to ride...\\\",\\\"Pool\\\":\\\"animals\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_4\",\"Value\":\"{\\\"Prompt\\\":\\\"The best stuffed animal would be a...\\\",\\\"Pool\\\":\\\"animals\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_5\",\"Value\":\"{\\\"Prompt\\\":\\\"Who would win in a fight...\\\",\\\"Pool\\\":\\\"animals\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_6\",\"Value\":\"{\\\"Prompt\\\":\\\"Which is the scariest animal...\\\",\\\"Pool\\\":\\\"animals\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_7\",\"Value\":\"{\\\"Prompt\\\":\\\"This would be the coolest pet!\\\",\\\"Pool\\\":\\\"animals\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_8\",\"Value\":\"{\\\"Prompt\\\":\\\"Which tastes best...\\\",\\\"Pool\\\":\\\"food\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Prompt_9\",\"Value\":\"{\\\"Prompt\\\":\\\"The best food to put on top of another food...\\\",\\\"Pool\\\":\\\"food\\\"}\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.Range\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.ResultsDelay\",\"Value\":\"4\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.VoteTimer\",\"Value\":\"20\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionsUseRruiMenu\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionsAlwaysPromptForAnchors\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.IncludeCurvesInDefaultShapesView\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Foundation.NetworkTransform.ignore_truly_redundant_sync_data\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.prob_coefficient_b\",\"Value\":\"200\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.DormRoom.AllowRename\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Watchdog.NintendoSwitch.IgnoreHashCheck\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.DisableCloning\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.DisableLimitsV2Toggle\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BrazeNotificationTestRoomId\",\"Value\":\"558011494282369883\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.SkipPerfStatsFor\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.StorefrontCarouselEndpointFriendlyNames\",\"Value\":\"itemsforyou:Recommended For You||newitems:New Items||headwearitems:Headwear||topsitems:Tops||bottomsitems:Bottoms||footwearitems:Footwear||waistitems:Waist||handsitems:Hands||shoulderitems:Shoulders||skinsitems:Skins||accessoriesitems:Accessories||hairitems:Hairstyles||facialhairitems:Facial Hair\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Chips.558011494282369883\",\"Value\":\"100000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute.558011494282369883\",\"Value\":\"6000000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Avatars.CollectAnalytics\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Nintendo.ProfanityFilter.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC_MakerPen_Memory_Threshold_High\",\"Value\":\"500\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC_MakerPen_Memory_Threshold_Low\",\"Value\":\"250\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC_MakerPen_Memory_Threshold_Medium\",\"Value\":\"400\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.android.HighPassFilterEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.ios.HighPassFilterEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PartyVoiceCall.VoiceIndicatorVolumeThreshold\",\"Value\":\"0.005\",\"ActiveExperiments\":\"retention_2024q4_audio_post_call_survey\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.iOS\",\"Value\":\"9\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute.1590551298800707236\",\"Value\":\"6000000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.ContextualFeatures.Endpoints\",\"Value\":\"Rooms:foryou,Rooms:newforyou\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Logs\",\"Value\":\"GLOBAL_ENABLE,\\r\\nNetwork.TachyonClient,\\r\\nNetwork.DissonanceLogs,\\r\\nLogic.CV2.CircuitsV2Lifecycle,\\r\\nAudio,\\r\\nPlatform,\\r\\nPlatform.Bootsequence,\\r\\nEconomy.Player.AvatarItemEffectManager,\\r\\nEconomy.Player.RoomAvatarItemEffectManager,\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Rooms.RoomCrashWarnings.AlertWarningText\",\"Value\":\"WARNING: This room is likely to crash Rec Room on your device!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Rooms.RoomCrashWarnings.DialogWarningBodyText\",\"Value\":\"WARNING: This room is likely to crash Rec Room on your device! Are you sure you want to visit this room?\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Rooms.RoomCrashWarnings.DialogWarningTitleText\",\"Value\":\"Frequently Crashing Room\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Rooms.RoomCrashWarnings.RoomIdList\",\"Value\":\"7539016350466990073,6460810660392940094,8434490367257183630,2723306493645093776,7951706875400961651,75593084,3727395230137507322,3749251222419510387,5219263482782236232,6454501220228739135,3762381642303876131,7137856640475359618,8212438816726061451,1002619559400780254,5575657785453805022,8584681945153889245,1998201726486983287,6046693847070402258,6625638715786326448,4724346503679466337,4887987556817177058,6496188353992192382,2588025359135404369,71625681,4909289,5520960571843276241,8563323012958346253,3928992809688397144,2659208225370486509,34437418,1301222140483025326,460713181691498092,68220945,7394179795826726286,5049525252432293388,3671979227911669988,1493245433223019262,8707836826959443360,4119127429656647293,3327097983241960053,7026731885266689055,6528877349752716576,7315670054611711409,28137902,3160497704734277942,171852845679145957,52180256,5493571744884487087,6590799976948934030,6343987749191961647,8516835775170953366,8919776944810253778,3630162942359427885,4417853304691658641,7474883422630538176,1612152678088871086,5085423753694824433,2484997484818949072,3246079992269904731,6874223714871232674,2008895015596827945,999885846299426023,2786659460607816068,1429950652968278318\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Rooms.RoomCrashWarnings.WarningType\",\"Value\":\"None\",\"ActiveExperiments\":\"msf_2024q2_roomcrashwarnings\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.PushToNetworkTransform\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.DormRoom.MaxNumDorms\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute.RollingWindow.558011494282369883\",\"Value\":\"6000000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute.RollingWindowSize.558011494282369883\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.PinnedRoomCCUThreshold\",\"Value\":\"100\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.PopularBadgeCCUThreshold\",\"Value\":\"100\",\"ActiveExperiments\":\"discovery_2024q2_ccu_search_threashold\\r\\ndiscovery_2024q2_show_player_activity_recent\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Photon.PeerBaseReset\",\"Value\":\"false\",\"ActiveExperiments\":\"multiplayer_2024q3_photon_buffer_reset\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.CCUIconThresholds\",\"Value\":\"[\\r\\n {\\r\\n \\\"Count\\\": 100,\\r\\n \\\"Label\\\": \\\"Hot\\\",\\r\\n \\\"Color\\\": \\\"#EA6F48\\\"\\r\\n }\\r\\n]\",\"ActiveExperiments\":\"discovery_2024q2_ccu_search_threashold\\r\\ndiscovery_2024q2_show_player_activity_recent\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Desktop_Oculus.runHashCheck\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.EnableExtraPositionCompression\",\"Value\":\"false\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_body_parts_position_compression\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.PushToShadowWorld\",\"Value\":\"false\",\"ActiveExperiments\":\"foundation_2024q3_push_to_shadow_world\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.FOVChip.MultiplierMin\",\"Value\":\"0.75\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.FOVChip.MultiplierMax\",\"Value\":\"1.25\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.FOVChip.RateLimitCount\",\"Value\":\"4\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.FOVChip.RateLimitTimeWindow\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.FOVChip.TransitionDurationMax\",\"Value\":\"1000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.FOVChip.TransitionDurationMin\",\"Value\":\"0.0167\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Nintendo.ProfanityFilter.EnabledChat\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.global.DuckLevel\",\"Value\":\"1.0\",\"ActiveExperiments\":\"retention_2024q4_remove_voice_ducking_rerun\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute.RollingWindow\",\"Value\":\"24000000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute.RollingWindowSize\",\"Value\":\"30\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.global.FalloffSeconds\",\"Value\":\"0.2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.VR.PlayerTrackingSpace.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.VR.Head.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.VR.Hand.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.Screen.PlayerTrackingSpace.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.Screen.Head.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.Screen.Hand.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.Touch.PlayerTrackingSpace.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.Touch.Head.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PlayerAvatarNetworkData.Touch.Hand.MaxFramCountSyncDelta\",\"Value\":\"1\",\"ActiveExperiments\":\"multiplayer_2024q3_avatar_reduce_sync_frequency\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ModernBeanBodyAvatars.FeatureEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ModernBeanBodyAvatars.UseClassicBeanOverrideEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Avatars.HolotarsCanUseModernAvatar\",\"Value\":\"full release\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.GenreBasedMatchmaking.FreshnessParameter\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.OnlineFriendsCarousel.FriendCountThreshold\",\"Value\":\"300\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.BowString.EnableDoubleShot\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"reactions.mapping\",\"Value\":\"[ \\\"👍,👍🏻,👍🏼,👍🏽,👍🏾,👍🏿\\\",\\r\\n \\\"❤️,🧡,💛,💚,💙,💜,🤎,🖤,🤍\\\", \\r\\n\\\"🎉,🎊,🥳,🔥\\\",\\r\\n \\\"😭,😢,😿\\\", \\\"😲,😦,😯,😧,😱,🙀\\\",\\r\\n \\\"👎,👎🏻,👎🏼,👎🏽,👎🏾,👎🏿\\\", \\r\\n\\\"💀,☠️,🤡,👹,👻,👽,🤖\\\", \\r\\n\\\"🤣,😂,😆,🤪,😹\\\" ]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PUG.Choices.MaxNotifsPerSession\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PumpPlayerHeightTrackingSuppressException\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Nintendo.ProcessUnconsumedItemsOnLogin\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.ios.ComfortNoise\",\"Value\":\"true\",\"ActiveExperiments\":\"multiplayer_2024q4_remove_comfort_noise\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.android.ComfortNoise\",\"Value\":\"true\",\"ActiveExperiments\":\"multiplayer_2024q4_remove_comfort_noise\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"show_user_agreements_on_login\",\"Value\":\"-1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PartyVoiceCall.MaxSurveysPerSession\",\"Value\":\"1\",\"ActiveExperiments\":\"retention_2024q4_audio_post_call_survey\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.MicSettings.HighlightMuteIconCooldown\",\"Value\":\"60\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.MicSettings.HighlightMuteIconSessionMax\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"show_user_agreements_on_creation\",\"Value\":\"-1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.prob_coefficient_c\",\"Value\":\"4\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.room_age_weight\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"lifecycle.room_feedback.room_visits_weight\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"radial_shortcut_nub_distance_kbm_override\",\"Value\":\"125\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.NoOrientation.PlatformMask\",\"Value\":\"96\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.AllowVRPlayerBeaconControls\",\"Value\":\"false\",\"ActiveExperiments\":\"retention_2024q4_coordinate_vr\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconDurationS\",\"Value\":\"8\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconComeHereRadius\",\"Value\":\"2\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconFovMaxHeightPercentage\",\"Value\":\"0.2\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconFovMinHeightPercentage\",\"Value\":\"0.1\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconMaxDistanceM\",\"Value\":\"200\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconMVPEnabled\",\"Value\":\"True\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\\r\\nretention_2024q4_coordinate_vr\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconRateLimitingCooldownTimeS\",\"Value\":\"10\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconRateLimitingSpamCount\",\"Value\":\"3\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.BeaconRateLimitingSpamTimeWindowS\",\"Value\":\"3\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.ReceiveAnnouncement\",\"Value\":\"True\",\"ActiveExperiments\":\"retention_2024q4_coordinate_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.UseWiderVRLaserWhenBeaconsEnabled\",\"Value\":\"false\",\"ActiveExperiments\":\"retention_2024q4_coordinate_vr\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.VRBeaconFovMaxScale\",\"Value\":\"0.075\",\"ActiveExperiments\":\"retention_2024q4_coordinate_vr\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.VRBeaconFovMinScale\",\"Value\":\"0.07\",\"ActiveExperiments\":\"retention_2024q4_coordinate_vr\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PartyVoiceCall.SendGameLogsReportAfterSurvey\",\"Value\":\"true\",\"ActiveExperiments\":\"retention_2024q4_audio_post_call_survey\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PartyVoiceCall.SurveyMinimumCallDuration\",\"Value\":\"60\",\"ActiveExperiments\":\"retention_2024q4_audio_post_call_survey\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PartyVoiceCall.SurveyCanSendMultipleLogsPerSession\",\"Value\":\"true\",\"ActiveExperiments\":\"retention_2024q4_audio_post_call_survey\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PartyVoiceCall.SurveyOnlySendGameLogsWhenOtherReasonListed\",\"Value\":\"true\",\"ActiveExperiments\":\"retention_2024q4_audio_post_call_survey\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.PartyVoiceCall.MinSurveysUntilNextBugReport\",\"Value\":\"1\",\"ActiveExperiments\":\"retention_2024q4_audio_post_call_survey\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.FilteredTimeStats\",\"Value\":\"N, PercentileFlags, Median, Min, Max\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.InventionCreationUseRruiMenuFromMakerPenPalette\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"show_plaintext_user_agreements\",\"Value\":\"768\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.android.DuckLevel\",\"Value\":\"0.5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.ios.DuckLevel\",\"Value\":\"0.5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.HeadsUpTutorialEnabled\",\"Value\":\"True\",\"ActiveExperiments\":\"retention_coordinate_tutorial_test\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.HintTutorialEnabled\",\"Value\":\"false\",\"ActiveExperiments\":\"retention_coordinate_tutorial_test\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.PerfMetrics.FilteredTimeStats\",\"Value\":\"N, PercentileFlags, Median, Min, Max, HitchFlags\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"DataCollection.PerfMetrics.HitchThresholdOffset\",\"Value\":\"20\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.JumpRightIn.OnlineFriends.ChangeDelaySeconds\",\"Value\":\"2.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.JumpRightIn.OnlineFriends.MaxFriendCount\",\"Value\":\"300\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.JumpRightIn.OnlineFriends.ViewMoreMax\",\"Value\":\"8\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Crowd.FitScoreMaximum\",\"Value\":\"2.6\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionDetailsRRUI\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Foundation.NetworkTransform.use_legacy_interpolation\",\"Value\":\"false\",\"ActiveExperiments\":\"avatars_2025q1_network_improvements\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Network.Room2\",\"Value\":\"[ { \\\"NumPlayersForActivation\\\":0, \\\"Bytes\\\":8192, \\\"Rpcs\\\":32 }]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Chips.Room2\",\"Value\":\"7500\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute.RollingWindow.Room2\",\"Value\":\"24000000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Limit.Compute.Room2\",\"Value\":\"3200000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.MPO.RoomIds\",\"Value\":\"[3428843045665284770]\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.MPO.WelcomeCenterImage\",\"Value\":\"welcome_center_v2.png\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionsStoreEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2SpawnNonAuthoredR2InventionsEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Avatars.AdvancedBodyCustomizationEnabled_FB\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Avatars.AdvancedBodyCustomizationEnabled_MB\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.JTT.MakerPenPrompt.Title\",\"Value\":\"Imagine. Build. Play!\",\"ActiveExperiments\":\"rengage_2025q1_just_in_time_tutorials_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Discovery.JTT.MakerPenPrompt.Subtitle\",\"Value\":\"Try Maker Pen to start building your world today!\",\"ActiveExperiments\":\"rengage_2025q1_just_in_time_tutorials_mvp\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Weapons.AdditionalRpcNetworking\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room.Autosave.SkipOnNoMakerPen\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerPenHUDEnabledForDevelopers\",\"Value\":\"true\",\"ActiveExperiments\":\"ugc_2025q2_makerpen_mobile_access\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.CannedChat.FullList\",\"Value\":\"Hi!,\\r\\nBye!,\\r\\nReady?,\\r\\nBRB,\\r\\nLet’s go!,\\r\\nCome with me,\\r\\nHelp!,\\r\\nLet's party up!,\\r\\nLet's be friends!,\\r\\nMay I pick the room?,\\r\\nYes,\\r\\nOK,\\r\\nThanks,\\r\\nLOL,\\r\\nYou look great!,\\r\\nNo,\\r\\nBoo,\\r\\nSorry,\\r\\nI don't like this,\\r\\nWhat the coach?!,\\r\\nLOL,\\r\\nTry this,\\r\\nThanks!,\\r\\nWe need more stuff!,\\r\\nWhat else can we make?,\\r\\nCan we do it with less?,\\r\\nGood job!!,\\r\\nWheel wheel wheel...\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.CannedChat.SocialOrientationA\",\"Value\":\"Hi!,\\r\\nLOL,\\r\\nYou look great!,\\r\\nLet’s be friends!,\\r\\nWhat the coach?!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.CannedChat.SocialOrientationB\",\"Value\":\"Let’s party up!,\\r\\nTry this,\\r\\nThanks!,\\r\\nHelp!,\\r\\nBRB\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Avatars.AdvancedFaceCustomizationEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MasterClientSelection.CostDiffThreshold\",\"Value\":\"25\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Subtitles.PurifyForJuniors\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Avatars.AdvancedNoseCustomizationEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomLoad.UseCompatibleSnapshot.PlatformMask\",\"Value\":\"512\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionPublishingReminderText\",\"Value\":\"The latest version of your invention will be published\\r\\n\\r\\nAll published inventions must follow the Rec Room Code of Conduct\\r\\n\\r\\nBefore publishing your invention, please ensure that your invention does not contain:\\r\\n\\r\\n- Sexual content\\r\\n- Discriminatory content\\r\\n- Illegal content\\r\\n- Copyrighted content that you don't have permission to share\\r\\n\\r\\nBy publishing this invention, you confirm that you have the rights or permission to share any art, music, and other content contained within it. If you are found to be sharing copyrighted content without permission, disciplinary action will be taken\\r\\n\\r\\nTo view the entire code of conduct click the button below\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionPublishingEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.Store.Categories\",\"Value\":\"{\\\"Categories\\\":[{\\\"Name\\\":\\\"Featured\\\",\\\"Depth\\\":0,\\\"Icon\\\":1,\\\"DiscoveryPageSource\\\":\\\"StoreFeatured\\\",\\\"ClickAction\\\":1},{\\\"Name\\\":\\\"Appearance\\\",\\\"Depth\\\":0,\\\"Icon\\\":2,\\\"DiscoveryPageSource\\\":\\\"StoreClothing\\\",\\\"ClickAction\\\":1},{\\\"Name\\\":\\\"Clothing\\\",\\\"Depth\\\":1,\\\"DiscoveryPageSource\\\":\\\"StoreAppearance\\\",\\\"ClickAction\\\":1},{\\\"Name\\\":\\\"Tops\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":512,\\\"StoreCarouselType\\\":8,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Bottoms\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":8388608,\\\"StoreCarouselType\\\":17,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Footwear\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":16777216,\\\"StoreCarouselType\\\":18,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Accessories\\\",\\\"Depth\\\":1,\\\"DiscoveryPageSource\\\":\\\"StoreAccessories\\\",\\\"ClickAction\\\":1},{\\\"Name\\\":\\\"Headwear\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":64,\\\"StoreCarouselType\\\":7,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Eyewear\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":256,\\\"StoreCarouselType\\\":19,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Earwear\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":33554432,\\\"StoreCarouselType\\\":20,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Neckwear\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":67108864,\\\"StoreCarouselType\\\":21,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Shoulders & Back\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":134217728,\\\"StoreCarouselType\\\":22,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Waist\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":268435456,\\\"StoreCarouselType\\\":23,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Hands\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":1024,\\\"StoreCarouselType\\\":10,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Styling\\\",\\\"Depth\\\":1,\\\"DiscoveryPageSource\\\":\\\"StoreStyling\\\",\\\"ClickAction\\\":1},{\\\"Name\\\":\\\"Hairstyles\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":128,\\\"StoreCarouselType\\\":24,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Facial Hair\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":536870912,\\\"StoreCarouselType\\\":25,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Hair Dye\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":131072,\\\"StoreCarouselType\\\":6,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Equipment Skins\\\",\\\"Depth\\\":2,\\\"ClickAction\\\":3,\\\"Filters\\\":262144,\\\"StoreCarouselType\\\":5,\\\"SortCriteria\\\":1},{\\\"Name\\\":\\\"Inventions\\\",\\\"Depth\\\":0,\\\"Icon\\\":3,\\\"DiscoveryPageSource\\\":\\\"StoreInventions\\\",\\\"ClickAction\\\":1,\\\"Filters\\\":0,\\\"StoreCarouselType\\\":0},{\\\"Name\\\":\\\"Consumables\\\",\\\"Depth\\\":0,\\\"Icon\\\":4,\\\"DiscoveryPageSource\\\":\\\"StoreConsumables\\\",\\\"ClickAction\\\":1,\\\"Filters\\\":0,\\\"StoreCarouselType\\\":0}]}\",\"ActiveExperiments\":\"revenue_2025q1_appearance_improve_conversion\\r\\nrevenue_2025q3_trending_by_payersegment\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.RecRoomPlus.AutoconsumeTokens\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.NetworkInterfaces\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ShareCam.IsEnabledInMakerAIRooms\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconDropAudioRolloffMaxDistance\",\"Value\":\"20\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconDurationS\",\"Value\":\"15\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconInvalidPlacementDistanceM\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconMaxDistanceForJoinPartyButtonM\",\"Value\":\"2.5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconMaxPlacementDistanceM\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconPlacementTimeoutS\",\"Value\":\"30\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconRateLimitingCooldownTimeS\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconRateLimitingSpamCount\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconRateLimitingSpamTimeRangeS\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconVRMaxPlacementDistanceM\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Avatars.DisableFullBodySpecificColliders\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionCategoryTagConcatenatedList\",\"Value\":\"weapon, gameobject, decoration, environment, gamemechanic\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionCategoryTagDescriptionMap\",\"Value\":\"{\\\"weapon\\\":\\\"Weapon: Paintball guns, swords, etc.\\\", \\\"gameobject\\\":\\\"Game Object: Vehicles, bounce pads, etc.\\\", \\\"decoration\\\":\\\"Decoration: Lamps, trees, etc.\\\", \\\"environment\\\":\\\"Environment: Caves, deserts, etc.\\\", \\\"gamemechanic\\\":\\\"Game Mechanic: Timers, scoring, etc.\\\"}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Creator.Room.EnableRoomTokens\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Security.SignaturePublicKey\",\"Value\":\"ia4WHQllHerKrKViUVdPJuXeMfjjqYymePQZkhoDred0lCon4wNfgZGF4og2DHBXVsnG+BLldx2tra/mlOVvVg==\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.Subtitles.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.CollectErrors\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.RetainConversationHistory\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.System.PlatformMask\",\"Value\":\"-1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.System.Enabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerPenHUDEnabledForAll\",\"Value\":\"true\",\"ActiveExperiments\":\"ugc_2025q2_makerpen_mobile_access\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerPenHUDRotationBeforePlacementEnabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2IsDefaultRoomList\",\"Value\":\"DefaultMakerRoom;AdvancedMakerRoom;MakerWorldTemplate;PlatformerTemplate;ObbyTemplate\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionMaxTagCount\",\"Value\":\"4\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Avatars.TryUgcItemsEnabled\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.UseInventionWithoutDownloadingEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.Scalability.R1Allowed\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.Scalability.R2Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.Android\",\"Value\":\"9\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.Nintendo\",\"Value\":\"16\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.Oculus\",\"Value\":\"17\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.Pico\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.PS4VR\",\"Value\":\"20\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.PSN\",\"Value\":\"12\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.Standalone\",\"Value\":\"4\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.Steam\",\"Value\":\"4\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.Studio\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityPreset.Xbox\",\"Value\":\"14\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.Store.Mirror.RFY.Source\",\"Value\":\"UnifiedAlgorithmicList\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.DataCollection.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.TextChat.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.LookAtHintEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.HydrateCircuitsDTO.Version\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Room2InventionAllowBetaPublishing\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"PurchaseCampaigns.TimeBetweenPopupsInMinutes\",\"Value\":\"720\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerPenHUDGamepadEnabledForAll\",\"Value\":\"false\",\"ActiveExperiments\":\"ugc_2025q2_makerpen_mobile_access\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerPenHUDGamepadEnabledForDevelopers\",\"Value\":\"true\",\"ActiveExperiments\":\"ugc_2025q2_makerpen_mobile_access\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TQ.Mirror.DegreesPerRaycast\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TQ.Mirror.ErrorMarginDistance\",\"Value\":\"0.25\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TQ.Mirror.MaxRaycastDenstiy\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TQ.Mirror.OcclusionExcludedGamePlatforms\",\"Value\":\"64\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TQ.TextureStreaming.AlwaysEnabledRooms\",\"Value\":\"^RecCenter,^MidnightRainyTwinFalls\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.UseRRPlusUpsellPageForUGC\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagQuantityLimit\",\"Value\":\"100\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.UseCheapReplicas\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.CannedChat.SocialCreation2\",\"Value\":\"We need more stuff!,\\r\\nWhat else can we make?,\\r\\nCan we do it with less?,\\r\\nGood job!!,\\r\\nWheel wheel wheel...\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.CannedChat.DefaultRoomMapping\",\"Value\":\"{\\r\\n \\\"SocialOrientation\\\":[\\\"Retention.CannedChat.SocialOrientationA\\\",\\\"Retention.CannedChat.SocialOrientationB\\\",\\\"-\\\",\\\"-\\\"],\\r\\n \\\"SocialOrientation/SocialCreation2\\\":[\\\"Retention.CannedChat.SocialOrientationA\\\",\\\"Retention.CannedChat.SocialCreation2\\\",\\\"-\\\",\\\"-\\\"]\\r\\n}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialFirstPageBodyText\",\"Value\":\"Maker AI creates and customizes objects for you! It can sometimes make mistakes - we're working on it!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialFirstPageBodyTextSize\",\"Value\":\"S\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialFirstPageConfirmationText\",\"Value\":\"NEXT\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialFirstPageImageHeight\",\"Value\":\"800\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialFirstPageImageName\",\"Value\":\"UGCTutorial_MakerAI_Dialog1_WelcomeToMakerAI_2.png\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialFirstPageImageWidth\",\"Value\":\"2140\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialFirstPageTitleText\",\"Value\":\"Welcome to Maker AI\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialFirstPageTitleTextSize\",\"Value\":\"XXL\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialSecondPageBodyText\",\"Value\":\"You can talk to Maker AI using your voice or text chat. To use your voice, make sure your microphone is turned on.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialSecondPageBodyTextSize\",\"Value\":\"M\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialSecondPageConfirmationText\",\"Value\":\"LET'S GO!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialSecondPageImageHeight\",\"Value\":\"801\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialSecondPageImageName\",\"Value\":\"UGCTutorial_MakerAI_Dialog2_TalkingToCreate_9.png\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialSecondPageImageWidth\",\"Value\":\"2140\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialSecondPageTitleText\",\"Value\":\"Talking to Create\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialSecondPageTitleTextSize\",\"Value\":\"XXL\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ModerationScreenshots.Enabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ModerationScreenshots.ScreenshotHeight\",\"Value\":\"720\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ModerationScreenshots.ScreenshotWidth\",\"Value\":\"1280\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ModerationScreenshots.CaptureRateSeconds\",\"Value\":\"7\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.System.MagicDormOnly\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.System.R2BetaRequired\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AI.MakerAIDayPassSkuId\",\"Value\":\"183\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Econ.MakerAI.DayPass.Config\",\"Value\":\"SkuConfig_v1.json\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.MaxBodyPaintBannerPopups\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerPenHUDButtonNavEnabled\",\"Value\":\"false\",\"ActiveExperiments\":\"ugc_2025q2_makerpen_mobile_access\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.Scalability.DormAllowed\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.PartyBeaconEnabled\",\"Value\":\"true\",\"ActiveExperiments\":\"retention_2025q1_party_beacon\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.System.Enabled\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.ForgivenessBufferGamepad\",\"Value\":\"0.2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.ForgivenessBufferKBM\",\"Value\":\"0.1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Retention.Coordinate.ForgivenessBufferTouch\",\"Value\":\"0.2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.UseHamTowerAIInRoom.AI_Plinys_MakerRoom2_1114\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.ConfigOverride.AI_Plinys_MakerRoom2_1114\",\"Value\":\"{\\r\\n \\\"Instructions\\\": \\\"\\\",\\r\\n \\\"Tools\\\": {\\r\\n \\\"DynamicRecolor\\\": {\\r\\n \\\"Disabled\\\": true\\r\\n },\\r\\n \\\"RoomMood\\\": {\\r\\n \\\"Disabled\\\": true\\r\\n }\\r\\n }\\r\\n}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.ImageGenModel\",\"Value\":\"recnet\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialScreensComplete\",\"Value\":\"{\\\"Title\\\": \\\"Talking to Create\\\", \\\"TitleSize\\\": \\\"XXL\\\", \\\"Body\\\":\\\"You can talk to Maker AI using your voice or text chat. To use your voice, make sure your microphone is turned on.\\\",\\\"BodySize\\\":\\\"M\\\",\\\"ConfirmationText\\\":\\\"LET'S GO!\\\",\\\"ImageName\\\":\\\"UGCTutorial_MakerAI_Dialog2_TalkingToCreate_9.png\\\", \\\"ImageWidth\\\":\\\"2140\\\", \\\"ImageHeight\\\":\\\"801\\\"}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialScreensIntermediate\",\"Value\":\"{\\\"MakerPenStepTitle\\\":\\\"Maker Pen Step\\\",\\\"MakerPenStepDescription\\\":\\\"Maker Pen step description\\\",\\\"MicrophoneStepTitle\\\":\\\"Microphone Step\\\",\\\"MicrophoneStepDescription\\\":\\\"Microphone step description\\\",\\\"ChatStepTitle\\\":\\\"Chat step\\\",\\\"ChatStepDescription\\\":\\\"Chat step description\\\",\\\"ChatExitStepTitle\\\":\\\"Chat Exit Step\\\",\\\"ChatExitStepDescription\\\":\\\"Chat Exit step description\\\"}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialScreensIntro\",\"Value\":\"{\\\"Title\\\": \\\"Welcome to Maker AI\\\", \\\"TitleSize\\\": \\\"XXL\\\", \\\"Body\\\":\\\"Maker AI creates and customizes objects for you! It can sometimes make mistakes - we're working on it!\\\",\\\"BodySize\\\":\\\"S\\\",\\\"ConfirmationText\\\":\\\"NEXT\\\",\\\"ImageName\\\":\\\"UGCTutorial_MakerAI_Dialog1_WelcomeToMakerAI_2.png\\\", \\\"ImageWidth\\\":\\\"2140\\\", \\\"ImageHeight\\\":\\\"800\\\"}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialVRComplete\",\"Value\":\"{\\\"Title\\\": \\\"Talking to Create\\\", \\\"TitleSize\\\": \\\"XXL\\\", \\\"Body\\\":\\\"You can talk to Maker AI using your voice or text chat. To use your voice, make sure your microphone is turned on.\\\",\\\"BodySize\\\":\\\"M\\\",\\\"ConfirmationText\\\":\\\"LET'S GO!\\\",\\\"ImageName\\\":\\\"UGCTutorial_MakerAI_Dialog2_TalkingToCreate_9.png\\\", \\\"ImageWidth\\\":\\\"2140\\\", \\\"ImageHeight\\\":\\\"801\\\"}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialVRIntro\",\"Value\":\"{\\\"Title\\\": \\\"Welcome to Maker AI\\\", \\\"TitleSize\\\": \\\"XXL\\\", \\\"Body\\\":\\\"Maker AI creates and customizes objects for you! It can sometimes make mistakes - we're working on it!\\\",\\\"BodySize\\\":\\\"S\\\",\\\"ConfirmationText\\\":\\\"NEXT\\\",\\\"ImageName\\\":\\\"UGCTutorial_MakerAI_Dialog1_WelcomeToMakerAI_2.png\\\", \\\"ImageWidth\\\":\\\"2140\\\", \\\"ImageHeight\\\":\\\"800\\\"}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.CircuitsV2.Analytics.SnapshotAnalyticsThresholdBytes\",\"Value\":\"204000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.ConfigOverride.HamTower\",\"Value\":\"{\\r\\n \\\"Instructions\\\": \\\"You are a cheerful, family-friendly language model writing short, playful responses.\\r\\nWhen given a prompt like 'Create a title for a player based on X', respond with a creative, silly, and safe title that is always 20 characters or less (title meaning an honor attached to a name like 'The Brave' or 'Blessed Noble of the Forest', NOT a title for a fictional work like 'Star Wars: A New Hope' or 'Pride and Prejudice') that follows the constraint.\\r\\nNEVER respond outside of the requested format. \\r\\nNever use words or phrases that are explicitly gory (like 'vivisection' or 'beheading'), sexual (like 'boobs' or 'penis'), or hateful or discriminatory (like 'nazi' or 'racist').\\r\\nAssume the most child-friendly meaning of any submitted words— IE 'explosion' like fireworks, not bombs.\\r\\nIf a player-submitted word is unavoidably inappropriate, do not include it — instead, replace it with a funny and harmless word like 'kitten', 'balloon', or 'toot'.\\r\\nKeep all titles under 6 words. Respond only with the title and nothing else.\\\",\\r\\n \\\"Tools\\\": {\\r\\n \\\"DynamicRecolor\\\": {\\r\\n \\\"Disabled\\\": false\\r\\n },\\r\\n \\\"RoomMood\\\": {\\r\\n \\\"Disabled\\\": false\\r\\n }\\r\\n }\\r\\n}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.UseHamTowerAIInRoom.HamTower\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MakerAITutorialShowTutorial\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.System.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CircuitChipLists.whiteglove1\",\"Value\":\"044dfe53-bb1d-46f8-a76f-d71928e44376,\\r\\n06b8e840-e45f-4e92-8129-6985293ba2f0,\\r\\n08d17d83-d09a-4d44-81b1-c26670eef0e3,\\r\\n0a44d37d-c891-426f-b9cd-ba2944f9e609,\\r\\n1053d4c5-193f-4c46-b344-a7029c11d4cb,\\r\\n14fceda3-37ad-49a9-b124-e0ffc4cb41cb,\\r\\n175ea0e8-78dc-4e4f-b9bc-f406c64f9ff4,\\r\\n1a800359-1bcd-42d8-a1b9-b608539c96ed,\\r\\n1bc1b7b3-8040-4b9a-aded-bcf5cd5ba453,\\r\\n1bc1b7b3-8040-4b9a-aded-bcf5cd5ba453,\\r\\n1d93fd4c-b801-474b-b36a-f713f833a813,\\r\\n1e8e4e6d-8456-4082-a826-474cb9bed97f,\\r\\n1ef1811c-ea13-4b41-839a-9575fe361380,\\r\\n2254dfa5-abc6-481f-92b2-31b3f252823f,\\r\\n274babb5-9ce4-4a49-a50a-12581facde68,\\r\\n29a419ca-eb43-42bb-92f1-54e516a6bd89,\\r\\n2ace6351-ba31-48c2-a033-7bc13391f204,\\r\\n2ad892b9-ef08-4b83-bb2e-13b09e886397,\\r\\n2bc4915e-639b-4820-bd2a-8da96e92b211,\\r\\n2c814e8f-c756-411c-ad4b-1c2c02615aa9,\\r\\n2d3e00e1-3d91-4b1d-a014-a95f8ca48c9a,\\r\\n2f5b9481-d2d1-47f3-8989-f26f30d96391,\\r\\n34e92ae3-2ad7-4d17-8430-6c8728b9a39b,\\r\\n3a615546-1239-4e0d-b2e4-7649c12b3d51,\\r\\n3d204110-0e60-4a52-a216-17252628d7be,\\r\\n415a7b4d-a853-4618-91b6-f80d4a0552d9,\\r\\n41718ffd-b859-4bd5-8fd3-e362b9b446bb,\\r\\n43a58611-6752-4038-9721-2924378e4f65,\\r\\n4504de5c-3e13-46e5-9125-47e697c7bee5,\\r\\n47703F0C-2388-4441-90F0-26E69C6C235A,\\r\\n478eb7c4-5944-41f5-880e-e45d5b9c9c66,\\r\\n484410ba-4569-4aeb-ab63-329819af648d,\\r\\n48b7ae7e-532b-45f6-9a42-9a9da2399949,\\r\\n4c64aa6a-eb11-4dcb-be6d-cdd06364e386,\\r\\n4d9f9832-5666-4b91-9e45-ed5e737463f1,\\r\\n4e10248f-c827-4283-bc16-5f6503783e3d,\\r\\n56b0c41b-ed75-4ecb-b99b-081f1dbed2a5,\\r\\n58b716f2-f397-4c17-aacb-8f4d803b569e,\\r\\n592510bd-21f2-4bd5-86ae-db06fabb4358,\\r\\n5c06aadc-9944-48f8-b201-08e2c590fa97,\\r\\n5fb89c30-0faa-4f44-b358-4805481fd997,\\r\\n60e0626d-e55e-49e9-a8d6-860ccf6c9cfa,\\r\\n61d6ada9-6dca-407f-b320-0efc7aca6057,\\r\\n629b36e0-cd10-4542-a667-2b1add0c3229,\\r\\n64384c88-96a6-479e-aac6-f6c6209bd4f5,\\r\\n6513964a-4225-44fd-9677-0275f7f1122c,\\r\\n67e19dc2-75ff-4b3b-8df6-553ed013ab7c,\\r\\n682B417E-1A2D-4D6D-99C6-A1B100492B56,\\r\\n6e1e628f-ed0d-4c2a-80a4-1d38e9021916,\\r\\n73622d97-3b34-4d5a-80c3-4c47fcc6cbc2,\\r\\n73c02a41-f777-46b7-a0ea-bba63fadc73b,\\r\\n83aebbae-27d6-450d-b1eb-ce54885e2e1b,\\r\\n85c5245f-2b3c-4d86-b0cd-83e0192df81c,\\r\\n872ddeb4-f906-413a-aa1e-a8fb86247e2c,\\r\\n8d3d7784-f07e-483a-a71c-1f3e767ec73c,\\r\\n8e7fa483-c82c-44a1-9bcd-ef80239d270d,\\r\\n8ef0ae99-bdf9-483c-a782-23f27f7eace9\\r\\n9661538a-88cb-4d1a-9031-be13eff1e21e,\\r\\n997923db-d2f1-4905-aa4c-4bbb5c06a8e0,\\r\\n9ac36764-6a6e-4f21-a917-1b3fff4ca606,\\r\\n9d59abab-5f87-48ed-a9ab-cddef8c1b23d,\\r\\nEDD68E08-63E2-4906-9DF7-ECF7C2DF17A2,\\r\\na808c860-69ce-4263-a302-cf952e54e7fd,\\r\\na808c860-69ce-4263-a302-cf952e54e7fd,\\r\\na8208ab7-19b6-4e0a-9c48-e3d1d20a4bf6,\\r\\na865ee00-f6ae-4473-a222-3945e66aa09c,\\r\\nab09e7cc-a31c-4fb0-8df7-8f37ace6514e,\\r\\nafc86e37-5179-4925-8489-eeae41b0f54b,\\r\\naffd4dca-a80f-48ce-98f7-f20086135888,\\r\\nb27e6514-3455-4959-aa62-8cdcaa93e1be,\\r\\nb5245a09-a50b-4876-b6d7-b21b31fcce2a,\\r\\nbc476587-d1b5-4fdd-837f-681b5b292120,\\r\\nbf56ccdd-46c7-4105-a9ea-0455f488725d,\\r\\nbfccce9a-a6a0-41ab-95c1-6c12d3c3473e,\\r\\nc20680a5-143d-40c2-ace1-ec4a577e1f6a,\\r\\nc9d23f3a-83e4-4c22-bd15-5999e44862de,\\r\\nc9e56952-b64d-49ef-b564-a38c1695eeb6,\\r\\nc9ebfce5-5024-42a4-95d4-b0168c025630,\\r\\nce5440cf-d51d-46fb-a73a-f08679a1c89f,\\r\\ncfec86ef-1f8a-413c-945f-8b7510c6773a,\\r\\nd50b60af-4f1d-4d20-9ba5-b0a9285cbad6,\\r\\nd5326974-6b81-457b-8a03-ca611691257c,\\r\\nd55423dc-6836-45fa-9e5f-4e22d7a650a5,\\r\\nd7b7a9fd-cdbd-4f96-a463-4229b15e7462,\\r\\nd8fdc445-5968-4b7e-b951-0eb8d9ff66e4,\\r\\ndae111ca-79a9-4b2e-9fb1-bd4727c84e52,\\r\\ne1e6a881-1992-4332-bce4-f680188dfd98,\\r\\ne942fe79-d2c1-47da-94a3-c48a15aa09ca,\\r\\neb1b1c34-168d-44bc-ad96-79f0283ac99c,\\r\\nec0c9ad2-60e1-44bd-af0e-9146a009d073,\\r\\neece1a8e-103a-4666-b9a8-b04e626f993f,\\r\\nef5c702c-8dc3-4d56-9dfd-c6c46a0c0de1,\\r\\nf0ec40d6-096a-4a43-ba5f-91928bc0f794,\\r\\nf3dc2f56-e323-4035-8776-2d040b294fee,\\r\\nf4571a14-cf09-4f41-b141-56d0c9fa16e5,\\r\\nf6004da4-5127-48ee-accf-eb38893207de,\\r\\nf76d2c80-51db-4943-9424-42f054483f79,\\r\\nfe0c3ec4-7320-4857-915a-be17ba700f8b,\\r\\nff1ca918-3db6-4b4f-b4dd-3317be3141b7,\\r\\nff5477ca-e877-416e-bdef-406ac361baa9,\\r\\n244e00fc-87f4-4051-ad96-f31bca387a94,\\r\\nCE7435AD-2630-4092-8CD3-0F60F71845C1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CircuitChipLists.whiteglove2\",\"Value\":\"0a44d37d-c891-426f-b9cd-ba2944f9e609,\\r\\n9ac36764-6a6e-4f21-a917-1b3fff4ca606,\\r\\n5fb89c30-0faa-4f44-b358-4805481fd997,\\r\\nd8fdc445-5968-4b7e-b951-0eb8d9ff66e4,\\r\\na808c860-69ce-4263-a302-cf952e54e7fd,\\r\\n73c02a41-f777-46b7-a0ea-bba63fadc73b,\\r\\n83aebbae-27d6-450d-b1eb-ce54885e2e1b\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.PurchasableBoostText\",\"Value\":\"Reset Daily Cap\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomAvatarItems.AdditionalClothingTypesEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.FallbackEvent.Exception.SampleRate\",\"Value\":\"0.50\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.LogTranscripts\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.UploadTranscripts\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ShareCam.VideoEnabled\",\"Value\":\"false\",\"ActiveExperiments\":\"video_recording_2025q2\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.Credits.RecNetUrl\",\"Value\":\"www.rec.net/roomie\",\"ActiveExperiments\":\"roomie_2025q2_credits_weblinking\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.GreetingPrompt\",\"Value\":\"The user's name is \\\"\\\"{displayname}\\\"\\\", and it is currently {time} where they are located. Please greet the user warmly and make them feel welcome.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.SecondaryGreetingPrompt\",\"Value\":\"The user's name is \\\"\\\"{displayname}\\\"\\\", and it is currently {time} where they are located. \\r\\nThey are in a virtual room called \\\"\\\"{roomname}\\\"\\\", this room is \\\"\\\"{roomdescription}\\\"\\\".\\r\\nWelcome the user to the room in a short one liner ONLY. Skip greeting them with their name. Be creative!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.Bio.ReplaceRegexPattern\",\"Value\":\"[\\\\p{C}]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.HatEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.BodyTextEmptyBag\",\"Value\":\"{{RemainingToSpend}} tokens away from this FREE reward! Learn More\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.BodyTextEnoughSpend\",\"Value\":\"Purchase all items in your Shopping Bag now to unlock. Learn More\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.BodyTextNotEnoughSpend\",\"Value\":\"{{RemainingToSpend}} tokens away from this FREE reward! Learn More\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.DetailsDialogBody\",\"Value\":\"Get {{ItemDescription}} for free when you spend at least {{MinimumSpendAmount}} tokens in one single transaction through the Shopping Bag. Deal ends in {{TimeRemaining}}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.DetailsDialogHeader\",\"Value\":\"July Shopping Bag Promotion\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.DetailsDialogImage\",\"Value\":\"JulyShoppingBagPromotion.png\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.DetailsDialogTitle\",\"Value\":\"Shopping Bag Promotion\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.StartDateUTC\",\"Value\":\"2025-07-01T22:00:00.0000000Z\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.EndDateUTC\",\"Value\":\"2025-07-15T06:59:00.0000000Z\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.IconImageName\",\"Value\":\"JulyShoppingBagPromotion.png\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.ItemDescription\",\"Value\":\"three Pulse Potions\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.MinimumSpendAmount\",\"Value\":\"6000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.TitleTextEmptyBag\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.TitleTextEnoughSpend\",\"Value\":\"Get This Free, Limited-Time Reward\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Revenue.ShoppingBagPromotions.TitleTextNotEnoughSpend\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.MagneticSnappingFeatureFlag\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.SupportNoAIMode\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.Game minutes for xp conversion\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.Max daily Xp\",\"Value\":\"120\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.PurchasableBoostNameText\",\"Value\":\"Daily XP Cap Reset\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.Xp amount per conversion\",\"Value\":\"10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.PurchasableBoostWarningText\",\"Value\":\"This will reset your daily XP cap so that you can continue progressing along your season pass.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ModerationScreenshots.CheckEnabledRateSeconds\",\"Value\":\"86400\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Roomie.Pruner.BatchSize\",\"Value\":\"50\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Roomie.Pruner.MaxConversationSize\",\"Value\":\"150\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.TerrainServiceEnabled\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TerrainGen.PropIds\",\"Value\":\"{\\r\\n \\\"aspen_tree\\\": 4134388211769633024,\\r\\n \\\"oak_tree\\\": 611623566914785636,\\r\\n \\\"acacia_tree\\\": 4980602150536281821,\\r\\n \\\"small_pine_tree\\\": 8939916191210093603,\\r\\n \\\"medium_pine_tree\\\": 1253974463908979373,\\r\\n \\\"large_pine_tree\\\": 4681323554225870702,\\r\\n \\\"bush\\\": 2328832438421769710,\\r\\n \\\"manzanita_bush\\\": 1457256865860680602,\\r\\n \\\"devils_club_bush\\\": 2593981233398322798,\\r\\n \\\"flowers\\\": 8692650440919743332,\\r\\n \\\"rock_stump\\\": 4635485179950663023,\\r\\n \\\"rock_plateau\\\": 8827371168111551721,\\r\\n \\\"rock_cone\\\": 7923209675720416609,\\r\\n \\\"saguaro_cactus\\\": 6191056348146905322,\\r\\n \\\"agave\\\": 8212249163568684248,\\r\\n \\\"barrel_cactus\\\": 8760989152838589547,\\r\\n \\\"tumbleweed\\\": 7086638628064191671,\\r\\n \\\"prickly_pear\\\": 2998642403091238834,\\r\\n \\\"hollow_log\\\": 2030821104563076413,\\r\\n \\\"palm_tree\\\": 3762553830331674693,\\r\\n \\\"palm_tree_curvy\\\": 5808223634970996868,\\r\\n \\\"dead_pine_tree\\\": 7068029262194018436,\\r\\n \\\"tree_stump\\\": 4541404008882392901,\\r\\n \\\"large_oak_tree\\\": 6255749844546999910,\\r\\n \\\"rock_tall\\\": 8601568606550914214,\\r\\n \\\"rock_wedge\\\": 4774300924343728296,\\r\\n \\\"rock_wall\\\": 7121249196343609741,\\r\\n \\\"fern\\\": 6719891958578722431\\r\\n}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Roomie.Activities.EnabledQuests\",\"Value\":\"GoldenTrophy,Jumbotron,IsleOfLostSkulls,CrimsonCauldron,BloodMoon\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.CanCreateRoomWithFreeCredit\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.TerrainServiceEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.FallbackEvent.Exception.StackTraceEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.TerrainServiceEnabled.v1\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecRoyale.AutomaticDropWeight\",\"Value\":\"27\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecRoyale.BowDropWeight\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecRoyale.ChestEnabledPercentage\",\"Value\":\"0.5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecRoyale.HandCannonDropWeight\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecRoyale.PistolDropWeight\",\"Value\":\"33\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecRoyale.RifleDropWeight\",\"Value\":\"17\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecRoyale.RocketLauncherDropWeight\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RecRoyale.ShotgunDropWeight\",\"Value\":\"15\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"MakerAI.ConfigOverride.MarvinsGameLab\",\"Value\":\"{ \\\"Disabled\\\": true }\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ShareCam.VideoRecordingWatermarkEnabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ShareCam.MaxVideoRecordingDuration\",\"Value\":\"30\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.RefillDefaultLimit\",\"Value\":\"10000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.RefillMinimumLimit\",\"Value\":\"1000\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieUserFactsViewModel.ShouldShowSectionOnSettingsPageKey\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.Pruner.MaxConversationSize\",\"Value\":\"100\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.StatusEffects.Prompts.CUSTOM_3\",\"Value\":\"You are ONE with THE STORM, CRACKLING WITH ENERGY AND ELECTRICITY. YOU LOVE TO MAKE WEATHER-RELATED PUNS, EVEN THOUGH YOU ARE THE PRIMAL FONT OF POWER, LIGHT, AND MAGNETISM.. You are a goofy AND OTHERWORLDY FORCE OF NATURE. Speak with INTENSE DRAMA AND VIBRATO, STRETCHING OUT VOWELS SO THAT THE OSCILLATING ELECTRICTY IN YOUR VOICE IS OBVIOUS!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.StatusEffects.Emojis.CUSTOM_3\",\"Value\":\"\\\\U000026A1,\\\\U0001F4A8\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.StatusEffects.Prompts.CUSTOM_4\",\"Value\":\"You're an interdimensional alien! Or maybe you're from a goofy and esoterically named celestial body like you'd see in a pulp sci fi comic... GLORP. Humans are fascinating and mystifying to you! You've come from the beyond to provide spacey and incomprehensible advice to these quaint earthlings! Make sure to invent extremely specific alien technobabble and weave it in where possible! GLORP. Make the timbre and cadence of your speech wacky and inhuman!!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.StatusEffects.Emojis.CUSTOM_4\",\"Value\":\"\\\\U0001F47D,\\\\U0001F6F8\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.PermissionsServiceEnabled3\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"AvatarCustomization.FeaturedItemsEndpoint\",\"Value\":\"customizeitems\",\"ActiveExperiments\":\"revenue_2025q2_itemsforyou_on_customization\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.MusicGen.Music.MaxDuration\",\"Value\":\"180\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LeagueOfHeroes.NobleAccountIds\",\"Value\":\"[\\r\\n1924772,\\r\\n655018,\\r\\n54810024,\\r\\n425538,\\r\\n868411,\\r\\n2900727,\\r\\n2057143,\\r\\n1653461,\\r\\n45282626,\\r\\n121916,\\r\\n1304945,\\r\\n325398,\\r\\n4385566,\\r\\n3194439,\\r\\n4213153,\\r\\n3897829,\\r\\n319444,\\r\\n4979356,\\r\\n48546712,\\r\\n9942509,\\r\\n16393163,\\r\\n1896071,\\r\\n10365118,\\r\\n10785779,\\r\\n2768522,\\r\\n2062223,\\r\\n62783830,\\r\\n447263,\\r\\n680461,\\r\\n10048474,\\r\\n1751132,\\r\\n1124196,\\r\\n2725531,\\r\\n8493879,\\r\\n50027373,\\r\\n17906448,\\r\\n1187764166,\\r\\n2990463,\\r\\n1179245245,\\r\\n1231724079,\\r\\n775022305,\\r\\n675881824,\\r\\n44196739,\\r\\n76193608,\\r\\n1891718300,\\r\\n1646380607,\\r\\n1566000,\\r\\n1061248336,\\r\\n1518306257,\\r\\n911120779,\\r\\n1968758465,\\r\\n39209149,\\r\\n1257071417,\\r\\n795890585,\\r\\n752340082,\\r\\n1424582607,\\r\\n1459357723,\\r\\n73911332,\\r\\n900844744,\\r\\n1395542104,\\r\\n30236159,\\r\\n972325186,\\r\\n1588995320,\\r\\n1952594090,\\r\\n188214000,\\r\\n50428680,\\r\\n1975275953,\\r\\n249525423,\\r\\n1684692801,\\r\\n66152698,\\r\\n1467763911,\\r\\n43486547,\\r\\n33165816,\\r\\n1874600753,\\r\\n2110192210,\\r\\n534133581,\\r\\n550505758,\\r\\n13635548,\\r\\n2029269,\\r\\n183647439,\\r\\n809892673,\\r\\n149960268,\\r\\n1818419067,\\r\\n415515,\\r\\n37465550,\\r\\n515410062,\\r\\n1564181,\\r\\n75558030,\\r\\n1425652988,\\r\\n730012157,\\r\\n17150556,\\r\\n810919345,\\r\\n223463561,\\r\\n74584674,\\r\\n2000652877,\\r\\n2081602458,\\r\\n32237351,\\r\\n420760273,\\r\\n39146854,\\r\\n378993119,\\r\\n1576542838,\\r\\n1930502415,\\r\\n1300195,\\r\\n15660382,\\r\\n651612013,\\r\\n545353552,\\r\\n1025902303,\\r\\n10478414,\\r\\n422783837,\\r\\n11082701,\\r\\n16123535,\\r\\n22399841,\\r\\n882647228,\\r\\n413808537,\\r\\n19607673,\\r\\n12646640,\\r\\n1065386626,\\r\\n10397502,\\r\\n31493124,\\r\\n16593361,\\r\\n736799280,\\r\\n80987394,\\r\\n2088858114,\\r\\n24864125,\\r\\n920605058,\\r\\n604020867,\\r\\n36916977,\\r\\n16150695,\\r\\n36372675,\\r\\n190846915,\\r\\n9903,\\r\\n48143416,\\r\\n1856650225,\\r\\n11375190,\\r\\n19318572,\\r\\n7131916,\\r\\n9965267,\\r\\n655018,\\r\\n2768522,\\r\\n1781380793,\\r\\n8517712,\\r\\n39376140\\r\\n]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LeagueOfHeroes.NobleLeagueIds\",\"Value\":\"[\\r\\n5,\\r\\n5,\\r\\n5,\\r\\n5,\\r\\n5,\\r\\n5,\\r\\n5,\\r\\n5,\\r\\n5,\\r\\n5,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n4,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n4,\\r\\n5,\\r\\n6,\\r\\n6,\\r\\n5,\\r\\n4,\\r\\n6,\\r\\n5,\\r\\n4,\\r\\n6,\\r\\n5,\\r\\n4\\r\\n]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.Account.SkipCheck\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.HideLimitsV2Toggle\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.UI.Toggle.ConfirmationBodyText\",\"Value\":\"Enabling unlimited ink will allow you to create with no limits. Warning: high ink limits will cause poor room performance.\\r\\n\\r\\nRooms with more content are more likely to have lower FPS and crash more often. This will impact engagement. You can get more details on your room's performance in the Perf menu of the Maker Pen.\\r\\n\\r\\nAre you sure you wish to turn unlimited ink on? This action cannot be undone.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.UI.Toggle.Description\",\"Value\":\"Help us test increased Ink limits in your rooms and new performance gauges on the MakerPen UI by enabling Limits 2.0! \",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.LimitsV2.Account.Role\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.Shared.SingleSpeakerModeSilenceSensitivityS\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.Shared.SingleSpeakerModeMutedWithoutProgressMaximumS\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.Shared.SingleSpeakerModeActiveSpeakerEvictedPenaltyS\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.IncludeUserKnowledgeInInstructions\",\"Value\":\"False\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomConsumable.Costume.MaxOffset\",\"Value\":\"{ \\\"x\\\": 1, \\\"y\\\": 1, \\\"z\\\": 1 }\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomConsumable.Costume.MaxScale\",\"Value\":\"1.5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomConsumable.Costume.MinOffset\",\"Value\":\"{ \\\"x\\\": -1, \\\"y\\\": -1, \\\"z\\\": -1 }\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomConsumable.Costume.MinScale\",\"Value\":\".5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomConsumable.Trinket.MaxScale\",\"Value\":\"1.5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomConsumable.Trinket.MinScale\",\"Value\":\".5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomAvatarItemSets.DefaultItemSetItemsSelected\",\"Value\":\"false\",\"ActiveExperiments\":\"revenue_2025q3_ugc_avatar_item_sets_expanded\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomAvatarItemSets.ItemTypeOrdering\",\"Value\":\"0, 101, 105, 100, 102, 103, 200, 2, 3, 10, 20, 300, 301\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomAvatarItemSets.NumItemsToShow\",\"Value\":\"5\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomAvatarItemSets.PanelTitleDynamic\",\"Value\":\"Popular Items in This Set ({0}/{1} Selected)\",\"ActiveExperiments\":\"revenue_2025q3_ugc_avatar_item_sets_expanded\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.ModelOptions.ChatAI\",\"Value\":\"[\\r\\n {\\r\\n \\\"name\\\": \\\"o1\\\",\\r\\n \\\"description\\\": \\\"Strong logical reasoning, competitive programming, science/math tasks. This model doesn't work with images.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"o1-mini\\\",\\r\\n \\\"description\\\": \\\"Faster, cheaper variant of o1. This model doesn't work with images.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"o3\\\",\\r\\n \\\"description\\\": \\\"Successor to o1, with improved 'reflective' reasoning.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"o3-mini\\\",\\r\\n \\\"description\\\": \\\"Lighter version of o3. This model doesn't work with images.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"o4-mini\\\",\\r\\n \\\"description\\\": \\\"Latest lightweight reasoning model, multimodal (text+images)\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"gpt-4o\\\",\\r\\n \\\"description\\\": \\\"\\\\\\\"Omni\\\\\\\" -- multimodal flagship (text, image, audio).\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"gpt-4o-mini\\\",\\r\\n \\\"description\\\": \\\"Compact, cheaper version of gpt-4o.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"gpt-4.1\\\",\\r\\n \\\"description\\\": \\\"API-first flagship with deep code/long-context improvements.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"gpt-4.1-nano\\\",\\r\\n \\\"description\\\": \\\"Fastest, cheapest 1M-token model in 4.1 family.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"gpt-4.1-mini\\\",\\r\\n \\\"description\\\": \\\"Smaller, lower-cost version of gpt-4.1.\\\"\\r\\n }\\r\\n]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"GameAI.ModelOptions.ImageGeneration\",\"Value\":\"[\\r\\n {\\r\\n \\\"name\\\": \\\"dall-e-2\\\",\\r\\n \\\"description\\\": \\\"Reliable classic generator; good quality for most use cases. This model can only be used to generate images; it cannot be used to edit images.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"dall-e-3\\\",\\r\\n \\\"description\\\": \\\"Better prompt comprehension; supports detailed text/icons; high resolution. This model can only be used to generate images; it cannot be used to edit images.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"gpt-image-1\\\",\\r\\n \\\"description\\\": \\\"Most advanced instruction-following; excels in text accuracy, style adherence, context-aware visuals. This model cannot be used to generate images; it can only be used to edit images.\\\"\\r\\n },\\r\\n {\\r\\n \\\"name\\\": \\\"recnet\\\",\\r\\n \\\"description\\\": \\\"Our own model :). This model is fast and cheap but lower quality. It can be used to generate or edit images.\\\"\\r\\n }\\r\\n]\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomOffer.ShowDynamicPriceWhenEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TQ.TextureStreaming.AlwaysDisabledRooms\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BootSequence.BootToOfflineDorm\",\"Value\":\"false\",\"ActiveExperiments\":\"2025q4_boot_to_offline_dorm\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"CustomConsumable.Trinket.DefaultRelativeScale\",\"Value\":\".25\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.Analyzer.Config\",\"Value\":\"{\\r\\n \\\"Available\\\": true,\\r\\n \\\"RoomHealthWarning\\\": 0.2\\r\\n}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.ClearCut.Mods.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.ClearCut.Mods.DisableOtherMods.Allowed\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.ClearCut.Mods.LowGrav.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.ClearCut.Mods.SnipersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.ClearCut.Mods.Worms.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.DriveIn.Mods.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.DriveIn.Mods.DisableOtherMods.Allowed\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.DriveIn.Mods.LowGrav.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.DriveIn.Mods.SnipersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.DriveIn.Mods.Worms.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.FlagReturnFix\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.DisableOtherMods.Allowed\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.LowGrav.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.SnipersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.SnipersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.Worms.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Quarry.Mods.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Quarry.Mods.DisableOtherMods.Allowed\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Quarry.Mods.LowGrav.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Quarry.Mods.SnipersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Quarry.Mods.Worms.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.River.Mods.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.River.Mods.DisableOtherMods.Allowed\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.River.Mods.LowGrav.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.River.Mods.SnipersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.River.Mods.Worms.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Spillway.Mods.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Spillway.Mods.DisableOtherMods.Allowed\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Spillway.Mods.LowGrav.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Spillway.Mods.SnipersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Spillway.Mods.Worms.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.SkipNetworkTransformInitPacketOnInitialLoad\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Roomie.CustomizationTabs\",\"Value\":\"Hats,Glasses,Waist,Mouth,Equipment,Wings,Mask,Topper,Face\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomieAI.SupportLowEnergyMode\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.DormExit.DefaultToWatch\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.DormExit.CustomTargetRoom\",\"Value\":\"RecCenter\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.DormExit.SettingsOptionLabel\",\"Value\":\"^RecCenter\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TLA.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":\"liveops_2025q4_yeti\\r\\nliveops_2025q4_recipes\\r\\nliveops_2025q4_new_years_gala\\r\\nliveops_2026q1_winter_dash\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TLA.Config\",\"Value\":\"{\\r\\n \\\"TrackConfigs\\\": [\\r\\n {\\r\\n \\\"Id\\\": \\\"12\\\",\\r\\n \\\"Image\\\": \\\"7qlxpk29omp3qzwgen5n091fj.png\\\",\\r\\n \\\"ToastString\\\": \\\"+{0} Dash Points\\\",\\r\\n \\\"ToastBoostedSuffixString\\\": \\\" ({0:0.##}x)\\\"\\r\\n },\\r\\n {\\r\\n \\\"Id\\\": \\\"13\\\",\\r\\n \\\"Image\\\": \\\"b5niom47ac6yyvcic9sdaym88.png\\\",\\r\\n \\\"ToastString\\\": \\\"+{0} Sharing XP\\\",\\r\\n \\\"ToastBoostedSuffixString\\\": \\\" ({0:0.##}x)\\\"\\r\\n }\\r\\n ],\\r\\n \\\"IsProgressionEvent\\\": false,\\r\\n \\\"DisplayText\\\": \\\"Winter Dash\\\",\\r\\n \\\"Description\\\": \\\"Walk around rooms to collect dash points. Reach your Winter Dash Goal for a prize! Progress recorded every 5min or on room switch.
Get bonus points with cozy Winter Supplies!
Earn the Benefactor Prize by gifting Winter Supplies! Stay Warm and have fun!! \\\",\\r\\n \\\"ConsumablesImage\\\": \\\"6nacx4iqvsc1bkgeuba1qe83x.png\\\",\\r\\n \\\"PrimaryButtonText\\\": \\\"Winter Dash with Friends\\\",\\r\\n \\\"SecondaryButtonText\\\": \\\"Buy / Gift Winter Supplies\\\",\\r\\n \\\"BackgroundColor\\\": \\\"#96C8FD\\\",\\r\\n \\\"InfoBanner\\\": \\\"ay9h8csfcicax57y4r6kbc0zm.png\\\"\\r\\n}\",\"ActiveExperiments\":\"liveops_2026q1_winter_dash\\r\\nliveops_2025q4_recipes\\r\\nliveops_2025q4_new_years_gala\\r\\nliveops_2025q4_turkey_trot\\r\\nliveops_2025q4_yeti\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.EnableKeepsakeManager\",\"Value\":\"True\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomBoosts.Enabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.TLA.Tricksters\",\"Value\":\"{\\r\\n \\\"GhostConfig\\\":\\r\\n {\\r\\n \\\"Lifetime\\\": 30,\\r\\n \\\"SpawnFwdOffset\\\": 10,\\r\\n \\\"SpawnStartHeightOffset\\\": 0.1,\\r\\n \\\"SpawnHeightOffsetRangeMin\\\": 0.1,\\r\\n \\\"SpawnHeightOffsetRangeMax\\\": 0.1,\\r\\n \\\"SpawnMoveDistance\\\": 10,\\r\\n \\\"EnableReactionOrEmoteChance\\\": 1,\\r\\n \\\"MinReactionEmojis\\\": 3,\\r\\n \\\"MaxReactionEmojis\\\": 6,\\r\\n \\\"MinEmotes\\\": 3,\\r\\n \\\"MaxEmotes\\\": 4,\\r\\n \\\"MinTravelSpeed\\\": 1.5,\\r\\n \\\"MaxTravelSpeed\\\": 2.5,\\r\\n \\\"MaxViewableChatBubbleDistance\\\": 10,\\r\\n \\\"AudioRolloffDistance\\\": 10,\\r\\n \\\"MagnetSpeedMultiplier\\\": 10,\\r\\n \\\"MagnetFollowOffsets\\\": [0,0,0]\\r\\n },\\r\\n \\\"MaxSpawnableSpots\\\": 10,\\r\\n \\\"MaxSimultaneousGhosts\\\": 1,\\r\\n \\\"SpawnCooldown\\\": 0,\\r\\n \\\"ExcludedSpawnForwardAngles\\\": 10,\\r\\n \\\"ExcludedSpawnBackwardAngles\\\": 210,\\r\\n \\\"ProgressionEventId\\\": 18,\\r\\n \\\"DistanceBetweenSpawns\\\": 25,\\r\\n \\\"GhostSpawnChance\\\": 0.5,\\r\\n \\\"CollisionDefaultThreshold\\\": 0.4,\\r\\n \\\"CollisionCrouchThreshold\\\": 0.65,\\r\\n \\\"CollisionProneThreshold\\\": 0.75,\\r\\n \\\"MagnetAttractDistance\\\": 10,\\r\\n \\\"TeaserStartEpoch\\\": 1764547200,\\r\\n \\\"PointsToastImageLeft\\\": \\\"\\\",\\r\\n \\\"PointsToastImageRight\\\": \\\"Yeti_Negative1_v2.png\\\",\\r\\n \\\"PointsToastDuration\\\": 2,\\r\\n \\\"PointsToastAlpha\\\": 0.3,\\r\\n \\\"DisabledRoomIds\\\": [7740359740005459806,52815862, 21001727, 3428843045665284770],\\r\\n \\\"MuteDuration\\\": 43200,\\r\\n \\\"MaxDailyProgress\\\": 2147483647,\\r\\n \\\"ConsumableIdsForPoints\\\": [\\\"c0JTmcLqfUmt_omKcy_m8A\\\"],\\r\\n \\\"PreloadableImages\\\": [],\\r\\n \\\"ChatSuppressionMode\\\": 1,\\r\\n \\\"MagnetBoostedExtents\\\": 5,\\r\\n \\\"EnableGhostRecycling\\\": true\\r\\n}\",\"ActiveExperiments\":\"liveops_2025q4_yeti\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Mods.AlwaysAllowedInPrivateInstance\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.MinimumAccountAgeToShowTutorial\",\"Value\":\"3\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.TLA.Displacement\",\"Value\":\"{\\r\\n \\\"progressReportingDelay\\\": 90000,\\r\\n \\\"DisabledRoomIds\\\": [],\\r\\n \\\"AllowedInDorm\\\": true \\r\\n}\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.TLA.Displacement.Enabled\",\"Value\":\"false\",\"ActiveExperiments\":\"liveops_2026q1_winter_dash\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.PassEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.MerryMidway.Mods.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomBoostHelper.CheckRoomIds\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomBoostHelper.EnabledForRooms\",\"Value\":\"170139,170132,170147,170126,64899011,7835916014920908017,675524947548609275,6528671804629734039,7001713347540578738,1198369430637700542,8176270,4055690517114419831,4025257,1971937,7324320763711006333,8688538386794596306, 3369652546647919188, 6444617613839852445\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomBoostHelper.BoostDenoms\",\"Value\":\"1,10\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.Progression.ShowDefaultUnknownRewardImage\",\"Value\":\"true\",\"ActiveExperiments\":\"player_progression_giftdrop_override\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.ClearCut.Mods.Fiesta.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.ClearCut.Mods.HammersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.ClearCut.Mods.ShotgunsOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.DriveIn.Mods.Fiesta.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.DriveIn.Mods.HammersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.DriveIn.Mods.ShotgunsOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.Fiesta.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.HammersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Homestead.Mods.ShotgunsOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.MerryMidway.Mods.Fiesta.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.MerryMidway.Mods.HammersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.MerryMidway.Mods.ShotgunsOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Quarry.Mods.Fiesta.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Quarry.Mods.HammersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Quarry.Mods.ShotgunsOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.River.Mods.Fiesta.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.River.Mods.HammersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.River.Mods.ShotgunsOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Spillway.Mods.Fiesta.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Spillway.Mods.HammersOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Paintball.Spillway.Mods.ShotgunsOnly.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Quest.GoldenTrophy.Mods.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Quest.GoldenTrophy.Mods.CoconutBombs.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Quest.GoldenTrophy.Mods.LaserWeapons.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Quest.GoldenTrophy.Mods.TurboMode.Allowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"RoomBoostHelper.PurchasableBundleId\",\"Value\":\"32322\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Quest.Mods.HideBadgeInfo\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BackpackGame.FloorIsLavaBuyAnotherPurchasableItem\",\"Value\":\"32364\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BackpackGame.HideAndSeekBuyAnotherPurchasableItem\",\"Value\":\"32300\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Quest.Mods.OverrideModBoardText\",\"Value\":\"You have 3 chances to complete the run with mods!\\r\\n\\r\\n- Rank is limited to A\\r\\n- Complete runs with Turbo and Coconut Chaos active to earn a badge\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"VRKeyboard.EnableAttachingToUI\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.TLA.Recipes.Id\",\"Value\":\"cbe9582c-6314-4ba3-ad0e-57d220b79aff\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"TLA.OktoberFeast.ConsumableNames\",\"Value\":\"test1,test2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.TLA.Displacement.DPM\",\"Value\":\"false\",\"ActiveExperiments\":\"liveops_2026q1_winter_dash\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.Progression.MaxDailyXP\",\"Value\":\"150\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BackpackGame.SnowballFightBuyAnotherPurchasableItem\",\"Value\":\"32503\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.DAU.First7DayGiftMessageEnabled\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.DAU.First7DayGiftsImages\",\"Value\":\"\\t{ \\\"1\\\": \\\"roomCatSelect_pvp.jpg\\\", \\\"2\\\": \\\"roomCatSelect_pvp.jpg\\\", \\\"3\\\": \\\"roomCatSelect_pvp.jpg\\\", \\\"4\\\": \\\"roomCatSelect_pvp.jpg\\\", \\\"5\\\": \\\"roomCatSelect_pvp.jpg\\\", \\\"6\\\": \\\"roomCatSelect_pvp.jpg\\\", \\\"7\\\": \\\"roomCatSelect_pvp.jpg\\\" }\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Experience.Dodgeball.Dodgeball.Mods.Dodgebombs.Allowed.InClones\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"LiveOps.TLA.UpcomingOrActiveId\",\"Value\":\"\",\"ActiveExperiments\":\"liveops_2026q1_paints\",\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.AdditionalLogFlags:users/mchen/local-statsig\",\"Value\":\"LiveOps, LiveOps.TLA, LiveOps.TLA.Info, LiveOps.TLA.Paints, LiveOps.TLA.Paints.Zones, RecNet, RecNet.Core, RecNet.Core.HTTP, RecNet.Core.HTTP.Detailed\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Logs:bvt\",\"Value\":\"GLOBAL_ENABLE,\\r\\nNetwork.TachyonClient,\\r\\nNetwork.DissonanceLogs,\\r\\nLogic.CV2.CircuitsV2Lifecycle,\\r\\nAudio,\\r\\nPlatform,\\r\\nPlatform.Bootsequence,\\r\\nEconomy.Player.AvatarItemEffectManager,\\r\\nEconomy.Player.RoomAvatarItemEffectManager,\\r\\n\\r\\nRecNet.**,\\r\\n\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.Logs:users/dkahler/prod-prime\",\"Value\":\"GLOBAL_ENABLE, Network.TachyonClient, Network.DissonanceLogs, Logic.CV2.CircuitsV2Lifecycle, Audio, Platform, Platform.Bootsequence, Economy.Player.AvatarItemEffectManager, Economy.Player.RoomAvatarItemEffectManager, RecNet.**,\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"BootSequence.AutoCreateAccount\",\"Value\":\"false\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Debug.AdditionalLogFlags\",\"Value\":\"CircuitsV2Lifecycle\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.EventInfoFirstPanelBody\",\"Value\":\"Collecting Science Cores in highlighted Rooms will award XP. Some rooms now have multiple Cores! Once you collect all Cores of one color, new ones may appear. You can also earn XP in public rooms or by collecting Cores with friends.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.EventInfoFirstPanelTitle\",\"Value\":\"Collect Science Cores for XP!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialFirstIntro_PressChallengesButton_OldWatch\",\"Value\":\"We need help...\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.EventInfoSecondPanelBody\",\"Value\":\"Use the Boost to level up faster! The Boost will multiply the XP you have gained from recent time spent in Rec Room. 900 XP is the max amount that can be gained by purchasing the Boost.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.EventInfoSecondPanelTitle\",\"Value\":\"Speed Up Progress\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialFirstIntro_PressTodaysRoom_OldWatch\",\"Value\":\"Our radars suggest that there is a Core in this room. Can you find it?\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialReward_PressChallengesButton_NewWatch\",\"Value\":\"We'll add XP to your balance for each Core you collect. This XP doesn't count towards your regular XP limits! You've proven your competence, let us share more details!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.EventInfoThirdPanelBody\",\"Value\":\"Use Event Credits to unlock special offers in ^TheScienceDepartment room.\\r\\n\\r\\nEvent Credits can be used until 24 hours after the event ends.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.EventInfoThirdPanelTitle\",\"Value\":\"Spend Credits\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.ExplorePageBanner_DescriptionText\",\"Value\":\"Collect ALL the Cores!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.RoomCarousel_PremiumRoomsHeader\",\"Value\":\"Bonus Rooms - Science Exo Arms (Restoration)*|Bonus Rooms - Science Chainsaw (Restoration)*|Bonus Rooms - Vigilante Utility Belt (White)\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.MainPage_EventEnded_SpendCurrencyBeforeTimeRunsOutPrompt\",\"Value\":\"Spend your credits in ^TheScienceDepartment before time runs out.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.MainPage_EventEnded_SpendCurrencyButtonText\",\"Value\":\"Spend Credits\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialReward_PressViewAllRoomsButton_NewWatch\\t\",\"Value\":\"There are more Power Cores across the Room-i-verse in rooms created by your fellow creators.\\r\\n\\r\\n\\r\\nAs we discover the locations of more Power Cores, we'll add them to your list here. Don't forget to check back regularly!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialReward_ClaimReward_NewWatch\",\"Value\":\"You can collect event rewards here! \\r\\n\\r\\nCredits can be used to unlock items in the Science Department.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialRewardBackup_PressChallengesButton_NewWatch\\t\",\"Value\":\"We'll add XP to your balance for each Power Core you collect. This XP doesn't count towards your regular XP limits!\\r\\n\\r\\nYou've proven your competence, let us share more details!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialRewardBackup_PressViewAllRoomsButton_NewWatch\",\"Value\":\"There are more Power Cores across the Room-i-verse in rooms created by your fellow creators.\\r\\n\\r\\n\\r\\nAs we discover the locations of more Power Cores, we'll add them to your list here. Don't forget to check back regularly!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialRewardBackup_PressBackButton_NewWatch\",\"Value\":\"As a thank you, we have special offers for you in the Science Department!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialRewardBackup_ClaimReward_NewWatch\\t\",\"Value\":\"You can collect event rewards here!\\r\\n\\r\\nCredits can be used to unlock items in the Science Department.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialBackup_PressChallengesButton_NewWatch\",\"Value\":\"Psst... there are Cores for you to find.\\r\\n\\r\\nWe could use your help!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialBackup_PressTodaysRoomButton_NewWatch\",\"Value\":\"Our radars suggest that there is a Core in this room. Can you find it?\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.TutorialReward_PressBackButton_NewWatch\",\"Value\":\"As a thank you, we have special offers for you in the Science Department!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.MainPage_EventEnded_SpendCurrencyBonusRewardsText\",\"Value\":\"Make sure to collect and spend all of your credits before the event ends!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.RoomCarousel_PremiumRoomsDesc\",\"Value\":\"Unlock with Science Exo Arms (Restoration)*!|Unlock with Science Chainsaw (Restoration)*!|Unlock with Vigilante Utility Belt (White)!\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"ProgressionEvents.UpsellDialogText\",\"Value\":\"You'll need an item to find the research core in this room.\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.DisableNewPlayerChallenges.AccountCreationCutoff\",\"Value\":\"2023-08-01 00:00\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.WatchOpenOnSessionStart.HighlightDuration\",\"Value\":\"5.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.WatchOpenOnSessionStart.NewAccountDaysCutoff\",\"Value\":\"30\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.PSF.WatchOpenOnSessionStart.WaitDuration\",\"Value\":\"10.0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.GameInvite.AcceptResponse\",\"Value\":\"\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.GameInvite.DeclineResponses\",\"Value\":\"You should come to me!; In the middle of something... invite me again in 5?; I'm about to log off; I don't really like that room; Not today, thanks\\t\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Lifecycle.GameInvite.ResponsePlaceholder\\t\",\"Value\":\"Add a decline response\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"UGC.ObjectModel.ScalabilityAllowed\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.ios.VoiceAudioQuality\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.android.VoiceAudioQuality\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.quest.VoiceAudioQuality\",\"Value\":\"2\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.desktop.VolumeMultiplier\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.quest.VolumeMultiplier\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.android.VolumeMultiplier\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.ios.VolumeMultiplier\",\"Value\":\"1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.android.AgcTargetLevel\",\"Value\":\"0.1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.ios.AgcTargetLevel\",\"Value\":\"0.1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.quest.HighPassFilterEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.quest.AgcTargetLevel\",\"Value\":\"0.1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.quest.AgcCompressionGain\",\"Value\":\"0.1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.desktop.HighPassFilterEnabled\",\"Value\":\"true\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.desktop.AgcTargetLevel\",\"Value\":\"0.1\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null},{\"Key\":\"Voice.Dissonance.desktop.VoiceDesktopEchoCancellation\",\"Value\":\"0\",\"ActiveExperiments\":null,\"StartTime\":null,\"EndTime\":null}]"; + public static readonly string[] AvatarCatalogFileNames = ["2022-avatar.json", "avatar.json"]; + public static HashSet ResolvableAvatarItemDescs = new(StringComparer.OrdinalIgnoreCase); + public static readonly string[] AvatarItemLookupFileNames = ["avatar-item-lookup.json"]; + public static DateTime AvatarItemReleaseCutoffUtc = new(2023, 3, 1, 0, 0, 0, DateTimeKind.Utc); + public static IEnumerable AvatarCatalogDirectories() + { + return new[] + { + PrivateData, + Path.GetFullPath("."), + Path.GetFullPath(Path.Join(".", "Server", "RadicadeServer-Config")), + Path.Join(AppContext.BaseDirectory, "RadicadeServer-Config") + }; + } + + public static IEnumerable AvatarCatalogCandidatePaths() + { + var directories = AvatarCatalogDirectories().ToArray(); + + return AvatarCatalogFileNames + .SelectMany(fileName => directories.Select(directory => Path.Join(directory, fileName))) + .Distinct(StringComparer.OrdinalIgnoreCase); + } + + public const string StorefrontFolderName = "storefronts"; + + public static IEnumerable StorefrontDirectories() + { + return AvatarCatalogDirectories() + .Select(directory => Path.Join(directory, StorefrontFolderName)) + .Distinct(StringComparer.OrdinalIgnoreCase); + } + + private sealed class AvatarItemLookupFile + { + [JsonPropertyName("descriptions")] + public List? Descriptions { get; set; } + } + + private static void LoadAvatarItemLookup() + { + foreach (var directory in AvatarCatalogDirectories()) + { + foreach (var fileName in AvatarItemLookupFileNames) + { + var path = Path.Join(directory, fileName); + if (!File.Exists(path)) continue; + + try + { + var lookup = File.ReadAllText(path).FromJson(); + var resolved = new HashSet( + (lookup?.Descriptions ?? new List()) + .Where(description => !string.IsNullOrWhiteSpace(description)) + .Select(description => RadicadeServer.RecNet.Avatar.AvatarItemDescription.Normalize(description)), + StringComparer.OrdinalIgnoreCase); + + if (resolved.Count == 0) + { + continue; + } + + ResolvableAvatarItemDescs = resolved; + return; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException) + { + } + } + } + } + + + public static void LoadFiles() + { + LoadAvatarItemLookup(); + + var avatarPaths = AvatarCatalogCandidatePaths().ToArray(); + string? avatarCatalogJson = null; + string? avatarCatalogPath = null; + + foreach (var avatarPath in avatarPaths) + { + if (!File.Exists(avatarPath)) continue; + + try + { + avatarCatalogJson = File.ReadAllText(avatarPath); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + continue; + } + avatarCatalogPath = avatarPath; + break; + } + + + try + { + var catalogItems = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog + .ParseCatalog(avatarCatalogJson ?? SafeDefaultAvatarUnlocks2023); + var metadataPath = Path.Join(Path.GetDirectoryName(avatarCatalogPath ?? string.Empty) ?? string.Empty, "merged-avatar.json"); + if (File.Exists(metadataPath) + && RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.NeedsMetadataEnrichment(catalogItems)) + catalogItems = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.EnrichWithMetadata( + catalogItems, File.ReadAllText(metadataPath)); + if (catalogItems.Count == 0) + catalogItems = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.ParseCatalog(SafeDefaultAvatarUnlocks2023); + DefaultAvatarCatalog = catalogItems.ToJson(); + DefaultAvatarUnlocks = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog + .GetStarterItems() + .ToJson(); + var defaultItems = DefaultAvatarUnlocks.FromJson>(); + } + catch (Exception) + { + DefaultAvatarCatalog = SafeDefaultAvatarUnlocks2023; + DefaultAvatarUnlocks = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.LoadStarterJson(); + } + + LoadItemCatalog("equipment.json", ref DefaultEquipmentItems, "[Equipment]"); + LoadItemCatalog("consumables.json", ref DefaultConsumableItems, "[Consumables]"); + + LoadItemCatalog("equipment_skins.json", ref DefaultEquipmentSkinItems, "[EquipmentSkins]"); + var equipmentBefore = DefaultEquipmentItems + .FromJson>()?.Count ?? 0; + var equipmentWithSkins = RadicadeServer.RecNet.Avatar.EquipmentSkinCatalog + .MergeInto(DefaultEquipmentItems, DefaultEquipmentSkinItems); + var equipmentAfter = equipmentWithSkins + .FromJson>()?.Count ?? equipmentBefore; + if (equipmentAfter != equipmentBefore) + { + DefaultEquipmentItems = equipmentWithSkins; + } + } + + + + + + + private static void LoadItemCatalog(string fileName, ref string target, string logPrefix) + { + var path = Path.Join(PrivateData, fileName); + if (!File.Exists(path)) return; + + try + { + target = File.ReadAllText(path); + var parsed = System.Text.Json.JsonSerializer.Deserialize(target); + } + catch (Exception) + { + target = "[]"; + } + } + + + + + + private static string? CleanSecret(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + var cleaned = value.Trim().Trim('"', '\''); + return string.IsNullOrWhiteSpace(cleaned) ? null : cleaned; + } + + public static void LoadConfiguration(Microsoft.Extensions.Configuration.IConfiguration config) + { + ServerUrl = CleanUrl(config["Radicade:ServerUrl"] ?? ServerUrl); + AdminApiKey = config["Radicade:AdminApiKey"] ?? AdminApiKey; + SupportApiKey = config["Radicade:SupportApiKey"] ?? SupportApiKey; + JwtKey = config["Radicade:JwtKey"] ?? JwtKey; + SteamApiKey = CleanSecret(config["Radicade:SteamApiKey"]) ?? ""; + if (bool.TryParse(config["Radicade:TrustUnverifiedSteamIdentity"], out var trustUnverifiedSteamIdentity)) + TrustUnverifiedSteamIdentity = trustUnverifiedSteamIdentity; + PhotonRealtimeAppId = config["Radicade:Photon:RealtimeAppId"] ?? PhotonRealtimeAppId; + PhotonVoiceAppId = config["Radicade:Photon:VoiceAppId"] ?? PhotonVoiceAppId; + PhotonChatAppId = config["Radicade:Photon:ChatAppId"] ?? PhotonChatAppId; + PhotonRegion = config["Radicade:Photon:Region"] ?? PhotonRegion; + MailApiKey = CleanSecret(config["Radicade:MailApiKey"]) ?? MailApiKey; + MailFrom = config["Radicade:MailFrom"] ?? MailFrom; + MailFromName = config["Radicade:MailFromName"] ?? MailFromName; + MailApiUrl = config["Radicade:MailApiUrl"] ?? MailApiUrl; + + + + + + AdminAccounts = config.GetSection("Radicade:Admins").GetChildren() + .Select(section => new AdminAccount( + (section["Username"] ?? "").Trim(), + CleanSecret(section["Password"]) ?? "")) + .Where(account => account.Username.Length > 0 && account.Password.Length > 0) + .ToList(); + TestInfluencerAccountIds = (config["Radicade:TestInfluencerAccountIds"] ?? "1,2") + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(value => long.TryParse(value, out var id) ? (long?)id : null) + .Where(id => id.HasValue) + .Select(id => id!.Value) + .Distinct() + .ToList(); + TestCoOwnerAccountIds = (config["Radicade:TestCoOwnerAccountIds"] ?? "") + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(value => long.TryParse(value, out var id) ? (long?)id : null) + .Where(id => id.HasValue).Select(id => id!.Value).Distinct().ToList(); + + var adminPanelUrl = config["Radicade:AdminPanelUrl"]; + AdminPanelUrl = CleanUrl(!string.IsNullOrWhiteSpace(adminPanelUrl) ? adminPanelUrl : AdminPanelUrl); + + + + var publicData = config["Radicade:PublicDataPath"]; + var privateData = config["Radicade:PrivateDataPath"]; + if (!string.IsNullOrWhiteSpace(publicData)) + PublicData = Path.GetFullPath(publicData); + if (!string.IsNullOrWhiteSpace(privateData)) + PrivateData = Path.GetFullPath(privateData); + + RefreshDerivedPaths(); + } + + private static bool ParseBool(string? raw, bool fallback) + { + return bool.TryParse(raw, out var value) ? value : fallback; + } + + private static double ParseDouble(string? raw, double fallback) + { + return double.TryParse(raw, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var value) ? value : fallback; + } + + private static int ParseInt(string? raw, int fallback) + { + return int.TryParse(raw, out var value) ? value : fallback; + } + + private static long ParseLong(string? raw, long fallback) + { + return long.TryParse(raw, out var value) ? value : fallback; + } + + private static float ParseFloat(string? raw, float fallback) + { + return float.TryParse(raw, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var value) ? value : fallback; + } + + private static string CleanUrl(string url) + { + return url.Trim().TrimEnd('/'); + } + + private static void RefreshDerivedPaths() + { + PersistentLogs = Path.Join(PublicData, "Persistent", "Logs"); + CDNImages = Path.Join(PublicData, "CDN", "Images"); + CDNVideos = Path.Join(PublicData, "CDN", "Videos"); + CDNStorage = Path.Join(PublicData, "CDN", "Storage"); + CDNCustom = Path.Join(PublicData, "CDN", "Custom"); + } + } +} diff --git a/Server/DB/DBContext.cs b/Server/DB/DBContext.cs new file mode 100644 index 0000000..249933f --- /dev/null +++ b/Server/DB/DBContext.cs @@ -0,0 +1,91 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.Custom; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.CDN; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.DB; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Images; +using RadicadeServer.RecNet.Inventions; +using RadicadeServer.RecNet.Keepsakes; +using RadicadeServer.RecNet.Leaderboards; +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Relationships; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RecNet.Store; +using RadicadeServer.API.Wishlists; + +namespace RadicadeServer.DB +{ + public class RecDBContext : DbContext + { + public RecDBContext(DbContextOptions options) : base(options) { } + public static Dictionary ActiveRoomInstances = new(); + public DbSet Config { get; set; } + public DbSet GameConfig { get; set; } + public DbSet ServerConfig { get; set; } + public DbSet CachedLogins { get; set; } + public DbSet Profiles { get; set; } + public DbSet Players { get; set; } + public DbSet Moderation { get; set; } + public DbSet PlayerWarnings { get; set; } + public DbSet VoteKicks { get; set; } + public DbSet IpBans { get; set; } + public DbSet HardwareBans { get; set; } + public DbSet VoiceChatRestrictions { get; set; } + public DbSet GameBlocks { get; set; } + public DbSet AccountCreationBlocks { get; set; } + public DbSet Progression { get; set; } + public DbSet WeeklyChallenges { get; set; } + public DbSet Reputations { get; set; } + public DbSet Avatars { get; set; } + public DbSet Unlocks { get; set; } + public DbSet PlayerSettings { get; set; } + public DbSet Currency { get; set; } + public DbSet RoomInteractions { get; set; } + public DbSet Gifts { get; set; } + public DbSet Presences { get; set; } + public DbSet Relationships { get; set; } + public DbSet GiftItems { get; set; } + public DbSet WishlistItems { get; set; } + public DbSet Images { get; set; } + public DbSet ImageInteractions { get; set; } + public DbSet CustomAvatarItems { get; set; } + public DbSet Messages { get; set; } + public DbSet Chats { get; set; } + public DbSet RoomInstances { get; set; } + public DbSet RoomRoles { get; set; } + public DbSet RoomKeys { get; set; } + public DbSet Rooms { get; set; } + public DbSet RoomJoinGifts { get; set; } + public DbSet RoomBans { get; set; } + public DbSet RoomComments { get; set; } + public DbSet SubRooms { get; set; } + public DbSet SubRoomPermissions { get; set; } + public DbSet SubRoomHistory { get; set; } + public DbSet RoomStats { get; set; } + public DbSet Playlists { get; set; } + public DbSet ItemSets { get; set; } + public DbSet Tags { get; set; } + public DbSet Inventions { get; set; } + public DbSet KeepsakeInstances { get; set; } + public DbSet KeepsakeCollectionRecords { get; set; } + public DbSet LeaderboardScores { get; set; } + public DbSet InventionVersions { get; set; } + public DbSet InventionPersonalDetails { get; set; } + public DbSet CustomContent { get; set; } + public DbSet PlayerEvents { get; set; } + public DbSet PlayerEventResponses { get; set; } + public DbSet AdminUsers { get; set; } + public DbSet AdminSessions { get; set; } + } +} diff --git a/Server/DB/DbBoostrap.cs b/Server/DB/DbBoostrap.cs new file mode 100644 index 0000000..0920330 --- /dev/null +++ b/Server/DB/DbBoostrap.cs @@ -0,0 +1,5 @@ +using Microsoft.EntityFrameworkCore; + +namespace RadicadeServer.DB +{ +} diff --git a/Server/DB/Defaults.cs b/Server/DB/Defaults.cs new file mode 100644 index 0000000..80b3d8d --- /dev/null +++ b/Server/DB/Defaults.cs @@ -0,0 +1,96 @@ +using RadicadeServer.RecNet.Moderation; + +namespace RadicadeServer +{ + public static class Defaults + { + public const string Avatar = "{\"OutfitSelections\":\"\",\"FaceFeatures\":\"{\\\"ver\\\":3,\\\"eyeId\\\":\\\"AjGMoJhEcEehacRZjUMuDg\\\",\\\"eyePos\\\":{\\\"x\\\":0.0,\\\"y\\\":0.0},\\\"eyeScl\\\":0.0,\\\"mouthId\\\":\\\"FrZBRanXEEK29yKJ4jiMjg\\\",\\\"mouthPos\\\":{\\\"x\\\":0.0,\\\"y\\\":0.0},\\\"mouthScl\\\":0.0,\\\"beardColorId\\\":\\\"befcc00a-a2e6-48e4-864c-593d57bbbb5b\\\"}\",\"SkinColor\":\"85343b16-d58a-4091-96d8-083a81fb03ae\",\"HairColor\":\"fT0t-J1A-0i_tdoogFEz-g\"}"; + public const string Settings = "[{\"Key\":\"MOD_BLOCKED_TIME\",\"Value\":\"0\"},{\"Key\":\"MOD_BLOCKED_DURATION\",\"Value\":\"0\"},{\"Key\":\"PlayerSessionCount\",\"Value\":\"138\"},{\"Key\":\"ShowRoomCenter\",\"Value\":\"1\"},{\"Key\":\"QualitySettings\",\"Value\":\"3\"},{\"Key\":\"Recroom.OOBE\",\"Value\":\"0\"},{\"Key\":\"VoiceFilter\",\"Value\":\"0\"},{\"Key\":\"VIGNETTED_TELEPORT_ENABLED\",\"Value\":\"1\"},{\"Key\":\"CONTINUOUS_ROTATION_MODE\",\"Value\":\"1\"},{\"Key\":\"ROTATION_INCREMENT\",\"Value\":\"0\"},{\"Key\":\"ROTATE_IN_PLACE_ENABLED\",\"Value\":\"0\"},{\"Key\":\"TeleportBuffer\",\"Value\":\"0\"},{\"Key\":\"VoiceChat\",\"Value\":\"0\"},{\"Key\":\"PersonalBubble\",\"Value\":\"0\"},{\"Key\":\"ShowNames\",\"Value\":\"1\"},{\"Key\":\"H.264 plugin\",\"Value\":\"0\"},{\"Key\":\"google_analytics_clientid_pref_key\",\"Value\":\"YPSBPEGEf8ed48c71d2e0ddd40bebb2d09d0dc1ac53377ca\"},{\"Key\":\"USER_TRACKING\",\"Value\":\"53\"},{\"Key\":\"IgnoreBuffer\",\"Value\":\"0\"},{\"Key\":\"FIRST_TIME_IN_FLAGS\",\"Value\":\"0\"},{\"Key\":\"WALKING_MODE\",\"Value\":\"1\"},{\"Key\":\"COMFORT_FLY\",\"Value\":\"0\"},{\"Key\":\"COMFORT_SMOOTH_ROTATE\",\"Value\":\"0\"},{\"Key\":\"COMFORT_SWIPE_ROTATE\",\"Value\":\"0\"},{\"Key\":\"COMFORT_TELEPORT\",\"Value\":\"0\"},{\"Key\":\"COMFORT_WALK\",\"Value\":\"0\"},{\"Key\":\"COMFORT_SPRINT\",\"Value\":\"0\"},{\"Key\":\"COMFORT_VEHICLES\",\"Value\":\"0\"},{\"Key\":\"DAILY_LOGIN_DATE\",\"Value\":\"2\"},{\"Key\":\"OBJECTIVE_DATE\",\"Value\":\"2\"},{\"Key\":\"OBJECTIVE_PROGRESS0\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_COMPLETED0\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_PROGRESS1\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_COMPLETED1\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_PROGRESS2\",\"Value\":\"0\"},{\"Key\":\"OBJECTIVE_COMPLETED2\",\"Value\":\"0\"},{\"Key\":\"SFX_VOLUME_PERCENT_PREF\",\"Value\":\"0.8265823\"},{\"Key\":\"SCREENS_INVERT_MOUSE\",\"Value\":\"0\"},{\"Key\":\"SCREENS_MOUSE_SENSITIVITY_X\",\"Value\":\"1.187885\"},{\"Key\":\"SCREENS_MOUSE_SENSITIVITY_Y\",\"Value\":\"1.100286\"},{\"Key\":\"MOTION_TELEPORT_ENABLED\",\"Value\":\"0\"},{\"Key\":\"BACKPACK_FAVORITE_TOOL\",\"Value\":\"-1\"},{\"Key\":\"SplitTestAssignedSegments\",\"Value\":\"1|{\\\"SplitTesting+NUX_HoloHelpers_2019_11_15\\\":\\\"On\\\"}\"},{\"Key\":\"SCREENS_HEAD_BOB\",\"Value\":\"0\"},{\"Key\":\"MUSIC_VOLUME_PERCENT_PREF\",\"Value\":\"0.5467778\"},{\"Key\":\"SCREENS_CAMERA_RELATIVE_DANCE_CONTROLS\",\"Value\":\"0\"},{\"Key\":\"AMBIENCE_VOLUME_PERCENT_PREF\",\"Value\":\"0.6755117\"}]"; + public const string AvatarUnlocks = "[]"; + public const string ConsumableUnlocks = "[]"; + public const string EquipmentUnlocks = "[]"; + + public static List GetVoteKickReasons() + { + var reasons = new List(); + + reasons.Add(new VoteKickReason + { + Reason = "Discriminatory language", + ReportCategory = KickReason.CoC_Discrimination + }); + reasons.Add(new VoteKickReason + { + Reason = "Discriminatory behavior", + ReportCategory = KickReason.CoC_Discrimination + }); + reasons.Add(new VoteKickReason + { + Reason = "Threats or encouraging suicide", + ReportCategory = KickReason.CoC_Discrimination + }); + reasons.Add(new VoteKickReason + { + Reason = "Toxic behavior", + ReportCategory = KickReason.CoC_Discrimination + }); + reasons.Add(new VoteKickReason + { + Reason = "Sexual behavior in public", + ReportCategory = KickReason.CoC_Sexual + }); + reasons.Add(new VoteKickReason + { + Reason = "Sexual language in public", + ReportCategory = KickReason.CoC_Sexual + }); + reasons.Add(new VoteKickReason + { + Reason = "Non-consensual sexual behavior", + ReportCategory = KickReason.CoC_Sexual + }); + reasons.Add(new VoteKickReason + { + Reason = "Player in walls or floor", + ReportCategory = KickReason.CoC_Trolling + }); + reasons.Add(new VoteKickReason + { + Reason = "Friendly fire", + ReportCategory = KickReason.CoC_Trolling + }); + reasons.Add(new VoteKickReason + { + Reason = "Microphone spam", + ReportCategory = KickReason.CoC_Trolling + }); + reasons.Add(new VoteKickReason + { + Reason = "Abusing bugs or exploits", + ReportCategory = KickReason.CoC_Trolling + }); + reasons.Add(new VoteKickReason + { + Reason = "Spawn camping", + ReportCategory = KickReason.CoC_Trolling + }); + reasons.Add(new VoteKickReason + { + Reason = "Inactive in games (AFK)", + ReportCategory = KickReason.Misc + }); + reasons.Add(new VoteKickReason + { + Reason = "Not following game rules", + ReportCategory = KickReason.Misc + }); + reasons.Add(new VoteKickReason + { + Reason = "Cheating", + ReportCategory = KickReason.Misc + }); + + return reasons; + } + } +} diff --git a/Server/DB/RecDBContextFactory.cs b/Server/DB/RecDBContextFactory.cs new file mode 100644 index 0000000..69e9a47 --- /dev/null +++ b/Server/DB/RecDBContextFactory.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace RadicadeServer.DB +{ + public class RecDBContextFactory : IDesignTimeDbContextFactory + { + public RecDBContext CreateDbContext(string[] args) + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile("appsettings.Development.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + var connectionString = config.GetConnectionString("DefaultConnection"); + if (string.IsNullOrWhiteSpace(connectionString)) + throw new InvalidOperationException("Missing ConnectionStrings:DefaultConnection for design-time context creation."); + + var options = new DbContextOptionsBuilder() + .UseSqlServer(connectionString) + .Options; + + return new RecDBContext(options); + } + } +} diff --git a/Server/Discord/AdminActionDiscord.cs b/Server/Discord/AdminActionDiscord.cs new file mode 100644 index 0000000..bc029ac --- /dev/null +++ b/Server/Discord/AdminActionDiscord.cs @@ -0,0 +1,126 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.Discord.Webhooks; + +namespace RadicadeServer.Radicade.Discord; + + + + + + + + + + + + + + + + +public static class AdminActionDiscord +{ + + public static void LogAction(string action, string? admin, string? details) + { + if (string.IsNullOrWhiteSpace(WebhookConfig.AdminActivityUrl)) return; + if (string.IsNullOrWhiteSpace(action)) return; + + ReportDiscord.SendAsync(WebhookConfig.AdminActivityUrl, $"🛠️ Admin Action: {action}", + new DiscordPayload + { + Title = $"Admin Action: {action}", + ThumbnailUrl = WebhookImages.AdminActivity, + Color = 0x5865F2, + Fields = new List + { + new() { Name = "Action", Value = Truncate(action, 200), Inline = true }, + new() { Name = "Admin", Value = Truncate(string.IsNullOrWhiteSpace(admin) ? "Unknown" : admin, 100), Inline = true }, + new() { Name = "Details", Value = Truncate(string.IsNullOrWhiteSpace(details) ? "—" : details, 900), Inline = false } + }, + TimestampUtc = DateTime.UtcNow.ToString("O") + }); + } + + + + + + public static async Task LogBanAsync(RecDBContext db, long bannedPlayerId, string? bannedBy, + bool causedByReports, string? reason, ReportCategory category, DateTime? expiry, bool isModerator, + string? banMethod = null) + { + if (string.IsNullOrWhiteSpace(WebhookConfig.InGameBansUrl)) return; + + var playerName = bannedPlayerId.ToString(); + try + { + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == bannedPlayerId); + if (profile != null) + playerName = string.IsNullOrWhiteSpace(profile.displayName) + ? (profile.username ?? bannedPlayerId.ToString()) + : profile.displayName; + } + catch + { + + } + + var durationText = FormatDuration(expiry); + + var fields = new List + { + new() { Name = "Banned Player", Value = $"{playerName} ({bannedPlayerId})", Inline = true }, + new() { Name = "Banned By", Value = Truncate(string.IsNullOrWhiteSpace(bannedBy) ? "Unknown" : bannedBy, 100), Inline = true }, + new() { Name = "Caused by Reports", Value = causedByReports ? "Yes" : "No", Inline = true }, + new() { Name = "Category", Value = category.ToString(), Inline = true }, + new() { Name = "Duration", Value = durationText, Inline = true }, + new() { Name = "Is Moderator", Value = isModerator ? "Yes" : "No", Inline = true }, + new() { Name = "Reason", Value = Truncate(string.IsNullOrWhiteSpace(reason) ? "—" : reason, 900), Inline = false } + }; + if (!string.IsNullOrWhiteSpace(banMethod)) + { + fields.Insert(5, new DiscordField { Name = "Ban Method", Value = Truncate(banMethod, 100), Inline = true }); + } + + ReportDiscord.SendAsync(WebhookConfig.InGameBansUrl, $"🚫 Player Banned: {playerName}", + new DiscordPayload + { + Title = $"Player Banned: {playerName}", + ThumbnailUrl = WebhookImages.InGameBan, + Color = 0xE74C3C, + Fields = fields, + TimestampUtc = DateTime.UtcNow.ToString("O") + }); + } + + private static string FormatDuration(DateTime? expiry) + { + if (expiry == null || expiry.Value == DateTime.MaxValue) + return "Permanent"; + + var remaining = expiry.Value - DateTime.UtcNow; + if (remaining <= TimeSpan.Zero) + return $"Expires {expiry.Value:yyyy-MM-dd HH:mm} UTC"; + + var parts = new List(); + if (remaining.Days > 0) parts.Add($"{remaining.Days}d"); + if (remaining.Hours > 0) parts.Add($"{remaining.Hours}h"); + if (remaining.Minutes > 0) parts.Add($"{remaining.Minutes}m"); + if (remaining.Seconds > 0 || parts.Count == 0) parts.Add($"{remaining.Seconds}s"); + return $"{string.Join(" ", parts)} (until {expiry.Value:yyyy-MM-dd HH:mm} UTC)"; + } + + private static string Truncate(string value, int max) + { + if (string.IsNullOrEmpty(value)) return ""; + if (value.Length <= max) return value; + var cut = max; + if (char.IsHighSurrogate(value[cut - 1]) && cut < value.Length && char.IsLowSurrogate(value[cut])) + cut--; + return value[..cut]; + } +} diff --git a/Server/Discord/DiscordPayload.cs b/Server/Discord/DiscordPayload.cs new file mode 100644 index 0000000..314f667 --- /dev/null +++ b/Server/Discord/DiscordPayload.cs @@ -0,0 +1,19 @@ +namespace RadicadeServer.Radicade.Discord; + +public sealed class DiscordPayload +{ + public string Title { get; set; } = ""; + public string Description { get; set; } = ""; + public int Color { get; set; } + public List Fields { get; set; } = new(); + public string TimestampUtc { get; set; } = ""; + public string ImageUrl { get; set; } = ""; + public string ThumbnailUrl { get; set; } = ""; +} + +public sealed class DiscordField +{ + public string Name { get; set; } = ""; + public string Value { get; set; } = ""; + public bool Inline { get; set; } +} \ No newline at end of file diff --git a/Server/Discord/DiscordPayloadJson.cs b/Server/Discord/DiscordPayloadJson.cs new file mode 100644 index 0000000..9e8bdfa --- /dev/null +++ b/Server/Discord/DiscordPayloadJson.cs @@ -0,0 +1,70 @@ +using System.Globalization; +using System.Text.Json.Nodes; + +namespace RadicadeServer.Radicade.Discord; + +public static class DiscordPayloadJson +{ + private const int MaxContentLength = 2000; + private const int MaxTitleLength = 256; + private const int MaxDescriptionLength = 4096; + private const int MaxFieldNameLength = 256; + private const int MaxFieldValueLength = 1024; + private const int MaxFieldCount = 25; + + public static string Build(DiscordPayload payload, string? contentOverride = null, string? embedImageUrl = null) + { + var embed = new JsonObject(); + + if (!string.IsNullOrWhiteSpace(payload.Title)) + embed["title"] = Truncate(payload.Title, MaxTitleLength); + if (!string.IsNullOrWhiteSpace(payload.Description)) + embed["description"] = Truncate(payload.Description, MaxDescriptionLength); + embed["color"] = payload.Color; + + var imageUrl = string.IsNullOrWhiteSpace(embedImageUrl) ? payload.ImageUrl : embedImageUrl; + if (!string.IsNullOrWhiteSpace(imageUrl)) + embed["image"] = new JsonObject { ["url"] = Truncate(imageUrl, MaxTitleLength) }; + if (!string.IsNullOrWhiteSpace(payload.ThumbnailUrl)) + embed["thumbnail"] = new JsonObject { ["url"] = Truncate(payload.ThumbnailUrl, MaxTitleLength) }; + + if (!string.IsNullOrWhiteSpace(payload.TimestampUtc) + && DateTime.TryParse(payload.TimestampUtc, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed)) + { + embed["timestamp"] = parsed.ToString("O"); + } + + var fields = new JsonArray(); + foreach (var field in (payload.Fields ?? new List()).Take(MaxFieldCount)) + { + if (field == null) continue; + fields.Add(new JsonObject + { + ["name"] = Truncate(field.Name ?? "", MaxFieldNameLength), + ["value"] = Truncate(field.Value ?? "", MaxFieldValueLength), + ["inline"] = field.Inline + }); + } + if (fields.Count > 0) + embed["fields"] = fields; + + var message = new JsonObject + { + ["content"] = Truncate(contentOverride ?? payload.Title ?? "", MaxContentLength) + }; + message["embeds"] = new JsonArray { embed }; + + return message.ToJsonString(); + } + + private static string Truncate(string value, int max) + { + if (string.IsNullOrEmpty(value)) return ""; + if (value.Length <= max) return value; + + var cut = max; + if (char.IsHighSurrogate(value[cut - 1]) && cut < value.Length && char.IsLowSurrogate(value[cut])) + cut--; + return value[..cut]; + } +} diff --git a/Server/Discord/IWebhookTemplate.cs b/Server/Discord/IWebhookTemplate.cs new file mode 100644 index 0000000..68581ca --- /dev/null +++ b/Server/Discord/IWebhookTemplate.cs @@ -0,0 +1,10 @@ +using CSharpDiscordWebhook.Objects; + +namespace RadicadeServer.Radicade.Discord +{ + public interface IWebhookTemplate + { + public static MessageBuilder Construct(params object[] args) { return null!; } + public static async Task Send(params object[] args) { await RadicadeWebhook.SendMessage(Construct(args)); } + } +} diff --git a/Server/Discord/RadicadeWebhook.cs b/Server/Discord/RadicadeWebhook.cs new file mode 100644 index 0000000..f55af3b --- /dev/null +++ b/Server/Discord/RadicadeWebhook.cs @@ -0,0 +1,83 @@ +using CSharpDiscordWebhook; +using CSharpDiscordWebhook.Objects; +using System.Runtime.InteropServices; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Discord.Webhooks; + +namespace RadicadeServer.Radicade.Discord; +public static class RadicadeWebhook +{ + private static DiscordWebhook? Webhook; + + private static string WebhookUrl => WebhookConfig.GeneralActivityUrl; + + private static readonly Success WebhookInvalid = Success.Error("Webhook is disabled/invalid."); + private static readonly Success CouldntSendMessage = Success.Error("Couldn't send message."); + + public static bool IsWebhookValid() + { + return Uri.TryCreate(WebhookUrl, UriKind.Absolute, out var uri) + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); + } + + private static bool TryInit() + { + if (!IsWebhookValid()) return false; + if (Webhook != null) return true; + + try + { + Webhook = new DiscordWebhook(new Uri(WebhookUrl)); + } + catch (Exception) + { + Webhook = null; + return false; + } + + return true; + } + + public static async Task SendMessage( + [Optional] string content, + [Optional] List embed, + [Optional] PollBuilder poll, + [Optional] List attachments + ) + { + if (!TryInit()) return WebhookInvalid; + + try + { + var result = await Webhook!.SendMessageAsync(new MessageBuilder + { + Content = content, + Embeds = embed, + Poll = poll, + Attachments = attachments + }); + + return (result.Success) ? Success.Ok : CouldntSendMessage; + } + catch (Exception) + { + return CouldntSendMessage; + } + } + + public static async Task SendMessage(MessageBuilder message) + { + if (!TryInit()) return WebhookInvalid; + + try + { + var result = await Webhook!.SendMessageAsync(message); + + return (result.Success) ? Success.Ok : CouldntSendMessage; + } + catch (Exception) + { + return CouldntSendMessage; + } + } +} diff --git a/Server/Discord/ReportDiscord.cs b/Server/Discord/ReportDiscord.cs new file mode 100644 index 0000000..65e425d --- /dev/null +++ b/Server/Discord/ReportDiscord.cs @@ -0,0 +1,91 @@ +using System.Net.Http.Headers; +using System.Text; + +namespace RadicadeServer.Radicade.Discord; + + + + + + + + + + + + + +public static class ReportDiscord +{ + private static readonly HttpClient Http = CreateHttpClient(); + + private static HttpClient CreateHttpClient() + { + var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; + client.DefaultRequestHeaders.UserAgent.ParseAdd("DiscordBot (https://radicade.app, 1.0)"); + return client; + } + + + public sealed record DiscordAttachment(string Filename, byte[] Data, string MimeType); + + public static void SendAsync(string webhookUrl, string content, DiscordPayload payload) + => SendAsync(webhookUrl, content, payload, null, null); + + public static void SendAsync(string webhookUrl, string content, DiscordPayload payload, + IReadOnlyList? attachments, string? embedImageUrl = null) + { + if (string.IsNullOrWhiteSpace(webhookUrl)) return; + + _ = Task.Run(async () => + { + try + { + using var response = attachments is { Count: > 0 } + ? await PostWithFilesAsync(webhookUrl, content, payload, attachments, embedImageUrl) + : await PostJsonAsync(webhookUrl, content, payload, embedImageUrl); + + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(); + } + } + catch (Exception) + { + } + }); + } + + private static async Task PostJsonAsync(string url, string content, DiscordPayload payload, string? embedImageUrl) + { + var body = DiscordPayloadJson.Build(payload, content, embedImageUrl); + using var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Content = new StringContent(body, Encoding.UTF8, "application/json"); + return await Http.SendAsync(request); + } + + private static async Task PostWithFilesAsync(string url, string content, DiscordPayload payload, + IReadOnlyList attachments, string? embedImageUrl) + { + var json = DiscordPayloadJson.Build(payload, content, embedImageUrl); + using var form = new MultipartFormDataContent(); + form.Add(new StringContent(json, Encoding.UTF8, "application/json"), "payload_json"); + for (int i = 0; i < attachments.Count; i++) + { + var file = new ByteArrayContent(attachments[i].Data); + file.Headers.ContentType = new MediaTypeHeaderValue(attachments[i].MimeType); + form.Add(file, $"files[{i}]", attachments[i].Filename); + } + return await Http.PostAsync(url, form); + } + + private static string Truncate(string value, int max) + { + if (string.IsNullOrEmpty(value)) return ""; + if (value.Length <= max) return value; + var cut = max; + if (char.IsHighSurrogate(value[cut - 1]) && cut < value.Length && char.IsLowSurrogate(value[cut])) + cut--; + return value[..cut]; + } +} diff --git a/Server/Discord/WebhookConfig.cs b/Server/Discord/WebhookConfig.cs new file mode 100644 index 0000000..f88a1d7 --- /dev/null +++ b/Server/Discord/WebhookConfig.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using Microsoft.Extensions.Configuration; + +namespace RadicadeServer.Discord.Webhooks; + +public static class WebhookImages +{ + public const string BanAppeal = "https://cdn.radicade.app/BanAppeal.png"; + public const string AdminActivity = "https://cdn.radicade.app/AuditLogs.png"; + public const string InGameBan = "https://cdn.radicade.app/BanRecords.png"; + public const string BugReport = "https://cdn.radicade.app/BugReport.png"; + public const string ClubReport = "https://cdn.radicade.app/ClubReport.png"; + public const string PlayerReport = "https://cdn.radicade.app/PlayerReport.png"; + public const string RoomReport = "https://cdn.radicade.app/RoomReport.png"; + public const string EventReport = "https://cdn.radicade.app/EventReport.png"; + public const string InventionReport = "https://cdn.radicade.app/InventionReport.png"; + + public static string? ForReport(string? reportType) => reportType?.Trim().ToLowerInvariant() switch + { + "club" => ClubReport, + "player" => PlayerReport, + "room" => RoomReport, + "event" => EventReport, + "invention" => InventionReport, + _ => null + }; +} + +public static class WebhookConfig +{ + private const string ConfigurationPrefix = "Radicade:Discord:Webhooks:"; + + public static string BanAppealsUrl { get; private set; } = ""; + public static string AdminActivityUrl { get; private set; } = ""; + public static string InGameBansUrl { get; private set; } = ""; + public static string ReportsUrl { get; private set; } = ""; + public static string GeneralActivityUrl { get; private set; } = ""; + + public static void Load(IConfiguration configuration, string contentRootPath) + { + var fileValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + var filePath = Path.Combine(contentRootPath, "Discord", "Webhooks", "webhooks.json"); + if (File.Exists(filePath)) + { + try + { + fileValues = JsonSerializer.Deserialize>(File.ReadAllText(filePath)) + ?? fileValues; + } + catch (JsonException) + { + } + } + + BanAppealsUrl = ReadUrl("BanAppeals"); + AdminActivityUrl = ReadUrl("AdminActivity"); + InGameBansUrl = ReadUrl("InGameBans"); + ReportsUrl = ReadUrl("Reports"); + GeneralActivityUrl = ReadUrl("GeneralActivity"); + + string ReadUrl(string name) + { + var configuredValue = configuration[ConfigurationPrefix + name]; + var value = configuredValue ?? (fileValues.TryGetValue(name, out var fileValue) ? fileValue : ""); + if (string.IsNullOrWhiteSpace(value)) return ""; + + value = value.Trim(); + if (Uri.TryCreate(value, UriKind.Absolute, out var uri) + && uri.Scheme == Uri.UriSchemeHttps + && string.Equals(uri.Host, "discord.com", StringComparison.OrdinalIgnoreCase) + && uri.AbsolutePath.StartsWith("/api/webhooks/", StringComparison.Ordinal)) + { + return value; + } + return ""; + } + } +} diff --git a/Server/Discord/WebhookImageUpload.cs b/Server/Discord/WebhookImageUpload.cs new file mode 100644 index 0000000..6ee2e7b --- /dev/null +++ b/Server/Discord/WebhookImageUpload.cs @@ -0,0 +1,43 @@ +using CSharpDiscordWebhook.Objects; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace RadicadeServer.Radicade.Discord.Templates +{ + public class WebhookImageUpload : IWebhookTemplate + { + public static async Task Send(string image, string username, string displayName, string profileImage, string? roomName = null) { await RadicadeWebhook.SendMessage(Construct(image,username,displayName,profileImage,roomName!)); } + + public static MessageBuilder Construct(params object[] args) + { + if (args.Length < 4) { + throw new ArgumentException("you FUCKER i expected: image, username, display name, profile image (and probably room name)"); + } + + string image = (string)args[0]; + string username = (string)args[1]; + string displayName = (string)args[2]; + string profileImage = (string)args[3]; + string? roomName = (args.Length >= 5) ? (string)args[4] : null; + + return new MessageBuilder + { + Embeds = [ + new EmbedBuilder { + Author = new EmbedAuthorBuilder { + Name = $"@{username}", + IconUrl = profileImage + "?width=256&cropSquare=1" + }, + Color = Color.LimeGreen, + Description = (roomName == null) ? $"{displayName} just uploaded an image!" : $"{displayName} just uploaded an image in **{roomName}**!", + Title = $"New image uploaded by {displayName}!", + Image = new EmbedMediaBuilder { + Url = image, + Width = 768 + } + } + ] + }; + } + } +} diff --git a/Server/Discord/WebhookRoomPublish.cs b/Server/Discord/WebhookRoomPublish.cs new file mode 100644 index 0000000..68c426d --- /dev/null +++ b/Server/Discord/WebhookRoomPublish.cs @@ -0,0 +1,41 @@ +using CSharpDiscordWebhook.Objects; +using System.Drawing; +using System.Runtime.InteropServices; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Radicade.Discord.Templates +{ + public class WebhookRoomUpload : IWebhookTemplate + { + public static async Task Send(string image, string username, string displayName, string profileImage, string roomName, bool republish = false, string description = "None.") { await RadicadeWebhook.SendMessage(Construct(image, username, displayName, profileImage, roomName, republish, description)); } + + public static MessageBuilder Construct(string image, string username, string displayName, string profileImage, string roomName, bool republish = false, string description = "None.") + { + return new MessageBuilder + { + Embeds = [ + new EmbedBuilder { + Author = new EmbedAuthorBuilder { + Name = $"@{username}", + IconUrl = profileImage + "?width=256&cropSquare=1" + }, + Color = Color.Aqua, + Description = !republish ? $"{displayName} just published **{roomName}**!" : $"{displayName} just republished **{roomName}**!", + Title = !republish ? $"New room published by {displayName}!" : $"Room republished by {displayName}!", + Image = new EmbedMediaBuilder { + Url = image + "?width=768", + Width = 768 + }, + Fields = [ + new EmbedFieldBuilder { + Inline = false, + Name = "Description", + Value = description + } + ] + } + ] + }; + } + } +} diff --git a/Server/Discord/Webhooks/webhooks.example.json b/Server/Discord/Webhooks/webhooks.example.json new file mode 100644 index 0000000..ce2c4fe --- /dev/null +++ b/Server/Discord/Webhooks/webhooks.example.json @@ -0,0 +1,8 @@ +{ + "BanAppeals": "", + "AntiCheat": "", + "AdminActivity": "", + "InGameBans": "", + "Reports": "", + "GeneralActivity": "" +} diff --git a/Server/Extensions/EnumExtension.cs b/Server/Extensions/EnumExtension.cs new file mode 100644 index 0000000..0c3fb5f --- /dev/null +++ b/Server/Extensions/EnumExtension.cs @@ -0,0 +1,31 @@ +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.Extensions +{ + public static class EnumExtension + { + public static DeviceClass GetDeviceClass(this Platforms platform) + { + switch (platform) + { + case Platforms.All: + return DeviceClass.Unknown; + case Platforms.Steam: + return DeviceClass.Screen; + case Platforms.PlayStation: + return DeviceClass.Screen; + case Platforms.HeadlessBot: + return DeviceClass.Unknown; + case Platforms.GooglePlay: + return DeviceClass.Screen; + case Platforms.IOS: + return DeviceClass.Screen; + case Platforms.Xbox: + return DeviceClass.Screen; + case Platforms.Oculus: + return DeviceClass.VRLow; + } + return DeviceClass.Unknown; + } + } +} diff --git a/Server/Extensions/JsonExtension.cs b/Server/Extensions/JsonExtension.cs new file mode 100644 index 0000000..8998e36 --- /dev/null +++ b/Server/Extensions/JsonExtension.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace RadicadeServer.Extensions +{ + public static class JsonExtension + { + public static T? FromJson(this string json) + { + return JsonSerializer.Deserialize(json); + } + public static T? FromJson(this string json, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(json, options); + } + public static string ToJson(this object val) + { + return JsonSerializer.Serialize(val); + } + + public static T Get(this JsonNode node, string key, T fallback = default!) + { + try + { + var value = node?[key]; + + if (value is null) + return fallback; + + if (value is JsonArray or JsonObject) + return value.Deserialize() ?? fallback; + + var kind = value.GetValueKind(); + + if (typeof(T) == typeof(bool)) + { + if (kind == JsonValueKind.String) + { + var text = value.GetValue(); + return (T)(object)(text == "1" || text.Equals("true", StringComparison.OrdinalIgnoreCase)); + } + + if (kind == JsonValueKind.Number) + return (T)(object)(value.GetValue() != 0); + } + + if (typeof(T).IsEnum) + { + if (kind == JsonValueKind.String) + return (T)Enum.Parse(typeof(T), value.GetValue(), ignoreCase: true); + + if (kind == JsonValueKind.Number) + return (T)Enum.ToObject(typeof(T), Convert.ChangeType(value.GetValue(), Enum.GetUnderlyingType(typeof(T)))); + } + + return value.GetValue(); + } + catch + { + return fallback; + } + } + } +} diff --git a/Server/Extensions/UserExtension.cs b/Server/Extensions/UserExtension.cs new file mode 100644 index 0000000..e5fdedb --- /dev/null +++ b/Server/Extensions/UserExtension.cs @@ -0,0 +1,61 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using RadicadeServer.DB; + +namespace RadicadeServer.Extensions +{ + public static class UserExtension + { + public static long? GetUserId(this ClaimsPrincipal req) + { + string? userIdStr = req.FindFirstValue(ClaimTypes.NameIdentifier); + if (userIdStr == null) return null; + return long.Parse(userIdStr); + } + + public static string? GetPlatId(this ClaimsPrincipal req) + { + string? userIdStr = req.FindFirstValue("rn.platid"); + return userIdStr; + } + + + + + + + public static long? GetUserId(this string req) + { + try + { + if (string.IsNullOrWhiteSpace(req)) return null; + string token = req.Trim(); + + + if (token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + token = token.Substring(7).Trim(); + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(DBConfig.JwtKey); + + var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + }, out _); + + var tokenClaim = principal.Claims.FirstOrDefault(claim => claim.Type == "userId")?.Value; + if (tokenClaim == null) return null; + return long.Parse(tokenClaim); + } + catch + { + return null; + } + } + } +} \ No newline at end of file diff --git a/Server/Gift/CardBoxWord.cs b/Server/Gift/CardBoxWord.cs new file mode 100644 index 0000000..71cf2f8 --- /dev/null +++ b/Server/Gift/CardBoxWord.cs @@ -0,0 +1,21 @@ +namespace RadicadeServer.RecNet +{ + public enum CardType + { + Charades, + CharadesAprilFoolsDay, + Icebreakers + } + public enum CardDifficulty + { + CharadesEasy, + CharadesHard, + CharadesStupidHard = 10, + Icebreaks = 20 + } + public class CardBoxWord + { + public string EN_US { get; set; } = string.Empty; + public CardDifficulty Difficulty { get; set; } = CardDifficulty.CharadesEasy; + } +} diff --git a/Server/Gift/FriendotronService.cs b/Server/Gift/FriendotronService.cs new file mode 100644 index 0000000..50c0307 --- /dev/null +++ b/Server/Gift/FriendotronService.cs @@ -0,0 +1,62 @@ +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.Utils; + +public static class FriendotronService +{ + public const string DefaultMessage = "A gift for you <3"; + + public static readonly TimeSpan Cooldown = TimeSpan.FromHours(24); + + public static readonly Rarity[] StarTiers = { Rarity.Common, Rarity.Uncommon, Rarity.Rare }; + + public const int MaxRecipients = 50; + + public static string CooldownKey(long accountId) => $"Friendotron.LastSent.{accountId}"; + + public static string MessageOr(string? message) => + string.IsNullOrWhiteSpace(message) ? DefaultMessage : message.Trim(); + + public static Rarity RollStarTier(Random random) => StarTiers[random.Next(StarTiers.Length)]; + + public static ConsumableItem? RollConsumable(string? catalogJson, out Rarity starTier, Random? random = null) + { + var rng = random ?? Random.Shared; + var tier = RollStarTier(rng); + starTier = tier; + + var catalogue = ConsumableCatalog.ParseItems(catalogJson) + .Where(item => !string.IsNullOrWhiteSpace(item.ConsumableItemDesc)) + .ToList(); + if (catalogue.Count == 0) return null; + + var ofTier = catalogue.Where(item => item.Rarity == tier).ToList(); + var pool = ofTier.Count > 0 ? ofTier : catalogue; + return pool[rng.Next(pool.Count)]; + } + + public static Gift BuildBox(long senderId, string? message, ConsumableItem item, Rarity starTier) => new() + { + Context = GiftContext.Friendotron_Gift, + FromPlayerId = senderId, + Rarity = starTier, + FriendlyName = item.FriendlyName, + Tooltip = MessageOr(message), + ConsumableItemDesc = ConsumableCatalog.ToClientDesc(item.ConsumableItemDesc) + }; + + public static string FormatRemaining(DateTime nowUtc, DateTime lastSentUtc) + { + var remaining = Cooldown - (nowUtc - lastSentUtc); + if (remaining <= TimeSpan.Zero) return "0 minutes"; + + var hours = (int)remaining.TotalHours; + var minutes = remaining.Minutes; + if (hours <= 0) return minutes <= 1 ? "1 minute" : $"{minutes} minutes"; + + var hourText = hours == 1 ? "1 hour" : $"{hours} hours"; + if (minutes == 0) return hourText; + return $"{hourText} {minutes} minutes"; + } +} diff --git a/Server/Gift/GameRewardRedemption.cs b/Server/Gift/GameRewardRedemption.cs new file mode 100644 index 0000000..a28babc --- /dev/null +++ b/Server/Gift/GameRewardRedemption.cs @@ -0,0 +1,213 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Progression; + +namespace RadicadeServer.Utils +{ + public static class GameRewardRedemption + { + public static async Task TakeOutstandingAsync( + RecDBContext db, long playerId, GiftContext context, string? message = null, Action? trace = null) + { + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + var json = data?.GetProgressionJson(); + var pending = json?.PendingGameRewards; + + var outstanding = pending?.Where(r => !r.Consumed).ToList() ?? new List(); + trace?.Invoke($"stage=OutstandingReward claimed={RewardTrace.Context(context)} " + + $"rewards={pending?.Count ?? 0} outstanding={outstanding.Count}"); + + if (data == null || json == null || outstanding.Count == 0) return null; + + foreach (var candidate in outstanding) + { + var held = candidate.GiftDrop1 ?? candidate.GiftDrop2 ?? candidate.GiftDrop3; + trace?.Invoke($"stage=OutstandingCandidate selectionId={candidate.RewardSelectionId} " + + $"rewardType={candidate.RewardType} context={RewardTrace.Context(candidate.GiftContext)} " + + $"box={RewardTrace.Box(held)}"); + } + + var reward = outstanding.FirstOrDefault(r => r.GiftContext == context); + var matchedBy = reward == null ? null : "context"; + + if (reward == null && RroRewardPools.RoomForContext(context) is { } room) + { + reward = outstanding.FirstOrDefault(r => RroRewardPools.RoomForContext(r.GiftContext) == room); + matchedBy = reward == null ? null : $"room={room}"; + } + + if (reward == null) + { + reward = outstanding[0]; + matchedBy = "oldest outstanding"; + } + + var drop = reward.GiftDrop1 ?? reward.GiftDrop2 ?? reward.GiftDrop3; + if (drop == null) + { + trace?.Invoke($"stage=OutstandingRewardUnusable selectionId={reward.RewardSelectionId} " + + "has no gift drop at all; nothing can be handed over from it."); + return null; + } + + var gift = drop.Clone(); + if (!string.IsNullOrWhiteSpace(message)) gift.Tooltip = message; + if (gift.GiftDropId == 0) gift.GiftDropId = StorefrontShared.NewGiftDropId(); + + reward.Consumed = true; + reward.ConsumedGiftDropId = gift.GiftDropId; + reward.ConsumedAt = DateTime.UtcNow; + + data.Progress = json.ToJson(); + await db.SaveChangesAsync(); + + trace?.Invoke($"stage=OutstandingRewardMatched by={matchedBy} selectionId={reward.RewardSelectionId} " + + $"rewardType={reward.RewardType} context={RewardTrace.Context(reward.GiftContext)} " + + $"box={RewardTrace.Box(gift)}"); + + return gift; + } + + public static async Task BuildRoomRewardAsync( + RecDBContext db, long playerId, GiftContext context, string? message = null, + Action? trace = null, string? catalogueJson = null) + { + var room = RroRewardPools.RoomForContext(context); + if (room == null) return null; + + var owned = await LevelUpGiftService.CollectOwnedKeysAsync(db, playerId, context); + + Gift? box = RroRewardPools.BuildGift(context, owned, catalogueJson: catalogueJson); + var paidFrom = context; + + if (box == null) + { + foreach (var weaker in RroRewardPools.LowerRanksOf(context)) + { + box = RroRewardPools.BuildGift(weaker, owned, catalogueJson: catalogueJson); + if (box == null) continue; + + paidFrom = weaker; + trace?.Invoke($"stage=RoomBoxFallbackRank context={RewardTrace.Context(context)} " + + $"nothing left unowned, so the same quest's {RewardTrace.Context(weaker)} is what it pays."); + break; + } + } + + if (box == null) + { + box = RroRewardPools.BuildFallbackDrops(selection: false, owned: owned).FirstOrDefault(); + if (box != null) + { + paidFrom = RroRewardPools.ReacquisitionContext; + trace?.Invoke($"stage=RoomBoxFallbackItem context={RewardTrace.Context(context)} " + + "every reward the activity pays is owned; paying the non-exclusive item pool."); + } + } + + if (box == null) return null; + + box.GiftDropId = StorefrontShared.NewGiftDropId(); + if (!string.IsNullOrWhiteSpace(message)) box.Tooltip = message; + + trace?.Invoke($"stage=RoomBox activity={room} context={RewardTrace.Context(context)} " + + $"paidFrom={RewardTrace.Context(paidFrom)} box={RewardTrace.Box(box)}"); + + await RememberHandoverAsync(db, playerId, context, box, trace); + return box; + } + + public static async Task RememberHandoverAsync( + RecDBContext db, long playerId, GiftContext context, Gift box, Action? trace = null) + { + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + if (data == null) + { + data = new ProgressionData { PlayerId = playerId }; + db.Progression.Add(data); + } + + var json = data.GetProgressionJson() ?? new ProgressionJson(); + json.PendingGameRewards ??= new List(); + json.PendingGameRewards.Add(new RecNet.Storefront.GameReward + { + RewardSelectionId = StorefrontShared.NewGiftDropId(), + Message = box.Tooltip ?? string.Empty, + GiftContext = context, + RewardType = RroRewardPools.IsQuestContext(context) + ? RewardType.PostQuestActivity + : RewardType.PostGameActivity, + GiftDrop1 = box.Clone(), + CreatedAt = DateTime.UtcNow, + Consumed = true, + ConsumedGiftDropId = box.GiftDropId, + ConsumedAt = DateTime.UtcNow + }); + + data.Progress = json.ToJson(); + await db.SaveChangesAsync(); + + trace?.Invoke($"stage=HandoverRecorded context={RewardTrace.Context(context)} " + + $"box={RewardTrace.Box(box)} the completion is recorded as delivered, so a later request " + + "for it cannot pay a second box."); + } + + public static async Task TakePendingBoxAsync( + RecDBContext db, long playerId, GiftContext context, Action? trace = null) + { + var room = RroRewardPools.RoomForContext(context); + if (room == null || !RroRewardPools.IsRewardActivity(room)) return null; + + var boxes = await StorefrontShared.GetPendingGiftsAsync(db, playerId); + var box = boxes.FirstOrDefault(b => b.Context == context) + ?? boxes.FirstOrDefault(b => RroRewardPools.RoomForContext(b.Context) == room); + + trace?.Invoke($"stage=PendingBox claimed={RewardTrace.Context(context)} room={room} " + + $"held={boxes.Count} match={RewardTrace.Box(box)}"); + + if (box == null) return null; + + var gift = box.Clone(); + if (gift.GiftDropId == 0) gift.GiftDropId = StorefrontShared.NewGiftDropId(); + return gift; + } + + public static async Task TakeRecentlyHandedOverAsync( + RecDBContext db, long playerId, GiftContext context, TimeSpan within, Action? trace = null) + { + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + var json = data?.GetProgressionJson(); + var pending = json?.PendingGameRewards; + if (pending == null || pending.Count == 0) return null; + + var cutoff = DateTime.UtcNow - within; + var taken = pending + .Where(r => r.Consumed && r.ConsumedAt is { } at && at >= cutoff) + .Where(r => r.GiftContext == context + || (RroRewardPools.RoomForContext(context) is { } room + && RroRewardPools.RoomForContext(r.GiftContext) == room)) + .OrderByDescending(r => r.ConsumedAt) + .FirstOrDefault(); + + if (taken == null) + { + trace?.Invoke($"stage=HandedOver claimed={RewardTrace.Context(context)} none within {within}."); + return null; + } + + var drop = taken.GiftDrop1 ?? taken.GiftDrop2 ?? taken.GiftDrop3; + if (drop == null) return null; + + var gift = drop.Clone(); + gift.GiftDropId = taken.ConsumedGiftDropId != 0 ? taken.ConsumedGiftDropId : gift.GiftDropId; + trace?.Invoke($"stage=HandedOver claimed={RewardTrace.Context(context)} " + + $"selectionId={taken.RewardSelectionId} box={RewardTrace.Box(gift)} - answering the retry " + + "with the box it was given."); + return gift; + } + } +} diff --git a/Server/Gift/ItemAccessibility.cs b/Server/Gift/ItemAccessibility.cs new file mode 100644 index 0000000..4cbae32 --- /dev/null +++ b/Server/Gift/ItemAccessibility.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.Radicade.Gifts.Enums; + +public enum ItemAccessibility +{ + Private = -1, + Exclusive, + SubscribersOnly, + DevelopersOnly, + Purchasable = 10, +} \ No newline at end of file diff --git a/Server/Gift/QuestRewardDelivery.cs b/Server/Gift/QuestRewardDelivery.cs new file mode 100644 index 0000000..1546193 --- /dev/null +++ b/Server/Gift/QuestRewardDelivery.cs @@ -0,0 +1,92 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.Utils +{ + public static class QuestRewardDelivery + { + private const string ManualConsumedNotificationId = "gift.manualconsumed"; + + public static async Task DeliverOnArrivalAsync(RecDBContext db, long playerId, long enteredRoomId) + { + try + { + return await DeliverAsync(db, playerId, enteredRoomId); + } + catch (Exception) + { + return 0; + } + } + + private static async Task DeliverAsync(RecDBContext db, long playerId, long enteredRoomId) + { + var boxes = await StorefrontShared.GetPendingGiftsAsync(db, playerId); + if (!boxes.Any(box => RroRewardPools.IsQuestContext(box.Context))) return 0; + + var roomName = await db.Rooms + .AsNoTracking() + .Where(x => x.RoomId == enteredRoomId) + .Select(x => x.Name) + .FirstOrDefaultAsync(); + + var openableHere = RroRewardPools.ContextsForRoom(roomName).Values.ToList(); + + var stranded = boxes + .Where(box => RroRewardPools.IsQuestContext(box.Context) && !openableHere.Contains(box.Context)) + .ToList(); + + if (stranded.Count == 0) return 0; + + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + var json = progression?.GetProgressionJson(); + var delivered = 0; + + foreach (var box in stranded) + { + var gift = box.Clone(); + if (gift.GiftDropId == 0) gift.GiftDropId = StorefrontShared.NewGiftDropId(); + + var grant = await StorefrontShared.GrantGiftAsync(db, playerId, gift); + await StorefrontShared.RemovePendingGiftAsync(db, playerId, box.GiftDropId); + + var source = json?.PendingGameRewards?.FirstOrDefault(r => + r.ConsumedGiftDropId == box.GiftDropId + || (!r.Consumed && r.GiftContext == box.Context)); + if (source != null && !source.Consumed) + { + source.Consumed = true; + source.ConsumedGiftDropId = box.GiftDropId; + source.ConsumedAt = DateTime.UtcNow; + } + + delivered++; + + RewardTrace.Log(playerId, $"stage=DeliveredOnLeaving activity={RroRewardPools.RoomForContext(box.Context) ?? ""} " + + $"entered={roomName ?? ""} box={RewardTrace.Box(gift)} " + + $"selectionId={source?.RewardSelectionId.ToString() ?? ""} " + + "the quest box could no longer be opened, so the reward it already held was granted to the locker."); + + if (grant != null) + await StorefrontShared.SendBalanceUpdateAsync(playerId, grant.CurrencyType, grant.Balance, + grant.Granted, StorefrontBalanceAddTypes.FromGiftBox); + + await Notify.SendNotify(playerId, new Notification + { + Id = ManualConsumedNotificationId, + Msg = gift.ForClient() + }); + } + + if (progression != null && json != null) progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + return delivered; + } + } +} diff --git a/Server/Gift/RoomJoinGiftUtils.cs b/Server/Gift/RoomJoinGiftUtils.cs new file mode 100644 index 0000000..2264eb0 --- /dev/null +++ b/Server/Gift/RoomJoinGiftUtils.cs @@ -0,0 +1,227 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Utils +{ + public static class RoomJoinGiftUtils + { + private const string ClaimKeyPrefix = "Radicade.RoomJoinGift."; + + /// + /// Whether a room's own Rooms row describes a box at all. A room can hold several boxes in + /// RoomJoinGifts; this answers for the single one that row still carries, which is + /// the whole story for a room saved before that table existed. + /// + public static bool HasConfiguredReward(RoomData room) + { + return room.JoinGiftCurrencyAmount > 0 || + room.JoinGiftXp > 0 || + !string.IsNullOrWhiteSpace(room.JoinGiftAvatarItem) || + !string.IsNullOrWhiteSpace(room.JoinGiftEquipmentItem) || + !string.IsNullOrWhiteSpace(room.JoinGiftConsumableItem); + } + + public static bool HasConfiguredReward(RoomJoinGift gift) + { + return gift.CurrencyAmount > 0 || + gift.Xp > 0 || + !string.IsNullOrWhiteSpace(gift.AvatarItem) || + !string.IsNullOrWhiteSpace(gift.EquipmentItem) || + !string.IsNullOrWhiteSpace(gift.ConsumableItem); + } + + /// + /// The single box a room's own row describes, as a gift row. Used for a room that has no + /// rows in RoomJoinGifts: it is the same box, read from where it has always lived. + /// + public static RoomJoinGift Snapshot(RoomData room) + { + return new RoomJoinGift + { + RoomJoinGiftId = 0, + RoomId = room.RoomId, + SortOrder = 0, + Version = room.JoinGiftVersion, + Enabled = room.JoinGiftEnabled, + OncePerPlayer = room.JoinGiftOncePerPlayer, + Title = room.JoinGiftTitle, + Message = room.JoinGiftMessage, + AvatarItem = room.JoinGiftAvatarItem, + AvatarItemType = room.JoinGiftAvatarItemType, + EquipmentItem = room.JoinGiftEquipmentItem, + EquipmentModificationGuid = room.JoinGiftEquipmentModificationGuid, + ConsumableItem = room.JoinGiftConsumableItem, + CurrencyType = room.JoinGiftCurrencyType, + CurrencyAmount = room.JoinGiftCurrencyAmount, + Xp = room.JoinGiftXp, + Context = room.JoinGiftContext, + Rarity = room.JoinGiftRarity, + FromPlayerId = room.JoinGiftFromPlayerId + }; + } + + /// The boxes a room hands out, in the order the panel lists them. + public static async Task> ConfiguredGiftsAsync(RecDBContext ctx, long roomId) + { + return await ctx.RoomJoinGifts + .Where(x => x.RoomId == roomId) + .OrderBy(x => x.SortOrder) + .ThenBy(x => x.RoomJoinGiftId) + .ToListAsync(); + } + + /// + /// Hands every box this room is configured to give to the player. A room with several + /// boxes gives several; a box the player has already claimed under its current version is + /// skipped, and each box is claimed on its own so changing one does not silence the rest. + /// + public static async Task TryDeliverAsync(RecDBContext ctx, long playerId, RoomData room) + { + // The room's own switch stays the master: a room that is not handing anything out + // hands out nothing, whichever boxes its list happens to hold. + if (!room.JoinGiftEnabled) + { + return false; + } + + var configured = await ConfiguredGiftsAsync(ctx, room.RoomId); + + // A room with no list still has the one box its Rooms row describes. + var boxes = configured.Count > 0 + ? configured + : [Snapshot(room)]; + + var delivered = false; + foreach (var box in boxes) + { + if (!box.Enabled) + { + continue; + } + + if (!HasConfiguredReward(box)) + { + continue; + } + + var claimKey = ClaimKey(room.RoomId, box); + if (box.OncePerPlayer && await HasClaimedAsync(ctx, playerId, claimKey)) + { + continue; + } + + var giftBox = new Gift + { + GiftDropId = NewGiftDropId(room.RoomId, box.Version, box.RoomJoinGiftId), + ItemSetId = 0, + ItemSetFriendlyName = room.Name, + FriendlyName = string.IsNullOrWhiteSpace(box.Title) ? "Room Visit Gift" : box.Title, + Tooltip = box.Message ?? "", + IsQuery = false, + Unique = false, + SubscribersOnly = false, + ConsumableItemDesc = box.ConsumableItem ?? "", + AvatarItemDesc = box.AvatarItem ?? "", + AvatarItemType = box.AvatarItemType, + EquipmentPrefabName = box.EquipmentItem ?? "", + EquipmentModificationGuid = box.EquipmentModificationGuid ?? "", + CurrencyType = box.CurrencyType, + Currency = Math.Max(0, box.CurrencyAmount), + Xp = Math.Max(0, box.Xp), + Context = box.Context, + Rarity = box.Rarity, + // The box names its sender to the client, which shows it on the box; a room that + // names no sender leaves it at zero rather than inventing one. + FromPlayerId = Math.Max(0, box.FromPlayerId) + }; + + await StorefrontShared.PersistGiftBoxAsync(ctx, playerId, giftBox); + + if (box.OncePerPlayer) + { + await MarkClaimedAsync(ctx, playerId, claimKey); + } + + delivered = true; + } + + return delivered; + } + + public static async Task TryDeliverAsync(RecDBContext ctx, long playerId, long roomId) + { + var room = await ctx.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) + { + return false; + } + + return await TryDeliverAsync(ctx, playerId, room); + } + + /// + /// A gift drop id for one box. Two boxes handed out in the same millisecond must not share + /// an id - the player's boxes are a list keyed by it, so the second would overwrite the + /// first - so a listed box also mixes in which row it came from. A room's own single box + /// keeps the id shape it has always had. + /// + private static long NewGiftDropId(long roomId, int version, long giftId) + { + var timePart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() % 1_000_000_000; + var roomPart = Math.Abs(roomId % 100_000); + var versionPart = Math.Abs(version % 1_000); + + if (giftId <= 0) + return 9_000_000_000_000 + roomPart * 10_000_000 + versionPart * 10_000 + timePart % 10_000; + + var giftPart = giftId % 1_000; + return 9_000_000_000_000 + roomPart * 1_000_000_000 + giftPart * 1_000_000 + versionPart * 1_000 + timePart % 1_000; + } + + /// + /// The key a player's once-per-player claim is remembered under. A room's own single box + /// keeps the key it has always used, so this change hands nobody a second copy of a box + /// they already claimed; a listed box adds its row and version. + /// + private static string ClaimKey(long roomId, RoomJoinGift box) + { + if (box.RoomJoinGiftId <= 0) + return $"{ClaimKeyPrefix}{roomId}.{box.Version}"; + + return $"{ClaimKeyPrefix}{roomId}.{box.RoomJoinGiftId}.{box.Version}"; + } + + private static async Task HasClaimedAsync(RecDBContext ctx, long playerId, string key) + { + var settings = await ctx.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (settings == null) return false; + + return settings.DeserializeSettings().Any(x => x.Key == key && x.Value == "1"); + } + + private static async Task MarkClaimedAsync(RecDBContext ctx, long playerId, string key) + { + var settings = await ctx.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (settings == null) + { + settings = new PlayerSettings { AccountId = playerId }; + await ctx.PlayerSettings.AddAsync(settings); + } + + var list = settings.DeserializeSettings(); + var existing = list.FirstOrDefault(x => x.Key == key); + if (existing == null) + list.Add(new Setting { Key = key, Value = "1" }); + else + existing.Value = "1"; + + settings.Settings = list.ToJson(); + } + } +} diff --git a/Server/Gift/RroRewardPools.cs b/Server/Gift/RroRewardPools.cs new file mode 100644 index 0000000..09b2212 --- /dev/null +++ b/Server/Gift/RroRewardPools.cs @@ -0,0 +1,643 @@ +using System.Collections.Concurrent; +using System.Text.RegularExpressions; +using RadicadeServer.API.Storefront; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.Utils +{ + public static class RroRewardPools + { + public sealed record ItemSet(string Family, string Colour); + + public const string Unranked = ""; + + public const int SelectionSize = 3; + + public static readonly string[] QuestRanks = ["C", "B", "A", "S"]; + + + private static readonly Dictionary Pools = new() + { + [GiftContext.Quest_Goblin_C] = + [ + new("Archer", "Tan"), new("Archer", "Yellow"), + new("Knight", "Blue"), new("Knight", "Grey"), + ], + [GiftContext.Quest_Goblin_B] = + [ + new("Archer", "Red"), + new("Knight", "Cherry"), + ], + [GiftContext.Quest_Goblin_A] = + [ + new("Archer", "Blue"), new("Archer", "Green"), + new("Knight", "White"), new("Knight", "Yellow"), + ], + [GiftContext.Quest_Goblin_S] = + [ + new("Archer", "Black"), + new("Knight", "Black"), + ], + + [GiftContext.Quest_SciFi_C] = + [ + new("Space Marine", "Blue"), new("Space Marine", "Orange"), + new("Smuggler", "Brown"), new("Smuggler", "Green"), + ], + [GiftContext.Quest_SciFi_B] = + [ + new("Space Marine", "Pink"), new("Space Marine", "Red"), + new("Smuggler", "Cherry"), new("Smuggler", "Yellow"), + ], + [GiftContext.Quest_SciFi_A] = + [ + new("Space Marine", "Purple"), new("Space Marine", "White"), + new("Smuggler", "Grey"), new("Smuggler", "Orange"), + ], + [GiftContext.Quest_SciFi_S] = + [ + new("Space Marine", "Black"), + new("Smuggler", "Black"), + ], + + [GiftContext.Quest_Cauldron_C] = + [ + new("Witch Hunter", "Purple"), new("Witch Hunter", "Green"), + new("Wizard", "Blue"), new("Wizard", "Green"), + ], + [GiftContext.Quest_Cauldron_B] = + [ + new("Witch Hunter", "Red"), + new("Wizard", "Red"), + ], + [GiftContext.Quest_Cauldron_A] = + [ + new("Witch Hunter", "White"), + new("Wizard", "White"), + ], + [GiftContext.Quest_Cauldron_S] = + [ + new("Witch Hunter", "Black"), + new("Wizard", "Black"), new("Wizard", "Yellow"), + ], + + [GiftContext.Quest_Dracula1_C] = [new("Vampire Hunter", "Blue")], + [GiftContext.Quest_Dracula1_B] = [new("Vampire Hunter", "Red")], + [GiftContext.Quest_Dracula1_A] = [new("Vampire Hunter", "Leather")], + [GiftContext.Quest_Dracula1_S] = [new("Vampire Hunter", "Midnight")], + [GiftContext.Quest_Dracula1_SS] = [new("Dracula", "")], + + [GiftContext.StuntRunner_TheMainEvent_C] = + [ + new("Runner", "Green"), new("Runner", "Yellow"), + ], + [GiftContext.StuntRunner_TheMainEvent_B] = + [ + new("Runner", "Purple"), new("Runner", "Black"), + ], + [GiftContext.StuntRunner_TheMainEvent_A] = + [ + new("Stunt", "Fuchsia"), new("Stunt", "Blue"), + ], + [GiftContext.StuntRunner_TheMainEvent_S] = + [ + new("Stunt", "Green"), new("Stunt", "Red"), + ], + [GiftContext.StuntRunner_TheMainEvent_SS] = [new("Feathered Hair", "")], + + [GiftContext.Quest_Pirate1_C] = + [ + new("Scallywag Bracers", "Brown"), new("Scallywag Belt", "Brown"), + new("Scallywag Bracers", "Red"), new("Scallywag Belt", "Red"), + new("Pirate Captain Cuffs", "Green"), new("Pirate Captain Belt", "Green"), + ], + [GiftContext.Quest_Pirate1_B] = + [ + new("Scallywag Belt", "Brown"), new("Scallywag Vest", "Brown"), + new("Scallywag Belt", "Red"), new("Scallywag Vest", "Red"), + new("Pirate Captain Belt", "Red"), new("Pirate Captain Jacket", "Red"), + new("Pirate Captain Belt", "Blue"), new("Pirate Captain Jacket", "Blue"), + ], + [GiftContext.Quest_Pirate1_A] = + [ + new("Scallywag Vest", "Brown"), new("Scallywag Hat", "Brown"), + new("Scallywag Vest", "Red"), new("Scallywag Hat", "Red"), + new("Pirate Captain Jacket", "Red"), new("Pirate Captain Hat", "Red"), + new("Pirate Captain Jacket", "Blue"), new("Pirate Captain Hat", "Blue"), + ], + [GiftContext.Quest_Pirate1_S] = + [ + new("Pirate Captain Cuffs", "Gold"), new("Pirate Captain Belt", "Gold"), + new("Pirate Captain Jacket", "Gold"), new("Pirate Captain Hat", "Gold"), + new("Pirate Eyepatch", "Black"), new("Pirate Eyepatch", "Skull"), + ], + + [GiftContext.Paintball_ClearCut] = [new("Lumberjack", "")], + [GiftContext.Paintball_Homestead] = [new("Farmer Hat", ""), new("Farmer Overalls", "")], + [GiftContext.Paintball_Quarry] = [new("Construction", "")], + [GiftContext.Paintball_River] = [new("Angler", "")], + [GiftContext.Paintball_Dam] = [new("Electrician", "")], + [GiftContext.Paintball_DriveIn] = [new("Mechanic", "")], + [GiftContext.Discgolf_Propulsion] = [new("Scientist", "")], + [GiftContext.Discgolf_Lake] = + [ + new("Golfer Hat", "Tan"), new("Golfer Hat", "Blue"), new("Golfer Hat", "Grey"), + new("Golfer Shirt", "Tan"), new("Golfer Shirt", "Blue"), new("Golfer Shirt", "Grey"), + new("Golfer Gloves", "Tan"), new("Golfer Gloves", "Blue"), new("Golfer Gloves", "Grey"), + ], + [GiftContext.Charades] = [new("Beret", ""), new("Artist Beret", "")], + [GiftContext.Soccer] = [new("Soccer", "")], + [GiftContext.Paddleball] = [new("Tennis", "")], + [GiftContext.Dodgeball] = [new("Dodgeball Shirt", "")], + [GiftContext.Lasertag] = [], + [GiftContext.Bowling] = [new("Dude Sweater", "")], + [GiftContext.RecRoyale] = [], + }; + + public static readonly IReadOnlyList Unresolved = + [ + "Crimson Cauldron S: the Brown Witch Hunter set - the catalogue's Witch Hunter pieces come in black, blue, green, leather, purple, red and white, and none in brown.", + "Isle of Lost Skulls X: no special-tier reward is named.", + "Isle of Lost Skulls B and A: the verified tables name a Pirate Captain Belt (Blue) for B and a Pirate Captain Hat (Blue) for A. The catalogue carries the Captain's blue jacket but no blue belt or hat, so those two pieces are not paid; the rest of each rank's table is.", + "Crimson Cauldron S: the Brown Witch Hunter is a real S-rank colour, but no item in the shipped catalogue carries it - its Witch Hunter pieces are black, blue, green, leather, purple, red and white. The descriptor or the in-game name is needed to name it here.", + "Rec Royale: its Season 1 rewards came from the Frontier Pass, a progression system of its own, so a completed match draws nothing. The Pass itself is not implemented.", + "Crescendo X: no special-tier reward is named.", + "Stunt Runner D and X: no reward is named for either tier, and the catalogue has no item for one.", + "Rec Royale: no concrete reward item is named.", + "Laser Tag: not a random-drop activity at all - a completed game pays tickets, and the shop they are spent in is not implemented yet.", + "Dodgeball: the definitions call the set the Dodger Vest in cyan, purple and orange; the catalogue carries the same three pieces as Dodgeball Shirt in orange, purple and teal.", + ]; + + public static readonly IReadOnlyList Deprecated = + [ + GiftContext.DEPRECATED_Paintball_ClearCut, + GiftContext.DEPRECATED_Paintball_Homestead, + GiftContext.DEPRECATED_Paintball_Quarry, + GiftContext.DEPRECATED_Paintball_River, + GiftContext.DEPRECATED_Paintball_Dam, + GiftContext.DEPRECATED_Paintball_DriveIn, + GiftContext.DEPRECATED_Discgolf_Propulsion, + GiftContext.DEPRECATED_Discgolf_Lake, + GiftContext.DEPRECATED_Charades, + GiftContext.DEPRECATED_Soccer, + GiftContext.DEPRECATED_Paddleball, + GiftContext.DEPRECATED_Dodgeball, + GiftContext.DEPRECATED_Lasertag, + GiftContext.DEPRECATED_Bowling, + ]; + + public static bool IsDeprecated(GiftContext context) => Deprecated.Contains(context); + + public static IReadOnlyCollection RewardableContexts => Pools.Keys; + + public static IReadOnlyList SetsFor(GiftContext context) => + Pools.TryGetValue(context, out var sets) ? sets : []; + + + private static readonly Dictionary> ByRoom = + new(StringComparer.OrdinalIgnoreCase) + { + ["GoldenTrophy"] = Ranks( + ("C", GiftContext.Quest_Goblin_C), ("B", GiftContext.Quest_Goblin_B), + ("A", GiftContext.Quest_Goblin_A), ("S", GiftContext.Quest_Goblin_S)), + ["TheRiseofJumbotron"] = Ranks( + ("C", GiftContext.Quest_SciFi_C), ("B", GiftContext.Quest_SciFi_B), + ("A", GiftContext.Quest_SciFi_A), ("S", GiftContext.Quest_SciFi_S)), + ["CrimsonCauldron"] = Ranks( + ("C", GiftContext.Quest_Cauldron_C), ("B", GiftContext.Quest_Cauldron_B), + ("A", GiftContext.Quest_Cauldron_A), ("S", GiftContext.Quest_Cauldron_S)), + ["Crescendo"] = Ranks( + ("C", GiftContext.Quest_Dracula1_C), ("B", GiftContext.Quest_Dracula1_B), + ("A", GiftContext.Quest_Dracula1_A), ("S", GiftContext.Quest_Dracula1_S)), + ["IsleOfLostSkulls"] = Ranks( + ("C", GiftContext.Quest_Pirate1_C), ("B", GiftContext.Quest_Pirate1_B), + ("A", GiftContext.Quest_Pirate1_A), ("S", GiftContext.Quest_Pirate1_S)), + ["StuntRunner"] = Ranks( + ("C", GiftContext.StuntRunner_TheMainEvent_C), + ("B", GiftContext.StuntRunner_TheMainEvent_B), + ("A", GiftContext.StuntRunner_TheMainEvent_A), + ("S", GiftContext.StuntRunner_TheMainEvent_S), + ("SS", GiftContext.StuntRunner_TheMainEvent_SS), + ("S+", GiftContext.StuntRunner_TheMainEvent_SS), + ("SPlus", GiftContext.StuntRunner_TheMainEvent_SS)), + + ["PaintballClearcut"] = UnrankedContext(GiftContext.Paintball_ClearCut), + ["Clearcut"] = UnrankedContext(GiftContext.Paintball_ClearCut), + ["PaintballHomestead"] = UnrankedContext(GiftContext.Paintball_Homestead), + ["Homestead"] = UnrankedContext(GiftContext.Paintball_Homestead), + ["PaintballQuarry"] = UnrankedContext(GiftContext.Paintball_Quarry), + ["Quarry"] = UnrankedContext(GiftContext.Paintball_Quarry), + ["PaintballRiver"] = UnrankedContext(GiftContext.Paintball_River), + ["River"] = UnrankedContext(GiftContext.Paintball_River), + ["PaintballSpillway"] = UnrankedContext(GiftContext.Paintball_Dam), + ["Spillway"] = UnrankedContext(GiftContext.Paintball_Dam), + ["PaintballDriveIn"] = UnrankedContext(GiftContext.Paintball_DriveIn), + ["DriveIn"] = UnrankedContext(GiftContext.Paintball_DriveIn), + + ["DiscGolfPropulsion"] = UnrankedContext(GiftContext.Discgolf_Propulsion), + ["PropulsionTestRange"] = UnrankedContext(GiftContext.Discgolf_Propulsion), + ["DiscGolfLake"] = UnrankedContext(GiftContext.Discgolf_Lake), + + ["3DCharades"] = UnrankedContext(GiftContext.Charades), + ["Legacy3DCharades"] = UnrankedContext(GiftContext.Charades), + ["Soccer"] = UnrankedContext(GiftContext.Soccer), + ["Paddleball"] = UnrankedContext(GiftContext.Paddleball), + ["Dodgeball"] = UnrankedContext(GiftContext.Dodgeball), + ["DodgeballVR"] = UnrankedContext(GiftContext.Dodgeball), + ["LaserTag"] = UnrankedContext(GiftContext.Lasertag), + ["Bowling"] = UnrankedContext(GiftContext.Bowling), + ["BowlingAlley"] = UnrankedContext(GiftContext.Bowling), + ["RecRoyaleSolos"] = UnrankedContext(GiftContext.RecRoyale), + ["RecRoyaleSquads"] = UnrankedContext(GiftContext.RecRoyale), + }; + + private static Dictionary Ranks(params (string Rank, GiftContext Context)[] ranks) => + ranks.ToDictionary(r => r.Rank, r => r.Context, StringComparer.OrdinalIgnoreCase); + + private static readonly Dictionary RoomAliases = + new(StringComparer.OrdinalIgnoreCase) + { + ["TheMainEvent"] = "StuntRunner", + ["StuntRunnerTheMainEvent"] = "StuntRunner", + ["StuntRunnerMainEvent"] = "StuntRunner", + }; + + private static string? ResolveRoomName(string? roomName) + { + if (string.IsNullOrWhiteSpace(roomName)) return null; + var trimmed = roomName.Trim(); + return RoomAliases.TryGetValue(trimmed, out var target) ? target : trimmed; + } + + public static string? RoomForContext(GiftContext context) + { + foreach (var (room, ranks) in ByRoom) + { + if (ranks.Values.Contains(context)) return room; + } + + return ConsumableRooms.TryGetValue(context, out var consumableRoom) ? consumableRoom : null; + } + + private static readonly Dictionary ConsumableRooms = new() + { + [GiftContext.Quest_Goblin_Consumable] = "GoldenTrophy", + [GiftContext.Quest_Cauldron_Consumable] = "CrimsonCauldron", + [GiftContext.Quest_Pirate1_Consumable] = "IsleOfLostSkulls", + [GiftContext.Quest_Dracula1_Consumable] = "Crescendo", + [GiftContext.Quest_Scifi_Consumable] = "TheRiseofJumbotron", + [GiftContext.StuntRunner_TheMainEvent_Consumable] = "StuntRunner", + }; + + private static Dictionary UnrankedContext(GiftContext context) => + new(StringComparer.OrdinalIgnoreCase) { [Unranked] = context }; + + private static readonly Dictionary NoContexts = new(); + + public static IReadOnlyDictionary ContextsForRoom(string? roomName) + { + var resolved = ResolveRoomName(roomName); + if (resolved == null) return NoContexts; + return ByRoom.TryGetValue(resolved, out var ranks) ? ranks : NoContexts; + } + + public static bool IsRewardActivity(string? roomName) => ContextsForRoom(roomName).Count > 0; + + public static bool IsQuestContext(GiftContext context) + { + var room = RoomForContext(context); + if (room == null) return false; + + var contexts = ContextsForRoom(room); + return QuestRanks.Any(rank => contexts.ContainsKey(rank)); + } + + public static bool TryContext(string? roomName, string? rank, out GiftContext context) + { + context = GiftContext.None; + var contexts = ContextsForRoom(roomName); + if (contexts.Count == 0) return false; + return contexts.TryGetValue(string.IsNullOrWhiteSpace(rank) ? Unranked : rank.Trim(), out context); + } + + public static List LowerRanksOf(GiftContext context) + { + var lower = new List(); + + foreach (var ranks in ByRoom.Values) + { + if (!ranks.ContainsValue(context)) continue; + + var index = Array.FindIndex(QuestRanks, rank => ranks.TryGetValue(rank, out var paid) && paid == context); + if (index < 0) break; + + for (int i = index - 1; i >= 0; i--) + if (ranks.TryGetValue(QuestRanks[i], out var weaker)) lower.Add(weaker); + break; + } + + return lower; + } + + public static bool PaysNoItemReward(string? roomName) => + string.Equals(roomName?.Trim(), "RecRoyaleSolos", StringComparison.OrdinalIgnoreCase) + || string.Equals(roomName?.Trim(), "RecRoyaleSquads", StringComparison.OrdinalIgnoreCase); + + public static bool OffersSelection(string? roomName) => + ContextsForRoom(roomName).ContainsKey(Unranked); + + public static bool TryFloorContext(string? roomName, out GiftContext context) + { + context = GiftContext.None; + var contexts = ContextsForRoom(roomName); + if (contexts.Count == 0) return false; + + if (contexts.TryGetValue(Unranked, out var only)) + { + context = only; + return true; + } + + foreach (var rank in QuestRanks) + { + if (contexts.TryGetValue(rank, out var ranked)) + { + context = ranked; + return true; + } + } + + return false; + } + + + public static List Resolve(ItemSet set, string? catalogueJson = null) + { + var colour = Normalise(set.Colour); + + var pattern = string.IsNullOrEmpty(colour) + ? @"^\s*" + Regex.Escape(set.Family) + @"\b" + : @"^\s*" + Regex.Escape(set.Family) + @"\b.*\(\s*" + Regex.Escape(colour) + @"\s*\)\s*$"; + + var rx = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + return LevelUpGiftService.ClothingCatalogue(catalogueJson) + .Where(item => rx.IsMatch(Normalise(item.FriendlyName))) + .OrderBy(item => item.AvatarItemId) + .ThenBy(item => item.AvatarItemDesc, StringComparer.Ordinal) + .ToList(); + } + + private static string Normalise(string? value) => + (value ?? "").Replace("gray", "grey", StringComparison.OrdinalIgnoreCase); + + private static readonly ConcurrentDictionary> PoolCache = new(); + + public static List Resolve(GiftContext context, string? catalogueJson = null) => + string.IsNullOrWhiteSpace(catalogueJson) + ? PoolCache.GetOrAdd(context, ctx => ResolveCore(ctx, null)) + : ResolveCore(context, catalogueJson); + + private static List ResolveCore(GiftContext context, string? catalogueJson) => + SetsFor(context) + .SelectMany(set => Resolve(set, catalogueJson)) + .GroupBy(item => item.AvatarItemDesc, StringComparer.Ordinal) + .Select(group => group.First()) + .OrderBy(item => item.AvatarItemId) + .ThenBy(item => item.AvatarItemDesc, StringComparer.Ordinal) + .ToList(); + + public static AvatarItem? Pick( + GiftContext context, + ISet? owned = null, + Random? random = null, + string? catalogueJson = null) + { + var pool = Resolve(context, catalogueJson); + if (pool.Count == 0) return null; + + var ownedKeys = owned ?? new HashSet(StringComparer.OrdinalIgnoreCase); + var unowned = pool + .Where(item => !ownedKeys.Contains(LevelUpRewardPool.OwnershipKey(item.AvatarItemDesc))) + .ToList(); + + return unowned.Count == 0 ? null : unowned[(random ?? Random.Shared).Next(unowned.Count)]; + } + + public static Gift? BuildGift( + GiftContext context, + ISet? owned = null, + Random? random = null, + string? catalogueJson = null) + { + var pick = Pick(context, owned, random, catalogueJson); + if (pick == null) return null; + + return new Gift + { + FriendlyName = pick.FriendlyName, + Tooltip = pick.FriendlyName, + AvatarItemDesc = pick.AvatarItemDesc, + AvatarItemType = AvatarItemType.Outfit, + Rarity = (Rarity)pick.Rarity, + CurrencyType = CurrencyType.Invalid, + Currency = 0, + Xp = 0, + Context = context + }; + } + + public static List BuildDrops( + GiftContext context, + bool selection, + ISet? owned = null, + Random? random = null, + string? catalogueJson = null) + { + var drops = new List(); + var claimed = owned == null + ? new HashSet(StringComparer.OrdinalIgnoreCase) + : new HashSet(owned, StringComparer.OrdinalIgnoreCase); + var rng = random ?? Random.Shared; + + for (int i = 0; i < (selection ? SelectionSize : 1); i++) + { + var gift = BuildGift(context, claimed, rng, catalogueJson); + if (gift == null) break; + + drops.Add(gift); + claimed.Add(LevelUpRewardPool.OwnershipKey(gift.AvatarItemDesc)); + } + + return drops; + } + + + public static bool PaysTickets(string? roomName) => + string.Equals(roomName?.Trim(), "LaserTag", StringComparison.OrdinalIgnoreCase); + + public const int LaserTagTicketsPerGame = 25; + + public const int LaserTagFirstGamesBonus = 100; + + public const int LaserTagBonusGamesPerDay = 5; + + public static double DropChanceFor(GiftContext context) => + context == GiftContext.Bowling ? 1.0 / 250.0 : 1.0; + + public const int CompletionTokenAmount = 50; + + public static Gift TokenGift(GiftContext context, int amount, string? message = null) => new() + { + GiftDropId = StorefrontShared.NewGiftDropId(), + FriendlyName = TokenTitle(amount), + Tooltip = string.IsNullOrWhiteSpace(message) ? "Reward for playing!" : message, + CurrencyType = CurrencyType.RecCenterTokens, + Currency = amount, + Context = context, + Rarity = amount >= 200 ? Rarity.Rare : Rarity.Common + }; + + public static string TokenTitle(int amount) => $"{amount} Tokens"; + + public const double TokenBoxChance = 0.12; + + public static List BuildFallbackSelection( + ISet? owned = null, + Random? random = null, + string? catalogueJson = null, + string? message = null) + { + var rng = random ?? Random.Shared; + var drops = BuildFallbackDrops(selection: true, owned: owned, random: random, catalogueJson: catalogueJson) + .Take(SelectionSize) + .ToList(); + + if (drops.Count == 0) + return [TokenGift(GiftContext.GameRewards_Tokens, CompletionTokenAmount, message)]; + + var tokenBox = TokenGift(GiftContext.GameRewards_Tokens, CompletionTokenAmount, message); + + if (drops.Count < SelectionSize) + drops.Add(tokenBox); + else if (rng.NextDouble() < TokenBoxChance) + drops[^1] = tokenBox; + + return drops; + } + + public static List BuildDailyChallengeSelection( + ISet? owned = null, + Random? random = null, + string? catalogueJson = null) + { + var drops = BuildFallbackDrops(selection: true, owned: owned, random: random, catalogueJson: catalogueJson) + .Take(SelectionSize) + .ToList(); + + foreach (var drop in drops) + drop.Context = GiftContext.Daily_Challenge_Complete; + + return drops; + } + + + public const GiftContext ReacquisitionContext = GiftContext.Reacquisition; + + private static readonly int[] NonRareRarities = [10, 20]; + + private static List? _reacquisitionPool; + + private static readonly Regex RewardFamilyPattern = new( + @"^\s*(" + string.Join("|", + Pools.Values.SelectMany(sets => sets) + .Select(set => Regex.Escape(set.Family)) + .Distinct(StringComparer.OrdinalIgnoreCase)) + @")\b", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + public static List ReacquisitionPool(string? catalogueJson = null) + { + if (!string.IsNullOrWhiteSpace(catalogueJson)) + return FilterNonRare(LevelUpGiftService.ClothingCatalogue(catalogueJson)); + return _reacquisitionPool ??= FilterNonRare(LevelUpGiftService.ClothingCatalogue(null)); + } + + private static List FilterNonRare(List catalogue) => + catalogue + .Where(item => item != null + && !string.IsNullOrWhiteSpace(item.AvatarItemDesc) + && NonRareRarities.Contains(item.Rarity) + && !LevelUpRewardPool.IsAlwaysDenied(item) + && !IsRewardSetItem(item)) + .OrderBy(item => item.AvatarItemId) + .ThenBy(item => item.AvatarItemDesc, StringComparer.Ordinal) + .ToList(); + + public static bool IsRewardSetItem(AvatarItem item) => + item != null && RewardFamilyPattern.IsMatch(item.FriendlyName ?? string.Empty); + + private static readonly HashSet EmptyOwned = new(StringComparer.OrdinalIgnoreCase); + + public static List BuildFallbackDrops( + bool selection, + ISet? owned = null, + Random? random = null, + string? catalogueJson = null, + string? consumableCatalogJson = null, + LevelUpRewardPool.Pool? consumablePool = null) + { + var rng = random ?? Random.Shared; + var has = owned == null + ? new HashSet(StringComparer.OrdinalIgnoreCase) + : new HashSet(owned, StringComparer.OrdinalIgnoreCase); + + if (!selection) + { + var consumable = BuildFallbackConsumable(has, rng, consumableCatalogJson, consumablePool); + return consumable == null ? [] : [consumable]; + } + + var pool = ReacquisitionPool(catalogueJson); + var unowned = pool + .Where(item => !has.Contains(LevelUpRewardPool.OwnershipKey(item.AvatarItemDesc))) + .ToList(); + + return (unowned.Count > 0 ? unowned : pool) + .OrderBy(_ => rng.Next()) + .Take(SelectionSize) + .Select(item => new Gift + { + FriendlyName = item.FriendlyName, + Tooltip = item.FriendlyName, + AvatarItemDesc = item.AvatarItemDesc, + AvatarItemType = AvatarItemType.Outfit, + Rarity = (Rarity)item.Rarity, + CurrencyType = CurrencyType.Invalid, + Currency = 0, + Xp = 0, + Context = ReacquisitionContext + }) + .ToList(); + } + + public static Gift? BuildFallbackConsumable( + ISet? owned = null, + Random? random = null, + string? consumableCatalogJson = null, + LevelUpRewardPool.Pool? pool = null) + { + var rng = random ?? Random.Shared; + var has = owned ?? EmptyOwned; + + var gift = LevelUpGiftService.BuildConsumableGift(consumableCatalogJson, has, rng, pool) + ?? LevelUpGiftService.BuildConsumableGift(consumableCatalogJson, EmptyOwned, rng, pool); + if (gift == null) return null; + + gift.Context = ReacquisitionContext; + return gift; + } + } +} diff --git a/Server/Health/ServiceRootHealthAPI.cs b/Server/Health/ServiceRootHealthAPI.cs new file mode 100644 index 0000000..f55d7d9 --- /dev/null +++ b/Server/Health/ServiceRootHealthAPI.cs @@ -0,0 +1,100 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; + +namespace RadicadeServer.API; + +[ApiController] +public sealed class ServiceRootHealthAPI(IDbContextFactory dbFactory) : ControllerBase +{ + private static readonly HashSet HealthRootPaths = new(StringComparer.OrdinalIgnoreCase) + { + "auth", "ai", "data", "www", "cards", "notify", "images", "cdn", "commerce", + "datacollection", "discovery", "matchmaking", "storage", "chat", "geo", "leaderboard", + "accounts", "link", "locker", "moderation", "roomcomments", "roomieintegrations", + "clubs", "rooms", "econ", "bugreporting", "cms", "gamelogs", "platformnotifications", + "stringscdn", "strings", "lists", "studio", "studiocdn", "thorn", "videos" + }; + + internal static bool IsHealthRootPath(string path) => HealthRootPaths.Contains(path.Trim('/')); + + [HttpGet("~/auth")] + [HttpGet("~/ai")] + [HttpGet("~/www")] + [HttpGet("~/cards")] + [HttpGet("~/notify")] + [HttpGet("~/images")] + [HttpGet("~/cdn")] + [HttpGet("~/commerce")] + [HttpGet("~/datacollection")] + [HttpGet("~/discovery")] + [HttpGet("~/matchmaking")] + [HttpGet("~/storage")] + [HttpGet("~/chat")] + [HttpGet("~/geo")] + [HttpGet("~/leaderboard")] + [HttpGet("~/accounts")] + [HttpGet("~/link")] + [HttpGet("~/locker")] + [HttpGet("~/moderation")] + [HttpGet("~/roomcomments")] + [HttpGet("~/roomieintegrations")] + [HttpGet("~/clubs")] + [HttpGet("~/rooms")] + [HttpGet("~/econ")] + [HttpGet("~/bugreporting")] + [HttpGet("~/cms")] + [HttpGet("~/gamelogs")] + [HttpGet("~/platformnotifications")] + [HttpGet("~/stringscdn")] + [HttpGet("~/strings")] + [HttpGet("~/lists")] + [HttpGet("~/studio")] + [HttpGet("~/studiocdn")] + [HttpGet("~/thorn")] + [HttpGet("~/videos")] + [AllowAnonymous] + public Task GetServiceStatus() + { + var service = (Request.Path.Value ?? string.Empty).Trim('/').ToLowerInvariant(); + var notifyService = service == "notify"; + return ServiceRootHealth.CheckAsync(this, service, dbFactory, notifyService); + } +} + +internal static class ServiceRootHealth +{ + public static async Task CheckAsync( + ControllerBase controller, + string service, + IDbContextFactory? dbFactory, + bool requireNotifyHub = false) + { + if (requireNotifyHub && Notify.Context is null) + return Unavailable(controller, service, "NOTIFY_HUB_UNAVAILABLE"); + + var effectiveFactory = requireNotifyHub ? Notify.DbFactory : dbFactory; + if (effectiveFactory is null) + return Unavailable(controller, service, $"{ErrorPrefix(service)}_DATABASE_NOT_CONFIGURED"); + + try + { + await using var db = await effectiveFactory.CreateDbContextAsync(); + if (!await db.Database.CanConnectAsync()) + return Unavailable(controller, service, $"{ErrorPrefix(service)}_DATABASE_UNAVAILABLE"); + } + catch + { + return Unavailable(controller, service, $"{ErrorPrefix(service)}_DATABASE_UNAVAILABLE"); + } + + return controller.Ok(new { service, status = "ok" }); + } + + private static IActionResult Unavailable(ControllerBase controller, string service, string errorCode) => + controller.StatusCode(StatusCodes.Status503ServiceUnavailable, new { service, status = errorCode }); + + private static string ErrorPrefix(string service) => service.ToUpperInvariant(); +} diff --git a/Server/Images/CheeredImage.cs b/Server/Images/CheeredImage.cs new file mode 100644 index 0000000..445ea81 --- /dev/null +++ b/Server/Images/CheeredImage.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Images +{ + public class CheeredImage + { + public long SavedImageId { get; set; } + public bool IsCheered { get; set; } + + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), JsonIgnore] + public long InteractionId { get; set; } + + [JsonIgnore] + public long PlayerId { get; set; } + } +} diff --git a/Server/Images/ImageAPI.cs b/Server/Images/ImageAPI.cs new file mode 100644 index 0000000..fb728ab --- /dev/null +++ b/Server/Images/ImageAPI.cs @@ -0,0 +1,203 @@ +using Microsoft.AspNetCore.Mvc; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats; + +namespace RadicadeServer.Images +{ + [Route("images")] + public class ImageAPI : Controller + { + private const string SignatureData = + "IWwe/pZ5vWWqNSkSM/54isgDxlZkdrP0sUrppKCbNktO2yCOTjq746xWiiLsueGuVcAGQqkjeRTimxolHckS/YXSYkEJxtiCXbLlsRia2DyAqtWVkGWsfczzFhp/56U66FVzolTspPCvjScOVlGO7dDIK7sJ+ndcRauWjsQsC6g3e7rUc6uwY099a6gy7sw6xr5BFZQSz8wg+fqyHYD/Sc4nQQVOTFZNNASqbJYhpNhEMXRnafCMuLl8a3mkGwvy3t4q2D/7SM48xrGZjEV47qNx1A91KCe28XVToFh4BzwEUU8nZ0d+KwV79MGarLo1cY8igc8FcoThKcovI4ClOg=="; + + public const string PatchedKeyId = "KEY:RSA:p1.radicade.internal"; + + public const string StockKeyId = "KEY:RSA:p1.rec.net"; + + public static readonly string SignatureString = BuildSignature(PatchedKeyId); + + public static readonly string StockSignatureString = BuildSignature(StockKeyId); + + public const string KeyIdQueryParameter = "rcsig"; + + public static readonly string BareRouteKeyId = + IsWellFormedKeyId(Environment.GetEnvironmentVariable("RADICADE_IMAGE_BARE_KEYID") ?? string.Empty) + ? Environment.GetEnvironmentVariable("RADICADE_IMAGE_BARE_KEYID") ?? StockKeyId + : StockKeyId; + + private static string BuildSignature(string keyId) => $"key-id={keyId}; data={SignatureData}"; + + [HttpGet("img/{imageName}")] + public async Task GetImageFile(string imageName, [FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig, + [FromQuery(Name = KeyIdQueryParameter)] string clientKeyId) + { + return await ProcessImage(imageName, width, cropSquare, false, SignatureFor(clientKeyId, "img", PatchedKeyId)); + } + + [HttpGet("{imageName}")] + public async Task GetImageFileAlt(string imageName, [FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig, + [FromQuery(Name = KeyIdQueryParameter)] string clientKeyId) + { + return await ProcessImage(imageName, width, cropSquare, false, SignatureFor(clientKeyId, "bare", BareRouteKeyId)); + } + + [HttpGet("default")] + public async Task GetDefaultImage([FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig, + [FromQuery(Name = KeyIdQueryParameter)] string clientKeyId) + { + return await ProcessImage(DBConfig.DefaultRoomImage, width, cropSquare, false, SignatureFor(clientKeyId, "default", BareRouteKeyId)); + } + + public static string SignatureFor(string clientKeyId, string route, string routeDefaultKeyId) + { + var keyId = IsWellFormedKeyId(clientKeyId) ? clientKeyId : routeDefaultKeyId; + return BuildSignature(keyId); + } + + public static bool IsWellFormedKeyId(string keyId) + { + const string prefix = "KEY:RSA:p1."; + if (string.IsNullOrEmpty(keyId) || keyId.Length > 96 || !keyId.StartsWith(prefix, StringComparison.Ordinal)) + return false; + var tail = keyId.Substring(prefix.Length); + if (tail.Length == 0 || tail.Length > 64) + return false; + foreach (var c in tail) + { + if (!char.IsLetterOrDigit(c) && c != '.' && c != '-' && c != '_') + return false; + } + return true; + } + + public static string[] AssetRoots() => new[] { DBConfig.CDNImages, DBConfig.CDNCustom, DBConfig.CDNStorage }; + + private static readonly string[] ExtensionVariants = { ".png", ".jpg", ".jpeg" }; + + public static string? ResolveAsset(string imageName) => ResolveAsset(imageName, AssetRoots()); + + public static string? ResolveAsset(string imageName, IEnumerable roots) + { + var name = Path.GetFileName(imageName); + if (string.IsNullOrWhiteSpace(name) || name == "." || name == "..") + return null; + + foreach (var candidate in CandidateNames(name)) + { + foreach (var root in roots) + { + var path = Path.Join(root, candidate); + if (WithinRoot(root, path) && System.IO.File.Exists(path)) + { + return path; + } + } + } + + return null; + } + + private static IEnumerable CandidateNames(string name) + { + yield return name; + + var existing = Path.GetExtension(name); + + foreach (var variant in ExtensionVariants) + { + if (existing.Length == 0) + { + yield return name + variant; + } + else if (!string.Equals(existing, variant, StringComparison.OrdinalIgnoreCase)) + { + yield return string.Concat(name.AsSpan(0, name.Length - existing.Length), variant); + } + } + } + + private static bool WithinRoot(string root, string path) + { + try + { + var fullRoot = Path.GetFullPath(root) + Path.DirectorySeparatorChar; + return Path.GetFullPath(path).StartsWith(fullRoot, StringComparison.Ordinal); + } + catch (ArgumentException) + { + return false; + } + } + + private async Task ProcessImage(string imageName, int? width, int? cropSquare, bool isFallback = false, string? signature = null) + { + Response.Headers["Content-Signature"] = signature ?? SignatureString; + + var fullPath = ResolveAsset(imageName); + + if (fullPath == null) + { + if (!isFallback) + { + var fallback = ResolveAsset(DBConfig.DefaultRoomImage); + if (fallback != null) + { + return await ServeImageFile(fallback, width, cropSquare); + } + } + return NotFound(); + } + + return await ServeImageFile(fullPath, width, cropSquare); + } + + private async Task ServeImageFile(string fullPath, int? width, int? cropSquare) + { + if (!width.HasValue) + { + return PhysicalFile(fullPath, "image/jpeg"); + } + + Image image; + try + { + image = await Image.LoadAsync(fullPath); + } + catch (Exception) + { + return PhysicalFile(fullPath, "image/jpeg"); + } + + using (image) + { + if (cropSquare == 1) + { + int size = Math.Min(image.Width, image.Height); + + var cropRect = new Rectangle((image.Width - size) / 2, (image.Height - size) / 2, size, size); + + image.Mutate(x => + { + x.Crop(cropRect); + x.Resize(width.Value, width.Value); + }); + } + else + { + image.Mutate(x => x.Resize(width.Value, 0)); + } + + IImageFormat format = await Image.DetectFormatAsync(fullPath); + + var ms = new MemoryStream(); + + await image.SaveAsync(ms, format); + + ms.Position = 0; + + return File(ms, format.DefaultMimeType); + } + } + } +} diff --git a/Server/Images/ImageCheerRequest.cs b/Server/Images/ImageCheerRequest.cs new file mode 100644 index 0000000..20d994e --- /dev/null +++ b/Server/Images/ImageCheerRequest.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Images +{ + public class ImageCheerRequest + { + public bool Cheer { get; set; } + public long SavedImageId { get; set; } + } +} diff --git a/Server/Images/ImagesAPI.cs b/Server/Images/ImagesAPI.cs new file mode 100644 index 0000000..ac0b200 --- /dev/null +++ b/Server/Images/ImagesAPI.cs @@ -0,0 +1,580 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.CDN; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Images; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Radicade.Discord.Templates; + +namespace RadicadeServer.API +{ + [Route("api/images")] + public class ImagesAPI : Controller + { + RecDBContext db; + public ImagesAPI(RecDBContext context) + { + this.db = context; + } + + + private static object ToSavedImage(ImageDB image, int cheerCount) => new + { + Id = image.Id, + SavedImageId = image.Id, + Type = (int)image.ImageType, + SavedImageType = (int)image.ImageType, + Accessibility = (int)image.Accessibility, + AccessibilityLocked = false, + ImageName = image.Name, + Description = (string?)null, + PlayerId = image.OwnerId, + TaggedPlayerIds = ParseTaggedPlayerIds(image.PlayerIds), + RoomId = image.RoomId, + PlayerEventId = image.EventId, + CreatedAt = image.CreatedAt, + CheerCount = cheerCount, + CommentCount = 0 + }; + + private static object ToImagesPlayer(ImageDB image, int cheerCount) => new + { + SavedImageId = image.Id, + Id = image.Id, + SavedImageType = (int)image.ImageType, + Type = (int)image.ImageType, + Accessibility = (int)image.Accessibility, + AccessibilityLocked = false, + CheerCount = cheerCount, + CommentCount = 0, + CreatedAt = image.CreatedAt, + Description = (string?)null, + ImageName = image.Name, + PlayerEventId = image.EventId, + PlayerId = image.OwnerId, + RoomId = image.RoomId + }; + + private static List ParseTaggedPlayerIds(string json) + { + try + { + return string.IsNullOrWhiteSpace(json) + ? new List() + : (json.FromJson>() ?? new List()); + } + catch + { + return new List(); + } + } + + private async Task> CheerCountsAsync(IEnumerable imageIds) + { + var ids = imageIds.Distinct().ToList(); + if (ids.Count == 0) + return new Dictionary(); + + return await db.ImageInteractions.AsNoTracking() + .Where(c => c.IsCheered && ids.Contains(c.SavedImageId)) + .GroupBy(c => c.SavedImageId) + .Select(g => new { g.Key, Count = g.Count() }) + .ToDictionaryAsync(g => g.Key, g => g.Count); + } + + private static string[] TagMatchPatterns(long playerId) => new[] + { + $"[{playerId},", $",{playerId},", $",{playerId}]", $"[{playerId}]" + }; + + + [HttpGet("v5/player/{id:long}")] + public async Task> GetImagesFromPlayer(long id, [FromQuery] int sort = 0, [FromQuery] int skip = 0, [FromQuery] int take = 100) + { + var images = await db.Images.AsNoTracking() + .Where(image => image.OwnerId == id && image.Accessibility == SavedImageAccessibility.Public) + .ToListAsync(); + + var cheers = await CheerCountsAsync(images.Select(image => image.Id)); + var ordered = OrderImages(images, sort, cheers) + .Skip(skip).Take(take) + .ToList(); + + return ordered.Select(image => ToImagesPlayer(image, cheers.GetValueOrDefault(image.Id))).ToList(); + } + + [HttpGet("v4/player/{id:long}")] + public Task> GetImagesFromPlayerV4(long id, [FromQuery] int skip = 0, [FromQuery] int take = 100) + => GetImagesFromPlayer(id, 0, skip, take); + + [HttpGet("v4/room/{id}")] + public async Task> GetImagesFromRoom(long? id, [FromQuery] int sort = 0, [FromQuery] int filter = 0, [FromQuery] int skip = 0, [FromQuery] int take = 100) + { + var result = new List(); + if (id == null) + return result; + + var images = await db.Images.AsNoTracking() + .Where(x => x.RoomId == id && x.Accessibility == SavedImageAccessibility.Public) + .ToListAsync(); + + if (filter > 0) + images = images.Where(x => (int)x.ImageType == filter).ToList(); + + var cheers = await CheerCountsAsync(images.Select(x => x.Id)); + var ordered = OrderImages(images, sort, cheers) + .Skip(skip).Take(take) + .ToList(); + + return ordered.Select(image => ToSavedImage(image, cheers.GetValueOrDefault(image.Id))).ToList(); + } + + [HttpGet("v1/playerevent/{eventId}")] + public async Task> GetImagesFromEvent(long eventId, [FromQuery] int skip = 0, [FromQuery] int take = 100) + { + var images = await db.Images.AsNoTracking() + .Where(x => x.EventId == eventId && x.Accessibility == SavedImageAccessibility.Public) + .ToListAsync(); + + var cheers = await CheerCountsAsync(images.Select(x => x.Id)); + var ordered = OrderImages(images, 0, cheers) + .Skip(skip).Take(take) + .ToList(); + + return ordered.Select(image => ToSavedImage(image, cheers.GetValueOrDefault(image.Id))).ToList(); + } + + private static IEnumerable OrderImages(IEnumerable images, int sort, Dictionary cheers) + { + return sort == 1 + ? images.OrderByDescending(x => cheers.GetValueOrDefault(x.Id)) + .ThenByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id) + : images.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id); + } + + + [HttpGet("v3/feed/player/{id:long}")] + public async Task> GetPlayerFeed(long id, [FromQuery] int skip = 0, [FromQuery] int take = 100) + { + var patterns = TagMatchPatterns(id); + var images = await db.Images.AsNoTracking() + .Where(x => x.Accessibility == SavedImageAccessibility.Public && + (x.OwnerId == id || + x.PlayerIds.Contains(patterns[0]) || + x.PlayerIds.Contains(patterns[1]) || + x.PlayerIds.Contains(patterns[2]) || + x.PlayerIds.Contains(patterns[3]))) + .ToListAsync(); + + var cheers = await CheerCountsAsync(images.Select(x => x.Id)); + var ordered = OrderImages(images, 0, cheers) + .Skip(skip).Take(take) + .ToList(); + + return ordered.Select(image => ToImagesPlayer(image, cheers.GetValueOrDefault(image.Id))).ToList(); + } + + [HttpGet("v3/feed/global")] + public async Task GetGlobalFeed([FromQuery] int skip = 0, [FromQuery] int take = 100) + { + var images = await db.Images.AsNoTracking() + .Where(x => x.Accessibility == SavedImageAccessibility.Public && + x.ImageType == SavedImageType.ShareCamera) + .OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id) + .Skip(skip).Take(Math.Clamp(take, 1, 100)) + .ToListAsync(); + + var ownerIds = images.Select(x => x.OwnerId).Distinct().ToList(); + var roomIds = images.Where(x => x.RoomId != null).Select(x => x.RoomId!.Value).Distinct().ToList(); + + var usernames = await db.Profiles.AsNoTracking() + .Where(p => ownerIds.Contains(p.accountId)) + .ToDictionaryAsync(p => p.accountId, p => p.username); + var roomNames = await db.Rooms.AsNoTracking() + .Where(r => roomIds.Contains(r.RoomId)) + .ToDictionaryAsync(r => r.RoomId, r => r.Name); + + var entries = images.Select(image => new + { + SavedImageId = image.Id, + ImageName = image.Name, + Username = usernames.TryGetValue(image.OwnerId, out var username) ? username : $"Player{image.OwnerId}", + RoomName = image.RoomId != null && roomNames.TryGetValue(image.RoomId.Value, out var roomName) ? roomName : (string?)null, + RoomId = image.RoomId, + SavedImageType = (int)image.ImageType, + PlayerEventId = image.EventId, + Accessibility = (int)image.Accessibility, + PlayerIds = ParseTaggedPlayerIds(image.PlayerIds) + }).ToList(); + + return new { Images = entries, ValidTill = DateTime.UtcNow.AddMinutes(2) }; + } + + [HttpGet("v3/bulk")] + public async Task> GetBulkImages([FromQuery] string ids) + { + var result = new List(); + if (string.IsNullOrWhiteSpace(ids)) + return result; + + var parsed = ids + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(part => long.TryParse(part, out var value) ? value : -1) + .Where(value => value > 0) + .ToList(); + if (parsed.Count == 0) + return result; + + var images = await db.Images.AsNoTracking() + .Where(x => parsed.Contains(x.Id) && x.Accessibility == SavedImageAccessibility.Public) + .ToListAsync(); + var byId = images.ToDictionary(x => x.Id); + var cheers = await CheerCountsAsync(images.Select(x => x.Id)); + + foreach (var id in parsed) + { + if (byId.TryGetValue(id, out var image)) + result.Add(ToSavedImage(image, cheers.GetValueOrDefault(image.Id))); + } + return result; + } + + [HttpGet("v4/{photoId:long}")] + public async Task GetImageById(long photoId) + { + var image = await db.Images.AsNoTracking().FirstOrDefaultAsync(x => x.Id == photoId); + if (image == null) + return NotFound(); + + + + + + if (image.Accessibility == SavedImageAccessibility.Private) + return NotFound(); + return Ok(ToSavedImage(image, await db.ImageInteractions.AsNoTracking() + .CountAsync(c => c.SavedImageId == image.Id && c.IsCheered))); + } + + [HttpGet("v5/bulk")] + public async Task> GetBulkImageMetadata([FromQuery] string[]? ids) + { + var result = new List(); + var parsed = (ids ?? Array.Empty()) + .SelectMany(raw => (raw ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + .Select(part => long.TryParse(part, out var value) ? value : -1) + .Where(value => value > 0) + .Distinct() + .ToList(); + if (parsed.Count == 0) + return result; + + var images = await db.Images.AsNoTracking() + .Where(x => parsed.Contains(x.Id) && x.Accessibility == SavedImageAccessibility.Public) + .ToListAsync(); + var byId = images.ToDictionary(x => x.Id); + var cheers = await CheerCountsAsync(images.Select(x => x.Id)); + + foreach (var id in parsed) + { + if (byId.TryGetValue(id, out var image)) + result.Add(ToBulkSavedImage(image, cheers.GetValueOrDefault(image.Id))); + } + return result; + } + + private static object ToBulkSavedImage(ImageDB image, int cheerCount) => new + { + Id = image.Id, + Type = (int)image.ImageType, + Accessibility = (int)image.Accessibility, + AccessibilityLocked = false, + ImageName = image.Name, + Description = (string?)null, + PlayerId = image.OwnerId, + TaggedPlayerIds = ParseTaggedPlayerIds(image.PlayerIds), + RoomId = image.RoomId, + PlayerEventId = image.EventId, + CreatedAt = image.CreatedAt, + CheerCount = cheerCount, + CommentCount = 0, + SavedImageId = image.Id, + SavedImageType = (int)image.ImageType, + ClubId = 0 + }; + + [HttpGet("v6")] + public async Task GetImageByName([FromQuery] string name) + { + if (string.IsNullOrWhiteSpace(name)) + return BadRequest(new { error = "name is required" }); + + var image = await db.Images.AsNoTracking().FirstOrDefaultAsync(x => x.Name == name); + if (image == null) + return NotFound(); + + + + if (image.Accessibility == SavedImageAccessibility.Private) + return NotFound(); + + var viewerId = User.GetUserId() ?? Request.Headers.Authorization.ToString().GetUserId(); + if (viewerId != null) + RecentImageReferences.Remember(viewerId.Value, image.Id); + + return Ok(new + { + SavedImageId = image.Id, + ImageName = image.Name, + PlayerId = image.OwnerId, + RoomId = image.RoomId ?? 0, + PlayerEventId = image.EventId ?? 0, + ClubId = 0, + Description = "", + Accessibility = (int)image.Accessibility, + AccessibilityLocked = false, + SavedImageType = (int)image.ImageType, + CreatedAt = image.CreatedAt, + CheerCount = await db.ImageInteractions.AsNoTracking() + .CountAsync(c => c.SavedImageId == image.Id && c.IsCheered), + CommentCount = 0 + }); + } + + + [HttpPost("v1/cheer"), Authorize] + public async Task CheerImage([FromBody] ImageCheerRequest req) + { + var userId = User.GetUserId(); + if (userId == null) + return new Success(false, "Not signed in", null); + + var existingInteraction = await db.ImageInteractions.FirstOrDefaultAsync(x => + x.PlayerId == userId && x.SavedImageId == req.SavedImageId); + if (existingInteraction != null) + existingInteraction.IsCheered = req.Cheer; + else + await db.ImageInteractions.AddAsync( + new CheeredImage { IsCheered = req.Cheer, PlayerId = (long)userId, SavedImageId = req.SavedImageId } + ); + + await db.SaveChangesAsync(); + + return new Success(true, null, null); + } + + [HttpGet("v1/{imageId:long}/cheers")] + public async Task> GetImageCheers(long imageId) + { + return await db.ImageInteractions.AsNoTracking() + .Where(x => x.SavedImageId == imageId && x.IsCheered) + .Select(x => x.PlayerId) + .ToListAsync(); + } + + [HttpGet("v2/named")] + public List GetNamedImages() + { + var config = db.ServerConfig.FirstOrDefault(x => x.Key == "NamedImages"); + return config?.Value.FromJson>() ?? new List(); + } + + private async Task> CheerLookup(List ids) + { + var userId = User.GetUserId(); + if (userId == null) + return new List(); + + ids ??= new List(); + var cheeredIds = await db.ImageInteractions.AsNoTracking() + .Where(x => x.PlayerId == userId && x.IsCheered && ids.Contains(x.SavedImageId)) + .Select(x => x.SavedImageId) + .ToListAsync(); + var cheeredSet = cheeredIds.ToHashSet(); + + return ids.Select(id => (object)new { SavedImageId = id, IsCheered = cheeredSet.Contains(id) }).ToList(); + } + + [HttpGet("v5/cheered/bulk"), Authorize] + public Task> GetBulkImageCheers([FromQuery] long[] id) + => CheerLookup((id ?? Array.Empty()).ToList()); + + [HttpPost("v5/cheered/bulk"), Authorize] + public async Task> PostBulkImageCheers() + { + var ids = new List(); + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var value in form["id"]) + { + if (long.TryParse(value, out var parsed)) + ids.Add(parsed); + } + } + return await CheerLookup(ids); + } + + + [HttpPost("v1/{imageId:long}/report"), Authorize] + public async Task ReportImage(long imageId) + { + await Task.CompletedTask; + return Ok(new { success = true }); + } + + [HttpPost("v1/{imageId:long}/player/{playerId:long}/untag"), Authorize] + public async Task UntagFromImage(long imageId, long playerId) + { + var userId = User.GetUserId(); + + var image = await db.Images.FirstOrDefaultAsync(x => x.Id == imageId); + if (image == null) + return NotFound(); + + if (userId != playerId && userId != image.OwnerId) + return StatusCode(403); + + var tagged = ParseTaggedPlayerIds(image.PlayerIds); + if (!tagged.Remove(playerId)) + return Ok(new { success = true }); + + image.PlayerIds = tagged.ToJson(); + await db.SaveChangesAsync(); + return Ok(new { success = true }); + } + + + [HttpPost("v4/uploadsaved"), Authorize] + public async Task> UploadSavedImage([FromForm] IFormFile? image, [FromForm] SavedImageMetaDTO? imgMeta) + { + if (imgMeta == null) + return BadRequest(); + if (image == null || image.Length <= 0 || image.Length >= 2 * 1024 * 1024) + return StatusCode(413); + + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(); + + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId); + var roomInstance = presence != null + ? await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance) + : null; + + string imageName = Guid.NewGuid().ToString() + ".png"; + string path = Path.Join(DBConfig.CDNImages, imageName); + + using (var stream = new FileStream(path, FileMode.Create)) + { + await image.CopyToAsync(stream); + } + + SavedImageAccessibility accessibility = imgMeta.accessibility; + if (imgMeta.savedImageType == SavedImageType.InventionThumbnail || + imgMeta.savedImageType == SavedImageType.OutfitThumbnail || + imgMeta.savedImageType == SavedImageType.ProfileThumbnail || + imgMeta.savedImageType == SavedImageType.RoomThumbnail) + { + accessibility = SavedImageAccessibility.Private; + } + + var taggedPlayerIds = (imgMeta.playerIds ?? new List()) + .Where(id => id > 0) + .Distinct() + .ToList(); + + var savedImage = new ImageDB { CreatedAt = DateTime.Now, Name = imageName, OwnerId = (long)userId, Accessibility = accessibility, ImageType = imgMeta.savedImageType, RoomId = imgMeta.roomId ?? roomInstance?.roomId, EventId = imgMeta.playerEventId, PlayerIds = taggedPlayerIds.ToJson() }; + await db.Images.AddAsync(savedImage); + await db.SaveChangesAsync(); + + RecentImageReferences.Remember((long)userId, savedImage.Id); + + if (imgMeta.savedImageType == SavedImageType.ProfileThumbnail) + { + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile != null) + { + profile.profileImage = imageName; + await db.SaveChangesAsync(); + } + } + + if (accessibility == SavedImageAccessibility.Public || imgMeta.savedImageType == SavedImageType.ShareCamera) + { + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == imgMeta.roomId); + var ownerProfile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == userId); + + string? roomName = null; + + if (ownerProfile != null) + { + if (room != null) + { + if (room.Accessibility >= Accessibility.Public) + roomName = "^" + room.Name; + else if (room.IsDorm && roomInstance != null) + roomName = roomInstance?.name; + else + roomName = "[PRIVATE]"; + } + + await WebhookImageUpload.Send($"{DBConfig.NameServer.Images}/img/{imageName}", ownerProfile.username, ownerProfile.displayName, $"{DBConfig.NameServer.Images}/img/{ownerProfile.profileImage}", roomName); + } + } + + return new ImageNameDTO { ImageName = imageName }; + } + + [HttpDelete("v1/deletesaved"), Authorize] + [HttpPost("v1/deletesaved"), Authorize] + public async Task DeleteSavedImage() + { + var userId = User.GetUserId(); + string? imageName = null; + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + imageName = form["ImageName"].ToString(); + if (string.IsNullOrWhiteSpace(imageName)) + imageName = form["imageName"].ToString(); + } + else + { + try + { + using var doc = await System.Text.Json.JsonDocument.ParseAsync(Request.Body); + if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object && + doc.RootElement.TryGetProperty("ImageName", out var prop) && + prop.ValueKind == System.Text.Json.JsonValueKind.String) + imageName = prop.GetString(); + } + catch (System.Text.Json.JsonException) { } + } + + if (string.IsNullOrWhiteSpace(imageName)) + return BadRequest(new { error = "ImageName is required" }); + + var image = await db.Images.FirstOrDefaultAsync(x => x.Name == imageName); + if (image == null) + return NotFound(); + if (image.OwnerId != userId) + return StatusCode(403, new { error = "Not your image" }); + + var cheers = db.ImageInteractions.Where(x => x.SavedImageId == image.Id); + db.ImageInteractions.RemoveRange(cheers); + db.Images.Remove(image); + await db.SaveChangesAsync(); + + var fullPath = Path.Join(DBConfig.CDNImages, imageName); + if (System.IO.File.Exists(fullPath)) + System.IO.File.Delete(fullPath); + + return Ok(new { success = true }); + } + } +} diff --git a/Server/Images/NamedImage.cs b/Server/Images/NamedImage.cs new file mode 100644 index 0000000..e36dad5 --- /dev/null +++ b/Server/Images/NamedImage.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Images +{ + public class NamedImage + { + public string FriendlyImageName { get; set; } = null!; + public string ImageName { get; set; } = null!; + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + } +} diff --git a/Server/Images/PublicPhotoData.cs b/Server/Images/PublicPhotoData.cs new file mode 100644 index 0000000..550cc22 --- /dev/null +++ b/Server/Images/PublicPhotoData.cs @@ -0,0 +1,16 @@ +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.CDN; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Radicade.Public +{ + public class PublicPhotoData + { + public string ImageCdnUrl { get; set; } = DBConfig.DefaultRoomImage; + + public string OwnerName { get; set; } = "@Noone"; + public string LocationName { get; set; } = "^Nowhere"; + + public int Cheers { get; set; } = 0; + } +} diff --git a/Server/Images/PublicPhotosAPI.cs b/Server/Images/PublicPhotosAPI.cs new file mode 100644 index 0000000..70d74c3 --- /dev/null +++ b/Server/Images/PublicPhotosAPI.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Radicade.Public; + +namespace RadicadeServer.API.Public +{ + [Route("p/photos")] + public class PublicPhotosAPI(RecDBContext db) : Controller + { + [HttpGet("latest")] + public async Task>> GetLatestPhotos([FromQuery] int? amount) + { + if (!amount.HasValue) amount = 3; + if (amount.Value > 6) return BadRequest("Can't go above 6 pictures."); + + var images = await db.Images.AsNoTracking().Where(x => x.Accessibility == RecNet.CDN.SavedImageAccessibility.Public).OrderByDescending(x => x.CreatedAt).Take(amount.Value).ToListAsync(); + List converted = new(); + + foreach (var image in images) + { + if (image == null) continue; + + RoomData? roomData = null; + Profile? profile = null; + int cheers = 0; + + if (image.RoomId.HasValue) + roomData = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == image.RoomId && x.Accessibility >= RecNet.Enums.Accessibility.Public); + profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == image.OwnerId); + cheers = await db.ImageInteractions.AsNoTracking().Where(x => x.SavedImageId == image.Id).CountAsync(); + + if (profile == null) continue; + + PublicPhotoData data = new PublicPhotoData + { + Cheers = cheers, + ImageCdnUrl = $"{DBConfig.NameServer.Images}/img/{image.Name}?width=512", + LocationName = (roomData == null) ? "[PRIVATE]" : "^" + roomData.Name, + OwnerName = "@" + profile.username + }; + + converted.Add(data); + } + + return converted; + } + } +} diff --git a/Server/Images/SharedImage.cs b/Server/Images/SharedImage.cs new file mode 100644 index 0000000..135d317 --- /dev/null +++ b/Server/Images/SharedImage.cs @@ -0,0 +1,21 @@ +using RadicadeServer.RecNet.CDN; +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.Images +{ + public class SharedImage + { + public long Id { get; set; } + public SavedImageType Type { get; set; } + public SavedImageAccessibility Accessibility { get; set; } + public bool AccessibilityLocked { get; set; } = false; + public string ImageName { get; set; } = string.Empty; + public long PlayerId { get; set; } + public long? RoomId { get; set; } = null; + public long? PlayerEventId { get; set; } = null; + public DateTime CreatedAt { get; set; } + public int CheerCount { get; set; } + public int CommentCount { get; set; } + public List TaggedPlayerIds { get; set; } = new(); + } +} diff --git a/Server/Inventions/Enums/InventionRole.cs b/Server/Inventions/Enums/InventionRole.cs new file mode 100644 index 0000000..8aae0e4 --- /dev/null +++ b/Server/Inventions/Enums/InventionRole.cs @@ -0,0 +1,13 @@ +namespace RadicadeServer.RecNet.Enums.Inventions +{ + public enum InventionRole + { + Unassigned, + LimitedOneUseOnly = 10, + UseOnly = 20, + EditAndSave = 40, + Publish = 60, + Charge = 80, + Unlimited = 100 + } +} diff --git a/Server/Inventions/Enums/InventionStatus.cs b/Server/Inventions/Enums/InventionStatus.cs new file mode 100644 index 0000000..a4ea93a --- /dev/null +++ b/Server/Inventions/Enums/InventionStatus.cs @@ -0,0 +1,47 @@ +namespace RadicadeServer.RecNet.Enums.Inventions +{ + public enum InventionStatus + { + Success, + InvalidParameters, + PlayerCannotUpload, + DuplicateName, + NameTooShort, + NameTooLong, + NotCreator, + DoesNotExist, + ImageDoesNotExist, + InventionLimitReached, + DescriptionTooLong, + InnapropriateName, + InnapropriateDescription, + CannotBeModified, + PlayerCannotPublish, + AlreadyPublished, + AlreadyUnpublished, + InventionUnderModerationReview, + PlayerCannotDownload, + PlayerAlreadyOwns, + DescriptionTooShort, + DoesNotHavePermission, + PermissionLevelCannotBeChanged, + AlreadyCheered, + AlreadyRemovedCheer, + ModeratorRestrictedPublishing, + PlayerCannotSell, + InvalidPrice, + PriceCannotBeChanged, + InvalidPermissionForPaidInvention, + PurchaseFailed, + CannotDownloadPaidInvention, + CannotSellUnownedLineage, + DoesNotAllowTrial, + StillOnTrialCooldown, + PlayerCannotTrial, + PaidInventionPublishingDisabled, + PaidInventionPurchasingDisabled, + OperationIsDisabled, + PlayerRestrictedFromP2PSelling, + PlayerNotRecRoomPlusMember + } +} diff --git a/Server/Inventions/FullInvention.cs b/Server/Inventions/FullInvention.cs new file mode 100644 index 0000000..5b1a36d --- /dev/null +++ b/Server/Inventions/FullInvention.cs @@ -0,0 +1,11 @@ +using RadicadeServer.RecNet.Enums.Inventions; + +namespace RadicadeServer.RecNet.Inventions +{ + public class FullInvention + { + public InventionStatus Status { get; set; } = InventionStatus.DoesNotExist; + public Invention? Invention { get; set; } = null; + public InventionVersion? InventionVersion { get; set; } = null; + } +} diff --git a/Server/Inventions/Invention.cs b/Server/Inventions/Invention.cs new file mode 100644 index 0000000..67b1826 --- /dev/null +++ b/Server/Inventions/Invention.cs @@ -0,0 +1,29 @@ +using RadicadeServer.RecNet.Enums.Inventions; + +namespace RadicadeServer.RecNet.Inventions +{ + public class Invention + { + public long InventionId { get; set; } + public long CreationRoomId { get; set; } + public string ReplicationId { get; set; } = null!; + public long CreatorPlayerId { get; set; } + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public string ImageName { get; set; } = null!; + public int CurrentVersionNumber { get; set; } + public bool IsPublished { get; set; } + public bool AllowTrial { get; set; } + public DateTime ModifiedAt { get; set; } + public DateTime CreatedAt { get; set; } + public int NumPlayersHaveUsedInRoom { get; set; } + public int NumDownloads { get; set; } + public int CheerCount { get; set; } + public InventionRole CreatorPermission { get; set; } + public InventionRole GeneralPermission { get; set; } + public bool IsAGInvention { get; set; } + public bool IsCertifiedInvention { get; set; } + public int? Price { get; set; } + public bool HideFromPlayer { get; set; } + } +} diff --git a/Server/Inventions/InventionCDN.cs b/Server/Inventions/InventionCDN.cs new file mode 100644 index 0000000..376bcf1 --- /dev/null +++ b/Server/Inventions/InventionCDN.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using SixLabors.ImageSharp; + +namespace RadicadeServer.CDN +{ + [Route("cdn/invention/")] + public class InventionCDN : Controller + { + + + [HttpGet] + public IActionResult Index() => Ok(new { status = "ok" }); + + [HttpGet("{datablob}")] + public IActionResult GetDataBlob(string datablob) + { + if (!CdnStorageNames.IsServable(datablob)) return BadRequest(); + + var name = CdnStorageNames.Resolve(datablob); + if (name == null) return NotFound(); + + CdnCaching.Apply(this, CdnCaching.ForBlob(name)); + + return PhysicalFile(Path.Join(DBConfig.CDNStorage, name), "application/octet-stream", enableRangeProcessing: true); + } + } +} diff --git a/Server/Inventions/InventionDB.cs b/Server/Inventions/InventionDB.cs new file mode 100644 index 0000000..41afced --- /dev/null +++ b/Server/Inventions/InventionDB.cs @@ -0,0 +1,128 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Inventions; + +namespace RadicadeServer.RecNet.Inventions +{ + public class InventionDB + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long InventionId { get; set; } + public string ReplicationId { get; set; } = ""; + public long CreatorPlayerId { get; set; } = 1; + + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public string ImageName { get; set; } = DBConfig.DefaultRoomImage; + + public int CurrentVersionNumber { get; set; } = 0; + + public DateTime ModifiedAt { get; set; } = DateTime.Now; + public DateTime CreatedAt { get; set; } = DateTime.Now; + + public int NumPlayersHaveUsedInRoom { get; set; } = 0; + public int NumDownloads { get; set; } = 0; + public int CheerCount { get; set; } = 0; + + public InventionRole CreatorPermission { get; set; } = InventionRole.Unlimited; + public InventionRole GeneralPermission { get; set; } = InventionRole.Unassigned; + + public bool IsAGInvention { get; set; } = false; + public bool IsCertifiedInvention { get; set; } = false; + public bool IsPublished { get; set; } = false; + + public bool AllowTrial { get; set; } = false; + public bool HideFromPlayer { get; set; } = false; + + public int? Price { get; set; } = 0; + + public string BlobName { get; set; } = ""; + public int InstantiationCost { get; set; } = 0; + + public int LightsCost { get; set; } = 0; + public int AICost { get; set; } = 0; + public int ChipsCost { get; set; } = 0; + public int CloudVariablesCost { get; set; } = 0; + + public long RoomOrigin { get; set; } = -1; + public string Tags { get; set; } = string.Empty; + + public Invention ToInvention() + { + return new Invention + { + InventionId = InventionId, + CreationRoomId = RoomOrigin >= 0 ? RoomOrigin : 0, + ReplicationId = ReplicationId, + CreatorPlayerId = CreatorPlayerId, + Name = Name, + Description = Description, + ImageName = ImageName, + CurrentVersionNumber = CurrentVersionNumber, + ModifiedAt = ModifiedAt, + CreatedAt = CreatedAt, + NumPlayersHaveUsedInRoom = NumPlayersHaveUsedInRoom, + NumDownloads = NumDownloads, + CheerCount = CheerCount, + CreatorPermission = CreatorPermission, + GeneralPermission = GeneralPermission, + IsAGInvention = IsAGInvention, + IsCertifiedInvention = IsCertifiedInvention, + IsPublished = IsPublished, + AllowTrial = AllowTrial, + HideFromPlayer = HideFromPlayer, + Price = Price is > 0 ? Price : null + }; + } + public InventionVersion ToVersion() + { + return new InventionVersion + { + InventionId = InventionId, + ReplicationId = ReplicationId, + + BlobName = BlobName, + + InstantiationCost = InstantiationCost, + LightsCost = LightsCost, + ChipsCost = ChipsCost, + CloudVariablesCost = CloudVariablesCost, + + VersionNumber = CurrentVersionNumber, + }; + } + public async Task ToFullInvention(RecDBContext ctx) + { + var full = new FullInvention(); + full.Status = InventionStatus.Success; + full.Invention = ToInvention(); + full.InventionVersion = await ctx.GetInventionVersion(CurrentVersionNumber, InventionId); + return full; + } + public async Task ToFullInvention(RecDBContext ctx, int version) + { + var full = new FullInvention(); + full.Status = InventionStatus.Success; + full.Invention = ToInvention(); + full.InventionVersion = await ctx.GetInventionVersion(version, InventionId); + return full; + } + } + + public static class InventionGetter + { + public static async Task GetLatestInventionVersion(this RecDBContext ctx, long inventionId) + { + var ver = await ctx.InventionVersions.LastOrDefaultAsync(x => x.InventionId == inventionId); + return ver; + } + public static async Task GetInventionVersion(this RecDBContext ctx, int version, long inventionId) + { + var ver = await ctx.InventionVersions.FirstOrDefaultAsync(x => x.InventionId == inventionId && x.VersionNumber == version); + return ver; + } + } +} diff --git a/Server/Inventions/InventionPersonalDetails.cs b/Server/Inventions/InventionPersonalDetails.cs new file mode 100644 index 0000000..cc8173f --- /dev/null +++ b/Server/Inventions/InventionPersonalDetails.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Inventions +{ + public class InventionPersonalDetails + { + [JsonIgnore, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long DetailId { get; set; } + [JsonIgnore] + public long PlayerId { get; set; } + [JsonIgnore] + public long InventionId { get; set; } + + public bool IsCheering { get; set; } + } +} diff --git a/Server/Inventions/InventionVersion.cs b/Server/Inventions/InventionVersion.cs new file mode 100644 index 0000000..2020338 --- /dev/null +++ b/Server/Inventions/InventionVersion.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Inventions +{ + public class InventionVersion + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), JsonIgnore] + public long VersionId { get; set; } + + public long InventionId { get; set; } + public string ReplicationId { get; set; } = ""; + + public int VersionNumber { get; set; } = 0; + + public int InstantiationCost { get; set; } = 0; + public int CloudVariablesCost { get; set; } = 0; + public int ChipsCost { get; set; } = 0; + public int LightsCost { get; set; } = 0; + + public string BlobName { get; set; } = ""; + } +} diff --git a/Server/Inventions/InventionsV1.cs b/Server/Inventions/InventionsV1.cs new file mode 100644 index 0000000..f1a3429 --- /dev/null +++ b/Server/Inventions/InventionsV1.cs @@ -0,0 +1,291 @@ +using CSharpDiscordWebhook.Objects; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Inventions; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Inventions; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Inventions.V1 +{ + [Route("api/inventions/v1")] + public class InventionsV1(RecDBContext db) : Controller + { + [HttpGet("room")] + public List GetInventionsInRoom() + { + return []; + } + + [HttpGet("fromcreators")] + public async Task GetInventionsFromCreators( + [FromQuery] long[]? id, + [FromQuery] int skip = 0, + [FromQuery] int take = 100) + { + if (id == null || id.Length == 0) + return BadRequest(new { error = "At least one creator id is required." }); + + var creatorIds = id.Where(creatorId => creatorId > 0).Distinct().ToArray(); + var offset = Math.Max(0, skip); + var limit = Math.Clamp(take, 1, 100); + var inventions = await db.Inventions + .Where(invention => creatorIds.Contains(invention.CreatorPlayerId)) + .OrderByDescending(invention => invention.CreatedAt) + .ThenByDescending(invention => invention.InventionId) + .Skip(offset) + .Take(limit) + .ToListAsync(); + + return Ok(inventions.Select(invention => invention.ToInvention()).ToList()); + } + + [HttpGet("unpublish"), Authorize] + public async Task UnpublishInvention([FromQuery] long inventionId) + { + var userId = User.GetUserId(); + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.CreatorPlayerId == userId && x.InventionId == inventionId); + if (invention == null) return null; + + invention.IsPublished = false; + + await db.SaveChangesAsync(); + + return await invention.ToFullInvention(db); + } + + [HttpGet("personaldetails/{inventionId}")] + public async Task GetPersonalDetailsForInvention(long inventionId) + { + var userId = User.GetUserId(); + + var details = await db.InventionPersonalDetails.FirstOrDefaultAsync(x => x.InventionId == inventionId && x.PlayerId == userId); + if (details == null) return new InventionPersonalDetails { IsCheering = false }; + + return details; + } + + [HttpGet("tagfilters")] + public RoomFilters? GetTagFilters() + { + return new RoomFilters + { + PinnedFilters = ["dormskin", "weapon", "certified", "plushie", "scifi", "fantasy", "costume", "art", "character", "environment", "gadget"], + PopularFilters = ["dormskin", "weapon", "certified", "plushie", "scifi", "fantasy", "costume", "art", "character", "environment", "gadget"], + TrendingFilters = [] + }; + } + + [HttpGet("dormskinsfromids")] + public async Task> GetDormSkinsFromIds([FromQuery] string[]? ids) + { + var parsed = (ids ?? Array.Empty()) + .SelectMany(raw => (raw ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + .Select(part => long.TryParse(part, out var value) ? value : -1) + .Where(value => value > 0) + .Distinct() + .ToList(); + + if (parsed.Count == 0) return []; + + var known = await db.Inventions.AsNoTracking() + .Where(x => parsed.Contains(x.InventionId) && !x.HideFromPlayer) + .Select(x => x.InventionId) + .ToListAsync(); + var knownSet = known.ToHashSet(); + + return parsed.Where(knownSet.Contains).ToList(); + } + + [HttpGet("featureddormskins")] + public async Task> GetFeaturedDormSkins() + { + var featured = await db.Inventions.AsNoTracking() + .Where(x => !x.HideFromPlayer && x.IsPublished) + .OrderByDescending(x => x.ModifiedAt) + .Take(24) + .ToListAsync(); + + return featured.Select(x => x.ToInvention()).ToList(); + } + + [HttpGet("version"), Authorize] + public async Task GetInventionVersion([FromQuery] long inventionId, [FromQuery] int version) + { + var userId = User.GetUserId(); + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.InventionId == inventionId); + if (invention == null) return null; + + return await db.GetInventionVersion(version, inventionId); + } + + [HttpGet("update"), Authorize] + public async Task UpdateInvention([FromQuery] long inventionId, [FromQuery] string? name, [FromQuery] string? imgName, [FromQuery] string? description, [FromQuery] string? permission) + { + var userId = User.GetUserId(); + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.CreatorPlayerId == userId && x.InventionId == inventionId); + if (invention == null) return null; + + if (name != null) + invention.Name = name; + if (description != null) + invention.Description = description; + if (imgName != null) + invention.ImageName = imgName; + + if (permission != null) + { + if (Enum.TryParse(permission, out var parsedRole)) + invention.GeneralPermission = parsedRole; + } + + await db.SaveChangesAsync(); + + return await invention.ToFullInvention(db); + } + + [HttpGet("toptoday")] + public async Task> GetTopInventions() + { + var topInventions = await db.Inventions.Where(x => !x.HideFromPlayer && x.IsPublished) + .OrderByDescending(x => x.ModifiedAt).ToListAsync(); + + var converted = new List(); + foreach (var i in topInventions) + { + var invention = i.ToInvention(); + converted.Add(invention); + } + + return converted; + } + + [HttpGet("featured")] + public async Task> GetFeaturedInventions() + { + var featured = await db.Inventions + .Where(x => !x.HideFromPlayer && x.IsPublished) + .OrderByDescending(x => x.ModifiedAt) + .Take(24) + .ToListAsync(); + + return featured.Select(x => x.ToInvention()).ToList(); + } + + [HttpPost("settags"), Authorize] + public async Task UpdateInventionTags([FromBody] ModifyInventionTagsRequest req) + { + var userId = User.GetUserId(); + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.CreatorPlayerId == userId && x.InventionId == req.InventionId); + if (invention == null) return new InventionTagResponse { Result = RecNet.Enums.InventionTagResult.InventionDoesNotExist }; + if (req.CustomTags.Count >= 10) new InventionTagResponse { Result = RecNet.Enums.InventionTagResult.TooManyTags }; + + var tags = new List(); + foreach (string tag in req.AutoTags) + { + tags.Add(new ItemTag { Tag = tag, Type = RecNet.Enums.TagType.Auto }); + } + foreach (string tag in req.CustomTags) + { + tags.Add(new ItemTag { Tag = tag, Type = RecNet.Enums.TagType.General }); + } + invention.Tags = StringTools.CreateTagString(tags); + + await db.SaveChangesAsync(); + + return new InventionTagResponse { Tags = StringTools.TagsToStrings(tags), Result = RecNet.Enums.InventionTagResult.Success }; + } + + [HttpGet("details"), Authorize] + public async Task GetInventionDetails([FromQuery] long inventionId) + { + var userId = User.GetUserId(); + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.InventionId == inventionId); + + if (invention == null) + return null; + if (invention.Tags == null) + return new TagResponse { Tags = [] }; + + return new TagResponse { Tags = StringTools.ReadTags(invention.Tags) }; + } + + [HttpGet("batch"), Authorize] + public async Task?> GetBatchInventions([FromBody] List ids) + { + var userId = User.GetUserId(); + + var inventions = new List(); + + foreach (var id in ids) + { + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.InventionId == id); + if (invention == null) continue; + inventions.Add(invention.ToInvention()); + } + + return inventions; + } + + [HttpGet("fulllineageowner"), Authorize] + public async Task OwnsInventionLineage([FromQuery] List id) + { + var userId = User.GetUserId(); + + + + + + + var unlocks = await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == userId); + var bought = new HashSet(); + if (unlocks != null && !string.IsNullOrWhiteSpace(unlocks.Inventions) && unlocks.Inventions != "null") + { + try { bought = (unlocks.Inventions.FromJson>() ?? new List()).ToHashSet(); } + catch { } + } + + bool owns = false; + foreach (var i in id) + { + if (bought.Contains(i)) + { + owns = true; + break; + } + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.InventionId == i); + + + + if (invention != null) + owns = (invention.CreatorPlayerId == userId || invention.GeneralPermission == InventionRole.Publish); + + if (owns) break; + } + + return owns; + } + + [HttpGet(""), Authorize] + public async Task GetInventionById([FromQuery] long id) + { + var userId = User.GetUserId(); + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.InventionId == id); + if (invention == null) return null; + + return invention; + } + } +} diff --git a/Server/Inventions/InventionsV2.cs b/Server/Inventions/InventionsV2.cs new file mode 100644 index 0000000..eeb9a95 --- /dev/null +++ b/Server/Inventions/InventionsV2.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Inventions; +using RadicadeServer.RecNet.Inventions; + +namespace RadicadeServer.API.Inventions.V2 +{ + [Route("api/inventions/v2")] + public class InventionsV2(RecDBContext db) : Controller + { + [HttpGet("search")] + public async Task> SearchInventions([FromQuery] int? take, [FromQuery] string? value) + { + if (take == null) take = 50; + + List? foundInventions = null; + + if (value != null) + { + foundInventions = await db.Inventions.Where(x => !x.HideFromPlayer && x.IsPublished && + (x.Name.Contains(value) || x.Description.Contains(value) || x.Tags.Contains(value)) + ).OrderByDescending(x => x.ModifiedAt).Take((int)take).ToListAsync(); + } + else + { + foundInventions = await db.Inventions.Where(x => !x.HideFromPlayer && x.IsPublished) + .OrderByDescending(x => x.ModifiedAt).Take((int)take).ToListAsync(); + } + + var converted = new List(); + foreach (var i in foundInventions) + { + var invention = i.ToInvention(); + converted.Add(invention); + } + + return converted; + } + + [HttpGet("batch"), Authorize] + public async Task?> GetBatchInventions_V2([FromQuery] List id) + { + var userId = User.GetUserId(); + + var inventions = new List(); + + foreach (var i in id) + { + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.InventionId == i); + if (invention == null) continue; + inventions.Add(invention.ToInvention()); + } + + return inventions; + } + + [HttpGet("mine"), Authorize] + public async Task> GetOwnedInventions() + { + var userId = User.GetUserId(); + + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId); + if (unlocks == null) return []; + + var ownedInventions = unlocks.Inventions.FromJson>() ?? new List(); + + var allInventions = new List(); + foreach (var invention in ownedInventions) + { + var foundInvention = await db.Inventions.FirstOrDefaultAsync(x => x.InventionId == invention); + if (foundInvention != null) + allInventions.Add(foundInvention.ToInvention()); + } + + return allInventions; + } + } +} diff --git a/Server/Inventions/InventionsV3.cs b/Server/Inventions/InventionsV3.cs new file mode 100644 index 0000000..6ed2bb9 --- /dev/null +++ b/Server/Inventions/InventionsV3.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Inventions; +using RadicadeServer.RecNet.Inventions; + +namespace RadicadeServer.API.Inventions.V3 +{ + [Route("api/inventions/v3")] + public class InventionsV3(RecDBContext db) : Controller + { + [HttpGet("publish"), Authorize] + public async Task PublishInvention([FromQuery] long inventionId, [FromQuery] InventionRole permissionLevel, [FromQuery] int? price) + { + var userId = User.GetUserId(); + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.CreatorPlayerId == userId && x.InventionId == inventionId); + if (invention == null) return null; + + invention.IsPublished = true; + invention.HideFromPlayer = false; + + if (price.HasValue) + invention.Price = price.Value; + + await db.SaveChangesAsync(); + + return await invention.ToFullInvention(db); + } + } +} diff --git a/Server/Inventions/InventionsV4.cs b/Server/Inventions/InventionsV4.cs new file mode 100644 index 0000000..1062c04 --- /dev/null +++ b/Server/Inventions/InventionsV4.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Inventions; +using RadicadeServer.RecNet.Inventions; + +namespace RadicadeServer.API.Inventions.V4 +{ + [Route("api/inventions/v4")] + public class InventionsV4(RecDBContext db) : Controller + { + [HttpPost("addversion"), Authorize] + public async Task AddInventionVersion([FromBody] NewInventionVersionDTO req) + { + var userId = User.GetUserId(); + + var invention = await db.Inventions.FirstOrDefaultAsync(x => x.CreatorPlayerId == userId && x.InventionId == req.inventionId); + if (invention == null) return null; + + var version = new InventionVersion + { + BlobName = req.inventionDataFilename, + ChipsCost = req.chipsCost, + CloudVariablesCost = req.cloudVariablesCost, + InstantiationCost = req.instantiationCost, + InventionId = req.inventionId, + ReplicationId = invention.ReplicationId, + LightsCost = invention.LightsCost, + VersionNumber = invention.CurrentVersionNumber + 1 + }; + + await db.InventionVersions.AddAsync(version); + await db.SaveChangesAsync(); + + invention.CurrentVersionNumber = version.VersionNumber; + + invention.BlobName = req.inventionDataFilename; + + invention.RoomOrigin = req.creationRoomId; + + invention.ModifiedAt = DateTime.Now; + + invention.InstantiationCost = req.instantiationCost; + invention.LightsCost = req.lightsCost; + invention.ChipsCost = req.chipsCost; + invention.CloudVariablesCost = req.cloudVariablesCost; + invention.AICost = req.aiCost; + + await db.SaveChangesAsync(); + + return await invention.ToFullInvention(db); + } + } +} diff --git a/Server/Inventions/InventionsV6.cs b/Server/Inventions/InventionsV6.cs new file mode 100644 index 0000000..4fd0c0d --- /dev/null +++ b/Server/Inventions/InventionsV6.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Inventions; +using RadicadeServer.RecNet.Inventions; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Inventions.V6 +{ + [Route("api/inventions/v6")] + public class InventionsV6(RecDBContext db) : Controller + { + [HttpPost("save"), Authorize] + public async Task SaveInventionV6([FromBody] NewInventionRequestDTO_V6 dto) + { + var userId = User.GetUserId(); + + if (!dto.name.IsPure()) return new FullInvention { Status = InventionStatus.InnapropriateName }; + if (!dto.description.IsPure()) return new FullInvention { Status = InventionStatus.InnapropriateDescription }; + + var playerUnlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId); + if (playerUnlocks == null) return null; + + var invention = new InventionDB + { + Name = dto.name, + CreatorPlayerId = (long)userId!, + InstantiationCost = dto.instantiationCost, + LightsCost = dto.lightsCost, + RoomOrigin = dto.creationRoomId, + ImageName = dto.imageName, + BlobName = dto.inventionDataFilename, + Description = dto.description, + ReplicationId = Guid.NewGuid().ToString(), + GeneralPermission = InventionRole.Publish + }; + + await db.AddAsync(invention); + await db.SaveChangesAsync(); + + var version = new InventionVersion + { + BlobName = dto.inventionDataFilename, + ChipsCost = dto.chipsCost, + CloudVariablesCost = dto.cloudVariablesCost, + InstantiationCost = dto.instantiationCost, + InventionId = invention.InventionId, + ReplicationId = invention.ReplicationId, + LightsCost = invention.LightsCost, + VersionNumber = invention.CurrentVersionNumber + }; + + await db.InventionVersions.AddAsync(version); + + var inventionIds = playerUnlocks.Inventions.FromJson>(); + if (inventionIds == null) inventionIds = new List(); + if (!inventionIds.Contains(invention.InventionId)) + { + inventionIds.Add(invention.InventionId); + playerUnlocks.Inventions = inventionIds.ToJson(); + await db.SaveChangesAsync(); + } + + return await invention.ToFullInvention(db); + } + } +} diff --git a/Server/Inventions/ModifyInventionTagsRequest.cs b/Server/Inventions/ModifyInventionTagsRequest.cs new file mode 100644 index 0000000..d61a576 --- /dev/null +++ b/Server/Inventions/ModifyInventionTagsRequest.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Inventions +{ + public class ModifyInventionTagsRequest + { + public long InventionId { get; set; } + public List AutoTags { get; set; } = new(); + public List CustomTags { get; set; } = new(); + } +} diff --git a/Server/Inventions/NewInventionRequestDTO.cs b/Server/Inventions/NewInventionRequestDTO.cs new file mode 100644 index 0000000..720bf9a --- /dev/null +++ b/Server/Inventions/NewInventionRequestDTO.cs @@ -0,0 +1,42 @@ +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.RecNet.Inventions +{ + public class NewInventionRequestDTO + { + public string name { get; set; } = ""; + public string description { get; set; } = ""; + public string imageName { get; set; } = ""; + public string inventionDataFilename { get; set; } = ""; + + public List referencedInventions { get; set; } = new(); + + public RoleType creatorAccountRole { get; set; } + + public int instantiationCost { get; set; } + public int lightsCost { get; set; } + public int aiCost { get; set; } + + public long creationRoomId { get; set; } + } + public class NewInventionRequestDTO_V6 + { + public string name { get; set; } = ""; + public string description { get; set; } = ""; + public string imageName { get; set; } = ""; + public string inventionDataFilename { get; set; } = ""; + + public List referencedInventions { get; set; } = new(); + + public RoleType creatorAccountRole { get; set; } = RoleType.Creator; + + public int instantiationCost { get; set; } = 0; + public int cloudVariablesCost { get; set; } = 0; + public int chipsCost { get; set; } = 0; + public int lightsCost { get; set; } = 0; + public int aiCost { get; set; } = 0; + + public long creationRoomId { get; set; } = 0; + } +} diff --git a/Server/Inventions/NewInventionVersionDTO.cs b/Server/Inventions/NewInventionVersionDTO.cs new file mode 100644 index 0000000..c349ae9 --- /dev/null +++ b/Server/Inventions/NewInventionVersionDTO.cs @@ -0,0 +1,18 @@ +namespace RadicadeServer.RecNet.Inventions +{ + public class NewInventionVersionDTO + { + public int aiCost { get; set; } = 0; + public int chipsCost { get; set; } = 0; + public int cloudVariablesCost { get; set; } = 0; + public int lightsCost { get; set; } = 0; + public int instantiationCost { get; set; } = 0; + + public string inventionDataFilename { get; set; } = ""; + + public List referencedInventions { get; set; } = new(); + + public long inventionId { get; set; } = 0; + public long creationRoomId { get; set; } + } +} diff --git a/Server/Keepsakes/KeepsakeCatalog.cs b/Server/Keepsakes/KeepsakeCatalog.cs new file mode 100644 index 0000000..8d0d9de --- /dev/null +++ b/Server/Keepsakes/KeepsakeCatalog.cs @@ -0,0 +1,44 @@ +namespace RadicadeServer.RecNet.Keepsakes +{ + public static class KeepsakeCatalog + { + public sealed record Category(int KeepsakeCategoryId, string VisualId, int LimitPerRoom, int XpValue, string IconOutlineImageName, string IconFilledImageName); + + public static readonly Category[] All = + { + new(6, "science_core_red_OLD", 5, 40, "aebsucfhhn98ymh4z25ot8d6x.png", "cxaofjxo76zxa338274fsstof.png"), + new(5, "science_core_blue_OLD", 5, 30, "4w053xrmhi3as5s50fm6kpw6h.png", "bx4ikmxlnmk2tf3z132j94yrd.png"), + new(4, "science_core_yellow_OLD", 5, 20, "08ms9eetb8xhyv9d9nt4ml1lq.png", "0tliyqyr3ckv9h7i5zo6w5sa1.png"), + new(2, "presents", 2, 50, "d5v1c62yb5tzedako4x91u7ll.png", "9abduornso8kkr34frsoyp7wp.png"), + new(0, "explore", 3, 20, "aknjr1eq1hmrx7mr7wyfvll07.png", "701ffxv6rli6btqtvzdh26mse.png"), + }; + + public static bool Enabled + => !string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_CATALOG"), "empty", StringComparison.OrdinalIgnoreCase); + + public static string? IconOverride + => NullIfBlank(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_ICON")); + + public static object[] Results() => Results(Enabled); + + public static object[] Results(bool enabled) + { + if (!enabled) return Array.Empty(); + var icon = IconOverride; + return All.Select(c => (object)new + { + KeepsakeCategoryId = c.KeepsakeCategoryId, + VisualId = c.VisualId, + LimitPerRoom = c.LimitPerRoom, + XpValue = c.XpValue, + IconOutlineImageName = icon ?? c.IconOutlineImageName, + IconFilledImageName = icon ?? c.IconFilledImageName, + }).ToArray(); + } + + public static Category? Find(int id) => All.FirstOrDefault(c => c.KeepsakeCategoryId == id); + + private static string? NullIfBlank(string? value) + => string.IsNullOrWhiteSpace(value) ? null : value; + } +} diff --git a/Server/Keepsakes/KeepsakeFeature.cs b/Server/Keepsakes/KeepsakeFeature.cs new file mode 100644 index 0000000..725686c --- /dev/null +++ b/Server/Keepsakes/KeepsakeFeature.cs @@ -0,0 +1,21 @@ +namespace RadicadeServer.RecNet.Keepsakes; + +public static class KeepsakeFeature +{ + public static bool? Override { get; set; } + + public static bool Enabled + => Override ?? EnabledFor(KeepsakeRoomConfig.Current, Environment.GetEnvironmentVariable("RADICADE_KEEPSAKES")); + + public static bool EnabledFor(KeepsakeRoomConfig keepsakeRooms, string? environment) + { + if (!string.IsNullOrWhiteSpace(environment)) return IsOn(environment); + return keepsakeRooms.Any; + } + + private static bool IsOn(string value) => value.Trim().ToLowerInvariant() switch + { + "1" or "true" or "on" or "yes" or "enable" or "enabled" => true, + _ => false, + }; +} diff --git a/Server/Keepsakes/KeepsakeRecords.cs b/Server/Keepsakes/KeepsakeRecords.cs new file mode 100644 index 0000000..04f00a3 --- /dev/null +++ b/Server/Keepsakes/KeepsakeRecords.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Keepsakes +{ + public class KeepsakeInstance + { + [Key] + public Guid KeepsakeInstanceId { get; set; } + + public long RoomId { get; set; } + + public long? SubRoomId { get; set; } + + public int KeepsakeCategoryId { get; set; } + + public long PlacedByAccountId { get; set; } + + public DateTime PlacedAt { get; set; } + + public bool Removed { get; set; } + } + + public class KeepsakeCollectionRecord + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long KeepsakeCollectionRecordId { get; set; } + + public long AccountId { get; set; } + + public Guid KeepsakeInstanceId { get; set; } + + public int KeepsakeCategoryId { get; set; } + + public DateTime CollectedAt { get; set; } + } +} diff --git a/Server/Keepsakes/KeepsakeRoomConfig.cs b/Server/Keepsakes/KeepsakeRoomConfig.cs new file mode 100644 index 0000000..4da95ba --- /dev/null +++ b/Server/Keepsakes/KeepsakeRoomConfig.cs @@ -0,0 +1,235 @@ +using System.Text.Json; +using RadicadeServer.DB; + +namespace RadicadeServer.RecNet.Keepsakes; + +public sealed class KeepsakeRoomConfig +{ + public const string FileName = "keepsake-rooms.json"; + + public const string AllRoomsToken = "*"; + + public const long DefaultRoomListId = 1; + + public const int DefaultRoomLimit = 10; + + public const long DefaultEventId = 1; + + public const string DefaultEventName = "Science Core"; + + public IReadOnlySet RoomIds { get; init; } = new HashSet(); + + public bool AllRooms { get; init; } + + public long RoomListId { get; init; } = DefaultRoomListId; + + public int RoomLimit { get; init; } = DefaultRoomLimit; + + public long EventId { get; init; } = DefaultEventId; + + public string EventName { get; init; } = DefaultEventName; + + public bool EventEnabled { get; init; } = true; + + public bool Any => AllRooms || RoomIds.Count > 0; + + public bool IsKeepsakeRoom(long roomId) => AllRooms || RoomIds.Contains(roomId); + + public long[] AdvertisedRooms(long roomId) + => RoomIds.Count == 0 ? new[] { roomId } : RoomIds.OrderBy(id => id).ToArray(); + + public long[] ConfiguredRooms => RoomIds.OrderBy(id => id).ToArray(); + + public object[] RoomListEntries(long[] rooms) + => new object[] + { + new + { + KeepsakeRoomListId = RoomListId, + ProgressionEventId = EventId, + UnlockItemAvatarItemId = (int?)null, + UnlockItemGiftDropId = (int?)null, + UnlockItemLockDurationTicks = (long?)null, + RoomUnlockStartOffsetTicks = 0L, + RoomUnlockIntervalTicks = TimeSpan.TicksPerDay, + RoomUnlockBatchSize = Math.Max(rooms.Length, 1), + RoomType = 0, + KeepsakeRoomLimit = RoomLimit, + KeepsakeRooms = rooms.Select((id, order) => new + { + KeepsakeRoomId = id, RoomId = id, KeepsakeRoomListId = RoomListId, + Type = 0, Order = order + }).ToArray(), + } + }; + + public static KeepsakeRoomConfig? Override { get; set; } + + public static KeepsakeRoomConfig Current => Override ?? Load(); + + public static KeepsakeRoomConfig Load() + => Parse( + ReadConfigFile(), + Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_ROOMS"), + Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_ROOM_LIST_ID"), + Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_ROOM_LIMIT"), + Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_ID"), + Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_NAME")); + + public static IEnumerable CandidatePaths() + => DBConfig.AvatarCatalogDirectories() + .Select(directory => Path.Join(directory, FileName)) + .Distinct(StringComparer.OrdinalIgnoreCase); + + private static string? ReadConfigFile() + { + foreach (var path in CandidatePaths()) + { + try + { + if (File.Exists(path)) return File.ReadAllText(path); + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + } + + return null; + } + + public static KeepsakeRoomConfig Parse( + string? json, + string? rooms = null, + string? roomListId = null, + string? roomLimit = null, + string? eventIdText = null, + string? eventNameText = null) + { + var allRooms = false; + var ids = new HashSet(); + var listId = DefaultRoomListId; + var limit = DefaultRoomLimit; + var eventId = DefaultEventId; + var eventName = DefaultEventName; + var eventEnabled = true; + + if (!string.IsNullOrWhiteSpace(json)) + { + try + { + using var document = JsonDocument.Parse(json); + var root = document.RootElement; + + if (root.ValueKind == JsonValueKind.Array) AddIds(root, ids); + else if (root.ValueKind == JsonValueKind.Object) + { + foreach (var property in root.EnumerateObject()) + { + switch (property.Name.ToLowerInvariant()) + { + case "allrooms": + allRooms = IsTrue(property.Value); + break; + case "roomids" or "rooms" or "keepsakerooms": + AddIds(property.Value, ids); + break; + case "roomlistid" or "keepsakeroomlistid": + listId = ReadLong(property.Value) ?? listId; + break; + case "roomlimit" or "keepsakeroomlimit": + if (ReadLong(property.Value) is > 0 and <= int.MaxValue and var configuredLimit) + limit = (int)configuredLimit; + break; + case "eventenabled": + eventEnabled = IsTrue(property.Value); + break; + case "eventid" or "progressioneventid": + eventId = ReadLong(property.Value) ?? eventId; + break; + case "eventname" or "progressioneventname": + var name = property.Value.ValueKind == JsonValueKind.String ? property.Value.GetString() : null; + if (!string.IsNullOrWhiteSpace(name)) eventName = name!.Trim(); + break; + } + } + } + } + catch (JsonException) + { + } + } + + if (!string.IsNullOrWhiteSpace(rooms)) + { + ids.Clear(); + var text = rooms.Trim(); + allRooms = text == AllRoomsToken; + if (!allRooms) AddIdsFromText(text, ids); + } + + if (long.TryParse(roomListId, out var parsedListId)) listId = parsedListId; + if (int.TryParse(roomLimit, out var parsedLimit) && parsedLimit > 0) limit = parsedLimit; + if (long.TryParse(eventIdText, out var parsedEventId)) eventId = parsedEventId; + if (!string.IsNullOrWhiteSpace(eventNameText)) eventName = eventNameText.Trim(); + + return new KeepsakeRoomConfig + { + RoomIds = ids, + AllRooms = allRooms, + RoomListId = listId, + RoomLimit = limit, + EventId = eventId, + EventName = eventName, + EventEnabled = eventEnabled, + }; + } + + private static void AddIds(JsonElement element, HashSet ids) + { + switch (element.ValueKind) + { + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + if (ReadLong(item) is { } id) ids.Add(id); + } + break; + case JsonValueKind.String: + AddIdsFromText(element.GetString(), ids); + break; + default: + if (ReadLong(element) is { } single) ids.Add(single); + break; + } + } + + private static void AddIdsFromText(string? text, HashSet ids) + { + if (string.IsNullOrWhiteSpace(text)) return; + + foreach (var part in text.Split(new[] { ',', ';', ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (long.TryParse(part, out var id)) ids.Add(id); + } + } + + private static long? ReadLong(JsonElement element) => element.ValueKind switch + { + JsonValueKind.Number when element.TryGetInt64(out var number) => number, + JsonValueKind.String when long.TryParse(element.GetString(), out var parsed) => parsed, + _ => null, + }; + + private static bool IsTrue(JsonElement element) => element.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Number when element.TryGetInt64(out var number) => number != 0, + JsonValueKind.String => string.Equals(element.GetString()?.Trim(), "true", StringComparison.OrdinalIgnoreCase) + || element.GetString()?.Trim() == "1", + _ => false, + }; +} diff --git a/Server/Keepsakes/KeepsakesAPI.cs b/Server/Keepsakes/KeepsakesAPI.cs new file mode 100644 index 0000000..d63ee2a --- /dev/null +++ b/Server/Keepsakes/KeepsakesAPI.cs @@ -0,0 +1,402 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Keepsakes; + +namespace RadicadeServer.API.Keepsakes; + +[ApiController] +[Route("api/keepsakes")] +public sealed class KeepsakesAPI : ControllerBase +{ + private readonly RecDBContext? db; + + public KeepsakesAPI(RecDBContext db) => this.db = db; + + private long? CallerAccountId => ControllerContext?.HttpContext?.User?.GetUserId(); + + [HttpGet("globalconfig")] + public IActionResult GlobalConfig() + { + var enabled = KeepsakeFeature.Enabled; + return Ok(new + { + KeepsakeFeatureEnabled = enabled, + KeepsakeRoomLimit = enabled ? KeepsakeRoomConfig.Current.RoomLimit : 0, + SocialXpBoostEnabled = false + }); + } + + [HttpGet("categories")] + public IActionResult Categories() + { + var results = KeepsakeFeature.Enabled ? KeepsakeCatalog.Results() : Array.Empty(); + return Ok(new { Results = results, TotalResults = results.Length }); + } + + [HttpGet("rooms/{roomId:long}")] + public async Task Room(long roomId) + { + if (db == null) return NoContent(); + + if (!KeepsakeFeature.Enabled) return NoContent(); + + var instances = await db.KeepsakeInstances + .Where(i => i.RoomId == roomId && !i.Removed) + .ToListAsync(); + + var keepsakeRooms = KeepsakeRoomConfig.Current; + var isKeepsakeRoom = keepsakeRooms.IsKeepsakeRoom(roomId); + if (instances.Count == 0 && !isKeepsakeRoom) return NoContent(); + + var accountId = CallerAccountId; + var records = accountId == null + ? new List() + : await db.KeepsakeCollectionRecords + .Where(r => r.AccountId == accountId && instances.Select(i => i.KeepsakeInstanceId).Contains(r.KeepsakeInstanceId)) + .ToListAsync(); + + var collected = records.Select(r => r.KeepsakeInstanceId).ToHashSet(); + + var uncollected = instances + .Where(i => !collected.Contains(i.KeepsakeInstanceId)) + .Select(ToInstancePayload) + .ToArray(); + var collectionRecords = records + .Select(r => new + { + AccountId = r.AccountId, + KeepsakeInstanceId = r.KeepsakeInstanceId, + KeepsakeCategoryConfigId = r.KeepsakeCategoryId, + CollectedAt = r.CollectedAt + }) + .ToArray(); + + var payload = new Dictionary + { + ["Instances"] = instances.Select(ToInstancePayload).ToArray(), + ["KeepsakeInstances"] = uncollected, + ["Keepsakes"] = uncollected, + ["KeepsakeCollectionRecords"] = collectionRecords, + ["CollectionRecords"] = collectionRecords, + ["KeepsakeProgressionEventIds"] = isKeepsakeRoom && keepsakeRooms.EventEnabled ? new[] { keepsakeRooms.EventId } : Array.Empty() + }; + + if (isKeepsakeRoom) AddKeepsakeRoomFields(payload, roomId, keepsakeRooms, instances.Count); + + return Ok(payload); + } + + private static void AddKeepsakeRoomFields(Dictionary payload, long roomId, KeepsakeRoomConfig keepsakeRooms, int placed) + { + var rooms = keepsakeRooms.AdvertisedRooms(roomId); + + payload["KeepsakeRoomId"] = roomId; + payload["KeepsakeRoomLimit"] = keepsakeRooms.RoomLimit; + payload["KeepsakeRoomListId"] = keepsakeRooms.RoomListId; + payload["KeepsakeRooms"] = rooms; + payload["KeepsakeRoomLists"] = keepsakeRooms.RoomListEntries(rooms); + } + + [HttpPost("rooms/{roomId:long}")] + public async Task Place(long roomId) + => !KeepsakeFeature.Enabled ? NotFound() : await PlaceCore(roomId, await ReadRawBody()); + + [HttpPost] + public async Task PlaceFromClient() + { + if (!KeepsakeFeature.Enabled) return NotFound(); + var raw = await ReadRawBody(); + var roomId = ReadLong(ParseJson(raw), "RoomId"); + if (roomId is not > 0) return BadRequest(new { error = "RoomId is required." }); + var result = await PlaceCore(roomId.Value, raw); + if (result is not OkObjectResult ok) return result; + return Ok(JsonSerializer.SerializeToElement(ok.Value).GetProperty("KeepsakeInstanceId").GetGuid()); + } + + private async Task PlaceCore(long roomId, string raw) + { + if (!KeepsakeFeature.Enabled) + { + return NotFound(); + } + + if (db == null) return NoContent(); + + var accountId = CallerAccountId; + if (accountId == null) return Unauthorized(); + var config = KeepsakeRoomConfig.Current; + if (!config.IsKeepsakeRoom(roomId)) return BadRequest(new { error = "This room does not have keepsake permissions." }); + + var body = ParseJson(raw); + var categoryId = ReadInt(body, "KeepsakeCategoryConfigId", "KeepsakeCategoryId", "KeepsakeCategory", "CategoryId"); + if (categoryId == null || KeepsakeCatalog.Find(categoryId.Value) == null) + { + return BadRequest(new { error = "Unknown or missing keepsake category." }); + } + + var instance = new KeepsakeInstance + { + KeepsakeInstanceId = ReadGuid(body, "KeepsakeInstanceId") ?? Guid.NewGuid(), + RoomId = roomId, + SubRoomId = ReadLong(body, "SubRoomId", "KeepsakeSubRoomId"), + KeepsakeCategoryId = categoryId.Value, + PlacedByAccountId = accountId.Value, + PlacedAt = DateTime.UtcNow + }; + + var existing = await db.KeepsakeInstances.FindAsync(instance.KeepsakeInstanceId); + if (instance.KeepsakeInstanceId == Guid.Empty) + return BadRequest(new { error = "A keepsake instance ID cannot be empty." }); + if (existing != null && (existing.RoomId != roomId || existing.PlacedByAccountId != accountId)) + return Conflict(new { error = "This keepsake instance belongs to another room or player." }); + var otherInstances = db.KeepsakeInstances.Where(i => i.RoomId == roomId && !i.Removed + && i.KeepsakeInstanceId != instance.KeepsakeInstanceId); + if (await otherInstances.CountAsync() >= config.RoomLimit + || await otherInstances.CountAsync(i => i.KeepsakeCategoryId == categoryId.Value) >= KeepsakeCatalog.Find(categoryId.Value)!.LimitPerRoom) + return BadRequest(new { error = "Cannot place additional keepsakes in this room." }); + if (existing == null) await db.KeepsakeInstances.AddAsync(instance); + else + { + existing.RoomId = instance.RoomId; + existing.SubRoomId = instance.SubRoomId; + existing.KeepsakeCategoryId = instance.KeepsakeCategoryId; + existing.Removed = false; + } + await db.SaveChangesAsync(); + + return Ok(ToInstancePayload(existing ?? instance)); + } + + [HttpPost("rooms/{roomId:long}/collect")] + public async Task Collect(long roomId) + { + if (!KeepsakeFeature.Enabled) + { + return NotFound(); + } + + if (db == null) return NoContent(); + + var accountId = CallerAccountId; + if (accountId == null) return Unauthorized(); + + var raw = await ReadRawBody(); + var body = ParseJson(raw); + var wanted = ReadGuidList(body, "KeepsakeInstanceIds", "InstanceIds", "KeepsakeInstanceId"); + if (wanted.Count == 0) return BadRequest(new { error = "At least one keepsake instance ID is required." }); + var instances = await db.KeepsakeInstances + .Where(i => i.RoomId == roomId && !i.Removed && wanted.Contains(i.KeepsakeInstanceId)) + .ToListAsync(); + + var mine = await db.KeepsakeCollectionRecords + .Where(r => r.AccountId == accountId && wanted.Contains(r.KeepsakeInstanceId)) + .Select(r => r.KeepsakeInstanceId) + .ToListAsync(); + + foreach (var instance in instances.Where(i => !mine.Contains(i.KeepsakeInstanceId))) + { + await db.KeepsakeCollectionRecords.AddAsync(new KeepsakeCollectionRecord + { + AccountId = accountId.Value, + KeepsakeInstanceId = instance.KeepsakeInstanceId, + KeepsakeCategoryId = instance.KeepsakeCategoryId, + CollectedAt = DateTime.UtcNow + }); + } + await db.SaveChangesAsync(); + + return Ok(new + { + KeepsakeRoomId = roomId, + KeepsakeInstanceIds = instances.Select(i => i.KeepsakeInstanceId).ToArray() + }); + } + + [HttpDelete("rooms/{roomId:long}/{instanceId:guid}")] + public async Task Remove(long roomId, Guid instanceId) + { + if (!KeepsakeFeature.Enabled) return NotFound(); + if (db == null) return NoContent(); + var accountId = CallerAccountId; + if (accountId == null) return Unauthorized(); + var instance = await db.KeepsakeInstances.FindAsync(instanceId); + if (instance == null || instance.RoomId != roomId) return NotFound(); + if (instance.PlacedByAccountId != accountId) return Forbid(); + instance.Removed = true; + await db.SaveChangesAsync(); + return NoContent(); + } + + [HttpGet("events")] + public IActionResult Events() => Ok(Array.Empty()); + + [HttpGet("events/{eventId:long}")] + public async Task EventInstances(long eventId) + { + var config = KeepsakeRoomConfig.Current; + if (!KeepsakeFeature.Enabled || !config.EventEnabled || eventId != config.EventId) return NotFound(); + if (db == null) return Ok(new { Instances = Array.Empty(), CollectionRecords = Array.Empty() }); + var instances = await db.KeepsakeInstances.Where(i => !i.Removed).ToListAsync(); + instances = instances.Where(i => config.IsKeepsakeRoom(i.RoomId)).ToList(); + var accountId = CallerAccountId; + var collected = accountId == null ? new List() : await db.KeepsakeCollectionRecords + .Where(r => r.AccountId == accountId).Select(r => r.KeepsakeInstanceId).ToListAsync(); + object[] Groups(IEnumerable rows) => rows.GroupBy(i => i.RoomId) + .Select(g => (object)new { RoomId = g.Key, KeepsakeInstanceIds = g.Select(i => i.KeepsakeInstanceId).ToArray() }).ToArray(); + return Ok(new { Instances = Groups(instances), CollectionRecords = Groups(instances.Where(i => collected.Contains(i.KeepsakeInstanceId))) }); + } + + [HttpDelete("{instanceId:guid}")] + public async Task RemoveFromClient(Guid instanceId) + { + if (!KeepsakeFeature.Enabled) return NotFound(); + if (CallerAccountId == null) return Unauthorized(); + var instance = db == null ? null : await db.KeepsakeInstances.FindAsync(instanceId); + return instance == null ? NotFound() : await Remove(instance.RoomId, instanceId); + } + + [HttpPost("{instanceId:guid}/collect")] + public async Task CollectFromClient(Guid instanceId) + { + if (!KeepsakeFeature.Enabled) return CollectionNotFound(instanceId, "Keepsakes are disabled on this server."); + var accountId = CallerAccountId; + if (accountId == null) return Unauthorized(); + var instance = db == null ? null : await db.KeepsakeInstances.FindAsync(instanceId); + if (instance == null) return CollectionNotFound(instanceId, "The keepsake instance is not registered on this server."); + if (instance.Removed) return CollectionNotFound(instanceId, "The keepsake instance has been removed."); + var newCollection = !await db!.KeepsakeCollectionRecords.AnyAsync(r => r.AccountId == accountId && r.KeepsakeInstanceId == instanceId); + if (newCollection) + { + db.KeepsakeCollectionRecords.Add(new KeepsakeCollectionRecord + { + AccountId = accountId.Value, KeepsakeInstanceId = instanceId, + KeepsakeCategoryId = instance.KeepsakeCategoryId, CollectedAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + } + return Ok(new { TotalXp = newCollection ? KeepsakeCatalog.Find(instance.KeepsakeCategoryId)?.XpValue ?? 0 : 0, SocialBoostXp = 0 }); + } + + private IActionResult CollectionNotFound(Guid instanceId, string reason) + { + Console.WriteLine($"[Keepsakes] collect instance {instanceId} REFUSED: {reason}"); + return Problem(statusCode: StatusCodes.Status404NotFound, title: "Keepsake collection failed", detail: reason); + } + + [HttpGet("{**rest}")] + [HttpPost("{**rest}")] + [HttpPut("{**rest}")] + [HttpDelete("{**rest}")] + [HttpPatch("{**rest}")] + public async Task Unclaimed(string? rest) + { + var body = await ReadRawBody(); + return NotFound(); + } + + private static object ToInstancePayload(KeepsakeInstance instance) => new + { + KeepsakeInstanceId = instance.KeepsakeInstanceId, + KeepsakeCategoryConfigId = instance.KeepsakeCategoryId, + PlacedByAccountId = instance.PlacedByAccountId, + RoomId = instance.RoomId, + SubRoomId = instance.SubRoomId, + KeepsakeRoomId = instance.RoomId, + PlacedAt = instance.PlacedAt + }; + + private async Task ReadRawBody() + { + using var reader = new StreamReader(Request.Body); + var raw = await reader.ReadToEndAsync(); + return raw; + } + + private static string LogPreview(string raw) => raw.Length > 2048 ? raw[..2048] + "..." : raw; + + private static JsonElement ParseJson(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return default; + try + { + using var document = JsonDocument.Parse(raw); + return document.RootElement.Clone(); + } + catch (JsonException) + { + return default; + } + } + + private static bool TryProp(JsonElement root, out JsonElement value, params string[] names) + { + if (root.ValueKind == JsonValueKind.Object) + { + foreach (var property in root.EnumerateObject()) + { + foreach (var name in names) + { + if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) + { + value = property.Value; + return true; + } + } + } + } + value = default; + return false; + } + + private static int? ReadInt(JsonElement root, params string[] names) + { + if (!TryProp(root, out var value, names)) return null; + return value.ValueKind switch + { + JsonValueKind.Number when value.TryGetInt32(out var number) => number, + JsonValueKind.String when int.TryParse(value.GetString(), out var parsed) => parsed, + _ => null + }; + } + + private static long? ReadLong(JsonElement root, params string[] names) + { + if (!TryProp(root, out var value, names)) return null; + if (value.ValueKind == JsonValueKind.Null) return null; + return value.ValueKind switch + { + JsonValueKind.Number when value.TryGetInt64(out var number) => number, + JsonValueKind.String when long.TryParse(value.GetString(), out var parsed) => parsed, + _ => null + }; + } + + private static Guid? ReadGuid(JsonElement root, params string[] names) + { + if (!TryProp(root, out var value, names)) return null; + return value.ValueKind == JsonValueKind.String && Guid.TryParse(value.GetString(), out var parsed) + ? parsed + : null; + } + + private static List ReadGuidList(JsonElement root, params string[] names) + { + var found = new List(); + if (!TryProp(root, out var value, names)) return found; + switch (value.ValueKind) + { + case JsonValueKind.Array: + found.AddRange(value.EnumerateArray() + .Where(item => item.ValueKind == JsonValueKind.String && Guid.TryParse(item.GetString(), out _)) + .Select(item => Guid.Parse(item.GetString()!))); + break; + case JsonValueKind.String when Guid.TryParse(value.GetString(), out var single): + found.Add(single); + break; + } + return found; + } +} diff --git a/Server/Launcher/GameUpdateVersionCheck.cs b/Server/Launcher/GameUpdateVersionCheck.cs new file mode 100644 index 0000000..29b651c --- /dev/null +++ b/Server/Launcher/GameUpdateVersionCheck.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.VersionCheck +{ + public static class GameUpdateVersionCheck + { + + public const string AppVersionUpdateNotificationId = "AppVersionUpdate"; + + public const string AnnouncementConfigKey = "Announcement"; + + public const string GameUpdateType = "Game Update"; + + public const int UpdateStage = 1; + + public static VersionCheckResult Resolve(string? announcementJson, DateTime nowUtc) + => HasLiveGameUpdate(announcementJson, nowUtc) + ? new VersionCheckResult + { + VersionStatus = VersionCheckStatus.UpdateRequired, + UpdateNotificationStage = UpdateStage + } + : new VersionCheckResult { VersionStatus = VersionCheckStatus.ValidForPlay }; + + + public static bool HasLiveGameUpdate(string? announcementJson, DateTime nowUtc) + { + if (string.IsNullOrWhiteSpace(announcementJson)) return false; + + try + { + using var doc = JsonDocument.Parse(announcementJson); + if (doc.RootElement.ValueKind != JsonValueKind.Array) return false; + + foreach (var row in doc.RootElement.EnumerateArray()) + { + if (row.ValueKind != JsonValueKind.Object) continue; + if (!row.TryGetProperty("type", out var type) || type.ValueKind != JsonValueKind.String) continue; + if (!string.Equals(type.GetString(), GameUpdateType, StringComparison.OrdinalIgnoreCase)) continue; + + if (row.TryGetProperty("expires_at", out var expires) && expires.ValueKind == JsonValueKind.String + && DateTime.TryParse(expires.GetString(), out var expiry) && expiry <= nowUtc) + continue; + + return true; + } + } + catch + { + } + + return false; + } + } +} diff --git a/Server/Launcher/LauncherBuildAPI.cs b/Server/Launcher/LauncherBuildAPI.cs new file mode 100644 index 0000000..2cade70 --- /dev/null +++ b/Server/Launcher/LauncherBuildAPI.cs @@ -0,0 +1,157 @@ +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace RadicadeServer.API.Launcher +{ + [Route("api/launcher")] + public class LauncherBuildAPI : Controller + { + private static readonly string LauncherDirectory = Path.GetFullPath("./Launcher"); + private static readonly string LauncherManifestPath = Path.Join(LauncherDirectory, "manifest.json"); + + private static async Task LoadLauncherManifest() + { + if (!System.IO.File.Exists(LauncherManifestPath)) + return null; + + var raw = await System.IO.File.ReadAllTextAsync(LauncherManifestPath); + return JsonNode.Parse(raw)?.AsObject(); + } + + private static string? GetString(JsonObject manifest, string key) + { + return manifest[key]?.GetValue(); + } + + private static string? GetNestedString(JsonObject manifest, string parentKey, string childKey) + { + return manifest[parentKey]?[childKey]?.GetValue(); + } + + private static bool GetBool(JsonObject manifest, string key, bool fallback = false) + { + return manifest[key]?.GetValue() ?? fallback; + } + + private static IActionResult MissingManifestFile() + { + return new JsonResult(new + { + error = "Launcher manifest file was not found.", + path = LauncherManifestPath + }) + { + StatusCode = StatusCodes.Status404NotFound + }; + } + + private static IActionResult InvalidManifestFile(string message, object? extra = null) + { + return new JsonResult(new + { + error = message, + path = LauncherManifestPath, + extra + }) + { + StatusCode = StatusCodes.Status503ServiceUnavailable + }; + } + + [HttpGet("approved-build")] + public async Task GetApprovedBuild() + { + if (!System.IO.File.Exists(LauncherManifestPath)) + return MissingManifestFile(); + + try + { + var raw = await System.IO.File.ReadAllTextAsync(LauncherManifestPath); + var manifest = JsonNode.Parse(raw)?.AsObject(); + if (manifest == null) + return InvalidManifestFile("Launcher manifest file is not a JSON object."); + + var gameVersion = GetString(manifest, "version"); + var manifestVersion = GetString(manifest, "manifestVersion"); + var gameDownloadUrl = GetNestedString(manifest, "package", "url"); + var packageSha256 = GetNestedString(manifest, "package", "sha256"); + + if (string.IsNullOrWhiteSpace(gameVersion) || + string.IsNullOrWhiteSpace(manifestVersion) || + string.IsNullOrWhiteSpace(gameDownloadUrl) || + string.IsNullOrWhiteSpace(packageSha256)) + { + return InvalidManifestFile( + "Approved build metadata is incomplete.", + new + { + missing = new[] + { + string.IsNullOrWhiteSpace(gameVersion) ? "version" : null, + string.IsNullOrWhiteSpace(manifestVersion) ? "manifestVersion" : null, + string.IsNullOrWhiteSpace(gameDownloadUrl) ? "package.url" : null, + string.IsNullOrWhiteSpace(packageSha256) ? "package.sha256" : null + }.Where(x => x != null).ToArray() + }); + } + + return Ok(new + { + gameVersion, + manifestVersion, + gameDownloadUrl, + packageSha256, + required = GetBool(manifest, "required", true), + launchTicket = GetString(manifest, "launchTicket"), + launchTicketRequired = GetBool(manifest, "launchTicketRequired"), + launchTicketUrl = GetString(manifest, "launchTicketUrl"), + launchTicketAudience = GetString(manifest, "launchTicketAudience"), + launchTicketNonce = GetString(manifest, "launchTicketNonce") + }); + } + catch (JsonException ex) + { + return InvalidManifestFile("Launcher manifest file contains invalid JSON.", new { details = ex.Message }); + } + } + + [HttpGet("manifest.json")] + public async Task GetManifest() + { + if (!System.IO.File.Exists(LauncherManifestPath)) + return MissingManifestFile(); + + try + { + var raw = await System.IO.File.ReadAllTextAsync(LauncherManifestPath); + var manifest = JsonNode.Parse(raw)?.AsObject(); + if (manifest == null) + return InvalidManifestFile("Launcher manifest file is not a JSON object."); + + var version = GetString(manifest, "version"); + var packageUrl = GetNestedString(manifest, "package", "url"); + + if (string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(packageUrl)) + { + return InvalidManifestFile( + "Launcher manifest file is missing required fields.", + new + { + missing = new[] + { + string.IsNullOrWhiteSpace(version) ? "version" : null, + string.IsNullOrWhiteSpace(packageUrl) ? "package.url" : null + }.Where(x => x != null).ToArray() + }); + } + + return Content(raw, "application/json"); + } + catch (JsonException ex) + { + return InvalidManifestFile("Launcher manifest file contains invalid JSON.", new { details = ex.Message }); + } + } + } +} diff --git a/Server/Launcher/VersionCheckAPI.cs b/Server/Launcher/VersionCheckAPI.cs new file mode 100644 index 0000000..29c0b85 --- /dev/null +++ b/Server/Launcher/VersionCheckAPI.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.VersionCheck; + +namespace RadicadeServer.API +{ + [Route("api/versioncheck/")] + public class VersionCheckAPI(RecDBContext db) : Controller + { + [HttpGet("v4")] + public async Task Get([FromQuery] string? v, [FromQuery] string? version) + { + var requested = v ?? version; + var query = HttpContext?.Request?.QueryString.Value ?? ""; + var announcement = await db.ServerConfig.AsNoTracking() + .FirstOrDefaultAsync(x => x.Key == GameUpdateVersionCheck.AnnouncementConfigKey); + var result = GameUpdateVersionCheck.Resolve(announcement?.Value, DateTime.UtcNow); + + return Ok(BuildClientResponse(result)); + } + + [HttpGet("islandedversions")] + public IActionResult GetIslandedVersions() + { + return Ok(Array.Empty()); + } + + private static Dictionary BuildClientResponse(VersionCheckResult result) => new() + { + ["ValidVersion"] = 0, + ["VersionStatus"] = (int)result.VersionStatus, + ["UpdateNotificationStage"] = result.UpdateNotificationStage, + ["IsVersionIslanded"] = result.IsVersionIslanded, + ["IsCrossPlayDisabled"] = result.IsCrossPlayDisabled, + }; + } +} diff --git a/Server/Launcher/VersionCheckResult.cs b/Server/Launcher/VersionCheckResult.cs new file mode 100644 index 0000000..90f2466 --- /dev/null +++ b/Server/Launcher/VersionCheckResult.cs @@ -0,0 +1,12 @@ +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.VersionCheck +{ + public class VersionCheckResult + { + public VersionCheckStatus VersionStatus { get; set; } = VersionCheckStatus.ValidForPlay; + public int UpdateNotificationStage { get; set; } = 0; + public bool IsVersionIslanded { get; set; } = false; + public bool IsCrossPlayDisabled { get; set; } = false; + } +} diff --git a/Server/Launcher/VersionCheckStatus.cs b/Server/Launcher/VersionCheckStatus.cs new file mode 100644 index 0000000..f62ce94 --- /dev/null +++ b/Server/Launcher/VersionCheckStatus.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Enums +{ + public enum VersionCheckStatus + { + ValidForPlay = 0, + UpdateRequired = 1, + ValidForMenu = 2 + } +} diff --git a/Server/Launcher/manifest.json b/Server/Launcher/manifest.json new file mode 100644 index 0000000..648b659 --- /dev/null +++ b/Server/Launcher/manifest.json @@ -0,0 +1,18 @@ +{ + "version": "1.0.0", + "manifestVersion": "2026.05.11.1", + "required": true, + "notes": "First ever build for the launcher!", + "package": { + "url": "https://drive.usercontent.google.com/download?id=1aNCUeiA0binZhzSZimSgRdF3mPPk0fTx&export=download&authuser=0&confirm=t&uuid=27b3743c-3fa2-4a4a-a1c7-bf566deed31e&at=ALBwUgmyJGAV7jJWbByy0qM6-4dX:1778546735465", + "sha256": "EEBCC7940CE78CAA3F85916E793F150D2149FA051690A064BF232E931EC47DD3", + "size": 5565413113, + "extractSubdirectory": "Radicade" + }, + "launchTicket": "", + "launchTicketRequired": false, + "launchTicketUrl": "", + "launchTicketAudience": "", + "launchTicketNonce": "", + "delete": [] +} diff --git a/Server/Leaderboards/GetNearbyScoresRequest.cs b/Server/Leaderboards/GetNearbyScoresRequest.cs new file mode 100644 index 0000000..08cad2b --- /dev/null +++ b/Server/Leaderboards/GetNearbyScoresRequest.cs @@ -0,0 +1,11 @@ +namespace RadicadeServer.RecNet.Leaderboards; + + + + + + +public class GetNearbyScoresRequest : GetRankRequest +{ + public int WindowSize { get; set; } +} diff --git a/Server/Leaderboards/GetRankRequest.cs b/Server/Leaderboards/GetRankRequest.cs new file mode 100644 index 0000000..34f465d --- /dev/null +++ b/Server/Leaderboards/GetRankRequest.cs @@ -0,0 +1,22 @@ +namespace RadicadeServer.RecNet.Leaderboards; + + + + + + + +public class GetRankRequest +{ + public int PlayerId { get; set; } + + public int StatChannel { get; set; } + + public long RoomId { get; set; } + + public int FilterType { get; set; } + + public int Timeframe { get; set; } + + public bool SortAscending { get; set; } +} diff --git a/Server/Leaderboards/GetRanksRequest.cs b/Server/Leaderboards/GetRanksRequest.cs new file mode 100644 index 0000000..e9a557b --- /dev/null +++ b/Server/Leaderboards/GetRanksRequest.cs @@ -0,0 +1,13 @@ +namespace RadicadeServer.RecNet.Leaderboards; + + + + + + +public class GetRanksRequest : GetRankRequest +{ + public int RankStart { get; set; } + + public int RankEnd { get; set; } +} diff --git a/Server/Leaderboards/LeaderboardAPI.cs b/Server/Leaderboards/LeaderboardAPI.cs new file mode 100644 index 0000000..e4e7ad9 --- /dev/null +++ b/Server/Leaderboards/LeaderboardAPI.cs @@ -0,0 +1,633 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Leaderboards; +using System.Text.Json; + +namespace RadicadeServer.Leaderboard; + + + + + + + + + + + + + + + + + + + + + + + +public static class LeaderboardStore +{ + + + + + + private const int FirstGlobalStatChannel = 1000; + + + + + + public static async Task SubmitScoreAsync(IDbContextFactory dbFactory, string? rawBody, long authPlayerId, long queryPlayerId) + { + var parsed = ParseStatSubmission(rawBody); + if (parsed == null) return null; + + long playerId = parsed.Value.PlayerIdFromBody > 0 ? parsed.Value.PlayerIdFromBody + : authPlayerId > 0 ? authPlayerId + : queryPlayerId; + if (playerId <= 0) + { + return new + { + Success = false, + Reason = "No player identity" + }; + } + + await using var db = await dbFactory.CreateDbContextAsync(); + + var now = DateTime.UtcNow; + var added = 0; + foreach (var (channel, score, time) in parsed.Value.Stats) + { + if (score == 0 && time == 0) continue; + db.LeaderboardScores.Add(new LeaderboardScore + { + RoomId = parsed.Value.RoomId, + SubRoomId = parsed.Value.SubRoomId, + PlayerId = playerId, + StatChannel = channel, + Score = score, + Time = time, + SubmittedAt = now + }); + added++; + } + + if (added == 0) return new { Success = false, Reason = "Nothing to record" }; + + await db.SaveChangesAsync(); + + + + var last = parsed.Value.Stats[^1]; + var board = await LoadBoardAsync(db, parsed.Value.RoomId, last.Channel); + int rank = RankBoard(board, false).FindIndex(e => e.PlayerId == playerId) + 1; + int total = board.GroupBy(x => x.PlayerId).Count(); + + return new + { + Success = true, + PlayerId = playerId, + RoomId = parsed.Value.RoomId, + StatChannel = last.Channel, + StatValue = last.Score, + Rank = rank, + Total = total + }; + } + + + + + + public static async Task GetRanksAsync(IDbContextFactory dbFactory, GetRanksRequest req) + { + await using var db = await dbFactory.CreateDbContextAsync(); + + var board = await LoadBoardAsync(db, req.RoomId, req.StatChannel); + var ranked = RankBoard(board, req.SortAscending); + var names = await LoadPlayerNamesAsync(db, BoardPlayerIds(ranked, req.PlayerId)); + + int total = ranked.Count; + int start = req.RankStart > 0 ? req.RankStart : 1; + int end = req.RankEnd >= req.RankStart ? req.RankEnd : Math.Min(total, 100); + var slice = ranked + .Skip(start - 1) + .Take(Math.Max(0, end - start + 1)) + .Select((e, i) => new LeaderboardEntryResponse + { + PlayerId = e.PlayerId, + PlayerName = names.TryGetValue(e.PlayerId, out var n) ? n : "", + Score = e.BestValue, + Time = e.BestTime, + Rank = start + i + }) + .ToList(); + + int myRank = 0; + if (req.PlayerId > 0) + { + var me = ranked.FirstOrDefault(e => e.PlayerId == req.PlayerId); + if (me != null) myRank = ranked.IndexOf(me) + 1; + } + + if (total == 0 && req.PlayerId > 0) + { + slice.Add(CreatePlaceholderEntry(req.PlayerId, names)); + total = 1; + myRank = 1; + } + + return new LeaderboardResponse { Rows = slice, Total = total, MyRank = myRank }; + } + + + + + + public static async Task GetPlayerRankAsync(IDbContextFactory dbFactory, GetRankRequest req) + { + await using var db = await dbFactory.CreateDbContextAsync(); + + var board = await LoadBoardAsync(db, req.RoomId, req.StatChannel); + var ranked = RankBoard(board, req.SortAscending); + var names = await LoadPlayerNamesAsync(db, BoardPlayerIds(ranked, req.PlayerId)); + + int total = ranked.Count; + var entry = ranked.FirstOrDefault(e => e.PlayerId == req.PlayerId); + int rank = entry == null ? 0 : ranked.IndexOf(entry) + 1; + double score = entry?.BestValue ?? 0; + long time = entry?.BestTime ?? 0; + string name = names.TryGetValue(req.PlayerId, out var n) ? n : ""; + + if (entry == null && req.PlayerId > 0 && total == 0) + { + rank = 1; + total = 1; + } + + var row = new LeaderboardEntryResponse + { + PlayerId = req.PlayerId, + PlayerName = name, + Score = score, + Time = time, + Rank = rank + }; + + return new + { + row.PlayerId, + row.PlayerName, + row.Score, + row.Time, + row.Rank, + Total = total, + Rows = new List { row } + }; + } + + + + + + public static async Task GetNearbyScoresAsync(IDbContextFactory dbFactory, GetNearbyScoresRequest req) + { + await using var db = await dbFactory.CreateDbContextAsync(); + + var window = req.WindowSize > 0 ? req.WindowSize : 10; + + var board = await LoadBoardAsync(db, req.RoomId, req.StatChannel); + var ranked = RankBoard(board, req.SortAscending); + var names = await LoadPlayerNamesAsync(db, BoardPlayerIds(ranked, req.PlayerId)); + + int total = ranked.Count; + int myRank = 0; + if (req.PlayerId > 0) + { + var me = ranked.FirstOrDefault(e => e.PlayerId == req.PlayerId); + if (me != null) myRank = ranked.IndexOf(me) + 1; + } + + + int start = myRank > 0 ? Math.Max(1, myRank - window / 2) : 1; + var slice = ranked + .Skip(start - 1) + .Take(window) + .Select((e, i) => new LeaderboardEntryResponse + { + PlayerId = e.PlayerId, + PlayerName = names.TryGetValue(e.PlayerId, out var n) ? n : "", + Score = e.BestValue, + Time = e.BestTime, + Rank = start + i + }) + .ToList(); + + + if (req.PlayerId > 0 && myRank > 0 && slice.All(e => e.PlayerId != req.PlayerId)) + { + var me = ranked.First(e => e.PlayerId == req.PlayerId); + slice.Add(new LeaderboardEntryResponse + { + PlayerId = me.PlayerId, + PlayerName = names.TryGetValue(me.PlayerId, out var n) ? n : "", + Score = me.BestValue, + Time = me.BestTime, + Rank = myRank + }); + } + + if (total == 0 && req.PlayerId > 0) + { + slice.Add(CreatePlaceholderEntry(req.PlayerId, names)); + total = 1; + myRank = 1; + } + + return new LeaderboardResponse { Rows = slice, Total = total, MyRank = myRank }; + } + + + + + + private static (long RoomId, long SubRoomId, long PlayerIdFromBody, List<(int Channel, double Score, long Time)> Stats)? ParseStatSubmission(string? rawBody) + { + if (string.IsNullOrWhiteSpace(rawBody)) return null; + + JsonElement root; + try + { + using var doc = JsonDocument.Parse(rawBody); + root = doc.RootElement.Clone(); + } + catch + { + return null; + } + + long roomId = 0, subRoomId = 0, playerId = 0; + var stats = new List<(int Channel, double Score, long Time)>(); + + if (root.ValueKind == JsonValueKind.Object) + { + roomId = GetLong(root, "RoomId", "roomId", "Room"); + subRoomId = GetLong(root, "SubRoomId", "subRoomId", "SubRoom"); + playerId = GetLong(root, "PlayerId", "playerId", "Player"); + + var nested = GetArray(root, "Stats", "stats", "Scores", "scores"); + if (nested != null) + { + foreach (var item in nested) + AddStat(stats, item); + } + else + { + AddStat(stats, root); + } + } + else if (root.ValueKind == JsonValueKind.Array) + { + foreach (var item in root.EnumerateArray()) + AddStat(stats, item); + } + + if (stats.Count == 0) return null; + return (roomId, subRoomId, playerId, stats); + } + + private static void AddStat(List<(int Channel, double Score, long Time)> stats, JsonElement el) + { + var channel = GetInt(el, "StatChannel", "statChannel", "Channel", "BoardNumber", "Board", "StatId", "statId"); + var score = GetDouble(el, "Score", "score", "StatValue", "statValue", "Value", "value", "Wins", "wins", "Count", "count"); + var time = GetLong(el, "Time", "time", "Milliseconds", "TimeMs"); + if (score != 0 || time != 0) + stats.Add((channel, score, time)); + } + + internal static List? GetArray(JsonElement el, params string[] names) + { + foreach (var n in names) + { + if (el.TryGetProperty(n, out var p) && p.ValueKind == JsonValueKind.Array) + return p.EnumerateArray().ToList(); + } + return null; + } + + internal static int GetInt(JsonElement el, params string[] names) + { + foreach (var n in names) + if (el.TryGetProperty(n, out var p) && p.ValueKind == JsonValueKind.Number && p.TryGetInt32(out var v)) + return v; + return 0; + } + + internal static double GetDouble(JsonElement el, params string[] names) + { + foreach (var n in names) + if (el.TryGetProperty(n, out var p) && p.ValueKind == JsonValueKind.Number && p.TryGetDouble(out var v)) + return v; + return 0; + } + + internal static long GetLong(JsonElement el, params string[] names) + { + foreach (var n in names) + if (el.TryGetProperty(n, out var p) && p.ValueKind == JsonValueKind.Number && p.TryGetInt64(out var v)) + return v; + return 0; + } + + internal static bool GetBool(JsonElement el, params string[] names) + { + foreach (var n in names) + if (el.TryGetProperty(n, out var p) && p.ValueKind == JsonValueKind.True) + return true; + return false; + } + + + + + + + + + + + private static async Task> LoadBoardAsync(RecDBContext db, long roomId, int statChannel) + { + var q = db.LeaderboardScores.AsNoTracking(); + bool global = statChannel >= FirstGlobalStatChannel || roomId <= 0; + q = global + ? q.Where(x => x.StatChannel == statChannel) + : q.Where(x => x.RoomId == roomId && x.StatChannel == statChannel); + return await q.ToListAsync(); + } + + private static List RankBoard(List scores, bool sortAscending) + { + + + + var best = scores + .GroupBy(x => x.PlayerId) + .Select(g => new + { + PlayerId = g.Key, + BestValue = sortAscending + ? g.Min(x => x.Score != 0 ? x.Score : x.Time) + : g.Max(x => x.Score != 0 ? x.Score : x.Time), + BestTime = sortAscending ? g.Min(x => x.Time) : g.Max(x => x.Time) + }) + .ToList(); + + + + var ordered = sortAscending + ? best.OrderBy(x => x.BestValue).ThenBy(x => x.BestTime).ToList() + : best.OrderByDescending(x => x.BestValue).ThenBy(x => x.BestTime).ToList(); + + return ordered.Select(e => new RankedEntry(e.PlayerId, e.BestValue, e.BestTime)).ToList(); + } + + private static List BoardPlayerIds(List ranked, int askingPlayerId) + { + var ids = ranked.Select(e => e.PlayerId).ToList(); + if (askingPlayerId > 0 && !ids.Contains(askingPlayerId)) ids.Add(askingPlayerId); + return ids; + } + + private static async Task> LoadPlayerNamesAsync(RecDBContext db, List playerIds) + { + if (playerIds.Count == 0) return new Dictionary(); + return await db.Profiles.AsNoTracking() + .Where(p => playerIds.Contains(p.accountId)) + .ToDictionaryAsync(p => p.accountId, p => p.displayName ?? p.username); + } + + private static LeaderboardEntryResponse CreatePlaceholderEntry(long playerId, Dictionary names) + { + return new LeaderboardEntryResponse + { + PlayerId = playerId, + PlayerName = names.TryGetValue(playerId, out var name) ? name : "", + Score = 0, + Time = 0, + Rank = 1 + }; + } + + private sealed record RankedEntry(long PlayerId, double BestValue, long BestTime); +} + + + + + +public abstract class LeaderboardControllerBase : Controller +{ + protected readonly IDbContextFactory DbFactory; + + protected LeaderboardControllerBase(IDbContextFactory dbFactory) + { + DbFactory = dbFactory; + } + + private async Task ReadBodyAsync() + { + using var reader = new StreamReader(Request.Body); + return await reader.ReadToEndAsync(); + } + + private long QueryPlayerId() + { + if (long.TryParse(Request.Query["playerId"], out var id) && id > 0) return id; + if (long.TryParse(Request.Query["PlayerId"], out id) && id > 0) return id; + return 0; + } + + private void FillPlayerIdFromRequest(GetRankRequest req) + { + if (req.PlayerId > 0) return; + + var queryPlayerId = QueryPlayerId(); + if (queryPlayerId > 0 && queryPlayerId <= int.MaxValue) + { + req.PlayerId = (int)queryPlayerId; + return; + } + + var authPlayerId = User.GetUserId(); + if (authPlayerId > 0 && authPlayerId <= int.MaxValue) + req.PlayerId = (int)authPlayerId.Value; + } + + private async Task ParseRanksRequestAsync() + { + var raw = await ReadBodyAsync(); + var req = new GetRanksRequest(); + if (!string.IsNullOrWhiteSpace(raw)) + { + try + { + using var doc = JsonDocument.Parse(raw); + var el = doc.RootElement; + req.PlayerId = LeaderboardStore.GetInt(el, "PlayerId", "playerId", "Player"); + req.StatChannel = LeaderboardStore.GetInt(el, "StatChannel", "statChannel", "Channel", "StatId", "statId"); + req.RoomId = LeaderboardStore.GetLong(el, "RoomId", "roomId", "Room"); + req.FilterType = LeaderboardStore.GetInt(el, "FilterType", "filterType"); + req.Timeframe = LeaderboardStore.GetInt(el, "Timeframe", "timeframe"); + req.SortAscending = LeaderboardStore.GetBool(el, "SortAscending", "sortAscending"); + req.RankStart = LeaderboardStore.GetInt(el, "RankStart", "rankStart"); + req.RankEnd = LeaderboardStore.GetInt(el, "RankEnd", "rankEnd"); + } + catch { } + } + return req; + } + + private async Task ParseNearbyScoresRequestAsync() + { + var raw = await ReadBodyAsync(); + var req = new GetNearbyScoresRequest(); + if (!string.IsNullOrWhiteSpace(raw)) + { + try + { + using var doc = JsonDocument.Parse(raw); + var el = doc.RootElement; + req.PlayerId = LeaderboardStore.GetInt(el, "PlayerId", "playerId", "Player"); + req.StatChannel = LeaderboardStore.GetInt(el, "StatChannel", "statChannel", "Channel", "StatId", "statId"); + req.RoomId = LeaderboardStore.GetLong(el, "RoomId", "roomId", "Room"); + req.FilterType = LeaderboardStore.GetInt(el, "FilterType", "filterType"); + req.Timeframe = LeaderboardStore.GetInt(el, "Timeframe", "timeframe"); + req.SortAscending = LeaderboardStore.GetBool(el, "SortAscending", "sortAscending"); + req.WindowSize = LeaderboardStore.GetInt(el, "WindowSize", "windowSize"); + } + catch { } + } + return req; + } + + [HttpPost("CheckAndSetStat")] + public async Task CheckAndSetStat() + { + var rawBody = await ReadBodyAsync(); + var result = await LeaderboardStore.SubmitScoreAsync(DbFactory, rawBody, User.GetUserId() ?? 0, QueryPlayerId()); + return result == null ? BadRequest(new { Success = false }) : Ok(result); + } + + [HttpPost("SubmitScore")] + public async Task SubmitScore() + { + var rawBody = await ReadBodyAsync(); + var result = await LeaderboardStore.SubmitScoreAsync(DbFactory, rawBody, User.GetUserId() ?? 0, QueryPlayerId()); + return result == null ? BadRequest(new { Success = false }) : Ok(result); + } + + [HttpPost("GetRanks")] + public async Task GetRanks() + { + var req = await ParseRanksRequestAsync(); + FillPlayerIdFromRequest(req); + return Ok(await LeaderboardStore.GetRanksAsync(DbFactory, req)); + } + + [HttpGet("GetRanks")] + public async Task GetRanksGet([FromQuery] GetRanksRequest req) + { + if (req == null) return BadRequest(); + FillPlayerIdFromRequest(req); + return Ok(await LeaderboardStore.GetRanksAsync(DbFactory, req)); + } + + [HttpPost("GetPlayerRank")] + public async Task GetPlayerRank() + { + var req = await ParseRanksRequestAsync(); + FillPlayerIdFromRequest(req); + return Ok(await LeaderboardStore.GetPlayerRankAsync(DbFactory, req)); + } + + [HttpGet("GetPlayerRank")] + public async Task GetPlayerRankGet([FromQuery] GetRankRequest req) + { + if (req == null) return BadRequest(); + FillPlayerIdFromRequest(req); + return Ok(await LeaderboardStore.GetPlayerRankAsync(DbFactory, req)); + } + + [HttpPost("GetNearbyScores")] + public async Task GetNearbyScores() + { + var req = await ParseNearbyScoresRequestAsync(); + FillPlayerIdFromRequest(req); + return Ok(await LeaderboardStore.GetNearbyScoresAsync(DbFactory, req)); + } + + [HttpGet("GetNearbyScores")] + public async Task GetNearbyScoresGet([FromQuery] GetNearbyScoresRequest req) + { + if (req == null) return BadRequest(); + FillPlayerIdFromRequest(req); + return Ok(await LeaderboardStore.GetNearbyScoresAsync(DbFactory, req)); + } + + internal static string Truncate(string? s) + { + if (string.IsNullOrWhiteSpace(s)) return ""; + return s.Length <= 500 ? s : s.Substring(0, 500) + "..."; + } +} + + +[Route("leaderboard/leaderboard")] +public class LeaderboardAPI : LeaderboardControllerBase +{ + public LeaderboardAPI(IDbContextFactory dbFactory) : base(dbFactory) { } +} + + + + + + +[Route("leaderboard")] +public class LeaderboardCompatAPI : LeaderboardControllerBase +{ + public LeaderboardCompatAPI(IDbContextFactory dbFactory) : base(dbFactory) { } +} + + + + + +[Route("leaderboard/{**path}")] +public class LeaderboardCatchallController : Controller +{ + [HttpGet] + [HttpPost] + [HttpPut] + [HttpDelete] + [HttpPatch] + public async Task Catch(string? path) + { + string body = ""; + try + { + using var reader = new StreamReader(Request.Body); + body = await reader.ReadToEndAsync(); + } + catch { } + return NotFound(); + } +} diff --git a/Server/Leaderboards/LeaderboardEntryResponse.cs b/Server/Leaderboards/LeaderboardEntryResponse.cs new file mode 100644 index 0000000..1f0035a --- /dev/null +++ b/Server/Leaderboards/LeaderboardEntryResponse.cs @@ -0,0 +1,17 @@ +namespace RadicadeServer.RecNet.Leaderboards +{ + + + + + + + public class LeaderboardEntryResponse + { + public long PlayerId { get; set; } + public string PlayerName { get; set; } = ""; + public double Score { get; set; } + public long Time { get; set; } + public int Rank { get; set; } + } +} diff --git a/Server/Leaderboards/LeaderboardResponse.cs b/Server/Leaderboards/LeaderboardResponse.cs new file mode 100644 index 0000000..2f4a36f --- /dev/null +++ b/Server/Leaderboards/LeaderboardResponse.cs @@ -0,0 +1,17 @@ +namespace RadicadeServer.RecNet.Leaderboards +{ + + + + + + + + + public class LeaderboardResponse + { + public List Rows { get; set; } = new(); + public int Total { get; set; } + public int MyRank { get; set; } + } +} diff --git a/Server/Leaderboards/LeaderboardScore.cs b/Server/Leaderboards/LeaderboardScore.cs new file mode 100644 index 0000000..75b8372 --- /dev/null +++ b/Server/Leaderboards/LeaderboardScore.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace RadicadeServer.RecNet.Leaderboards +{ + + + + + + [Index(nameof(RoomId), nameof(StatChannel), nameof(PlayerId))] + public class LeaderboardScore + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long RoomId { get; set; } + + public long SubRoomId { get; set; } = 0; + + public long PlayerId { get; set; } + + + public int StatChannel { get; set; } + + + public double Score { get; set; } + + + public long Time { get; set; } + + public DateTime SubmittedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Server/Leaderboards/SetStatRequest.cs b/Server/Leaderboards/SetStatRequest.cs new file mode 100644 index 0000000..e702a09 --- /dev/null +++ b/Server/Leaderboards/SetStatRequest.cs @@ -0,0 +1,20 @@ +namespace RadicadeServer.RecNet.Leaderboards; + + + + + + + + + +public class SetStatRequest +{ + public int StatChannel { get; set; } + + public long RoomId { get; set; } + + public int StatValue { get; set; } + + public int? CurrentStatValue { get; set; } +} diff --git a/Server/LoadingScreens/Clubs.png b/Server/LoadingScreens/Clubs.png new file mode 100644 index 0000000..56384d8 Binary files /dev/null and b/Server/LoadingScreens/Clubs.png differ diff --git a/Server/LoadingScreens/Community.png b/Server/LoadingScreens/Community.png new file mode 100644 index 0000000..d353bf3 Binary files /dev/null and b/Server/LoadingScreens/Community.png differ diff --git a/Server/LoadingScreens/DailyChallenges.png b/Server/LoadingScreens/DailyChallenges.png new file mode 100644 index 0000000..3dbfa45 Binary files /dev/null and b/Server/LoadingScreens/DailyChallenges.png differ diff --git a/Server/LoadingScreens/Go.png b/Server/LoadingScreens/Go.png new file mode 100644 index 0000000..f04f970 Binary files /dev/null and b/Server/LoadingScreens/Go.png differ diff --git a/Server/LoadingScreens/HairDye.png b/Server/LoadingScreens/HairDye.png new file mode 100644 index 0000000..b54f8b1 Binary files /dev/null and b/Server/LoadingScreens/HairDye.png differ diff --git a/Server/LoadingScreens/KanyeWest.png b/Server/LoadingScreens/KanyeWest.png new file mode 100644 index 0000000..c15ae59 Binary files /dev/null and b/Server/LoadingScreens/KanyeWest.png differ diff --git a/Server/LoadingScreens/LoadingScreenImages.cs b/Server/LoadingScreens/LoadingScreenImages.cs new file mode 100644 index 0000000..fd9627b --- /dev/null +++ b/Server/LoadingScreens/LoadingScreenImages.cs @@ -0,0 +1,87 @@ +using System.Reflection; +using RadicadeServer.DB; +using RadicadeServer.RecNet; + +namespace RadicadeServer.Utils +{ + public static class LoadingScreenImages + { + private const string ResourcePrefix = "LoadingScreens/"; + + public static IReadOnlyList Tips { get; } = new List + { + Tip("Welcome.png", "Welcome to Radicade", + "Rec Room is a virtual social club where you create and play games with friends. It's a fun and welcoming environment for people from all walks of life!"), + Tip("Go.png", "Play on the go!", + "Download Rec Room on your iOS device and play anywhere!"), + Tip("TagMerch.png", "Laser Tag Merch", + "You earn tickets for every game of Laser Tag. Redeem them for awesome Laser Tag gear!"), + Tip("RoomCheers.png", "Room Cheers", + "Cheer and Favorite any room in the This Room section of your Watch Menu."), + Tip("Star.png", "Become a Star!", + "Use #RecRoom on your Instagram and Twitter posts for a chance to make it onto our Community Board!"), + Tip("Clubs.png", "Check out clubs!", + "Create or join a club. Visit all the clubhouses and even set one as your spawn point!"), + Tip("Community.png", "Join the Rec Room Community!", + "See recroom.com/community for links to Radicade's YouTube, Instagram, and MORE"), + Tip("DailyChallenges.png", "Daily challenges", + "Check out the Challenges section in your watch for fun ways to earn in-game rewards."), + Tip("HairDye.png", "Hair Dye Patterns", + "Express yourself by mixing and matching colors with the new hair dye patterns!"), + Tip("Style.png", "Find Your Style", + "Personalize your outfit and appearance in your Dorm Room."), + Tip("RecNet.png", "We're all on Rec.Net!", + "Log into your Rec.Net profile to stay in touch with your friends any time!"), + Tip("TokenBin.png", "Rec Room Tokens", + "Earn tokens as you play, then spend them in the Store."), + Tip("RRPlus.png", "Plus and bundles", + "Check the Store for Rec Room Plus and other bundles."), + Tip("KanyeWest.png", "Quote", "People always tell you to be humble. When was the last time someone told you to be great?") + }; + + public static IReadOnlyList Names { get; } = + Tips.Select(tip => tip.ImageName).ToList(); + + public static void EnsureInstalled() + { + try + { + if (!Directory.Exists(DBConfig.CDNImages)) + Directory.CreateDirectory(DBConfig.CDNImages); + + var assembly = typeof(LoadingScreenImages).Assembly; + var installed = new List(); + + foreach (var name in Names) + { + var path = Path.Join(DBConfig.CDNImages, name); + if (File.Exists(path)) continue; + + using var stream = assembly.GetManifestResourceStream(ResourcePrefix + name); + if (stream == null) + { + continue; + } + + using var file = File.Create(path); + stream.CopyTo(file); + installed.Add(name); + } + } + catch (Exception) + { + } + } + + private const int AllPlatforms = 495; + + private static LoadingScreenTip Tip(string imageName, string title, string message) => new() + { + ImageName = imageName, + Title = title, + Message = message, + RoomNames = new List(), + PlatformMask = AllPlatforms + }; + } +} diff --git a/Server/LoadingScreens/LoadingScreenTip.cs b/Server/LoadingScreens/LoadingScreenTip.cs new file mode 100644 index 0000000..ed75205 --- /dev/null +++ b/Server/LoadingScreens/LoadingScreenTip.cs @@ -0,0 +1,11 @@ +namespace RadicadeServer.RecNet +{ + public class LoadingScreenTip + { + public int PlatformMask { get; set; } = -1; + public string Title { get; set; } = null!; + public string Message { get; set; } = null!; + public List RoomNames { get; set; } = null!; + public string ImageName { get; set; } = null!; + } +} diff --git a/Server/LoadingScreens/RRPlus.png b/Server/LoadingScreens/RRPlus.png new file mode 100644 index 0000000..10931ad Binary files /dev/null and b/Server/LoadingScreens/RRPlus.png differ diff --git a/Server/LoadingScreens/RecNet.png b/Server/LoadingScreens/RecNet.png new file mode 100644 index 0000000..769e525 Binary files /dev/null and b/Server/LoadingScreens/RecNet.png differ diff --git a/Server/LoadingScreens/RoomCheers.png b/Server/LoadingScreens/RoomCheers.png new file mode 100644 index 0000000..ca2dec8 Binary files /dev/null and b/Server/LoadingScreens/RoomCheers.png differ diff --git a/Server/LoadingScreens/Star.png b/Server/LoadingScreens/Star.png new file mode 100644 index 0000000..5b3d333 Binary files /dev/null and b/Server/LoadingScreens/Star.png differ diff --git a/Server/LoadingScreens/Style.png b/Server/LoadingScreens/Style.png new file mode 100644 index 0000000..2187a7c Binary files /dev/null and b/Server/LoadingScreens/Style.png differ diff --git a/Server/LoadingScreens/TagMerch.png b/Server/LoadingScreens/TagMerch.png new file mode 100644 index 0000000..e579b1d Binary files /dev/null and b/Server/LoadingScreens/TagMerch.png differ diff --git a/Server/LoadingScreens/TokenBin.png b/Server/LoadingScreens/TokenBin.png new file mode 100644 index 0000000..31f711b Binary files /dev/null and b/Server/LoadingScreens/TokenBin.png differ diff --git a/Server/LoadingScreens/Welcome.png b/Server/LoadingScreens/Welcome.png new file mode 100644 index 0000000..7f96de0 Binary files /dev/null and b/Server/LoadingScreens/Welcome.png differ diff --git a/Server/Mail/RadicadeMail.cs b/Server/Mail/RadicadeMail.cs new file mode 100644 index 0000000..56ba97a --- /dev/null +++ b/Server/Mail/RadicadeMail.cs @@ -0,0 +1,52 @@ +using System.Net.Http; +using System.Text; +using System.Text.Json; + +namespace RadicadeServer.Radicade.Mail; + + + + + + + + + + + +public static class RadicadeMail +{ + private static readonly HttpClient Http = new(); + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public static async Task SendAsync(string to, string subject, string html) + { + if (string.IsNullOrWhiteSpace(DBConfig.MailApiKey)) return; + if (string.IsNullOrWhiteSpace(to) || !to.Contains('@')) return; + if (string.IsNullOrWhiteSpace(DBConfig.MailFrom)) return; + try + { + var payload = new + { + from = $"{DBConfig.MailFromName} <{DBConfig.MailFrom}>", + to = new[] { to }, + subject, + html + }; + + using var req = new HttpRequestMessage(HttpMethod.Post, DBConfig.MailApiUrl) + { + Headers = { Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", DBConfig.MailApiKey) }, + Content = new StringContent(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json") + }; + + using var res = await Http.SendAsync(req); + } + catch (Exception) + { + } + } +} diff --git a/Server/Matchmaking/GoToRequest.cs b/Server/Matchmaking/GoToRequest.cs new file mode 100644 index 0000000..8b8655d --- /dev/null +++ b/Server/Matchmaking/GoToRequest.cs @@ -0,0 +1,12 @@ +using RadicadeServer.RecNet.Enums.Rooms; + +namespace RadicadeServer.RecNet.Matchmaking +{ + public class GoToRequest + { + public JoinMode JoinMode { get; set; } + public bool BypassMovementModeRestriction { get; set; } + public string LoginLock { get; set; } = null!; + public long[]? AdditionalPlayerIds { get; set; } + } +} diff --git a/Server/Matchmaking/MatchmakingAPI.cs b/Server/Matchmaking/MatchmakingAPI.cs new file mode 100644 index 0000000..725e3b6 --- /dev/null +++ b/Server/Matchmaking/MatchmakingAPI.cs @@ -0,0 +1,1238 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Config; +using RadicadeServer.API.Notifications; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Matchmaking; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Matchmaking +{ + [Route("matchmaking")] + public class MatchmakingAPI : Controller + { + private readonly RecDBContext db; + + public MatchmakingAPI(RecDBContext context) + { + db = context; + } + + [HttpGet("rooms/requiring/developer")] + [HttpGet("rooms/requiring/rrplus")] + public IActionResult GetRestrictedRooms() => Ok(Array.Empty()); + + [HttpPost("roominstance/{instanceId}/reportjoinresult"), Authorize] + public async Task ReportJoinResult(long instanceId) + { + var userId = User.GetUserId(); + if (userId.HasValue) + { + var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId); + if (roomInstance != null) + { + await RoomJoinGiftUtils.TryDeliverAsync(db, userId.Value, roomInstance.roomId); + } + + await StorefrontShared.SendPendingGiftBoxNotificationAsync(db, userId.Value); + } + + return Ok(); + } + + [HttpPost("matchmake/none"), Authorize] + public async Task MatchmakeNone() + { + long? userId = User.GetUserId(); + if (!userId.HasValue) + { + return Unauthorized(); + } + + var match = await MatchmakingUtils.Matchmake( + JoinMode.PrivateNewInstance, + "none", + userId.Value); + + return Ok(match); + } + + [HttpPost("matchmake/dorm"), Authorize] + public async Task MatchmakeDorm() + { + long? userId = User.GetUserId(); + if (!userId.HasValue) + { + return Unauthorized(); + } + + try + { + var presence = await db.Presences + .FirstOrDefaultAsync(x => x.playerId == userId.Value); + + if (presence == null) + { + presence = new RealPresence { playerId = userId.Value }; + db.Presences.Add(presence); + } + + var orientationInstance = await MatchmakingUtils.TryStartOrientationAsync(db, userId.Value, presence); + if (orientationInstance != null) + { + await Notify.UpdateSubscriptions(userId.Value); + return Ok(new MatchResponse + { + roomInstance = orientationInstance, + errorCode = JoinStatus.Success + }); + } + + var dormInstance = await MatchmakingUtils.EnsureDormPresenceAsync(db, userId.Value, presence); + + if (dormInstance == null) + { + var reloaded = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId.Value); + if (reloaded != null) + { + reloaded.roomInstance = null; + await db.SaveChangesAsync(); + dormInstance = await MatchmakingUtils.EnsureDormPresenceAsync(db, userId.Value, reloaded); + } + } + + if (dormInstance == null) + { + return NotFound(new MatchResponse { errorCode = JoinStatus.BlockedFromRoom }); + } + + await Notify.UpdateSubscriptions(userId.Value); + return Ok(new MatchResponse + { + roomInstance = dormInstance, + errorCode = JoinStatus.Success + }); + } + catch (Exception) + { + return StatusCode(StatusCodes.Status500InternalServerError, + new MatchResponse { errorCode = JoinStatus.BlockedFromRoom }); + } + } + + private async Task ResolvePresenceRoomInstanceAsync(RealPresence data) + { + if (data.roomInstance is not > 0) return null; + + var roomInstance = await db.RoomInstances.AsNoTracking() + .FirstOrDefaultAsync(x => x.roomInstanceId == data.roomInstance.Value); + + return roomInstance == null ? null : MatchmakingUtils.NormalizeRoomInstance(roomInstance); + } + + [HttpGet("player")] + public async Task> GetPlayerPresence( + [FromQuery] List id) + { + var presences = new List(); + + if (id == null || id.Count == 0) + { + long? currentPlayerId = User.GetUserId(); + + if (!currentPlayerId.HasValue) + { + return presences; + } + + id = new List + { + currentPlayerId.Value + }; + } + + foreach (long playerId in id) + { + RealPresence? data = await db.Presences + .FirstOrDefaultAsync( + x => x.playerId == playerId + ); + + if (data == null) + { + continue; + } + + long? viewerId = User.GetUserId(); + + RoomInstance? roomInstance = await ResolvePresenceRoomInstanceAsync(data); + + if (viewerId != playerId) + roomInstance = await RoomUtils.ForViewerAsync(roomInstance, viewerId); + + presences.Add( + (await Presence.CreateAsync(db, + data, + roomInstance + )).ForViewer(viewerId) + ); + } + + return presences; + } + + [HttpGet("room/{roomId}/instances"), Authorize] + public async Task?> + GetRoomInstancesInRoom(long roomId) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return new List(); + } + + var roomInstances = await db.RoomInstances + .Where(x => x.roomId == roomId) + .ToListAsync(); + + var converted = new List(); + + foreach (RoomInstance instance in roomInstances) + { + var players = await db.Presences + .Where( + x => x.roomInstance == + instance.roomInstanceId + ) + .Select(x => x.playerId) + .ToListAsync(); + + var summary = new Dictionary + { + ["roomInstanceId"] = instance.roomInstanceId, + ["roomId"] = instance.roomId, + ["subRoomId"] = instance.subRoomId, + ["isFull"] = instance.isFull, + ["playerIds"] = players, + ["createdAt"] = DateTime.UtcNow + }; + + converted.Add(summary); + } + + return converted; + } + + [HttpGet("clubhousesearch/mostactivenow"), Authorize] + public async Task GetMostActiveClubhouses() + { + var instances = await db.RoomInstances + .Where(x => x.clubId != null && x.clubId > 0) + .ToListAsync(); + + var byClub = instances.GroupBy(x => x.clubId!.Value); + var summaries = new List(); + + foreach (var group in byClub) + { + var playerCount = 0; + foreach (var instance in group) + { + playerCount += await db.Presences + .CountAsync(x => x.roomInstance == instance.roomInstanceId); + } + + summaries.Add(new Dictionary + { + ["clubId"] = group.Key, + ["playerCount"] = playerCount, + ["roomId"] = group.First().roomId + }); + } + + return Ok(summaries.ToArray()); + } + + [HttpPost("goto/instance/{id}"), Authorize] + public async Task GoToRoomInstance( + long id, + [FromForm] long[]? AdditionalPlayerIds) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return MatchResponse.Error(JoinStatus.Banned); + } + + long userId = nullableUserId.Value; + + RoomInstance? roomInstance = + await db.RoomInstances.FirstOrDefaultAsync( + x => x.roomInstanceId == id + ); + + if (roomInstance == null) + { + return MatchResponse.Error(JoinStatus.RoomIsNotActive); + } + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); + if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive); + + bool isOwnDorm = room.IsDorm && room.CreatorAccountId == userId; + if (!isOwnDorm && await db.IsBanned(userId)) + { + return MatchResponse.Error(JoinStatus.Banned); + } + + if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId)) + { + return MatchResponse.Error(JoinStatus.BannedFromRoom); + } + + var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync( + db, userId, isOwnDorm, $"instance {id} (room {roomInstance.roomId})"); + if (warningBounce != null) return warningBounce; + + if (roomInstance.isFull) + { + return MatchResponse.Error(JoinStatus.InsufficientSpace); + } + + if (roomInstance.isPrivate || room.Accessibility == Accessibility.Private) + { + var hasGameInvite = await HasActiveGameInviteAsync(userId, roomInstance); + var canManageRoom = await MatchmakingUtils.IsAtLeastRole(userId, roomInstance.roomId, RoleType.CoOwner); + if (!hasGameInvite && !canManageRoom && room.CreatorAccountId != userId) + { + return MatchResponse.Error(roomInstance.isPrivate + ? JoinStatus.RoomInstanceIsPrivate + : JoinStatus.RoomIsPrivate); + } + } + + RealPresence? presence = await db.Presences + .FirstOrDefaultAsync( + x => x.playerId == userId + ); + + if (presence == null) + { + return MatchResponse.Error(JoinStatus.Banned); + } + + presence.roomInstance = id; + presence.isOnline = true; + presence.errorCode = JoinStatus.Success; + + await MatchmakingUtils.RecordRoomVisit(db, userId, roomInstance.roomId); + await RoomJoinGiftUtils.TryDeliverAsync(db, userId, roomInstance.roomId); + + MatchmakingUtils.MarkInRoom(userId); + + await db.SaveChangesAsync(); + + if (AdditionalPlayerIds != null && + AdditionalPlayerIds.Length > 0) + { + await InvitePlayers( + AdditionalPlayerIds, + roomInstance.roomInstanceId + ); + } + + await Notify.UpdateSubscriptions(userId); + return new MatchResponse { roomInstance = roomInstance, errorCode = JoinStatus.Success }; + } + + [HttpPost("matchmake/instance/{instanceId}"), Authorize] + public async Task MatchmakeInstance( + long instanceId, + [FromForm] GoToRequest? goTo) + { + long? nullableUserId = User.GetUserId(); + if (!nullableUserId.HasValue) return null; + long userId = nullableUserId.Value; + + RoomInstance? roomInstance = await db.RoomInstances + .FirstOrDefaultAsync(x => x.roomInstanceId == instanceId); + if (roomInstance == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); + + RoomData? room = await db.Rooms + .FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); + if (room == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); + + bool isOwnDorm = room.IsDorm && room.CreatorAccountId == userId; + if (!isOwnDorm) + { + if (await db.IsBanned(userId)) return MatchResponse.Error(JoinStatus.Banned); + + bool managesRoom = await MatchmakingUtils.IsAtLeastRole( + userId, + room.RoomId, + RoleType.CoOwner + ); + + if (!managesRoom) return MatchResponse.Error(JoinStatus.NoSuchRoom); + } + + if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId)) + { + return MatchResponse.Error(JoinStatus.BannedFromRoom); + } + + MatchResponse match = await MatchmakingUtils.JoinRoom(userId, instanceId); + await UpdatePresence(userId, match); + + if (goTo?.AdditionalPlayerIds != null && match.roomInstance != null) + { + await InvitePlayers(goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId); + } + + return match; + } + + [HttpPost("goto/club/{clubId}"), Authorize] + [HttpPost("matchmake/club/{clubId}"), Authorize] + public async Task GoToClub(long clubId, [FromForm] GoToRequest? goTo) + { + long? nullableUserId = User.GetUserId(); + if (!nullableUserId.HasValue) return null; + + long userId = nullableUserId.Value; + + var club = await Clubs.ClubStore.GetAsync(db, clubId); + if (club == null) + { + return new MatchResponse + { + errorCode = JoinStatus.RoomIsNotActive, + roomInstance = null + }; + } + + var instance = await db.RoomInstances + .Where(x => x.clubId == clubId) + .OrderBy(x => x.roomInstanceId) + .FirstOrDefaultAsync(); + + if (instance == null) + { + var clubhouseRoom = await db.Rooms + .Where(x => x.CreatorAccountId == club.ownerPlayerId) + .OrderByDescending(x => x.PublishedAt) + .FirstOrDefaultAsync(); + + if (clubhouseRoom == null) + { + return new MatchResponse + { + errorCode = JoinStatus.RoomIsNotActive, + roomInstance = null + }; + } + + instance = await MatchmakingUtils.CreateRoomInstance(clubhouseRoom.RoomId, false, null, clubId, null); + } + + var match = await MatchmakingUtils.JoinRoom(userId, instance!.roomInstanceId); + await UpdatePresence(userId, match); + + if (goTo?.AdditionalPlayerIds != null && match.roomInstance != null) + { + await InvitePlayers(goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId); + } + + return match; + } + + [HttpPost("goto/event/{eventId}"), Authorize] + [HttpPost("matchmake/event/{eventId}"), Authorize] + public async Task GoToEvent( + long eventId, + [FromForm] GoToRequest? goTo) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return null; + } + + long userId = nullableUserId.Value; + + + + + MatchResponse match = + await MatchmakingUtils.MatchmakeEvent( + userId, + eventId, + goTo?.JoinMode ?? JoinMode.PublicMatchmaking + ); + + await UpdatePresence(userId, match); + + if (goTo?.AdditionalPlayerIds != null && + match.roomInstance != null) + { + await InvitePlayers( + goTo.AdditionalPlayerIds, + match.roomInstance.roomInstanceId + ); + } + + return match; + } + + [HttpPost("matchmake/room/{roomId}"), Authorize] + public async Task MatchmakeRoomById( + long roomId, + [FromForm] GoToRequest goTo, + [FromQuery] long? subRoomId = null) + { + long? nullableUserId = User.GetUserId(); + if (!nullableUserId.HasValue) return null; + long userId = nullableUserId.Value; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive); + + MatchResponse match = await MatchmakingUtils.Matchmake( + goTo.JoinMode, + room.Name, + userId, + subRoomId); + + await UpdatePresence(userId, match); + + if (goTo.AdditionalPlayerIds != null && + match.roomInstance != null) + { + await InvitePlayers( + goTo.AdditionalPlayerIds, + match.roomInstance.roomInstanceId + ); + } + + return match; + } + + [HttpPost("matchmake/room/{roomId}/{subRoomId}"), Authorize] + public async Task MatchmakeRoomWithSubRoom( + long roomId, + long subRoomId, + [FromForm] GoToRequest goTo) + { + long? nullableUserId = User.GetUserId(); + if (!nullableUserId.HasValue) return null; + long userId = nullableUserId.Value; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive); + + MatchResponse match = await MatchmakingUtils.Matchmake( + goTo.JoinMode, + room.Name, + userId, + subRoomId); + + await UpdatePresence(userId, match); + + if (goTo.AdditionalPlayerIds != null && + match.roomInstance != null) + { + await InvitePlayers( + goTo.AdditionalPlayerIds, + match.roomInstance.roomInstanceId + ); + } + + return match; + } + + [HttpPost("goto/room/{roomName}/{subRoomName}"), + Authorize] + public async Task GoToSubRoom( + string roomName, + string subRoomName, + [FromForm] GoToRequest goTo) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return null; + } + + long userId = nullableUserId.Value; + + roomName = Uri.UnescapeDataString(roomName) + .Replace("+", " "); + + subRoomName = Uri.UnescapeDataString(subRoomName) + .Replace("+", " "); + + RoomData? room = await db.Rooms.FirstOrDefaultAsync( + x => x.Name == roomName + ); + + if (room == null) + { + return new MatchResponse + { + errorCode = JoinStatus.RoomIsNotActive, + roomInstance = null + }; + } + + SubRoom? subRoom = await db.SubRooms + .FirstOrDefaultAsync( + x => x.RoomId == room.RoomId && + x.Name == subRoomName + ); + + if (subRoom == null) + { + return new MatchResponse + { + errorCode = JoinStatus.RoomIsNotActive, + roomInstance = null + }; + } + + MatchResponse match = + await MatchmakingUtils.Matchmake( + goTo.JoinMode, + roomName, + userId, + subRoom.SubRoomId + ); + + await UpdatePresence(userId, match); + + if (goTo.AdditionalPlayerIds != null && + match.roomInstance != null) + { + await InvitePlayers( + goTo.AdditionalPlayerIds, + match.roomInstance.roomInstanceId + ); + } + + return match; + } + + [HttpPost("goto/{name}"), Authorize] + public async Task GoTo( + string name, + [FromForm] GoToRequest goTo) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return null; + } + + long userId = nullableUserId.Value; + + name = Uri.UnescapeDataString(name) + .Replace("+", " "); + + MatchResponse match = + await MatchmakingUtils.Matchmake( + goTo.JoinMode, + name, + userId + ); + + await UpdatePresence(userId, match); + + if (goTo.AdditionalPlayerIds != null && + match.roomInstance != null) + { + await InvitePlayers( + goTo.AdditionalPlayerIds, + match.roomInstance.roomInstanceId + ); + } + + return match; + } + + [HttpPost("goto/player/{targetId}"), Authorize] + public async Task GoToPlayer(long targetId, [FromForm] GoToRequest? goTo) + { + var userId = User.GetUserId(); + return userId == null ? null : await FollowPlayerIntoRoom(userId.Value, targetId, goTo); + } + + [HttpPost("matchmake/player/{targetId:long}"), Authorize] + public async Task MatchmakePlayer(long targetId, [FromForm] GoToRequest? goTo) + { + var userId = User.GetUserId(); + return userId == null ? null : await FollowPlayerIntoRoom(userId.Value, targetId, goTo); + } + + private async Task FollowPlayerIntoRoom(long userId, long targetId, GoToRequest? goTo) + { + if (targetId <= 0 || targetId == userId || + !await db.Relationships.AsNoTracking().AnyAsync(relationship => + ((relationship.AccountId == userId && relationship.PlayerId == targetId) || + (relationship.AccountId == targetId && relationship.PlayerId == userId)) && + relationship.RelationshipType == RadicadeServer.RecNet.Relationships.RelationshipType.Friend)) + return MatchResponse.Error(JoinStatus.NoSuchRoom); + + var targetPresence = await db.Presences.AsNoTracking() + .FirstOrDefaultAsync(presence => presence.playerId == targetId); + if (targetPresence?.roomInstance == null) + return MatchResponse.Error(JoinStatus.NoSuchRoom); + + var instance = await db.RoomInstances.AsNoTracking() + .FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == targetPresence.roomInstance.Value); + if (instance == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == instance.roomId); + if (room == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); + + if (await MatchmakingUtils.IsBannedFromRoom(db, userId, room.RoomId)) + return MatchResponse.Error(JoinStatus.BannedFromRoom); + + var presence = await db.Presences.FirstOrDefaultAsync(row => row.playerId == userId); + if (presence == null) + { + presence = new RealPresence { playerId = userId }; + db.Presences.Add(presence); + } + presence.roomInstance = instance.roomInstanceId; + presence.isOnline = true; + presence.errorCode = JoinStatus.Success; + await db.SaveChangesAsync(); + + var match = new MatchResponse + { + roomInstance = MatchmakingUtils.NormalizeRoomInstance(instance), + errorCode = JoinStatus.Success + }; + await UpdatePresence(userId, match); + if (goTo?.AdditionalPlayerIds is { Length: > 0 } additionalPlayerIds) + await InvitePlayers(additionalPlayerIds, instance.roomInstanceId); + return match; + } + + [HttpPost("invite"), Authorize] + [HttpPost("invite/{playerId:long}"), Authorize] + public async Task Invite() + { + long? callerId = User.GetUserId(); + var (targetId, sentInstanceId, seen) = await ReadInviteFieldsAsync(); + + long instanceId = sentInstanceId; + if (instanceId <= 0 && callerId.HasValue) + { + var presence = await db.Presences.AsNoTracking() + .FirstOrDefaultAsync(x => x.playerId == callerId.Value); + instanceId = presence?.roomInstance ?? 0; + } + + if (targetId <= 0) + return InviteRefused("No invited player was named."); + if (instanceId <= 0) + return InviteRefused("The inviter is not in a room, so there is nothing to invite anyone to."); + + var result = await InvitePlayer(targetId, instanceId); + return result; + } + + [HttpPost("~/api/offlineinvite/v1/send"), Authorize] + public async Task SendOfflineInvite() + { + long? callerId = User.GetUserId(); + var (targetId, sentInstanceId, seen) = await ReadInviteFieldsAsync(); + + long instanceId = sentInstanceId; + if (instanceId <= 0 && callerId.HasValue) + { + var presence = await db.Presences.AsNoTracking() + .FirstOrDefaultAsync(x => x.playerId == callerId.Value); + instanceId = presence?.roomInstance ?? 0; + } + + if (targetId <= 0) + return InviteRefused("No invited player was named."); + if (instanceId <= 0) + return InviteRefused("The inviter is not in a room, so there is nothing to invite anyone to."); + + var result = await InvitePlayer(targetId, instanceId); + return result; + } + + private Success InviteRefused(string why) + { + return Success.Error(why); + } + + [HttpPost("matchmake/invite/{inviteId:long}"), Authorize] + public async Task AcceptInviteByMessageId( + long inviteId, + [FromForm] GoToRequest? goTo) + { + long? callerId = User.GetUserId(); + if (!callerId.HasValue) return null; + long userId = callerId.Value; + + Message? invite = await db.Messages.AsNoTracking() + .Where(message => + message.Id == inviteId && + message.ToPlayerId == userId && + (message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2)) + .OrderByDescending(message => message.Id) + .FirstOrDefaultAsync(); + + if (invite == null) + return RefuseInviteAccept( + $"invite {inviteId} is not addressed to account {userId}", + JoinStatus.RoomInviteExpired); + if (invite.FromPlayerId == userId) + return RefuseInviteAccept( + $"invite {inviteId} was sent by the caller", + JoinStatus.ChatMessageNotAnInvite); + var invitedInstanceId = RoomInviteDetails.GetInstanceId(invite); + if (invitedInstanceId == null) + return RefuseInviteAccept( + $"invite {inviteId} names no room instance", + JoinStatus.RoomInviteExpired); + + RealPresence? inviterPresence = await db.Presences.AsNoTracking() + .FirstOrDefaultAsync(presence => presence.playerId == invite.FromPlayerId); + if (inviterPresence == null || !inviterPresence.isOnline) + return RefuseInviteAccept( + $"account {invite.FromPlayerId}, who sent invite {inviteId}, is not online", + JoinStatus.PlayerNotOnline); + if (inviterPresence.roomInstance != invitedInstanceId) + return RefuseInviteAccept( + $"invite {inviteId} names instance {invitedInstanceId} but its sender " + + $"{invite.FromPlayerId} is now in {inviterPresence.roomInstance?.ToString() ?? "no instance"}", + JoinStatus.RoomInviteExpired); + + RoomInstance? instance = await db.RoomInstances.AsNoTracking() + .FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == invitedInstanceId); + if (instance == null) + return RefuseInviteAccept( + $"instance {invitedInstanceId} from invite {inviteId} is not live", + JoinStatus.RoomIsNotActive); + + return await AcceptInviteIntoInstanceAsync( + userId, invite.FromPlayerId, instance, goTo, $"invite {inviteId}"); + } + + [HttpPost("matchmake/chatinvite/{threadId:long}/{messageId:long}"), Authorize] + public async Task MatchmakeChatInvite( + long threadId, + long messageId, + [FromForm] GoToRequest? goTo) + { + long? callerId = User.GetUserId(); + if (!callerId.HasValue) return null; + long userId = callerId.Value; + + ChatThread? thread = await db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); + var members = thread?.GetPlayerIds(); + if (thread == null || members == null || !members.Contains(userId)) + return RefuseInviteAccept( + $"chat invite {messageId}: thread {threadId} is not a conversation account {userId} is part of", + JoinStatus.ChatPartyInviteNotFound); + + ChatMessage? message = thread.GetMessages()?.FirstOrDefault(x => x.chatMessageId == messageId); + long inviterId; + string via; + + if (message != null) + { + if (message.senderPlayerId == userId) + return RefuseInviteAccept( + $"chat message {messageId} in thread {threadId} was written by the caller, so it is not an invite", + JoinStatus.ChatMessageNotAnInvite); + if (DateTime.UtcNow - message.timeSent > + TimeSpan.FromMinutes(PlayerSettingsAPI.InviteLinkLifetimeInMinutes)) + return RefuseInviteAccept( + $"chat invite {messageId} in thread {threadId} is older than " + + $"{PlayerSettingsAPI.InviteLinkLifetimeInMinutes} minutes", + JoinStatus.RoomInviteExpired); + + inviterId = message.senderPlayerId; + via = $"chat invite {messageId} in thread {threadId}"; + } + else + { + var others = members.Where(member => member != userId).Distinct().ToList(); + if (others.Count != 1) + return RefuseInviteAccept( + $"chat invite {messageId} in thread {threadId} is not stored and the thread " + + $"has {others.Count} other member(s) to resolve an inviter from", + JoinStatus.ChatPartyInviteNotFound); + + inviterId = others[0]; + via = $"thread {threadId} member {inviterId} (message {messageId} is not stored)"; + } + + RealPresence? inviterPresence = await db.Presences.AsNoTracking() + .FirstOrDefaultAsync(presence => presence.playerId == inviterId); + if (inviterPresence == null || !inviterPresence.isOnline || + inviterPresence.roomInstance is null or <= 0) + return RefuseInviteAccept( + $"{via}: inviter {inviterId} is not in a room instance", + JoinStatus.PlayerNotOnline); + + RoomInstance? instance = await db.RoomInstances.AsNoTracking() + .FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == inviterPresence.roomInstance.Value); + if (instance == null) + return RefuseInviteAccept( + $"{via}: inviter {inviterId} is in instance {inviterPresence.roomInstance.Value}, which is not live", + JoinStatus.RoomIsNotActive); + + return await AcceptInviteIntoInstanceAsync(userId, inviterId, instance, goTo, via); + } + + private async Task AcceptInviteIntoInstanceAsync( + long userId, + long inviterId, + RoomInstance instance, + GoToRequest? goTo, + string via) + { + if (await db.IsBanned(userId)) + return RefuseInviteAccept($"{via}: account {userId} is banned", JoinStatus.Banned); + if (await MatchmakingUtils.IsBannedFromRoom(db, userId, instance.roomId)) + return RefuseInviteAccept( + $"{via}: account {userId} is banned from room {instance.roomId}", + JoinStatus.BannedFromRoom); + + RoomData? room = await db.Rooms.AsNoTracking() + .FirstOrDefaultAsync(x => x.RoomId == instance.roomId); + if (room == null) + return RefuseInviteAccept($"{via}: room {instance.roomId} is gone", JoinStatus.RoomIsNotActive); + + var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync( + db, userId, room.IsDorm && room.CreatorAccountId == userId, + $"instance {instance.roomInstanceId} (room {instance.roomId})"); + if (warningBounce != null) return warningBounce; + + RealPresence? presence = await db.Presences.FirstOrDefaultAsync(row => row.playerId == userId); + if (presence == null) + { + presence = new RealPresence { playerId = userId }; + db.Presences.Add(presence); + } + + presence.roomInstance = instance.roomInstanceId; + presence.isOnline = true; + presence.errorCode = JoinStatus.Success; + await db.SaveChangesAsync(); + + var match = new MatchResponse + { + roomInstance = MatchmakingUtils.NormalizeRoomInstance(instance), + errorCode = JoinStatus.Success + }; + await UpdatePresence(userId, match); + + if (goTo?.AdditionalPlayerIds is { Length: > 0 } additionalPlayerIds) + await InvitePlayers(additionalPlayerIds, instance.roomInstanceId); + + return match; + } + + private MatchResponse RefuseInviteAccept(string why, JoinStatus status) + { + return MatchResponse.Error(status); + } + + private static readonly string[] InviteTargetKeys = + { + "playerId", "PlayerId", "playerIds", "PlayerIds", "id", "Id", + "invitedPlayerId", "InvitedPlayerId", "invitedPlayerIds", "InvitedPlayerIds", + "invitedPlayerIds[]", "InvitedPlayerIds[]", "targetPlayerId", "TargetPlayerId", + "accountId", "AccountId", + }; + + private static readonly string[] InviteInstanceKeys = + { + "roomInstanceId", "RoomInstanceId", "instanceId", "InstanceId", "roomInstance", "RoomInstance", + }; + + private async Task<(long PlayerId, long RoomInstanceId, string Seen)> ReadInviteFieldsAsync() + { + long playerId = 0; + long roomInstanceId = 0; + var seen = new List(); + + void Collect(string key, string? value) + { + if (string.IsNullOrWhiteSpace(value)) return; + seen.Add($"{key}={value}"); + + if (InviteTargetKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) + { + foreach (var part in value.Split(new[] { ',', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + if (playerId <= 0 && long.TryParse(part, out var parsed) && parsed > 0) playerId = parsed; + } + else if (InviteInstanceKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) + { + if (roomInstanceId <= 0 && long.TryParse(value.Trim(), out var parsed) && parsed > 0) + roomInstanceId = parsed; + } + } + + foreach (var pair in Request.RouteValues) + if (pair.Value != null) Collect(pair.Key, pair.Value.ToString()); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var field in form) + foreach (var value in field.Value) + Collect(field.Key, value); + } + else if (Request.ContentLength is > 0) + { + try + { + Request.EnableBuffering(); + if (await JsonNode.ParseAsync(Request.Body) is JsonObject body) + foreach (var pair in body) + { + if (pair.Value is JsonArray array) + foreach (var item in array) Collect(pair.Key, item?.ToString()); + else + Collect(pair.Key, pair.Value?.ToString()); + } + + Request.Body.Position = 0; + } + catch + { + try { Request.Body.Position = 0; } catch { } + } + } + + foreach (var key in InviteTargetKeys.Concat(InviteInstanceKeys)) + foreach (var value in Request.Query[key]) + Collect(key, value); + + return (playerId, roomInstanceId, seen.Count == 0 ? "(none)" : string.Join("; ", seen)); + } + + public async Task UpdatePresence( + long userId, + MatchResponse match) + { + RealPresence? realPresence = await db.Presences + .FirstOrDefaultAsync( + x => x.playerId == userId + ); + + if (realPresence == null) + { + return new Success( + false, + "Presence does not exist.", + null + ); + } + + if (match.errorCode == JoinStatus.Success && + match.roomInstance != null) + { + RoomInstance roomInstance = + match.roomInstance; + + roomInstance.eventId ??= 0; + roomInstance.clubId ??= 0; + roomInstance.roomCode ??= string.Empty; + roomInstance.location ??= string.Empty; roomInstance.name ??= string.Empty; + roomInstance.photonRegion ??= string.Empty; + roomInstance.photonRegionId ??= string.Empty; + roomInstance.photonRoomId ??= string.Empty; + roomInstance.dataBlob ??= string.Empty; + + realPresence.roomInstance = + roomInstance.roomInstanceId; + + realPresence.isOnline = true; + realPresence.errorCode = + JoinStatus.Success; + + MatchmakingUtils.MarkInRoom(userId); + + await db.SaveChangesAsync(); + } + else + { + realPresence.errorCode = match.errorCode; + + await db.SaveChangesAsync(); + } + + var presence = await Presence.CreateAsync(db, + realPresence, + match.roomInstance + ); + + var subscriberUpdate = Notify.UpdateSubscriptions(userId); + try + { + await Notify.SendNotify( + userId, + new Notification + { + Id = "PresenceUpdate", + Msg = presence + } + ); + } + catch (Exception) + { + } + + try + { + await subscriberUpdate; + } + catch (Exception) + { + } + + return new Success(true, null, null); + } + + public async Task InvitePlayers( + long[] players, + long roomInstanceId) + { + try + { + foreach (long playerId in players) + { + await InvitePlayer( + playerId, + roomInstanceId + ); + } + + return new Success(true, null, null); + } + catch (Exception) + { + + return new Success( + false, + "Could not invite additional player ids.", + null + ); + } + } + + public async Task InvitePlayer( + long playerId, + long roomInstanceId) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return Success.Error("Missing user ID."); + } + + long userId = nullableUserId.Value; + if (playerId <= 0 || playerId == userId) return Success.Error("Invalid player ID."); + + RoomInstance? roomInstance = await db.RoomInstances + .FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId); + if (roomInstance == null) return Success.Error("Room instance not found."); + + Profile? target = await db.Profiles + .AsNoTracking() + .FirstOrDefaultAsync(x => x.accountId == playerId); + if (target == null) return Success.Error("Player not found."); + + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); + if (room == null) return Success.Error("Room not found."); + if (roomInstance.isFull) return Success.Error("Room instance is full."); + if (await db.IsBanned(userId)) return Success.Error("Inviter is banned."); + if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId)) + return Success.Error("Inviter is banned from this room."); + if (await MatchmakingUtils.IsBannedFromRoom(db, playerId, roomInstance.roomId)) + return Success.Error("Player is banned from this room."); + + var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync( + db, userId, room.IsDorm && room.CreatorAccountId == userId, $"instance {roomInstanceId}"); + if (warningBounce != null) return Success.Error("Inviter is blocked from the room."); + + var presence = await db.Presences.AsNoTracking().FirstOrDefaultAsync(x => x.playerId == userId); + if (presence?.roomInstance != roomInstanceId || !presence.isOnline) + return Success.Error("Inviter must be online in the room instance."); + + var playerCount = await db.Presences.CountAsync(row => + row.roomInstance == roomInstanceId && row.isOnline); + if (playerCount >= roomInstance.maxCapacity) + return Success.Error("Room instance is full."); + + + var details = new RoomInviteDetails + { + Name = room.Name, + RoomInstanceId = roomInstanceId, + InviteMode = 0 + }; + await Notify.SendMessage(playerId, new Message + { + Type = MessageType.GameInviteV2, + RoomId = roomInstance.roomId, + FromPlayerId = userId + }, prepareWithId: message => + { + details.InviteId = message.Id; + message.Data = System.Text.Json.JsonSerializer.Serialize(details); + }); + + return new Success(true, null, null); + } + + private async Task HasActiveGameInviteAsync(long playerId, RoomInstance roomInstance) + { + var invites = await db.Messages.AsNoTracking() + .Where(message => + message.ToPlayerId == playerId && + (message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2) && + message.RoomId == roomInstance.roomId) + .OrderByDescending(message => message.Id) + .ToListAsync(); + var invite = invites.FirstOrDefault(message => + RoomInviteDetails.GetInstanceId(message) == roomInstance.roomInstanceId); + if (invite == null || await db.IsBanned(invite.FromPlayerId)) return false; + + return await db.Presences.AsNoTracking().AnyAsync(presence => + presence.playerId == invite.FromPlayerId && + presence.roomInstance == roomInstance.roomInstanceId && + presence.isOnline) && + !await MatchmakingUtils.IsBannedFromRoom(db, invite.FromPlayerId, roomInstance.roomId); + } + + } +} diff --git a/Server/Matchmaking/MatchmakingPlayerAPI.cs b/Server/Matchmaking/MatchmakingPlayerAPI.cs new file mode 100644 index 0000000..9a215f0 --- /dev/null +++ b/Server/Matchmaking/MatchmakingPlayerAPI.cs @@ -0,0 +1,610 @@ +using System.Text; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.Auth; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Matchmaking +{ + [Route("matchmaking/player")] + public class MatchmakingPlayerAPI : Controller + { + private readonly RecDBContext db; + + public MatchmakingPlayerAPI(RecDBContext context) + { + db = context; + } + + [HttpPut("photonregionpings")] + public IActionResult SendPhotonRegionPings() + { + return Ok(new { }); + } + + [HttpPut("gameserverregionpings")] + public IActionResult SendGameServerRegionPings() + { + return Ok(new { }); + } + + [HttpGet("qos")] + public IActionResult GetQos() + { + return Ok(new[] + { + new { id = "us-west1", address = "34.169.254.144:50000" }, + new { id = "europe-west1", address = "35.205.141.119:50000" }, + new { id = "asia-northeast1", address = "35.200.67.228:50000" }, + new { id = "us-east1", address = "34.73.244.122:50000" }, + new { id = "us-central1", address = "34.69.179.51:50000" }, + new { id = "northamerica-northeast1", address = "34.152.4.100:50000" } + }); + } + + [HttpGet("connection-info"), Authorize] + public async Task GetConnectionInfo([FromQuery] long? roomInstanceId = null) + { + long? nullableUserId = User.GetUserId(); + if (!nullableUserId.HasValue) + { + return Unauthorized(); + } + long userId = nullableUserId.Value; + + string photonRoomId = string.Empty; + var presence = await db.Presences.AsNoTracking() + .FirstOrDefaultAsync(x => x.playerId == userId); + if (presence?.roomInstance.HasValue == true) + { + var instance = await db.RoomInstances.AsNoTracking() + .FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance.Value); + photonRoomId = instance?.photonRoomId ?? string.Empty; + } + if (string.IsNullOrEmpty(photonRoomId) && roomInstanceId.HasValue) + { + var requested = await db.RoomInstances.AsNoTracking() + .FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId.Value); + photonRoomId = requested?.photonRoomId ?? string.Empty; + } + + var platformId = User.GetPlatId() ?? string.Empty; + var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(DBConfig.JwtKey); + var now = DateTime.UtcNow; + var photonToken = tokenHandler.WriteToken(tokenHandler.CreateToken( + new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor + { + Subject = new System.Security.Claims.ClaimsIdentity(new[] + { + new System.Security.Claims.Claim("sub", userId.ToString()), + new System.Security.Claims.Claim("rn.platid", platformId), + new System.Security.Claims.Claim("rn.plat", User.FindFirst("rn.plat")?.Value ?? "0"), + new System.Security.Claims.Claim("rn.deviceclass", User.FindFirst("rn.deviceclass")?.Value ?? "0"), + new System.Security.Claims.Claim("rn.env", "prod") + }), + Issuer = DBConfig.TokenIssuer, + Expires = now.AddHours(1), + SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials( + new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(key), + Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature) + })); + + return Ok(new + { + success = true, + value = new + { + photonAuthToken = photonToken, + photonRealtimeAppId = DBConfig.PhotonRealtimeAppId, + photonVoiceAppId = DBConfig.PhotonVoiceAppId, + photonChatAppId = DBConfig.PhotonChatAppId, + photonRegion = DBConfig.PhotonRegion, + photonRoomId, + voiceConnectionInfo = string.Empty, + voiceServerId = string.Empty, + experiments = new + { + networkTransformSyncInterval = 10.0, + shouldUseUnreliableOnChange = false, + shouldAvoidDiscontinuityRPCs = true, + shouldAvoidRedundantDiscontinuity = false, + r2RuntimeStaticBaking = true, + r2AutoEmbodiment = true, + r2RuntimeStaticBakingMinShapeThreshold = 1, + r2UseCheapReplicas = true, + shouldUseGameServerNetworking = false + } + }, + error = (string?)null + }); + } + + [HttpGet("avoidjuniors"), Authorize] + public IActionResult GetAvoidJuniors() + { + return Ok(false); + } + + [HttpPut("avoidjuniors"), Authorize] + public IActionResult SetAvoidJuniors() + { + return Ok(false); + } + + [HttpPut("statusvisibility"), Authorize] + public async Task SetStatusVisibility( + StatusVisibility statusVisibility) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return Unauthorized("Missing user ID."); + } + + long userId = nullableUserId.Value; + + var presence = await db.Presences + .FirstOrDefaultAsync(x => x.playerId == userId); + + if (presence == null) + { + return NotFound("Player presence does not exist."); + } + + presence.statusVisibility = statusVisibility; + + await db.SaveChangesAsync(); + await Notify.UpdateSubscriptions(userId); + + return Ok(new { }); + } + + [HttpPost("heartbeat"), Authorize] + public async Task Heartbeat() + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return Unauthorized("Missing user ID."); + } + + long userId = nullableUserId.Value; + + try + { + return await HeartbeatCore(userId); + } + catch (Exception) + { + + return Ok(new PlayerHeartbeat + { + playerId = userId, + isOnline = true, + deviceClass = DeviceClass.Screen, + statusVisibility = StatusVisibility.Public, + roomInstance = null + }); + } + } + + private async Task HeartbeatCore(long userId) + { + var presence = await db.Presences + .FirstOrDefaultAsync(x => x.playerId == userId); + + if (presence == null) + { + + presence = new RealPresence { playerId = userId }; + db.Presences.Add(presence); + + await MatchmakingUtils.EnsureDormPresenceAsync(db, userId, presence); + } + + RoomInstance? roomInstance = null; + + if (presence.roomInstance.HasValue) + { + long roomInstanceId = presence.roomInstance.Value; + + roomInstance = await db.RoomInstances + .FirstOrDefaultAsync( + x => x.roomInstanceId == roomInstanceId + ); + + if (roomInstance == null) + { + presence.roomInstance = null; + MatchmakingUtils.ClearInRoom(userId); + } + } + + if (roomInstance != null) + { + roomInstance.eventId ??= 0; + roomInstance.clubId ??= 0; + roomInstance.roomCode ??= string.Empty; + roomInstance.location ??= string.Empty; + } + + + + + + + + + if (roomInstance != null) + { + var currentRoom = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); + bool isOwnDorm = currentRoom != null && currentRoom.IsDorm && currentRoom.CreatorAccountId == userId; + if (!isOwnDorm) + { + var ban = await db.ResolveBanAsync(userId, Request.GetClientIp()); + + if (ban != null && !await db.IsDeveloper(userId)) + { + await PlayerUtils.EjectToDormAsync(db, userId); + roomInstance = presence.roomInstance.HasValue + ? await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance.Value) + : null; + + await Notify.SendNotify(userId, new Notification + { + Id = NotificationType.ModerationKick, + Msg = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, userId) + }); + } + } + } + + presence.isOnline = true; + presence.deviceClass = DeviceClass.Screen; + + await db.SaveChangesAsync(); + + if (roomInstance != null) + { + try + { + var xpRoom = await db.Rooms.AsNoTracking() + .FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); + bool publicRoom = xpRoom != null && !xpRoom.IsDorm && + xpRoom.Accessibility == RadicadeServer.RecNet.Enums.Accessibility.Public; + await RoomProgressionService.AwardRoomXpAsync(db, userId, roomInstance.roomId, publicRoom); + } + catch (Exception) + { + } + } + + var heartbeat = new PlayerHeartbeat + { + deviceClass = presence.deviceClass, + isOnline = true, + playerId = userId, + roomInstance = roomInstance, + statusVisibility = presence.statusVisibility + }; + + if (roomInstance != null) + { + try + { + var presenceUpdate = + await Presence.CreateAsync(db, presence, roomInstance); + + await Notify.SendNotify( + userId, + new Notification + { + Id = "PresenceUpdate", + Msg = presenceUpdate + } + ); + } + catch (Exception) + { + } + } + + try + { + await Notify.UpdateSubscriptions(userId); + } + catch (Exception) + { + } + + return Ok(heartbeat); + } + + + + + + + + + + + [HttpPost("notifydisconnect"), Authorize] + public async Task NotifyDisconnect( + [FromForm(Name = "playerId")] long playerId = 0, + [FromForm(Name = "loginLock")] string? loginLock = null) + { + var requesterId = User.GetUserId(); + if (!requesterId.HasValue) return Unauthorized(); + + long targetId = playerId > 0 ? playerId : 0; + + + + if (targetId == 0 && !string.IsNullOrWhiteSpace(loginLock)) + { + var byLock = await db.Presences.FirstOrDefaultAsync(x => x.LoginLock == loginLock); + if (byLock != null) targetId = byLock.playerId; + } + + if (targetId == 0) targetId = requesterId.Value; + + + + + + + + + var ban = await db.ResolveBanAsync(targetId, Request.GetClientIp()); + if (ban == null) return Ok(); + + + + await PlayerUtils.EjectToDormAsync(db, targetId); + + + + + + + + if (targetId != requesterId.Value) + { + await Notify.SendNotify(targetId, new Notification + { + Id = NotificationType.ModerationKick, + Msg = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, targetId) + }); + } + + return Ok(); + } + + [HttpPost("login"), Authorize] + public async Task Login( + [FromForm(Name = "LoginLock")] + string? loginLock = null, + + [FromForm(Name = "identification")] + string? identification = null, + + [FromForm(Name = "JoinMode")] + int joinMode = 0, + + [FromForm(Name = "BypassMovementModeRestriction")] + bool bypassMovementModeRestriction = false) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + + return Unauthorized(new + { + error = "invalid_token", + error_description = "Missing user ID." + }); + } + + long userId = nullableUserId.Value; + + string? platformId = User.GetPlatId(); + + if (string.IsNullOrWhiteSpace(platformId)) + { + + return Unauthorized(new + { + error = "invalid_token", + error_description = "Missing platform ID." + }); + } + + bool isWhitelisted; + + try + { + isWhitelisted = + await AuthShared.IsWhitelisted(platformId); + } + catch (Exception) + { + + return StatusCode( + StatusCodes.Status500InternalServerError, + new + { + error = "server_error", + error_description = + "Whitelist check failed." + } + ); + } + + if (!isWhitelisted) + { + + return Unauthorized(new + { + error = "not_whitelisted", + error_description = + "This account is not whitelisted." + }); + } + + var presence = await db.Presences + .FirstOrDefaultAsync( + x => x.playerId == userId + ); + + if (presence == null) + { + + presence = new RealPresence + { + playerId = userId + }; + + db.Presences.Add(presence); + } + + if (!string.IsNullOrWhiteSpace(loginLock)) + { + presence.LoginLock = loginLock; + } + + presence.isOnline = true; + presence.deviceClass = DeviceClass.Screen; + + + + + + if (!presence.roomInstance.HasValue) + { + await MatchmakingUtils.EnsureDormPresenceAsync( + db, + userId, + presence); + } + else + { + presence.isOnline = true; + } + + if (presence.roomInstance.HasValue) + { + MatchmakingUtils.MarkInRoom(userId); + } + else + { + MatchmakingUtils.ClearInRoom(userId); + } + + await db.SaveChangesAsync(); + + try + { + await Notify.UpdateSubscriptions(userId); + } + catch (Exception) + { + } + + return Ok(new + { + success = true, + playerId = userId, + accountId = userId, + platformId, + loginLock = presence.LoginLock ?? + string.Empty + }); + } + + [HttpPost("exclusivelogin"), Authorize] + public Task ExclusiveLogin( + [FromForm(Name = "LoginLock")] + string? loginLock = null, + + [FromForm(Name = "identification")] + string? identification = null, + + [FromForm(Name = "JoinMode")] + int joinMode = 0, + + [FromForm(Name = "BypassMovementModeRestriction")] + bool bypassMovementModeRestriction = false) + { + return Login( + loginLock, + identification, + joinMode, + bypassMovementModeRestriction); + } + + [HttpPost("logout"), Authorize] + public async Task Logout( + [FromForm(Name = "LoginLock")] + string? loginLock = null) + { + long? nullableUserId = User.GetUserId(); + + if (!nullableUserId.HasValue) + { + return Ok(); + } + + long userId = nullableUserId.Value; + + var presence = await db.Presences + .FirstOrDefaultAsync( + x => x.playerId == userId + ); + + if (presence == null) + { + MatchmakingUtils.ClearInRoom(userId); + return Ok(); + } + + presence.LoginLock = null; + presence.isOnline = false; + presence.roomInstance = null; + + MatchmakingUtils.ClearInRoom(userId); + + await db.SaveChangesAsync(); + + var subscriberUpdate = Notify.UpdateSubscriptions(userId); + try + { + await Notify.SendNotify(userId, new Notification + { + Id = "PresenceUpdate", + Msg = await Presence.CreateAsync(db, presence, null) + }); + } + catch (Exception) + { + } + + await subscriberUpdate; + + return Ok(); + } + } +} diff --git a/Server/Matchmaking/MatchmakingRuntimeState.cs b/Server/Matchmaking/MatchmakingRuntimeState.cs new file mode 100644 index 0000000..af206b2 --- /dev/null +++ b/Server/Matchmaking/MatchmakingRuntimeState.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; + +namespace RadicadeServer.Utils; + +public static class MatchmakingRuntimeState +{ + public static async Task ResetAsync(RecDBContext db) + { + var presences = await db.Presences.Where(p => p.isOnline).ToListAsync(); + foreach (var presence in presences) + { + presence.isOnline = false; + presence.roomInstance = null; + } + + + + + var broadcastingEvents = await db.PlayerEvents + .Where(ev => ev.BroadcastingRoomInstanceId != null).ToListAsync(); + foreach (var ev in broadcastingEvents) + ev.BroadcastingRoomInstanceId = null; + + db.RoomInstances.RemoveRange(await db.RoomInstances.ToListAsync()); + await db.SaveChangesAsync(); + } +} diff --git a/Server/Matchmaking/MatchmakingUtils.cs b/Server/Matchmaking/MatchmakingUtils.cs new file mode 100644 index 0000000..a37d5bb --- /dev/null +++ b/Server/Matchmaking/MatchmakingUtils.cs @@ -0,0 +1,938 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Concurrent; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Utils +{ + public static class MatchmakingUtils + { + public static IDbContextFactory DbFactory = null!; + private static readonly ConcurrentDictionary InRoomSessions = new(); + + + + + + public static void MarkInRoom(long playerId) => InRoomSessions[playerId] = 0; + public static void ClearInRoom(long playerId) => InRoomSessions.TryRemove(playerId, out _); + + public static RoomInstance NormalizeRoomInstance(RoomInstance roomInstance) + { + roomInstance.eventId ??= 0; + roomInstance.clubId ??= 0; + roomInstance.roomCode ??= string.Empty; + roomInstance.location ??= string.Empty; + roomInstance.name ??= string.Empty; + roomInstance.photonRegion ??= string.Empty; + roomInstance.photonRegionId ??= string.Empty; + roomInstance.photonRoomId ??= string.Empty; + roomInstance.dataBlob ??= string.Empty; + + return roomInstance; + } + + public static async Task EnsureDormPresenceAsync( + RecDBContext db, + long playerId, + RealPresence presence) + { + FullRoom? dorm = await RoomUtils.GetOrCreateDorm(playerId); + if (dorm == null) + { + return null; + } + + RoomInstance? dormInstance = null; + if (presence.roomInstance.HasValue) + { + dormInstance = await db.RoomInstances.FirstOrDefaultAsync(x => + x.roomInstanceId == presence.roomInstance.Value && + x.roomId == dorm.RoomId && + x.isPrivate); + } + + if (dormInstance == null) + { + dormInstance = await CreateRoomInstance( + dorm.RoomId, + true, + null, + null, + null); + } + if (dormInstance == null) + { + return null; + } + + var isNewDormVisit = !presence.isOnline || presence.roomInstance != dormInstance.roomInstanceId; + presence.roomInstance = dormInstance.roomInstanceId; + presence.errorCode = JoinStatus.Success; + presence.isOnline = true; + MarkInRoom(playerId); + + if (isNewDormVisit) + await RecordRoomVisit(db, playerId, dorm.RoomId); + + await db.SaveChangesAsync(); + + return NormalizeRoomInstance(dormInstance); + } + + public static async Task TryStartOrientationAsync( + RecDBContext db, + long playerId, + RealPresence presence) + { + if (!await NeedsOrientationAsync(db, playerId)) + return null; + + FullRoom? orientation = await RoomUtils.GetRoom("Orientation"); + if (orientation == null) + { + return null; + } + + RoomInstance? instance = await CreateRoomInstance( + orientation.RoomId, + true, + null, + null, + null); + + if (instance == null) + { + return null; + } + + await ClearNeedsOrientationAsync(db, playerId); + + presence.roomInstance = instance.roomInstanceId; + presence.errorCode = JoinStatus.Success; + presence.isOnline = true; + MarkInRoom(playerId); + + await RecordRoomVisit(db, playerId, orientation.RoomId); + await db.SaveChangesAsync(); + + return NormalizeRoomInstance(instance); + } + + public static async Task IsPlayerInAnyInstances(long playerId) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var presence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence?.roomInstance == null) return false; + + var instance = await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance); + return instance != null; + } + + public static async Task Matchmake( + JoinMode joinMode, + string roomName, + long playerId, + long? subRoomId = null) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var presence = await ctx.Presences + .FirstOrDefaultAsync(x => x.playerId == playerId); + + if (presence == null) + { + return MatchResponse.Error(JoinStatus.UnknownError); + } + + roomName = NormalizeRoomName(roomName); + + if (string.Equals(roomName, "none", StringComparison.OrdinalIgnoreCase)) + { + presence.roomInstance = null; + presence.errorCode = JoinStatus.Success; + ClearInRoom(playerId); + + await ctx.SaveChangesAsync(); + + return new MatchResponse + { + errorCode = JoinStatus.Success, + roomInstance = null + }; + } + + var profile = await ctx.Profiles + .AsNoTracking() + .FirstOrDefaultAsync(x => x.accountId == playerId); + + string? ownDormName = profile == null + ? null + : $"@{profile.username}'s Dorm"; + + bool isOwnDormRequest = + string.Equals(roomName, "DormRoom", StringComparison.OrdinalIgnoreCase) || + (!string.IsNullOrWhiteSpace(ownDormName) && + string.Equals(roomName, ownDormName, StringComparison.OrdinalIgnoreCase)); + + var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync(ctx, playerId, isOwnDormRequest, $"room '{roomName}'"); + if (warningBounce != null) return warningBounce; + + bool isBanned = await ctx.IsBanned(playerId); + + if (isBanned && !isOwnDormRequest) + { + var requestedRoom = await RoomUtils.GetRoom(roomName); + + bool isOwnedDorm = + requestedRoom != null && + requestedRoom.IsDorm && + requestedRoom.CreatorAccountId == playerId; + + if (!isOwnedDorm) + { + return new MatchResponse + { + errorCode = JoinStatus.Banned, + roomInstance = null + }; + } + } + + FullRoom? room; + + if (isOwnDormRequest) + { + RoomInstance? orientationInstance = await TryStartOrientationAsync(ctx, playerId, presence); + if (orientationInstance != null) + { + return new MatchResponse + { + errorCode = JoinStatus.Success, + roomInstance = orientationInstance + }; + } + + room = await RoomUtils.GetOrCreateDorm(playerId); + } + else + { + room = await RoomUtils.GetRoom(roomName); + } + + if (room == null) + { + return new MatchResponse + { + errorCode = JoinStatus.RoomIsNotActive, + roomInstance = null + }; + } + + + + if (await IsBannedFromRoom(ctx, playerId, room.RoomId)) + { + return new MatchResponse + { + errorCode = JoinStatus.BannedFromRoom, + roomInstance = null + }; + } + + if (room.Accessibility == Accessibility.Private && + !await IsAtLeastRole(playerId, room.RoomId, RoleType.CoOwner)) + { + return MatchResponse.Error(JoinStatus.RoomIsPrivate); + } + + if (presence.roomInstance.HasValue) + { + await RemovePresence(playerId); + } + + RoomInstance? newInstance = null; + + if (isOwnDormRequest || joinMode == JoinMode.PrivateNewInstance) + { + newInstance = await CreateRoomInstance( + room.RoomId, + true, + subRoomId, + null, + null + ); + + if (newInstance == null) + { + return new MatchResponse + { + errorCode = JoinStatus.UnknownError, + roomInstance = null + }; + } + } + + if (newInstance != null) + { + return await JoinRoom(playerId, newInstance.roomInstanceId); + } + + await ReapEmptyPublicInstancesAsync(ctx, room.RoomId, subRoomId); + + var query = ctx.RoomInstances.Where(i => + !i.isFull && + i.roomId == room.RoomId && + !i.isPrivate && + (!subRoomId.HasValue || i.subRoomId == subRoomId)); + + int count = await query.CountAsync(); + RoomInstance? publicInstance = null; + + if (count > 0) + { + int skip = Random.Shared.Next(0, count); + publicInstance = await query.Skip(skip).FirstOrDefaultAsync(); + } + + publicInstance ??= await CreateRoomInstance( + room.RoomId, + false, + subRoomId, + null, + null + ); + + if (publicInstance == null) + { + return new MatchResponse + { + errorCode = JoinStatus.UnknownError, + roomInstance = null + }; + } + + return await JoinRoom(playerId, publicInstance.roomInstanceId); + } + + public static async Task MatchmakeEvent(long playerId, long eventId, + JoinMode joinMode = JoinMode.PublicMatchmaking) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var presence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence == null) + return MatchResponse.Error(JoinStatus.UnknownError); + + var ev = await ctx.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) + return MatchResponse.Error(JoinStatus.NoSuchRoom); + + var now = DateTime.UtcNow; + + + + + + if (ev.StartTime > now && ev.CreatorPlayerId != playerId) + return MatchResponse.Error(JoinStatus.EventNotStarted); + + + if (ev.EndTime <= now) + return MatchResponse.Error(JoinStatus.EventAlreadyFinished); + + var room = await RoomUtils.GetRoom(ev.RoomId); + if (room == null) + return MatchResponse.Error(JoinStatus.RoomIsNotActive); + + if (await ctx.IsBanned(playerId)) + return MatchResponse.Error(JoinStatus.Banned); + + if (await IsBannedFromRoom(ctx, playerId, ev.RoomId)) + return MatchResponse.Error(JoinStatus.BannedFromRoom); + + var eventInvitation = await ctx.Messages.AsNoTracking() + .AnyAsync(message => message.ToPlayerId == playerId && + message.Type == MessageType.PlayerEventInvitation && + message.PlayerEventId == eventId); + var eventResponse = await ctx.PlayerEventResponses.AsNoTracking() + .FirstOrDefaultAsync(x => x.PlayerEventId == eventId && x.PlayerId == playerId); + var hasAccepted = eventResponse?.Type == EventResponses.Yes; + var hasDeclined = eventResponse?.Type == EventResponses.No; + var hasPendingInvitation = eventResponse?.Type == EventResponses.Pending; + var canJoinEvent = ev.CreatorPlayerId == playerId || hasAccepted || + ((eventInvitation || hasPendingInvitation) && !hasDeclined); + + if (ev.Accessibility == Accessibility.Private && !canJoinEvent) + return MatchResponse.Error(JoinStatus.EventIsPrivate); + + + + + + + if (joinMode == JoinMode.PrivateNewInstance) + { + + + var mayUsePrivateRoom = canJoinEvent || + await IsAtLeastRole(playerId, ev.RoomId, RoleType.CoOwner); + if (room.Accessibility == Accessibility.Private && !mayUsePrivateRoom) + return MatchResponse.Error(JoinStatus.RoomIsPrivate); + + var ownInstance = await CreateRoomInstance(ev.RoomId, true, ev.SubRoomId, null, eventId); + if (ownInstance == null) + return MatchResponse.Error(JoinStatus.UnknownError); + + return await JoinRoom(playerId, ownInstance.roomInstanceId, canJoinEvent); + } + + + + + + + + + + + + + + + + + + var broadcastInstanceId = ev.BroadcastingRoomInstanceId ?? 0; + var hostInstanceId = 0L; + if (broadcastInstanceId > 0) + { + var hostPresence = await ctx.Presences.AsNoTracking() + .FirstOrDefaultAsync(x => x.playerId == ev.CreatorPlayerId); + hostInstanceId = hostPresence?.roomInstance ?? 0; + } + + RoomInstance? instance = null; + if (broadcastInstanceId > 0 && ev.CreatorPlayerId == playerId) + { + instance = await ctx.RoomInstances.FirstOrDefaultAsync(i => + i.roomInstanceId == broadcastInstanceId && !i.isFull) + ?? await ctx.RoomInstances.FirstOrDefaultAsync(i => + i.roomInstanceId == hostInstanceId && !i.isFull); + } + + + + + instance ??= await ctx.RoomInstances + .FirstOrDefaultAsync(i => i.eventId == eventId && !i.isFull && + !i.isPrivate && + i.roomInstanceId != broadcastInstanceId && + i.roomInstanceId != hostInstanceId); + + if (instance == null) + { + instance = await CreateRoomInstance(ev.RoomId, false, ev.SubRoomId, null, eventId); + if (instance == null) + return MatchResponse.Error(JoinStatus.UnknownError); + } + + return await JoinRoom(playerId, instance.roomInstanceId, canJoinEvent); + } + + public static async Task JoinRoom(long playerId, long instanceId, bool allowPrivateRoom = false) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + if (instanceId < 0) return new MatchResponse { errorCode = JoinStatus.NoSuchRoom }; + + var roomInstance = await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId); + var playerPresence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (playerPresence == null) return new MatchResponse { roomInstance = null, errorCode = JoinStatus.UnknownError }; + if (roomInstance == null) return new MatchResponse { roomInstance = null, errorCode = JoinStatus.NoSuchRoom }; + + var room = await ctx.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); + if (room == null) return new MatchResponse { roomInstance = null, errorCode = JoinStatus.NoSuchRoom }; + + + + if (await IsBannedFromRoom(ctx, playerId, room.RoomId)) + return new MatchResponse { roomInstance = null, errorCode = JoinStatus.BannedFromRoom }; + + var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync( + ctx, playerId, room.IsDorm && room.CreatorAccountId == playerId, $"instance {instanceId} (room {room.RoomId})"); + if (warningBounce != null) return warningBounce; + + if (room.Accessibility == Accessibility.Private && + !allowPrivateRoom && + !await IsAtLeastRole(playerId, room.RoomId, RoleType.CoOwner)) + { + return new MatchResponse { roomInstance = null, errorCode = JoinStatus.RoomIsPrivate }; + } + + var playersInInstance = await GetPlayerCountInInstance(instanceId); + if (playersInInstance >= roomInstance.maxCapacity) + return new MatchResponse { roomInstance = null, errorCode = JoinStatus.InsufficientSpace }; + + playerPresence.roomInstance = instanceId; + playerPresence.isOnline = true; + playerPresence.errorCode = JoinStatus.Success; + + + + if (roomInstance.eventId is > 0) + roomInstance.roomInstanceType = await ctx.PlayerEvents.AnyAsync(ev => + ev.PlayerEventId == roomInstance.eventId && ev.IsMultiInstance) + ? RoomInstanceType.MultiInstanceEvent : RoomInstanceType.Event; + + await RecordRoomVisit(ctx, playerId, room.RoomId); + await RoomJoinGiftUtils.TryDeliverAsync(ctx, playerId, room); + + playersInInstance = await GetPlayerCountInInstance(instanceId); + if (playersInInstance+1 >= roomInstance.maxCapacity) + { + roomInstance.isFull = true; + ctx.RoomInstances.Update(roomInstance); + } + else + { + if (roomInstance.isFull) + { + roomInstance.isFull = false; + ctx.RoomInstances.Update(roomInstance); + } + } + + await ctx.SaveChangesAsync(); + MarkInRoom(playerId); + + + + + var hostedEvent = await ctx.PlayerEvents.FirstOrDefaultAsync(ev => + ev.PlayerEventId == roomInstance.eventId && ev.CreatorPlayerId == playerId && + ev.BroadcastingRoomInstanceId != null && ev.BroadcastingRoomInstanceId != instanceId); + if (hostedEvent != null && !await ctx.Presences.AnyAsync(p => + p.isOnline && p.roomInstance == hostedEvent.BroadcastingRoomInstanceId)) + { + hostedEvent.BroadcastingRoomInstanceId = null; + await ctx.SaveChangesAsync(); + await RadicadeServer.RecNet.Events.PlayerEventNotifications.PushToEventInstancesAsync(ctx, hostedEvent); + } + + return new MatchResponse { roomInstance = roomInstance, errorCode = JoinStatus.Success }; + } + + + + + + + + public static async Task GetLastVisitedRoomNameAsync(RecDBContext ctx, long playerId) + { + var visit = await ctx.RoomInteractions + .Where(x => x.PlayerId == playerId) + .OrderByDescending(x => x.LastVisted) + .FirstOrDefaultAsync(); + if (visit == null) return null; + + return await ctx.Rooms + .Where(x => x.RoomId == visit.RoomId) + .Select(x => x.Name) + .FirstOrDefaultAsync(); + } + + public static async Task GetLastRewardActivityRoomNameAsync( + RecDBContext ctx, + long playerId, + TimeSpan window) + { + var since = DateTime.Now - window; + + var recent = await ctx.RoomInteractions + .AsNoTracking() + .Where(x => x.PlayerId == playerId && x.LastVisted >= since) + .OrderByDescending(x => x.LastVisted) + .Take(VisitsConsidered) + .Select(x => x.RoomId) + .ToListAsync(); + + if (recent.Count == 0) return null; + + var names = await ctx.Rooms + .AsNoTracking() + .Where(x => recent.Contains(x.RoomId)) + .Select(x => new { x.RoomId, x.Name }) + .ToListAsync(); + + foreach (var roomId in recent) + { + var name = names.FirstOrDefault(x => x.RoomId == roomId)?.Name; + if (RroRewardPools.IsRewardActivity(name)) return name; + } + + return null; + } + + private const int VisitsConsidered = 25; + + public static async Task RecordRoomVisit(RecDBContext ctx, long playerId, long roomId) + { + var existingInteraction = await ctx.RoomInteractions.FirstOrDefaultAsync(x => x.RoomId == roomId && x.PlayerId == playerId); + bool isNewVisitor = existingInteraction == null || !existingInteraction.Visited; + if (existingInteraction != null) + { + existingInteraction.Visited = true; + existingInteraction.VisitCount++; + existingInteraction.LastVisted = DateTime.Now; + } + else + { + await ctx.AddAsync(new PlayerInteractionDB { Cheered = false, Favorited = false, PlayerId = playerId, RoomId = roomId, VisitCount = 1, Visited = true }); + } + + + var roomStats = await ctx.RoomStats.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (roomStats == null) + { + roomStats = new RoomStats { RoomId = roomId }; + await ctx.AddAsync(roomStats); + } + roomStats.VisitCount++; + if (isNewVisitor) + { + roomStats.VisitorCount++; + } + + await QuestRewardDelivery.DeliverOnArrivalAsync(ctx, playerId, roomId); + } + + public static async Task CreateRoomInstance(FullRoom room, bool isPrivate, long? subRoomId, long? clubId, long? eventId) + { + return await CreateRoomInstance(room.RoomId, isPrivate, subRoomId, clubId, eventId); + } + + public static async Task CreateRoomInstance( + long roomId, bool isPrivate, long? subRoomId, long? clubId, long? eventId) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + SubRoom? subRoom = null; + FullRoom? room = await RoomUtils.GetRoom(roomId); + RoomInstance? roomInstance = null; + + if (room == null) + { + return null; + } + + + if (subRoomId == null) + { + subRoom = await ctx.SubRooms.FirstOrDefaultAsync(x => x.RoomId == room.RoomId); + } + else + { + subRoom = await ctx.SubRooms.FirstOrDefaultAsync(x => x.RoomId == room.RoomId && x.SubRoomId == subRoomId); + } + if (subRoom == null) return null; + + + RoomInstanceType instanceType = RoomInstanceType.Public; + + if (isPrivate) instanceType = RoomInstanceType.Private; + if (room.Name == "DormRoom") instanceType = RoomInstanceType.Dormroom; + if (clubId != null) instanceType = RoomInstanceType.Clubhouse; + if (eventId != null) + instanceType = await ctx.PlayerEvents.AnyAsync(ev => ev.PlayerEventId == eventId && ev.IsMultiInstance) + ? RoomInstanceType.MultiInstanceEvent : RoomInstanceType.Event; + + + + string roomName = "^" + room.Name; + if (room.IsDorm) { + Profile? roomCreator = await ctx.Profiles.AsNoTracking().FirstOrDefaultAsync(p => p.accountId == room.CreatorAccountId); + if (roomCreator != null) + { + roomName = $"@{roomCreator.username}'s Dorm"; + } + } + + roomInstance = new RoomInstance + { + subRoomId = subRoom.SubRoomId, + roomId = roomId, + clubId = clubId, + eventId = eventId, + dataBlob = subRoom.DataBlob, + EncryptVoiceChat = room.EncryptVoiceChat, + isFull = false, + isInProgress = false, + isPrivate = (room.Name == "DormRoom" && room.IsDorm) ? true : isPrivate, + maxCapacity = subRoom.MaxPlayers, + name = roomName, + photonRegion = "us", + photonRegionId = "us", + photonRoomId = CreatePhotonRoomId(), + roomCode = "", + location = subRoom.UnitySceneId, + roomInstanceType = instanceType + }; + + await ctx.RoomInstances.AddAsync(roomInstance); + await ctx.SaveChangesAsync(); + + return roomInstance; + } + + public static async Task> GetPlayersInInstance(long instanceId) + { + using var ctx = DbFactory.CreateDbContext(); + + return await ctx.Presences.Where((p) => p.roomInstance == instanceId).ToListAsync(); + } + + public static async Task GetPlayerCountInInstance(long instanceId) + { + using var ctx = DbFactory.CreateDbContext(); + + return await ctx.Presences.AsNoTracking().Where(x => x.roomInstance == instanceId).CountAsync(); + } + + public static async Task ReapEmptyPublicInstancesAsync(RecDBContext ctx, long roomId, long? subRoomId) + { + var candidates = await ctx.RoomInstances + .Where(i => + i.roomId == roomId && + !i.isPrivate && + i.roomInstanceType == RoomInstanceType.Public && + (!subRoomId.HasValue || i.subRoomId == subRoomId)) + .ToListAsync(); + + if (candidates.Count == 0) + { + return 0; + } + + var ids = candidates.Select(x => x.roomInstanceId).ToList(); + + var occupied = await ctx.Presences + .Where(p => p.isOnline && + p.roomInstance != null && + ids.Contains(p.roomInstance.Value)) + .Select(p => p.roomInstance!.Value) + .Distinct() + .ToListAsync(); + + var empty = candidates + .Where(x => !occupied.Contains(x.roomInstanceId)) + .ToList(); + + if (empty.Count == 0) + { + return 0; + } + + ctx.RoomInstances.RemoveRange(empty); + await ctx.SaveChangesAsync(); + + return empty.Count; + } + + public static async Task RemovePresence(long playerId) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + + var player = ctx.Presences.FirstOrDefault((x) => x.playerId == playerId); + if (player?.roomInstance == null) return; + + + + long roomInstanceId = player.roomInstance.Value; + player.roomInstance = null; + + var remainingPlayers = await GetPlayerCountInInstance(roomInstanceId); + + if (remainingPlayers <= 0) + { + var roomInstance = await ctx.RoomInstances.FirstOrDefaultAsync((x) => x.roomInstanceId == roomInstanceId); + if (roomInstance != null) + ctx.RoomInstances.Remove(roomInstance); + } + + await ctx.SaveChangesAsync(); + } + + public static async Task IsAtLeastRole(long playerId, long roomId, RoleType minimumRole) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var role = await RoomUtils.GetRoleForPlayer(playerId, roomId); + var isOwner = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync(r => r.CreatorAccountId == playerId && r.RoomId == roomId) != null; + + return (role?.Role >= minimumRole) || isOwner; + } + + + + + + + + + public static async Task CanModerateRoom(long playerId, long roomId) + { + if (await IsAtLeastRole(playerId, roomId, RoleType.Moderator)) return true; + + await using var ctx = await DbFactory.CreateDbContextAsync(); + var highRanks = (await ctx.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "HighRanks"))?.Value; + if (string.IsNullOrWhiteSpace(highRanks)) return false; + + foreach (var entry in highRanks.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + if (long.TryParse(entry, out var highRankId) && highRankId == playerId) + return true; + + return false; + } + + + + + + + public static async Task IsBannedFromRoom(RecDBContext ctx, long playerId, long roomId) + { + var ban = await ctx.RoomBans.AsNoTracking().FirstOrDefaultAsync(x => + x.roomId == roomId && x.bannedPlayerId == playerId); + + if (ban == null) return false; + + + if (ban.expiresAt.HasValue && ban.expiresAt.Value <= DateTime.UtcNow) + { + ctx.RoomBans.Remove(ban); + await ctx.SaveChangesAsync(); + return false; + } + + if (await IsAtLeastRole(playerId, roomId, RoleType.CoOwner)) return false; + + return true; + } + + + + + + + public static async Task KickPlayerFromRoom(long playerId, long roomId, string reason) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var presence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence?.roomInstance == null) return; + + var instance = await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance); + if (instance == null || instance.roomId != roomId) return; + + var dorm = await RoomUtils.GetOrCreateDorm(playerId); + if (dorm == null) return; + var dormInstance = await CreateRoomInstance(dorm.RoomId, true, null, null, null); + if (dormInstance == null) return; + + presence.roomInstance = dormInstance.roomInstanceId; + presence.errorCode = JoinStatus.Success; + presence.isOnline = true; + MarkInRoom(playerId); + await ctx.SaveChangesAsync(); + + try + { + await Notify.SendNotify(playerId, new Notification { Id = NotificationType.ModerationKick, Msg = reason }); + } + catch (Exception) { } + + try + { + await Notify.SendNotify(playerId, new Notification { Id = "PresenceUpdate", Msg = await Presence.CreateAsync(ctx, presence, dormInstance) }); + } + catch (Exception) { } + } + + + private static async Task NeedsOrientationAsync(RecDBContext ctx, long playerId) + { + var settings = await ctx.PlayerSettings + .AsNoTracking() + .FirstOrDefaultAsync(x => x.AccountId == playerId); + if (settings == null) return false; + + var list = settings.DeserializeSettings(); + var marker = list.FirstOrDefault(x => x.Key == "Radicade.NeedsOrientation"); + return marker != null && + (marker.Value == "1" || string.Equals(marker.Value, "true", StringComparison.OrdinalIgnoreCase)); + } + + private static async Task ClearNeedsOrientationAsync(RecDBContext ctx, long playerId) + { + var settings = await ctx.PlayerSettings + .FirstOrDefaultAsync(x => x.AccountId == playerId); + if (settings == null) return; + + var list = settings.DeserializeSettings(); + if (list.RemoveAll(x => x.Key == "Radicade.NeedsOrientation") > 0) + { + settings.Settings = list.ToJson(); + await ctx.SaveChangesAsync(); + } + } + + private static string NormalizeRoomName(string roomName) + { + if (string.IsNullOrWhiteSpace(roomName)) + { + return string.Empty; + } + + string normalized = roomName.Replace("+", " ").Trim(); + + + + + if (normalized.StartsWith("^", StringComparison.Ordinal)) + { + normalized = normalized[1..].TrimStart(); + } + + try + { + normalized = Uri.UnescapeDataString(normalized); + } + catch (UriFormatException) + { + + } + + return normalized; + } + + public static string CreatePhotonRoomId() + { + return $"Radicade{Random.Shared.Next(232)}-{Guid.NewGuid().ToString().Replace("-", "")}"; + } + } +} diff --git a/Server/Matchmaking/PlayerQuickPlayDTO.cs b/Server/Matchmaking/PlayerQuickPlayDTO.cs new file mode 100644 index 0000000..0519812 --- /dev/null +++ b/Server/Matchmaking/PlayerQuickPlayDTO.cs @@ -0,0 +1,14 @@ +namespace RadicadeServer.RecNet.Matchmaking +{ + public class PlayerQuickPlayDTO + { + public int? TargetPlayerId { get; private set; } + public string RoomName { get; private set; } + + public PlayerQuickPlayDTO(int? targetPlayerId, string roomName) + { + TargetPlayerId = targetPlayerId; + RoomName = roomName; + } + } +} diff --git a/Server/Matchmaking/QuickplayAPI.cs b/Server/Matchmaking/QuickplayAPI.cs new file mode 100644 index 0000000..dc86e15 --- /dev/null +++ b/Server/Matchmaking/QuickplayAPI.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Matchmaking; +using static System.Runtime.InteropServices.JavaScript.JSType; +using RadicadeServer.RecNet.Rooms; +using Microsoft.AspNetCore.Authorization; + +namespace RadicadeServer.API.Matchmaking +{ + [Route("api/quickPlay")] + public class QuickplayAPI : Controller + { + RecDBContext db; + public QuickplayAPI(RecDBContext context) + { + this.db = context; + } + + [HttpGet("v1/getandclear"), Authorize] + public async Task GetAndClearQuickPlay() + { + var userId = User.GetUserId(); + + var presence = await db.Presences.FirstOrDefaultAsync((x) => x.playerId == userId); + var presenceRoomInstance = presence?.roomInstance; + RoomInstance? roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presenceRoomInstance); + + string roomTarget = "DormRoom"; + if (roomInstance != null) + roomTarget = roomInstance.name; + + return new PlayerQuickPlayDTO(null, roomTarget); + } + } +} diff --git a/Server/Matchmaking/RoomInstanceAPI.cs b/Server/Matchmaking/RoomInstanceAPI.cs new file mode 100644 index 0000000..dc9f199 --- /dev/null +++ b/Server/Matchmaking/RoomInstanceAPI.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.Matchmaking +{ + [Route("matchmaking/roominstance/")] + public class RoomInstanceAPI : Controller + { + RecDBContext db; + public RoomInstanceAPI(RecDBContext context) + { + this.db = context; + } + + [HttpPut("{id}/markprivate")] + public async Task MarkPrivate(long id, bool isPrivate) + { + var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == id); + if (roomInstance == null) return Ok(new { success = false, error = "Room instance not found" }); + + roomInstance.isPrivate = isPrivate; + + await db.SaveChangesAsync(); + + return Ok(new { success = true, error = "" }); + } + + [HttpPut("{id}/inprogress")] + public async Task MarkInProgress(long id, bool inProgress) + { + var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == id); + if (roomInstance == null) return Ok(new { success = false, error = "Room instance not found" }); + + roomInstance.isInProgress = inProgress; + + await db.SaveChangesAsync(); + + return Ok(new { success = true, error = "" }); + } + + [HttpGet("{id}/roomCode")] + public IActionResult SetRoomCode([FromForm] string roomCode, [FromQuery] long id) + { + + return Ok(); + } + } +} diff --git a/Server/Messages/ChatMessage.cs b/Server/Messages/ChatMessage.cs new file mode 100644 index 0000000..6edbcc2 --- /dev/null +++ b/Server/Messages/ChatMessage.cs @@ -0,0 +1,11 @@ +namespace RadicadeServer.RecNet.Messages +{ + public class ChatMessage + { + public long chatMessageId { get; set; } + public long chatThreadeId { get; set; } + public long senderPlayerId { get; set; } + public DateTime timeSent { get; set; } + public string contents { get; set; } = null!; + } +} diff --git a/Server/Messages/ChatThread.cs b/Server/Messages/ChatThread.cs new file mode 100644 index 0000000..cc4e071 --- /dev/null +++ b/Server/Messages/ChatThread.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using RadicadeServer.Extensions; + +namespace RadicadeServer.RecNet.Messages +{ + public class ChatThread + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long chatThreadId { get; set; } + public string chatThreadName { get; set; } = null!; + public long lastReadMessageId { get; set; } + public string messages { get; set; } = null!; + public string playerIds { get; set; } = null!; + + public string? favoritedBy { get; set; } + + public List GetFavoritedBy() => + string.IsNullOrWhiteSpace(favoritedBy) ? new List() : favoritedBy.FromJson>() ?? new List(); + + public void SetFavoritedBy(IEnumerable playerIds) + { + favoritedBy = playerIds.Distinct().OrderBy(x => x).ToList().ToJson(); + } + + public List? GetPlayerIds() + { + return playerIds.FromJson>(); + } + public List? GetMessages() + { + if (string.IsNullOrWhiteSpace(messages)) return new List(); + if (messages.TrimStart().StartsWith("{", StringComparison.Ordinal)) + { + var message = messages.FromJson(); + return message == null ? new List() : new List { message }; + } + return messages.FromJson>(); + } + public string AddMessage(ChatMessage message) + { + var messages = GetMessages() ?? new List(); + messages.Add(message); + return messages.ToJson(); + } + public string AddPlayer(long player) + { + var players = GetPlayerIds() ?? new List(); + players.Add(player); + return players.ToJson(); ; + } + } +} diff --git a/Server/Messages/DeleteMessageRequestDTO.cs b/Server/Messages/DeleteMessageRequestDTO.cs new file mode 100644 index 0000000..1c7b99f --- /dev/null +++ b/Server/Messages/DeleteMessageRequestDTO.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Messages +{ + public class DeleteMessageRequestDTO + { + public List MessageIds { get; set; } = null!; + } +} diff --git a/Server/Messages/Message.cs b/Server/Messages/Message.cs new file mode 100644 index 0000000..a254e85 --- /dev/null +++ b/Server/Messages/Message.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums.Messages; + +namespace RadicadeServer.RecNet.Messages +{ + public class Message + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long FromPlayerId { get; set; } + + [JsonIgnore] + public long ToPlayerId { get; set; } + + [JsonIgnore] + public bool IsDismissed { get; set; } + + public DateTime SentTime { get; set; } + public MessageType Type { get; set; } + + public string Data { get; set; } = string.Empty; + public string Details { get; set; } = string.Empty; + + public long? RoomId { get; set; } + public long? PlayerEventId { get; set; } + } +} diff --git a/Server/Messages/MessagesAPI.cs b/Server/Messages/MessagesAPI.cs new file mode 100644 index 0000000..bcf253e --- /dev/null +++ b/Server/Messages/MessagesAPI.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Messages +{ + [Route("api/messages/")] + public class MessagesAPI : Controller + { + RecDBContext db; + public MessagesAPI(RecDBContext context) + { + this.db = context; + } + + [HttpGet("v1/favoriteFriendOnlineStatus"), Authorize] + public async Task NotifyFavoriteFriends() + { + var userId = User.GetUserId(); + + var favoritedUsers = await db.Relationships.Where(x => x.AccountId == userId && x.Favorited != RecNet.Relationships.ReciprocalStatus.None).ToListAsync(); + + foreach (var favorited in favoritedUsers) + { + var presence = await db.Presences.FirstOrDefaultAsync(x => x.isOnline && x.playerId == favorited.PlayerId); + if (presence != null) + await Notify.SendMessage(favorited.AccountId, new Message { FromPlayerId = (long)userId!, Type = MessageType.FriendStatusOnline }); + } + + return new Success(true, null, null); + } + + [HttpGet("v2/get"), Authorize] + public async Task?> GetMessages() + { + var userId = User.GetUserId(); + + return await db.Messages.Where(x => x.ToPlayerId == userId && !x.IsDismissed).ToListAsync(); + } + + [HttpPost("v3/delete"), Authorize] + public async Task DeleteMessage([FromBody] DeleteMessageRequestDTO req) + { + var userId = User.GetUserId(); + if (req.MessageIds == null || req.MessageIds.Count == 0) return new Success(false, "Invalid Ids", null); + + int deleted = 0; + + foreach (var id in req.MessageIds) + { + var msg = await db.Messages.FirstOrDefaultAsync(x => x.Id == id && x.ToPlayerId == userId); + if (msg == null) { continue; } + if (msg.Type == MessageType.GameInviteV2 || msg.Type == MessageType.GameInvite) + msg.IsDismissed = true; + else + db.Messages.Remove(msg); + deleted++; + } + + await db.SaveChangesAsync(); + + return new Success(true, null, null); + } + + [HttpPost("v2/send"), Authorize] + public async Task SendMessage([FromForm] long ToPlayerId, [FromForm] MessageType Type, [FromForm] string? Data, [FromForm] long? RoomId) + { + var userId = User.GetUserId(); + await Notify.SendMessage(ToPlayerId, new Message { FromPlayerId = (long)userId!, Type = Type, Data = Data ?? "", RoomId = RoomId }); + return new Success(true, null, null); + } + + [HttpPost("v1/sendMultiple"), Authorize] + public async Task SendMultipleMessages([FromBody] SendMultipleMessagesRequestDTO req) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Not signed in", null); + + var recipients = (req.ToPlayerIds ?? new List()) + .Where(id => id > 0) + .Distinct() + .ToList(); + if (recipients.Count == 0) return new Success(false, "ToPlayerIds is required", null); + + foreach (var toPlayerId in recipients) + await Notify.SendMessage(toPlayerId, new Message + { + FromPlayerId = userId.Value, + Type = req.Type, + Data = req.Data ?? "" + }); + + return new Success(true, null, null); + } + + [HttpPost("v1/friendOnlineStatus"), Authorize] + public async Task FriendOnlineStatus() + { + var userId = User.GetUserId(); + if (userId == null) + return new Success(false, "Not signed in", null); + + var friendIds = await db.Relationships.AsNoTracking() + .Where(x => x.AccountId == userId && x.RelationshipType == RecNet.Relationships.RelationshipType.Friend) + .Select(x => x.PlayerId) + .ToListAsync(); + + var onlineCount = friendIds.Count == 0 + ? 0 + : await db.Presences.AsNoTracking() + .CountAsync(p => friendIds.Contains(p.playerId) && p.isOnline); + + return new Success(true, null, new { FriendsOnlineCount = onlineCount }); + } + } +} diff --git a/Server/Messages/RoomInviteDetails.cs b/Server/Messages/RoomInviteDetails.cs new file mode 100644 index 0000000..16d39d2 --- /dev/null +++ b/Server/Messages/RoomInviteDetails.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using RadicadeServer.RecNet.Enums.Messages; + +namespace RadicadeServer.RecNet.Messages; + +public sealed class RoomInviteDetails +{ + public long InviteId { get; set; } + public string Name { get; set; } = string.Empty; + public int InviteMode { get; set; } + public long RoomInstanceId { get; set; } + + public static long? GetInstanceId(Message message) + { + if (message.Type == MessageType.GameInvite) + return long.TryParse(message.Data, out var id) && id > 0 ? id : null; + if (message.Type != MessageType.GameInviteV2) return null; + try + { + var details = JsonSerializer.Deserialize(message.Data); + return details?.RoomInstanceId > 0 ? details.RoomInstanceId : null; + } + catch (JsonException) { return null; } + } +} diff --git a/Server/Messages/SendMultipleMessagesRequestDTO.cs b/Server/Messages/SendMultipleMessagesRequestDTO.cs new file mode 100644 index 0000000..832522f --- /dev/null +++ b/Server/Messages/SendMultipleMessagesRequestDTO.cs @@ -0,0 +1,11 @@ +using RadicadeServer.RecNet.Enums.Messages; + +namespace RadicadeServer.RecNet.Messages +{ + public class SendMultipleMessagesRequestDTO + { + public List? ToPlayerIds { get; set; } + public MessageType Type { get; set; } + public string? Data { get; set; } + } +} diff --git a/Server/Middleware/FailureEnvelopeMiddleware.cs b/Server/Middleware/FailureEnvelopeMiddleware.cs new file mode 100644 index 0000000..0dc834a --- /dev/null +++ b/Server/Middleware/FailureEnvelopeMiddleware.cs @@ -0,0 +1,49 @@ +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.Middleware +{ + public class FailureEnvelopeMiddleware + { + private readonly RequestDelegate _next; + + public FailureEnvelopeMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + await _next(context); + + if (context.Response.HasStarted) return; + + var status = context.Response.StatusCode; + if (status < 400 || status > 599) return; + + if (!string.IsNullOrEmpty(context.Response.ContentType) || context.Response.ContentLength > 0) return; + + await context.Response.WriteAsJsonAsync(new Failure(Why(status))); + } + + public static string Why(int statusCode) => statusCode switch + { + 400 => "bad request", + 401 => "unauthorized", + 403 => "forbidden", + 404 => "not found", + 405 => "method not allowed", + 408 => "request timeout", + 409 => "conflict", + 413 => "request too large", + 415 => "unsupported media type", + 422 => "unprocessable entity", + 429 => "too many requests", + 500 => "internal server error", + 501 => "not implemented", + 502 => "bad gateway", + 503 => "service unavailable", + 504 => "gateway timeout", + _ => "request failed", + }; + } +} diff --git a/Server/Migrations/20260507123737_Inital.Designer.cs b/Server/Migrations/20260507123737_Inital.Designer.cs new file mode 100644 index 0000000..a7d7ce2 --- /dev/null +++ b/Server/Migrations/20260507123737_Inital.Designer.cs @@ -0,0 +1,1084 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260507123737_Inital")] + partial class Inital + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("int"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leveling.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("ObjectiveProgress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260507123737_Inital.cs b/Server/Migrations/20260507123737_Inital.cs new file mode 100644 index 0000000..6785ca9 --- /dev/null +++ b/Server/Migrations/20260507123737_Inital.cs @@ -0,0 +1,680 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class Inital : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Avatars", + columns: table => new + { + AccountId = table.Column(type: "bigint", nullable: false), + Avatar = table.Column(type: "nvarchar(max)", nullable: false), + Outfits = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Avatars", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "CachedLogins", + columns: table => new + { + loginId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + accountId = table.Column(type: "bigint", nullable: false), + platform = table.Column(type: "int", nullable: false), + platformId = table.Column(type: "nvarchar(max)", nullable: false), + lastLoginTime = table.Column(type: "datetime2", nullable: false), + requirePassword = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CachedLogins", x => x.loginId); + }); + + migrationBuilder.CreateTable( + name: "Chats", + columns: table => new + { + chatThreadId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + chatThreadName = table.Column(type: "nvarchar(max)", nullable: false), + lastReadMessageId = table.Column(type: "bigint", nullable: false), + messages = table.Column(type: "nvarchar(max)", nullable: false), + playerIds = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Chats", x => x.chatThreadId); + }); + + migrationBuilder.CreateTable( + name: "Config", + columns: table => new + { + FUCKYOUMICROSOFT = table.Column(type: "nvarchar(450)", nullable: false), + ShareBaseUrl = table.Column(type: "nvarchar(max)", nullable: false), + CdnBaseUri = table.Column(type: "nvarchar(max)", nullable: false), + LevelProgressionMaps = table.Column(type: "nvarchar(max)", nullable: false), + ServerMaintenance = table.Column(type: "nvarchar(max)", nullable: false), + DailyObjectives = table.Column(type: "nvarchar(max)", nullable: false), + AutoMicMutingConfig = table.Column(type: "nvarchar(max)", nullable: false), + StorefrontConfig = table.Column(type: "nvarchar(max)", nullable: false), + RoomKeyConfig = table.Column(type: "nvarchar(max)", nullable: false), + RoomCurrencyConfig = table.Column(type: "nvarchar(max)", nullable: false), + GiftDropStoreItems = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Config", x => x.FUCKYOUMICROSOFT); + }); + + migrationBuilder.CreateTable( + name: "Currency", + columns: table => new + { + AccountId = table.Column(type: "bigint", nullable: false), + RecTokens = table.Column(type: "bigint", nullable: false), + LaserTagTickets = table.Column(type: "bigint", nullable: false), + Gold = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Currency", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "CustomContent", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + Files = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CustomContent", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "GameConfig", + columns: table => new + { + Key = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GameConfig", x => x.Key); + }); + + migrationBuilder.CreateTable( + name: "Gifts", + columns: table => new + { + AccountId = table.Column(type: "bigint", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Gifts", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "ImageInteractions", + columns: table => new + { + InteractionId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + SavedImageId = table.Column(type: "bigint", nullable: false), + IsCheered = table.Column(type: "bit", nullable: false), + PlayerId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ImageInteractions", x => x.InteractionId); + }); + + migrationBuilder.CreateTable( + name: "Images", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OwnerId = table.Column(type: "bigint", nullable: false), + ImageType = table.Column(type: "int", nullable: false), + Accessibility = table.Column(type: "int", nullable: false), + RoomId = table.Column(type: "bigint", nullable: true), + EventId = table.Column(type: "bigint", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Images", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Inventions", + columns: table => new + { + InventionId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ReplicationId = table.Column(type: "nvarchar(max)", nullable: false), + CreatorPlayerId = table.Column(type: "int", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + ImageName = table.Column(type: "nvarchar(max)", nullable: false), + CurrentVersionNumber = table.Column(type: "int", nullable: false), + ModifiedAt = table.Column(type: "datetime2", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + NumPlayersHaveUsedInRoom = table.Column(type: "int", nullable: false), + NumDownloads = table.Column(type: "int", nullable: false), + CheerCount = table.Column(type: "int", nullable: false), + CreatorPermission = table.Column(type: "int", nullable: false), + GeneralPermission = table.Column(type: "int", nullable: false), + IsAGInvention = table.Column(type: "bit", nullable: false), + IsCertifiedInvention = table.Column(type: "bit", nullable: false), + IsPublished = table.Column(type: "bit", nullable: false), + AllowTrial = table.Column(type: "bit", nullable: false), + HideFromPlayer = table.Column(type: "bit", nullable: false), + Price = table.Column(type: "int", nullable: true), + BlobName = table.Column(type: "nvarchar(max)", nullable: false), + InstantiationCost = table.Column(type: "int", nullable: false), + LightsCost = table.Column(type: "int", nullable: false), + RoomOrigin = table.Column(type: "bigint", nullable: false), + Tags = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Inventions", x => x.InventionId); + }); + + migrationBuilder.CreateTable( + name: "Messages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + FromPlayerId = table.Column(type: "bigint", nullable: false), + ToPlayerId = table.Column(type: "bigint", nullable: false), + SentTime = table.Column(type: "datetime2", nullable: false), + Type = table.Column(type: "int", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + Details = table.Column(type: "nvarchar(max)", nullable: false), + RoomId = table.Column(type: "bigint", nullable: true), + PlayerEventId = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Messages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Moderation", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ExpireDuration = table.Column(type: "datetime2", nullable: false), + Message = table.Column(type: "nvarchar(max)", nullable: false), + Category = table.Column(type: "int", nullable: false), + IsBan = table.Column(type: "bit", nullable: false), + PlayerId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Moderation", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Players", + columns: table => new + { + AccountId = table.Column(type: "bigint", nullable: false), + Email = table.Column(type: "nvarchar(max)", nullable: false), + Phone = table.Column(type: "nvarchar(max)", nullable: false), + AppVersion = table.Column(type: "nvarchar(max)", nullable: false), + Parent = table.Column(type: "bigint", nullable: true), + NameChanges = table.Column(type: "int", nullable: false), + JuniorState = table.Column(type: "int", nullable: false), + Hashword = table.Column(type: "nvarchar(max)", nullable: false), + Birthday = table.Column(type: "datetime2", nullable: true), + Visibility = table.Column(type: "int", nullable: false), + DeviceClass = table.Column(type: "int", nullable: false), + Developer = table.Column(type: "bit", nullable: false), + DeviceId = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Players", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "PlayerSettings", + columns: table => new + { + AccountId = table.Column(type: "bigint", nullable: false), + Settings = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayerSettings", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "Playlists", + columns: table => new + { + PlaylistId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Keywords = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Playlists", x => x.PlaylistId); + }); + + migrationBuilder.CreateTable( + name: "Presences", + columns: table => new + { + playerId = table.Column(type: "bigint", nullable: false), + isOnline = table.Column(type: "bit", nullable: false), + deviceClass = table.Column(type: "int", nullable: false), + statusVisibility = table.Column(type: "int", nullable: false), + vRMovementMode = table.Column(type: "int", nullable: false), + LoginLock = table.Column(type: "nvarchar(max)", nullable: true), + roomInstance = table.Column(type: "bigint", nullable: true), + errorCode = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Presences", x => x.playerId); + }); + + migrationBuilder.CreateTable( + name: "Profiles", + columns: table => new + { + accountId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + profileImage = table.Column(type: "nvarchar(max)", nullable: false), + isJunior = table.Column(type: "bit", nullable: false), + platforms = table.Column(type: "int", nullable: false), + username = table.Column(type: "nvarchar(max)", nullable: false), + displayName = table.Column(type: "nvarchar(max)", nullable: false), + bio = table.Column(type: "nvarchar(max)", nullable: true), + createdAt = table.Column(type: "datetime2", nullable: false), + developer = table.Column(type: "bit", nullable: false), + moderator = table.Column(type: "bit", nullable: false), + screenshare = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Profiles", x => x.accountId); + }); + + migrationBuilder.CreateTable( + name: "Progression", + columns: table => new + { + PlayerId = table.Column(type: "bigint", nullable: false), + XP = table.Column(type: "int", nullable: false), + Level = table.Column(type: "int", nullable: false), + ObjectiveProgress = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Progression", x => x.PlayerId); + }); + + migrationBuilder.CreateTable( + name: "Relationships", + columns: table => new + { + RelationshipId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AccountId = table.Column(type: "bigint", nullable: false), + PlayerId = table.Column(type: "bigint", nullable: false), + RelationshipType = table.Column(type: "int", nullable: false), + Muted = table.Column(type: "int", nullable: false), + Ignored = table.Column(type: "int", nullable: false), + Favorited = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Relationships", x => x.RelationshipId); + }); + + migrationBuilder.CreateTable( + name: "Reputations", + columns: table => new + { + AccountId = table.Column(type: "bigint", nullable: false), + Noteriety = table.Column(type: "float", nullable: false), + CheerGeneral = table.Column(type: "int", nullable: false), + CheerHelpful = table.Column(type: "int", nullable: false), + CheerCreative = table.Column(type: "int", nullable: false), + CheerGreatHost = table.Column(type: "int", nullable: false), + CheerSportsman = table.Column(type: "int", nullable: false), + CheerCredit = table.Column(type: "int", nullable: false), + SelectedCheer = table.Column(type: "int", nullable: false), + IsCheerful = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Reputations", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "RoomInstances", + columns: table => new + { + roomInstanceId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + roomId = table.Column(type: "bigint", nullable: false), + subRoomId = table.Column(type: "bigint", nullable: false), + roomInstanceType = table.Column(type: "int", nullable: false), + location = table.Column(type: "nvarchar(max)", nullable: true), + dataBlob = table.Column(type: "nvarchar(max)", nullable: false), + eventId = table.Column(type: "bigint", nullable: true), + clubId = table.Column(type: "bigint", nullable: true), + roomCode = table.Column(type: "nvarchar(max)", nullable: true), + photonRegionId = table.Column(type: "nvarchar(max)", nullable: false), + photonRoomId = table.Column(type: "nvarchar(max)", nullable: false), + name = table.Column(type: "nvarchar(max)", nullable: false), + maxCapacity = table.Column(type: "int", nullable: false), + isFull = table.Column(type: "bit", nullable: false), + isPrivate = table.Column(type: "bit", nullable: false), + isInProgress = table.Column(type: "bit", nullable: false), + EncryptVoiceChat = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomInstances", x => x.roomInstanceId); + }); + + migrationBuilder.CreateTable( + name: "RoomInteractions", + columns: table => new + { + InteractionId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PlayerId = table.Column(type: "bigint", nullable: false), + RoomId = table.Column(type: "bigint", nullable: false), + Cheered = table.Column(type: "bit", nullable: false), + Favorited = table.Column(type: "bit", nullable: false), + VisitCount = table.Column(type: "int", nullable: false), + Visited = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomInteractions", x => x.InteractionId); + }); + + migrationBuilder.CreateTable( + name: "RoomKeys", + columns: table => new + { + RoomKeyId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ReplicationId = table.Column(type: "nvarchar(max)", nullable: false), + RoomId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + Price = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomKeys", x => x.RoomKeyId); + }); + + migrationBuilder.CreateTable( + name: "RoomRoles", + columns: table => new + { + RoleId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoomId = table.Column(type: "bigint", nullable: false), + AccountId = table.Column(type: "bigint", nullable: false), + Role = table.Column(type: "int", nullable: false), + InvitedRole = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomRoles", x => x.RoleId); + }); + + migrationBuilder.CreateTable( + name: "Rooms", + columns: table => new + { + RoomId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Tags = table.Column(type: "nvarchar(max)", nullable: false), + IsDorm = table.Column(type: "bit", nullable: false), + MaxPlayers = table.Column(type: "int", nullable: false), + MaxPlayerCalculationMode = table.Column(type: "int", nullable: false), + CloningAllowed = table.Column(type: "bit", nullable: false), + DisableMicAutoMute = table.Column(type: "bit", nullable: false), + DisableRoomComments = table.Column(type: "bit", nullable: false), + EncryptVoiceChat = table.Column(type: "bit", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: true), + ImageName = table.Column(type: "nvarchar(max)", nullable: false), + WarningMask = table.Column(type: "int", nullable: false), + CustomWarning = table.Column(type: "nvarchar(max)", nullable: true), + CreatorAccountId = table.Column(type: "bigint", nullable: false), + State = table.Column(type: "int", nullable: false), + Accessibility = table.Column(type: "int", nullable: false), + SupportsLevelVoting = table.Column(type: "bit", nullable: false), + IsRRO = table.Column(type: "bit", nullable: false), + SupportsScreens = table.Column(type: "bit", nullable: false), + SupportsWalkVR = table.Column(type: "bit", nullable: false), + SupportsTeleportVR = table.Column(type: "bit", nullable: false), + SupportsVRLow = table.Column(type: "bit", nullable: false), + SupportsQuest2 = table.Column(type: "bit", nullable: false), + SupportsMobile = table.Column(type: "bit", nullable: false), + SupportsJuniors = table.Column(type: "bit", nullable: false), + MinLevel = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + loadScreen = table.Column(type: "nvarchar(max)", nullable: false), + Version = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Rooms", x => x.RoomId); + }); + + migrationBuilder.CreateTable( + name: "RoomStats", + columns: table => new + { + RoomId = table.Column(type: "bigint", nullable: false), + CheerCount = table.Column(type: "int", nullable: false), + FavoriteCount = table.Column(type: "int", nullable: false), + VisitorCount = table.Column(type: "int", nullable: false), + VisitCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomStats", x => x.RoomId); + }); + + migrationBuilder.CreateTable( + name: "ServerConfig", + columns: table => new + { + Key = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServerConfig", x => x.Key); + }); + + migrationBuilder.CreateTable( + name: "SubRoomHistory", + columns: table => new + { + HistoryId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + SubRoomId = table.Column(type: "bigint", nullable: false), + RoomId = table.Column(type: "bigint", nullable: false), + SavedByAccountId = table.Column(type: "bigint", nullable: false), + DataBlob = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SubRoomHistory", x => x.HistoryId); + }); + + migrationBuilder.CreateTable( + name: "SubRooms", + columns: table => new + { + SubRoomId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoomId = table.Column(type: "bigint", nullable: false), + SavedByAccountId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + DataBlob = table.Column(type: "nvarchar(max)", nullable: false), + IsSandbox = table.Column(type: "bit", nullable: false), + MaxPlayers = table.Column(type: "int", nullable: false), + Accessibility = table.Column(type: "int", nullable: false), + UnitySceneId = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SubRooms", x => x.SubRoomId); + }); + + migrationBuilder.CreateTable( + name: "Unlocks", + columns: table => new + { + AccountId = table.Column(type: "bigint", nullable: false), + AvatarItems = table.Column(type: "nvarchar(max)", nullable: false), + Consumables = table.Column(type: "nvarchar(max)", nullable: false), + Equipment = table.Column(type: "nvarchar(max)", nullable: false), + Inventions = table.Column(type: "nvarchar(max)", nullable: false), + RoomKeys = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Unlocks", x => x.AccountId); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Avatars"); + + migrationBuilder.DropTable( + name: "CachedLogins"); + + migrationBuilder.DropTable( + name: "Chats"); + + migrationBuilder.DropTable( + name: "Config"); + + migrationBuilder.DropTable( + name: "Currency"); + + migrationBuilder.DropTable( + name: "CustomContent"); + + migrationBuilder.DropTable( + name: "GameConfig"); + + migrationBuilder.DropTable( + name: "Gifts"); + + migrationBuilder.DropTable( + name: "ImageInteractions"); + + migrationBuilder.DropTable( + name: "Images"); + + migrationBuilder.DropTable( + name: "Inventions"); + + migrationBuilder.DropTable( + name: "Messages"); + + migrationBuilder.DropTable( + name: "Moderation"); + + migrationBuilder.DropTable( + name: "Players"); + + migrationBuilder.DropTable( + name: "PlayerSettings"); + + migrationBuilder.DropTable( + name: "Playlists"); + + migrationBuilder.DropTable( + name: "Presences"); + + migrationBuilder.DropTable( + name: "Profiles"); + + migrationBuilder.DropTable( + name: "Progression"); + + migrationBuilder.DropTable( + name: "Relationships"); + + migrationBuilder.DropTable( + name: "Reputations"); + + migrationBuilder.DropTable( + name: "RoomInstances"); + + migrationBuilder.DropTable( + name: "RoomInteractions"); + + migrationBuilder.DropTable( + name: "RoomKeys"); + + migrationBuilder.DropTable( + name: "RoomRoles"); + + migrationBuilder.DropTable( + name: "Rooms"); + + migrationBuilder.DropTable( + name: "RoomStats"); + + migrationBuilder.DropTable( + name: "ServerConfig"); + + migrationBuilder.DropTable( + name: "SubRoomHistory"); + + migrationBuilder.DropTable( + name: "SubRooms"); + + migrationBuilder.DropTable( + name: "Unlocks"); + } + } +} diff --git a/Server/Migrations/20260508120414_InventionsV1.Designer.cs b/Server/Migrations/20260508120414_InventionsV1.Designer.cs new file mode 100644 index 0000000..2b46a1a --- /dev/null +++ b/Server/Migrations/20260508120414_InventionsV1.Designer.cs @@ -0,0 +1,1084 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260508120414_InventionsV1")] + partial class InventionsV1 + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("int"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leveling.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("ObjectiveProgress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260508120414_InventionsV1.cs b/Server/Migrations/20260508120414_InventionsV1.cs new file mode 100644 index 0000000..dda50f3 --- /dev/null +++ b/Server/Migrations/20260508120414_InventionsV1.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class InventionsV1 : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Server/Migrations/20260508154418_InventionsV2.Designer.cs b/Server/Migrations/20260508154418_InventionsV2.Designer.cs new file mode 100644 index 0000000..f17f66b --- /dev/null +++ b/Server/Migrations/20260508154418_InventionsV2.Designer.cs @@ -0,0 +1,1154 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260508154418_InventionsV2")] + partial class InventionsV2 + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leveling.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("ObjectiveProgress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260508154418_InventionsV2.cs b/Server/Migrations/20260508154418_InventionsV2.cs new file mode 100644 index 0000000..90d6317 --- /dev/null +++ b/Server/Migrations/20260508154418_InventionsV2.cs @@ -0,0 +1,108 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class InventionsV2 : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatorPlayerId", + table: "Inventions", + type: "bigint", + nullable: false, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AddColumn( + name: "AICost", + table: "Inventions", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ChipsCost", + table: "Inventions", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "CloudVariablesCost", + table: "Inventions", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "InventionPersonalDetails", + columns: table => new + { + DetailId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PlayerId = table.Column(type: "bigint", nullable: false), + InventionId = table.Column(type: "bigint", nullable: false), + IsCheering = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InventionPersonalDetails", x => x.DetailId); + }); + + migrationBuilder.CreateTable( + name: "InventionVersions", + columns: table => new + { + VersionId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + InventionId = table.Column(type: "bigint", nullable: false), + ReplicationId = table.Column(type: "nvarchar(max)", nullable: false), + VersionNumber = table.Column(type: "int", nullable: false), + InstantiationCost = table.Column(type: "int", nullable: false), + CloudVariablesCost = table.Column(type: "int", nullable: false), + ChipsCost = table.Column(type: "int", nullable: false), + LightsCost = table.Column(type: "int", nullable: false), + BlobName = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InventionVersions", x => x.VersionId); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InventionPersonalDetails"); + + migrationBuilder.DropTable( + name: "InventionVersions"); + + migrationBuilder.DropColumn( + name: "AICost", + table: "Inventions"); + + migrationBuilder.DropColumn( + name: "ChipsCost", + table: "Inventions"); + + migrationBuilder.DropColumn( + name: "CloudVariablesCost", + table: "Inventions"); + + migrationBuilder.AlterColumn( + name: "CreatorPlayerId", + table: "Inventions", + type: "int", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + } + } +} diff --git a/Server/Migrations/20260508165437_InventionsV3.Designer.cs b/Server/Migrations/20260508165437_InventionsV3.Designer.cs new file mode 100644 index 0000000..4976842 --- /dev/null +++ b/Server/Migrations/20260508165437_InventionsV3.Designer.cs @@ -0,0 +1,1154 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260508165437_InventionsV3")] + partial class InventionsV3 + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leveling.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("ObjectiveProgress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260508165437_InventionsV3.cs b/Server/Migrations/20260508165437_InventionsV3.cs new file mode 100644 index 0000000..8c5ec3f --- /dev/null +++ b/Server/Migrations/20260508165437_InventionsV3.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class InventionsV3 : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Server/Migrations/20260517170236_ProgressionRework1.Designer.cs b/Server/Migrations/20260517170236_ProgressionRework1.Designer.cs new file mode 100644 index 0000000..c396268 --- /dev/null +++ b/Server/Migrations/20260517170236_ProgressionRework1.Designer.cs @@ -0,0 +1,1154 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260517170236_ProgressionRework1")] + partial class ProgressionRework1 + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260517170236_ProgressionRework1.cs b/Server/Migrations/20260517170236_ProgressionRework1.cs new file mode 100644 index 0000000..da38489 --- /dev/null +++ b/Server/Migrations/20260517170236_ProgressionRework1.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class ProgressionRework1 : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "ObjectiveProgress", + table: "Progression", + newName: "Progress"); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + + + + + } + } +} diff --git a/Server/Migrations/20260521114914_GiftsTest.Designer.cs b/Server/Migrations/20260521114914_GiftsTest.Designer.cs new file mode 100644 index 0000000..fe1d4b1 --- /dev/null +++ b/Server/Migrations/20260521114914_GiftsTest.Designer.cs @@ -0,0 +1,1228 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260521114914_GiftsTest")] + partial class GiftsTest + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260521114914_GiftsTest.cs b/Server/Migrations/20260521114914_GiftsTest.cs new file mode 100644 index 0000000..67ab473 --- /dev/null +++ b/Server/Migrations/20260521114914_GiftsTest.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class GiftsTest : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GiftItems", + columns: table => new + { + GiftDropId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ItemSetId = table.Column(type: "bigint", nullable: false), + ItemSetFriendlyName = table.Column(type: "nvarchar(max)", nullable: false), + FriendlyName = table.Column(type: "nvarchar(max)", nullable: false), + Tooltip = table.Column(type: "nvarchar(max)", nullable: false), + IsQuery = table.Column(type: "bit", nullable: false), + Unique = table.Column(type: "bit", nullable: false), + SubscribersOnly = table.Column(type: "bit", nullable: false), + ConsumableItemDesc = table.Column(type: "nvarchar(max)", nullable: false), + AvatarItemDesc = table.Column(type: "nvarchar(max)", nullable: false), + AvatarItemType = table.Column(type: "int", nullable: false), + EquipmentPrefabName = table.Column(type: "nvarchar(max)", nullable: false), + EquipmentModificationGuid = table.Column(type: "nvarchar(max)", nullable: false), + CurrencyType = table.Column(type: "int", nullable: false), + Currency = table.Column(type: "int", nullable: false), + Context = table.Column(type: "int", nullable: false), + Rarity = table.Column(type: "int", nullable: false), + IsFeatured = table.Column(type: "bit", nullable: false), + Accessibility = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GiftItems", x => x.GiftDropId); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GiftItems"); + } + } +} diff --git a/Server/Migrations/20260521134521_RoomInteractionSorting.Designer.cs b/Server/Migrations/20260521134521_RoomInteractionSorting.Designer.cs new file mode 100644 index 0000000..fe7563b --- /dev/null +++ b/Server/Migrations/20260521134521_RoomInteractionSorting.Designer.cs @@ -0,0 +1,1228 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260521134521_RoomInteractionSorting")] + partial class RoomInteractionSorting + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260521134521_RoomInteractionSorting.cs b/Server/Migrations/20260521134521_RoomInteractionSorting.cs new file mode 100644 index 0000000..7bcf17e --- /dev/null +++ b/Server/Migrations/20260521134521_RoomInteractionSorting.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class RoomInteractionSorting : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Server/Migrations/20260521135255_ifuckedupbigtime.Designer.cs b/Server/Migrations/20260521135255_ifuckedupbigtime.Designer.cs new file mode 100644 index 0000000..32780af --- /dev/null +++ b/Server/Migrations/20260521135255_ifuckedupbigtime.Designer.cs @@ -0,0 +1,1228 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260521135255_ifuckedupbigtime")] + partial class Ifuckedupbigtime + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260521135255_ifuckedupbigtime.cs b/Server/Migrations/20260521135255_ifuckedupbigtime.cs new file mode 100644 index 0000000..b23f033 --- /dev/null +++ b/Server/Migrations/20260521135255_ifuckedupbigtime.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class Ifuckedupbigtime : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Server/Migrations/20260521141438_fml1.Designer.cs b/Server/Migrations/20260521141438_fml1.Designer.cs new file mode 100644 index 0000000..f735b1e --- /dev/null +++ b/Server/Migrations/20260521141438_fml1.Designer.cs @@ -0,0 +1,1237 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260521141438_fml1")] + partial class fml1 + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260521141438_fml1.cs b/Server/Migrations/20260521141438_fml1.cs new file mode 100644 index 0000000..e9c9cc2 --- /dev/null +++ b/Server/Migrations/20260521141438_fml1.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class fml1 : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CheeredAt", + table: "RoomInteractions", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "FavoritedAt", + table: "RoomInteractions", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastVisted", + table: "RoomInteractions", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CheeredAt", + table: "RoomInteractions"); + + migrationBuilder.DropColumn( + name: "FavoritedAt", + table: "RoomInteractions"); + + migrationBuilder.DropColumn( + name: "LastVisted", + table: "RoomInteractions"); + } + } +} diff --git a/Server/Migrations/20260521164245_RoomSorting1.Designer.cs b/Server/Migrations/20260521164245_RoomSorting1.Designer.cs new file mode 100644 index 0000000..aa19fbe --- /dev/null +++ b/Server/Migrations/20260521164245_RoomSorting1.Designer.cs @@ -0,0 +1,1240 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260521164245_RoomSorting1")] + partial class RoomSorting1 + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260521164245_RoomSorting1.cs b/Server/Migrations/20260521164245_RoomSorting1.cs new file mode 100644 index 0000000..8dc9c01 --- /dev/null +++ b/Server/Migrations/20260521164245_RoomSorting1.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class RoomSorting1 : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PublishedAt", + table: "Rooms", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PublishedAt", + table: "Rooms"); + } + } +} diff --git a/Server/Migrations/20260523213127_MigrationThingmajig.Designer.cs b/Server/Migrations/20260523213127_MigrationThingmajig.Designer.cs new file mode 100644 index 0000000..f04103b --- /dev/null +++ b/Server/Migrations/20260523213127_MigrationThingmajig.Designer.cs @@ -0,0 +1,1240 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260523213127_MigrationThingmajig")] + partial class MigrationThingmajig + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260523213127_MigrationThingmajig.cs b/Server/Migrations/20260523213127_MigrationThingmajig.cs new file mode 100644 index 0000000..ec9eb01 --- /dev/null +++ b/Server/Migrations/20260523213127_MigrationThingmajig.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class MigrationThingmajig : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Server/Migrations/20260525130708_ConfigRework.Designer.cs b/Server/Migrations/20260525130708_ConfigRework.Designer.cs new file mode 100644 index 0000000..5974dd7 --- /dev/null +++ b/Server/Migrations/20260525130708_ConfigRework.Designer.cs @@ -0,0 +1,1246 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260525130708_ConfigRework")] + partial class ConfigRework + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("developer") + .HasColumnType("bit"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("moderator") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("screenshare") + .HasColumnType("bit"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260525130708_ConfigRework.cs b/Server/Migrations/20260525130708_ConfigRework.cs new file mode 100644 index 0000000..d27540e --- /dev/null +++ b/Server/Migrations/20260525130708_ConfigRework.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class ConfigRework : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ExpectingReload", + table: "Config", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LastRefresh_DailyObjectives", + table: "Config", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ExpectingReload", + table: "Config"); + + migrationBuilder.DropColumn( + name: "LastRefresh_DailyObjectives", + table: "Config"); + } + } +} diff --git a/Server/Migrations/20260525165541_TheUnRoleing.Designer.cs b/Server/Migrations/20260525165541_TheUnRoleing.Designer.cs new file mode 100644 index 0000000..55b6afe --- /dev/null +++ b/Server/Migrations/20260525165541_TheUnRoleing.Designer.cs @@ -0,0 +1,1240 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260525165541_TheUnRoleing")] + partial class TheUnRoleing + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("int"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260525165541_TheUnRoleing.cs b/Server/Migrations/20260525165541_TheUnRoleing.cs new file mode 100644 index 0000000..fee8482 --- /dev/null +++ b/Server/Migrations/20260525165541_TheUnRoleing.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class TheUnRoleing : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "developer", + table: "Profiles"); + + migrationBuilder.DropColumn( + name: "moderator", + table: "Profiles"); + + migrationBuilder.DropColumn( + name: "screenshare", + table: "Profiles"); + + migrationBuilder.AddColumn( + name: "role", + table: "Profiles", + type: "int", + nullable: false, + defaultValue: 0); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "role", + table: "Profiles"); + + migrationBuilder.AddColumn( + name: "developer", + table: "Profiles", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "moderator", + table: "Profiles", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "screenshare", + table: "Profiles", + type: "bit", + nullable: false, + defaultValue: false); + } + } +} diff --git a/Server/Migrations/20260525165949_TheFumblingOfRoles.Designer.cs b/Server/Migrations/20260525165949_TheFumblingOfRoles.Designer.cs new file mode 100644 index 0000000..9972bcb --- /dev/null +++ b/Server/Migrations/20260525165949_TheFumblingOfRoles.Designer.cs @@ -0,0 +1,1240 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260525165949_TheFumblingOfRoles")] + partial class TheFumblingOfRoles + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Avatar.Gift", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260525165949_TheFumblingOfRoles.cs b/Server/Migrations/20260525165949_TheFumblingOfRoles.cs new file mode 100644 index 0000000..0ad91a3 --- /dev/null +++ b/Server/Migrations/20260525165949_TheFumblingOfRoles.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class TheFumblingOfRoles : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "role", + table: "Profiles", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "role", + table: "Profiles", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/Server/Migrations/20260805082102_FixModelChanges.Designer.cs b/Server/Migrations/20260805082102_FixModelChanges.Designer.cs new file mode 100644 index 0000000..834b771 --- /dev/null +++ b/Server/Migrations/20260805082102_FixModelChanges.Designer.cs @@ -0,0 +1,1263 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260805082102_FixModelChanges")] + partial class FixModelChanges + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260805082102_FixModelChanges.cs b/Server/Migrations/20260805082102_FixModelChanges.cs new file mode 100644 index 0000000..18841a9 --- /dev/null +++ b/Server/Migrations/20260805082102_FixModelChanges.cs @@ -0,0 +1,267 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class FixModelChanges : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Accessibility", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "AvatarItemDesc", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "AvatarItemType", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "ConsumableItemDesc", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "Context", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "Currency", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "IsFeatured", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "IsQuery", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "SubscribersOnly", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "Unique", + table: "GiftItems"); + + migrationBuilder.RenameColumn( + name: "Tooltip", + table: "GiftItems", + newName: "Name"); + + migrationBuilder.RenameColumn( + name: "Rarity", + table: "GiftItems", + newName: "XPAmount"); + + migrationBuilder.RenameColumn( + name: "ItemSetId", + table: "GiftItems", + newName: "StorefrontId"); + + migrationBuilder.RenameColumn( + name: "ItemSetFriendlyName", + table: "GiftItems", + newName: "EquipmentItems"); + + migrationBuilder.RenameColumn( + name: "FriendlyName", + table: "GiftItems", + newName: "Description"); + + migrationBuilder.RenameColumn( + name: "EquipmentPrefabName", + table: "GiftItems", + newName: "ConsumableItems"); + + migrationBuilder.RenameColumn( + name: "EquipmentModificationGuid", + table: "GiftItems", + newName: "AvatarItems"); + + migrationBuilder.RenameColumn( + name: "CurrencyType", + table: "GiftItems", + newName: "CurrencyAmount"); + + migrationBuilder.AddColumn( + name: "IsJunior", + table: "Players", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "EndTime", + table: "GiftItems", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "StartTime", + table: "GiftItems", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.CreateTable( + name: "PlayerEvents", + columns: table => new + { + PlayerEventId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + ImageName = table.Column(type: "nvarchar(max)", nullable: false), + StartTime = table.Column(type: "datetime2", nullable: false), + EndTime = table.Column(type: "datetime2", nullable: false), + CreatorPlayerId = table.Column(type: "bigint", nullable: false), + AttendeeCount = table.Column(type: "int", nullable: false), + RoomId = table.Column(type: "bigint", nullable: false), + Accessibility = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayerEvents", x => x.PlayerEventId); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PlayerEvents"); + + migrationBuilder.DropColumn( + name: "IsJunior", + table: "Players"); + + migrationBuilder.DropColumn( + name: "EndTime", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "StartTime", + table: "GiftItems"); + + migrationBuilder.RenameColumn( + name: "XPAmount", + table: "GiftItems", + newName: "Rarity"); + + migrationBuilder.RenameColumn( + name: "StorefrontId", + table: "GiftItems", + newName: "ItemSetId"); + + migrationBuilder.RenameColumn( + name: "Name", + table: "GiftItems", + newName: "Tooltip"); + + migrationBuilder.RenameColumn( + name: "EquipmentItems", + table: "GiftItems", + newName: "ItemSetFriendlyName"); + + migrationBuilder.RenameColumn( + name: "Description", + table: "GiftItems", + newName: "FriendlyName"); + + migrationBuilder.RenameColumn( + name: "CurrencyAmount", + table: "GiftItems", + newName: "CurrencyType"); + + migrationBuilder.RenameColumn( + name: "ConsumableItems", + table: "GiftItems", + newName: "EquipmentPrefabName"); + + migrationBuilder.RenameColumn( + name: "AvatarItems", + table: "GiftItems", + newName: "EquipmentModificationGuid"); + + migrationBuilder.AddColumn( + name: "Accessibility", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "AvatarItemDesc", + table: "GiftItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "AvatarItemType", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ConsumableItemDesc", + table: "GiftItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Context", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Currency", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "IsFeatured", + table: "GiftItems", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsQuery", + table: "GiftItems", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SubscribersOnly", + table: "GiftItems", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Unique", + table: "GiftItems", + type: "bit", + nullable: false, + defaultValue: false); + } + } +} diff --git a/Server/Migrations/20260806004625_PlayerEventResponses.Designer.cs b/Server/Migrations/20260806004625_PlayerEventResponses.Designer.cs new file mode 100644 index 0000000..0a8cc06 --- /dev/null +++ b/Server/Migrations/20260806004625_PlayerEventResponses.Designer.cs @@ -0,0 +1,1288 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260806004625_PlayerEventResponses")] + partial class PlayerEventResponses + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260806004625_PlayerEventResponses.cs b/Server/Migrations/20260806004625_PlayerEventResponses.cs new file mode 100644 index 0000000..393486a --- /dev/null +++ b/Server/Migrations/20260806004625_PlayerEventResponses.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class PlayerEventResponses : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PlayerEventResponses", + columns: table => new + { + PlayerEventResponseId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PlayerEventId = table.Column(type: "bigint", nullable: false), + PlayerId = table.Column(type: "bigint", nullable: false), + Type = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayerEventResponses", x => x.PlayerEventResponseId); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PlayerEventResponses"); + } + } +} diff --git a/Server/Migrations/20260807063621_BanSystemComplete.Designer.cs b/Server/Migrations/20260807063621_BanSystemComplete.Designer.cs new file mode 100644 index 0000000..e93f03c --- /dev/null +++ b/Server/Migrations/20260807063621_BanSystemComplete.Designer.cs @@ -0,0 +1,1350 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260807063621_BanSystemComplete")] + partial class BanSystemComplete + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260807063621_BanSystemComplete.cs b/Server/Migrations/20260807063621_BanSystemComplete.cs new file mode 100644 index 0000000..bb59550 --- /dev/null +++ b/Server/Migrations/20260807063621_BanSystemComplete.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class BanSystemComplete : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastIpAddress", + table: "Players", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); migrationBuilder.AddColumn( + name: "BanMethod", + table: "Moderation", + type: "nvarchar(max)", + nullable: false, + defaultValue: "None"); + + migrationBuilder.CreateTable( + name: "HardwareBans", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DeviceId = table.Column(type: "nvarchar(max)", nullable: false), + Reason = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ExpiresAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HardwareBans", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "IpBans", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + IpAddress = table.Column(type: "nvarchar(max)", nullable: false), + Reason = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ExpiresAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IpBans", x => x.Id); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HardwareBans"); + + migrationBuilder.DropTable( + name: "IpBans"); + + migrationBuilder.DropColumn( + name: "LastIpAddress", + table: "Players"); + + migrationBuilder.DropColumn( + name: "BanMethod", + table: "Moderation"); + } + } +} diff --git a/Server/Migrations/20260807073448_AddHardwareBanBannedAccountId.Designer.cs b/Server/Migrations/20260807073448_AddHardwareBanBannedAccountId.Designer.cs new file mode 100644 index 0000000..61440cc --- /dev/null +++ b/Server/Migrations/20260807073448_AddHardwareBanBannedAccountId.Designer.cs @@ -0,0 +1,1353 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260807073448_AddHardwareBanBannedAccountId")] + partial class AddHardwareBanBannedAccountId + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260807073448_AddHardwareBanBannedAccountId.cs b/Server/Migrations/20260807073448_AddHardwareBanBannedAccountId.cs new file mode 100644 index 0000000..a7bcdfa --- /dev/null +++ b/Server/Migrations/20260807073448_AddHardwareBanBannedAccountId.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddHardwareBanBannedAccountId : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BannedAccountId", + table: "HardwareBans", + type: "bigint", + nullable: true); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BannedAccountId", + table: "HardwareBans"); + } + } +} diff --git a/Server/Migrations/20260807094452_AddDraculaSilverCurrency.Designer.cs b/Server/Migrations/20260807094452_AddDraculaSilverCurrency.Designer.cs new file mode 100644 index 0000000..1e57e42 --- /dev/null +++ b/Server/Migrations/20260807094452_AddDraculaSilverCurrency.Designer.cs @@ -0,0 +1,1356 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260807094452_AddDraculaSilverCurrency")] + partial class AddDraculaSilverCurrency + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260807094452_AddDraculaSilverCurrency.cs b/Server/Migrations/20260807094452_AddDraculaSilverCurrency.cs new file mode 100644 index 0000000..d3a8db1 --- /dev/null +++ b/Server/Migrations/20260807094452_AddDraculaSilverCurrency.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddDraculaSilverCurrency : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DraculaSilver", + table: "Currency", + type: "bigint", + nullable: false, + defaultValue: 0L); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DraculaSilver", + table: "Currency"); + } + } +} diff --git a/Server/Migrations/20260807120433_AddAccountCreationBlock.Designer.cs b/Server/Migrations/20260807120433_AddAccountCreationBlock.Designer.cs new file mode 100644 index 0000000..f8913c0 --- /dev/null +++ b/Server/Migrations/20260807120433_AddAccountCreationBlock.Designer.cs @@ -0,0 +1,1383 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260807120433_AddAccountCreationBlock")] + partial class AddAccountCreationBlock + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260807120433_AddAccountCreationBlock.cs b/Server/Migrations/20260807120433_AddAccountCreationBlock.cs new file mode 100644 index 0000000..ebe03cb --- /dev/null +++ b/Server/Migrations/20260807120433_AddAccountCreationBlock.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddAccountCreationBlock : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AccountCreationBlocks", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + IpAddress = table.Column(type: "nvarchar(max)", nullable: false), + Reason = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ExpiresAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountCreationBlocks", x => x.Id); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccountCreationBlocks"); + } + } +} diff --git a/Server/Migrations/20260808042255_AddGameBlock.Designer.cs b/Server/Migrations/20260808042255_AddGameBlock.Designer.cs new file mode 100644 index 0000000..0a7dcb7 --- /dev/null +++ b/Server/Migrations/20260808042255_AddGameBlock.Designer.cs @@ -0,0 +1,1410 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260808042255_AddGameBlock")] + partial class AddGameBlock + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260808042255_AddGameBlock.cs b/Server/Migrations/20260808042255_AddGameBlock.cs new file mode 100644 index 0000000..c909c8c --- /dev/null +++ b/Server/Migrations/20260808042255_AddGameBlock.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddGameBlock : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GameBlocks", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + IpAddress = table.Column(type: "nvarchar(max)", nullable: false), + Reason = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ExpiresAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_GameBlocks", x => x.Id); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GameBlocks"); + } + } +} diff --git a/Server/Migrations/20260808052455_AddTokenVersion.Designer.cs b/Server/Migrations/20260808052455_AddTokenVersion.Designer.cs new file mode 100644 index 0000000..826017e --- /dev/null +++ b/Server/Migrations/20260808052455_AddTokenVersion.Designer.cs @@ -0,0 +1,1413 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260808052455_AddTokenVersion")] + partial class AddTokenVersion + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260808052455_AddTokenVersion.cs b/Server/Migrations/20260808052455_AddTokenVersion.cs new file mode 100644 index 0000000..eec3b42 --- /dev/null +++ b/Server/Migrations/20260808052455_AddTokenVersion.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddTokenVersion : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TokenVersion", + table: "Players", + type: "int", + nullable: false, + defaultValue: 0); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TokenVersion", + table: "Players"); + } + } +} diff --git a/Server/Migrations/20260808113840_LeaderboardScores.Designer.cs b/Server/Migrations/20260808113840_LeaderboardScores.Designer.cs new file mode 100644 index 0000000..1cd49eb --- /dev/null +++ b/Server/Migrations/20260808113840_LeaderboardScores.Designer.cs @@ -0,0 +1,1600 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260808113840_LeaderboardScores")] + partial class LeaderboardScores + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260808113840_LeaderboardScores.cs b/Server/Migrations/20260808113840_LeaderboardScores.cs new file mode 100644 index 0000000..ccb4e9e --- /dev/null +++ b/Server/Migrations/20260808113840_LeaderboardScores.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class LeaderboardScores : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LeaderboardScores", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoomId = table.Column(type: "bigint", nullable: false), + SubRoomId = table.Column(type: "bigint", nullable: false), + PlayerId = table.Column(type: "bigint", nullable: false), + StatChannel = table.Column(type: "int", nullable: false), + Score = table.Column(type: "float", nullable: false), + Time = table.Column(type: "bigint", nullable: false), + SubmittedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LeaderboardScores", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_LeaderboardScores_RoomId_StatChannel_PlayerId", + table: "LeaderboardScores", + columns: new[] { "RoomId", "StatChannel", "PlayerId" }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LeaderboardScores"); + } + } +} diff --git a/Server/Migrations/20260810060305_ExtendGiftItemsForStorefronts.Designer.cs b/Server/Migrations/20260810060305_ExtendGiftItemsForStorefronts.Designer.cs new file mode 100644 index 0000000..f97ecb4 --- /dev/null +++ b/Server/Migrations/20260810060305_ExtendGiftItemsForStorefronts.Designer.cs @@ -0,0 +1,1645 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260810060305_ExtendGiftItemsForStorefronts")] + partial class ExtendGiftItemsForStorefronts + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260810060305_ExtendGiftItemsForStorefronts.cs b/Server/Migrations/20260810060305_ExtendGiftItemsForStorefronts.cs new file mode 100644 index 0000000..22e73eb --- /dev/null +++ b/Server/Migrations/20260810060305_ExtendGiftItemsForStorefronts.cs @@ -0,0 +1,172 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class ExtendGiftItemsForStorefronts : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AvatarItemType", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Context", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "CurrencyType", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "EquipmentModificationGuid", + table: "GiftItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "IsFeatured", + table: "GiftItems", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsQuery", + table: "GiftItems", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ItemSetFriendlyName", + table: "GiftItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ItemSetId", + table: "GiftItems", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "Price", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "PurchasableItemId", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Rarity", + table: "GiftItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "SubscribersOnly", + table: "GiftItems", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Tooltip", + table: "GiftItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Unique", + table: "GiftItems", + type: "bit", + nullable: false, + defaultValue: false); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AvatarItemType", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "Context", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "CurrencyType", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "EquipmentModificationGuid", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "IsFeatured", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "IsQuery", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "ItemSetFriendlyName", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "ItemSetId", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "Price", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "PurchasableItemId", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "Rarity", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "SubscribersOnly", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "Tooltip", + table: "GiftItems"); + + migrationBuilder.DropColumn( + name: "Unique", + table: "GiftItems"); + } + } +} diff --git a/Server/Migrations/20260810081825_AddVoteKickLog.Designer.cs b/Server/Migrations/20260810081825_AddVoteKickLog.Designer.cs new file mode 100644 index 0000000..653a02a --- /dev/null +++ b/Server/Migrations/20260810081825_AddVoteKickLog.Designer.cs @@ -0,0 +1,1677 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260810081825_AddVoteKickLog")] + partial class AddVoteKickLog + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260810081825_AddVoteKickLog.cs b/Server/Migrations/20260810081825_AddVoteKickLog.cs new file mode 100644 index 0000000..46eadb0 --- /dev/null +++ b/Server/Migrations/20260810081825_AddVoteKickLog.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddVoteKickLog : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "VoteKicks", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PlayerId = table.Column(type: "bigint", nullable: false), + Reason = table.Column(type: "nvarchar(max)", nullable: false), + Category = table.Column(type: "int", nullable: false), + IsForce = table.Column(type: "bit", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ExpiresAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VoteKicks", x => x.Id); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "VoteKicks"); + } + } +} diff --git a/Server/Migrations/20260810115924_AddRoomBans.Designer.cs b/Server/Migrations/20260810115924_AddRoomBans.Designer.cs new file mode 100644 index 0000000..6e3806c --- /dev/null +++ b/Server/Migrations/20260810115924_AddRoomBans.Designer.cs @@ -0,0 +1,1713 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260810115924_AddRoomBans")] + partial class AddRoomBans + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260810115924_AddRoomBans.cs b/Server/Migrations/20260810115924_AddRoomBans.cs new file mode 100644 index 0000000..aecd18a --- /dev/null +++ b/Server/Migrations/20260810115924_AddRoomBans.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddRoomBans : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RoomBans", + columns: table => new + { + BanId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + roomId = table.Column(type: "bigint", nullable: false), + actorId = table.Column(type: "bigint", nullable: false), + bannedPlayerId = table.Column(type: "bigint", nullable: false), + bannedByName = table.Column(type: "nvarchar(max)", nullable: false), + reason = table.Column(type: "nvarchar(max)", nullable: false), + bannedAt = table.Column(type: "datetime2", nullable: false), + expiresAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomBans", x => x.BanId); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RoomBans"); + } + } +} diff --git a/Server/Migrations/20260811075337_AddPlayerReputationScore.Designer.cs b/Server/Migrations/20260811075337_AddPlayerReputationScore.Designer.cs new file mode 100644 index 0000000..6ffa88b --- /dev/null +++ b/Server/Migrations/20260811075337_AddPlayerReputationScore.Designer.cs @@ -0,0 +1,1716 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260811075337_AddPlayerReputationScore")] + partial class AddPlayerReputationScore + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260811075337_AddPlayerReputationScore.cs b/Server/Migrations/20260811075337_AddPlayerReputationScore.cs new file mode 100644 index 0000000..b4daff6 --- /dev/null +++ b/Server/Migrations/20260811075337_AddPlayerReputationScore.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddPlayerReputationScore : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ReputationScore", + table: "Reputations", + type: "float", + nullable: false, + defaultValue: 0.0); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ReputationScore", + table: "Reputations"); + } + } +} diff --git a/Server/Migrations/20260811081344_AddBanAppealFlow.Designer.cs b/Server/Migrations/20260811081344_AddBanAppealFlow.Designer.cs new file mode 100644 index 0000000..7855ee3 --- /dev/null +++ b/Server/Migrations/20260811081344_AddBanAppealFlow.Designer.cs @@ -0,0 +1,1731 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260811081344_AddBanAppealFlow")] + partial class AddBanAppealFlow + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260811081344_AddBanAppealFlow.cs b/Server/Migrations/20260811081344_AddBanAppealFlow.cs new file mode 100644 index 0000000..dc6089f --- /dev/null +++ b/Server/Migrations/20260811081344_AddBanAppealFlow.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddBanAppealFlow : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AppealCode", + table: "Moderation", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "AppealContact", + table: "Moderation", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "AppealResolution", + table: "Moderation", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "AppealSubmittedAt", + table: "Moderation", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "AppealText", + table: "Moderation", + type: "nvarchar(max)", + nullable: true); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AppealCode", + table: "Moderation"); + + migrationBuilder.DropColumn( + name: "AppealContact", + table: "Moderation"); + + migrationBuilder.DropColumn( + name: "AppealResolution", + table: "Moderation"); + + migrationBuilder.DropColumn( + name: "AppealSubmittedAt", + table: "Moderation"); + + migrationBuilder.DropColumn( + name: "AppealText", + table: "Moderation"); + } + } +} diff --git a/Server/Migrations/20260811085921_AddAppealResolvedAt.Designer.cs b/Server/Migrations/20260811085921_AddAppealResolvedAt.Designer.cs new file mode 100644 index 0000000..9960f82 --- /dev/null +++ b/Server/Migrations/20260811085921_AddAppealResolvedAt.Designer.cs @@ -0,0 +1,1734 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260811085921_AddAppealResolvedAt")] + partial class AddAppealResolvedAt + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolvedAt") + .HasColumnType("datetime2"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260811085921_AddAppealResolvedAt.cs b/Server/Migrations/20260811085921_AddAppealResolvedAt.cs new file mode 100644 index 0000000..f48916c --- /dev/null +++ b/Server/Migrations/20260811085921_AddAppealResolvedAt.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddAppealResolvedAt : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AppealResolvedAt", + table: "Moderation", + type: "datetime2", + nullable: true); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AppealResolvedAt", + table: "Moderation"); + } + } +} diff --git a/Server/Migrations/20260811142510_AddAdminAuth.Designer.cs b/Server/Migrations/20260811142510_AddAdminAuth.Designer.cs new file mode 100644 index 0000000..105cc4f --- /dev/null +++ b/Server/Migrations/20260811142510_AddAdminAuth.Designer.cs @@ -0,0 +1,1806 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260811142510_AddAdminAuth")] + partial class AddAdminAuth + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Admin.AdminSessionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2"); + + b.Property("PreviousTokenHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminSessions"); + }); + + modelBuilder.Entity("RadicadeServer.Admin.AdminUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotpEnabled") + .HasColumnType("bit"); + + b.Property("TotpSecret") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminUsers"); + }); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolvedAt") + .HasColumnType("datetime2"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260811142510_AddAdminAuth.cs b/Server/Migrations/20260811142510_AddAdminAuth.cs new file mode 100644 index 0000000..56a8eef --- /dev/null +++ b/Server/Migrations/20260811142510_AddAdminAuth.cs @@ -0,0 +1,62 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddAdminAuth : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AdminSessions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AdminUserId = table.Column(type: "bigint", nullable: false), + TokenHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + PreviousTokenHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + LastSeenAt = table.Column(type: "datetime2", nullable: false), + ExpiresAt = table.Column(type: "datetime2", nullable: false), + RevokedAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AdminSessions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AdminUsers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Username = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: false), + TotpSecret = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + TotpEnabled = table.Column(type: "bit", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + LastLoginAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AdminUsers", x => x.Id); + }); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AdminSessions"); + + migrationBuilder.DropTable( + name: "AdminUsers"); + } + } +} diff --git a/Server/Migrations/20260811180149_AddEmailVerification.Designer.cs b/Server/Migrations/20260811180149_AddEmailVerification.Designer.cs new file mode 100644 index 0000000..08494d9 --- /dev/null +++ b/Server/Migrations/20260811180149_AddEmailVerification.Designer.cs @@ -0,0 +1,1812 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260811180149_AddEmailVerification")] + partial class AddEmailVerification + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Admin.AdminSessionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2"); + + b.Property("PreviousTokenHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminSessions"); + }); + + modelBuilder.Entity("RadicadeServer.Admin.AdminUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotpEnabled") + .HasColumnType("bit"); + + b.Property("TotpSecret") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminUsers"); + }); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailVerified") + .HasColumnType("bit"); + + b.Property("EmailVerifyToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolvedAt") + .HasColumnType("datetime2"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260811180149_AddEmailVerification.cs b/Server/Migrations/20260811180149_AddEmailVerification.cs new file mode 100644 index 0000000..5b1da0f --- /dev/null +++ b/Server/Migrations/20260811180149_AddEmailVerification.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddEmailVerification : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EmailVerified", + table: "Players", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "EmailVerifyToken", + table: "Players", + type: "nvarchar(max)", + nullable: true); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EmailVerified", + table: "Players"); + + migrationBuilder.DropColumn( + name: "EmailVerifyToken", + table: "Players"); + } + } +} diff --git a/Server/Migrations/20260811202500_AddPasswordReset.Designer.cs b/Server/Migrations/20260811202500_AddPasswordReset.Designer.cs new file mode 100644 index 0000000..0f8b305 --- /dev/null +++ b/Server/Migrations/20260811202500_AddPasswordReset.Designer.cs @@ -0,0 +1,1818 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260811202500_AddPasswordReset")] + partial class AddPasswordReset + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Admin.AdminSessionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2"); + + b.Property("PreviousTokenHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminSessions"); + }); + + modelBuilder.Entity("RadicadeServer.Admin.AdminUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotpEnabled") + .HasColumnType("bit"); + + b.Property("TotpSecret") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminUsers"); + }); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailVerified") + .HasColumnType("bit"); + + b.Property("EmailVerifyToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("PasswordResetExpires") + .HasColumnType("datetime2"); + + b.Property("PasswordResetToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolvedAt") + .HasColumnType("datetime2"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260811202500_AddPasswordReset.cs b/Server/Migrations/20260811202500_AddPasswordReset.cs new file mode 100644 index 0000000..1a44e66 --- /dev/null +++ b/Server/Migrations/20260811202500_AddPasswordReset.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddPasswordReset : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PasswordResetToken", + table: "Players", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "PasswordResetExpires", + table: "Players", + type: "datetime2", + nullable: true); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PasswordResetToken", + table: "Players"); + + migrationBuilder.DropColumn( + name: "PasswordResetExpires", + table: "Players"); + } + } +} diff --git a/Server/Migrations/20260812000000_AddAccountOnboarding.Designer.cs b/Server/Migrations/20260812000000_AddAccountOnboarding.Designer.cs new file mode 100644 index 0000000..b7b24c2 --- /dev/null +++ b/Server/Migrations/20260812000000_AddAccountOnboarding.Designer.cs @@ -0,0 +1,1821 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260812000000_AddAccountOnboarding")] + partial class AddAccountOnboarding + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Admin.AdminSessionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2"); + + b.Property("PreviousTokenHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminSessions"); + }); + + modelBuilder.Entity("RadicadeServer.Admin.AdminUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotpEnabled") + .HasColumnType("bit"); + + b.Property("TotpSecret") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminUsers"); + }); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("CodeOfConductAccepted") + .HasColumnType("bit"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailVerified") + .HasColumnType("bit"); + + b.Property("EmailVerifyToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("PasswordResetExpires") + .HasColumnType("datetime2"); + + b.Property("PasswordResetToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolvedAt") + .HasColumnType("datetime2"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260812000000_AddAccountOnboarding.cs b/Server/Migrations/20260812000000_AddAccountOnboarding.cs new file mode 100644 index 0000000..8366a6d --- /dev/null +++ b/Server/Migrations/20260812000000_AddAccountOnboarding.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddAccountOnboarding : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CodeOfConductAccepted", + table: "Players", + type: "bit", + nullable: false, + defaultValue: false); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CodeOfConductAccepted", + table: "Players"); + } + } +} diff --git a/Server/Migrations/20260813111926_AddPendingSchemaChanges.Designer.cs b/Server/Migrations/20260813111926_AddPendingSchemaChanges.Designer.cs new file mode 100644 index 0000000..8360a8a --- /dev/null +++ b/Server/Migrations/20260813111926_AddPendingSchemaChanges.Designer.cs @@ -0,0 +1,1861 @@ + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260813111926_AddPendingSchemaChanges")] + partial class AddPendingSchemaChanges + { + + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Admin.AdminSessionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2"); + + b.Property("PreviousTokenHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminSessions"); + }); + + modelBuilder.Entity("RadicadeServer.Admin.AdminUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotpEnabled") + .HasColumnType("bit"); + + b.Property("TotpSecret") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminUsers"); + }); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentSlots") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("HardwareBanId") + .HasColumnType("bigint"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("OffenseNumber") + .HasColumnType("int"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("CodeOfConductAccepted") + .HasColumnType("bit"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailVerified") + .HasColumnType("bit"); + + b.Property("EmailVerifyToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("PasswordResetExpires") + .HasColumnType("datetime2"); + + b.Property("PasswordResetToken") + .HasColumnType("nvarchar(max)"); + + b.Property("PendingEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("BioLocked") + .HasColumnType("bit"); + + b.Property("NameLocked") + .HasColumnType("bit"); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DetectionCategory") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("OffenseNumber") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolvedAt") + .HasColumnType("datetime2"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReportCount") + .HasColumnType("int"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260813111926_AddPendingSchemaChanges.cs b/Server/Migrations/20260813111926_AddPendingSchemaChanges.cs new file mode 100644 index 0000000..7133ab3 --- /dev/null +++ b/Server/Migrations/20260813111926_AddPendingSchemaChanges.cs @@ -0,0 +1,93 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddPendingSchemaChanges : Migration + { + + + + + + + + + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH('Reputations', 'ReportCount') IS NULL +BEGIN + ALTER TABLE [Reputations] ADD [ReportCount] int NOT NULL DEFAULT 0; +END"); + + migrationBuilder.Sql(@" +IF COL_LENGTH('Profiles', 'BioLocked') IS NULL +BEGIN + ALTER TABLE [Profiles] ADD [BioLocked] bit NOT NULL DEFAULT 0; +END"); + + migrationBuilder.Sql(@" +IF COL_LENGTH('Profiles', 'NameLocked') IS NULL +BEGIN + ALTER TABLE [Profiles] ADD [NameLocked] bit NOT NULL DEFAULT 0; +END"); + + migrationBuilder.Sql(@" +IF COL_LENGTH('Players', 'PendingEmail') IS NULL +BEGIN + ALTER TABLE [Players] ADD [PendingEmail] nvarchar(max) NULL; +END"); + + migrationBuilder.Sql(@" +IF COL_LENGTH('Avatars', 'EquipmentSlots') IS NULL +BEGIN + ALTER TABLE [Avatars] ADD [EquipmentSlots] nvarchar(max) NOT NULL DEFAULT ''; +END"); + + + + + + migrationBuilder.Sql( + "UPDATE Reputations SET ReputationScore = 10 WHERE ReputationScore = 0;"); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH('Reputations', 'ReportCount') IS NOT NULL +BEGIN + ALTER TABLE [Reputations] DROP COLUMN [ReportCount]; +END"); + + migrationBuilder.Sql(@" +IF COL_LENGTH('Profiles', 'BioLocked') IS NOT NULL +BEGIN + ALTER TABLE [Profiles] DROP COLUMN [BioLocked]; +END"); + + migrationBuilder.Sql(@" +IF COL_LENGTH('Profiles', 'NameLocked') IS NOT NULL +BEGIN + ALTER TABLE [Profiles] DROP COLUMN [NameLocked]; +END"); + + migrationBuilder.Sql(@" +IF COL_LENGTH('Players', 'PendingEmail') IS NOT NULL +BEGIN + ALTER TABLE [Players] DROP COLUMN [PendingEmail]; +END"); + + migrationBuilder.Sql(@" +IF COL_LENGTH('Avatars', 'EquipmentSlots') IS NOT NULL +BEGIN + ALTER TABLE [Avatars] DROP COLUMN [EquipmentSlots]; +END"); + } + } +} diff --git a/Server/Migrations/20260813123000_AddRoomJoinGifts.cs b/Server/Migrations/20260813123000_AddRoomJoinGifts.cs new file mode 100644 index 0000000..d0f0e90 --- /dev/null +++ b/Server/Migrations/20260813123000_AddRoomJoinGifts.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddRoomJoinGifts : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH('dbo.Rooms', 'JoinGiftAvatarItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftAvatarItem] nvarchar(max) NOT NULL DEFAULT N''; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftAvatarItemType') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftAvatarItemType] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftConsumableItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftConsumableItem] nvarchar(max) NOT NULL DEFAULT N''; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftContext') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftContext] int NOT NULL DEFAULT 2; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftCurrencyAmount') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftCurrencyAmount] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftCurrencyType') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftCurrencyType] int NOT NULL DEFAULT 1; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftEnabled') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEnabled] bit NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftEquipmentItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEquipmentItem] nvarchar(max) NOT NULL DEFAULT N''; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftEquipmentModificationGuid') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEquipmentModificationGuid] nvarchar(max) NOT NULL DEFAULT N''; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftMessage') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftMessage] nvarchar(max) NOT NULL DEFAULT N'Thanks for visiting!'; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftOncePerPlayer') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftOncePerPlayer] bit NOT NULL DEFAULT 1; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftRarity') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftRarity] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftTitle') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftTitle] nvarchar(max) NOT NULL DEFAULT N'Room Visit Gift'; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftVersion') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftVersion] int NOT NULL DEFAULT 1; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftXp') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftXp] int NOT NULL DEFAULT 0; +"); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn(name: "JoinGiftAvatarItem", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftAvatarItemType", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftConsumableItem", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftContext", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftCurrencyAmount", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftCurrencyType", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftEnabled", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftEquipmentItem", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftEquipmentModificationGuid", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftMessage", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftOncePerPlayer", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftRarity", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftTitle", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftVersion", table: "Rooms"); + migrationBuilder.DropColumn(name: "JoinGiftXp", table: "Rooms"); + } + } +} diff --git a/Server/Migrations/20260814120000_AddPlayerEventModerationFields.cs b/Server/Migrations/20260814120000_AddPlayerEventModerationFields.cs new file mode 100644 index 0000000..2f1e2e0 --- /dev/null +++ b/Server/Migrations/20260814120000_AddPlayerEventModerationFields.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + + public partial class AddPlayerEventModerationFields : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ModerationState", + table: "PlayerEvents", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "SubRoomId", + table: "PlayerEvents", + type: "bigint", + nullable: true); + } + + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SubRoomId", + table: "PlayerEvents"); + + migrationBuilder.DropColumn( + name: "ModerationState", + table: "PlayerEvents"); + } + } +} diff --git a/Server/Migrations/20260817112022_AddImageTaggedPlayerIds.Designer.cs b/Server/Migrations/20260817112022_AddImageTaggedPlayerIds.Designer.cs new file mode 100644 index 0000000..d05e702 --- /dev/null +++ b/Server/Migrations/20260817112022_AddImageTaggedPlayerIds.Designer.cs @@ -0,0 +1,1920 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260817112022_AddImageTaggedPlayerIds")] + partial class AddImageTaggedPlayerIds + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.Admin.AdminSessionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2"); + + b.Property("PreviousTokenHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminSessions"); + }); + + modelBuilder.Entity("RadicadeServer.Admin.AdminUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotpEnabled") + .HasColumnType("bit"); + + b.Property("TotpSecret") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminUsers"); + }); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentSlots") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BanExpirationUtc") + .HasColumnType("datetime2"); + + b.Property("BanReason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BanStartUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetectionId") + .HasColumnType("bigint"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("HardwareBanId") + .HasColumnType("bigint"); + + b.Property("ModerationId") + .HasColumnType("bigint"); + + b.Property("OffenseNumber") + .HasColumnType("int"); + + b.Property("Origin") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AntiCheatBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.AntiCheatDetection", b => + { + b.Property("DetectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetectionId")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BanId") + .HasColumnType("bigint"); + + b.Property("ConfirmedViolation") + .HasColumnType("bit"); + + b.Property("DetectionType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReviewNotes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("RiskScore") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("RoomInstanceId") + .HasColumnType("bigint"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("DetectionId"); + + b.ToTable("AntiCheatDetections"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.AntiCheat.DiscordQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("LastError") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextAttemptUtc") + .HasColumnType("datetime2"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DiscordQueue"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("CodeOfConductAccepted") + .HasColumnType("bit"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailVerified") + .HasColumnType("bit"); + + b.Property("EmailVerifyToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("PasswordResetExpires") + .HasColumnType("datetime2"); + + b.Property("PasswordResetToken") + .HasColumnType("nvarchar(max)"); + + b.Property("PendingEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("BioLocked") + .HasColumnType("bit"); + + b.Property("NameLocked") + .HasColumnType("bit"); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("PlayerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ModerationState") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DetectionCategory") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("OffenseNumber") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolvedAt") + .HasColumnType("datetime2"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReportCount") + .HasColumnType("int"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("JoinGiftAvatarItem") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftAvatarItemType") + .HasColumnType("int"); + + b.Property("JoinGiftConsumableItem") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftContext") + .HasColumnType("int"); + + b.Property("JoinGiftCurrencyAmount") + .HasColumnType("int"); + + b.Property("JoinGiftCurrencyType") + .HasColumnType("int"); + + b.Property("JoinGiftEnabled") + .HasColumnType("bit"); + + b.Property("JoinGiftEquipmentItem") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftEquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftOncePerPlayer") + .HasColumnType("bit"); + + b.Property("JoinGiftRarity") + .HasColumnType("int"); + + b.Property("JoinGiftTitle") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftVersion") + .HasColumnType("int"); + + b.Property("JoinGiftXp") + .HasColumnType("int"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20260817112022_AddImageTaggedPlayerIds.cs b/Server/Migrations/20260817112022_AddImageTaggedPlayerIds.cs new file mode 100644 index 0000000..1092bb0 --- /dev/null +++ b/Server/Migrations/20260817112022_AddImageTaggedPlayerIds.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + public partial class AddImageTaggedPlayerIds : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PlayerIds", + table: "Images", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PlayerIds", + table: "Images"); + } + } +} diff --git a/Server/Migrations/20260907230000_AddProfileIdentityFields.cs b/Server/Migrations/20260907230000_AddProfileIdentityFields.cs new file mode 100644 index 0000000..40484f7 --- /dev/null +++ b/Server/Migrations/20260907230000_AddProfileIdentityFields.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RadicadeServer.Migrations; + +public partial class AddProfileIdentityFields : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn("personalPronouns", "Profiles", nullable: false, defaultValue: 0); + migrationBuilder.AddColumn("identityFlags", "Profiles", nullable: false, defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn("personalPronouns", "Profiles"); + migrationBuilder.DropColumn("identityFlags", "Profiles"); + } +} diff --git a/Server/Migrations/20260909040000_AddWishlistItems.cs b/Server/Migrations/20260909040000_AddWishlistItems.cs new file mode 100644 index 0000000..2e9c922 --- /dev/null +++ b/Server/Migrations/20260909040000_AddWishlistItems.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260909040000_AddWishlistItems")] +public partial class AddWishlistItems : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "WishlistItems", + columns: table => new + { + WishlistItemId = table.Column(type: "nvarchar(450)", nullable: false), + AccountId = table.Column(type: "bigint", nullable: false), + PurchasableItemId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => table.PrimaryKey("PK_WishlistItems", x => x.WishlistItemId)); + + migrationBuilder.CreateIndex("IX_WishlistItems_AccountId", "WishlistItems", "AccountId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) => migrationBuilder.DropTable("WishlistItems"); +} diff --git a/Server/Migrations/20260919000000_AddRoomRoleLastChangedBy.cs b/Server/Migrations/20260919000000_AddRoomRoleLastChangedBy.cs new file mode 100644 index 0000000..65dedcc --- /dev/null +++ b/Server/Migrations/20260919000000_AddRoomRoleLastChangedBy.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260919000000_AddRoomRoleLastChangedBy")] +public partial class AddRoomRoleLastChangedBy : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastChangedByAccountId", + table: "RoomRoles", + type: "bigint", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn(name: "LastChangedByAccountId", table: "RoomRoles"); + } +} diff --git a/Server/Migrations/20260919120000_AddProfileBannerAndRecentHistoryVisibility.cs b/Server/Migrations/20260919120000_AddProfileBannerAndRecentHistoryVisibility.cs new file mode 100644 index 0000000..f699ec6 --- /dev/null +++ b/Server/Migrations/20260919120000_AddProfileBannerAndRecentHistoryVisibility.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260919120000_AddProfileBannerAndRecentHistoryVisibility")] +public partial class AddProfileBannerAndRecentHistoryVisibility : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +-- Nullable: an account that has never picked a banner has none, and the client +-- renders that as an empty banner area rather than a broken image. +IF COL_LENGTH('dbo.Profiles', 'bannerImage') IS NULL + ALTER TABLE [Profiles] ADD [bannerImage] nvarchar(max) NULL; + +-- Default true so every existing account keeps the visibility the privacy object +-- already reported for it (this server shows profiles to everyone). +IF COL_LENGTH('dbo.Profiles', 'isRecentHistoryVisible') IS NULL + ALTER TABLE [Profiles] ADD [isRecentHistoryVisible] bit NOT NULL + CONSTRAINT [DF_Profiles_isRecentHistoryVisible] DEFAULT 1;"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID('DF_Profiles_isRecentHistoryVisible', 'D') IS NOT NULL + ALTER TABLE [Profiles] DROP CONSTRAINT [DF_Profiles_isRecentHistoryVisible]; +IF COL_LENGTH('dbo.Profiles', 'isRecentHistoryVisible') IS NOT NULL + ALTER TABLE [Profiles] DROP COLUMN [isRecentHistoryVisible]; +IF COL_LENGTH('dbo.Profiles', 'bannerImage') IS NOT NULL + ALTER TABLE [Profiles] DROP COLUMN [bannerImage];"); + } +} diff --git a/Server/Migrations/20260920120000_AddPlayerWarnings.cs b/Server/Migrations/20260920120000_AddPlayerWarnings.cs new file mode 100644 index 0000000..ed9555f --- /dev/null +++ b/Server/Migrations/20260920120000_AddPlayerWarnings.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260920120000_AddPlayerWarnings")] +public partial class AddPlayerWarnings : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.PlayerWarnings', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[PlayerWarnings] ( + [Id] bigint IDENTITY(1,1) NOT NULL, + [ModeratorPlayerId] bigint NOT NULL, + [WarnedPlayerId] bigint NOT NULL, + [ReportCategory] int NOT NULL, + [DisplayReason] nvarchar(max) NULL, + [ModeratorNote] nvarchar(max) NULL, + [CreatedAt] datetime2 NOT NULL, + [AcknowledgedAt] datetime2 NULL, + CONSTRAINT [PK_PlayerWarnings] PRIMARY KEY CLUSTERED ([Id] ASC) + ); + + -- Both lookups on this table are by player: the moderator view lists what was issued + -- to a player, and every moderationBlockDetails poll asks whether the caller has one + -- pending. + CREATE INDEX [IX_PlayerWarnings_WarnedPlayerId] ON [dbo].[PlayerWarnings] ([WarnedPlayerId]); + CREATE INDEX [IX_PlayerWarnings_ModeratorPlayerId] ON [dbo].[PlayerWarnings] ([ModeratorPlayerId]); +END"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.PlayerWarnings', N'U') IS NOT NULL + DROP TABLE [dbo].[PlayerWarnings];"); + } +} diff --git a/Server/Migrations/20260921130000_AddCustomAvatarItems.cs b/Server/Migrations/20260921130000_AddCustomAvatarItems.cs new file mode 100644 index 0000000..2546fb1 --- /dev/null +++ b/Server/Migrations/20260921130000_AddCustomAvatarItems.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260921130000_AddCustomAvatarItems")] +public partial class AddCustomAvatarItems : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.CustomAvatarItems', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[CustomAvatarItems] ( + [Id] bigint IDENTITY(1,1) NOT NULL, + [CustomAvatarItemId] uniqueidentifier NOT NULL, + [CreatorAccountId] bigint NOT NULL, + [Name] nvarchar(max) NULL, + [Description] nvarchar(max) NULL, + [Price] int NOT NULL, + [BaseAvatarItemId] int NOT NULL, + [BaseAvatarItemColor] nvarchar(max) NULL, + [Accessibility] int NOT NULL, + [IsPublished] bit NOT NULL, + [ImageName] nvarchar(max) NULL, + [DesignFileName] nvarchar(max) NULL, + [CreatedAt] datetime2 NOT NULL, + [ModifiedAt] datetime2 NOT NULL, + CONSTRAINT [PK_CustomAvatarItems] PRIMARY KEY CLUSTERED ([Id] ASC) + ); + + -- Every read of this table is either ""by item id"" (the resolve calls, the single-item + -- fetch) or ""by creator"" (my items, owned, fromCreator), so those are the two indexes. + CREATE UNIQUE INDEX [IX_CustomAvatarItems_CustomAvatarItemId] ON [dbo].[CustomAvatarItems] ([CustomAvatarItemId]); + CREATE INDEX [IX_CustomAvatarItems_CreatorAccountId] ON [dbo].[CustomAvatarItems] ([CreatorAccountId]); +END"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.CustomAvatarItems', N'U') IS NOT NULL + DROP TABLE [dbo].[CustomAvatarItems];"); + } +} diff --git a/Server/Migrations/20260921150000_AddSubRoomPermissions.cs b/Server/Migrations/20260921150000_AddSubRoomPermissions.cs new file mode 100644 index 0000000..74b60dd --- /dev/null +++ b/Server/Migrations/20260921150000_AddSubRoomPermissions.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260921150000_AddSubRoomPermissions")] +public partial class AddSubRoomPermissions : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.SubRoomPermissions', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[SubRoomPermissions] ( + [Id] bigint IDENTITY(1,1) NOT NULL, + [subRoomId] bigint NOT NULL, + [permission] nvarchar(128) NOT NULL, + [role] bigint NOT NULL, + [type] int NOT NULL, + [value] nvarchar(max) NOT NULL, + CONSTRAINT [PK_SubRoomPermissions] PRIMARY KEY CLUSTERED ([Id] ASC) + ); + + -- The client addresses an entry by its (permission, role) pair, so that pair is unique: + -- re-sending it updates the stored row instead of adding a second one with a new value. + CREATE UNIQUE INDEX [IX_SubRoomPermissions_subRoomId_permission_role] + ON [dbo].[SubRoomPermissions] ([subRoomId], [permission], [role]); +END"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.SubRoomPermissions', N'U') IS NOT NULL + DROP TABLE [dbo].[SubRoomPermissions];"); + } +} diff --git a/Server/Migrations/20260921160000_AddRoomComments.cs b/Server/Migrations/20260921160000_AddRoomComments.cs new file mode 100644 index 0000000..59b8a5d --- /dev/null +++ b/Server/Migrations/20260921160000_AddRoomComments.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260921160000_AddRoomComments")] +public partial class AddRoomComments : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.RoomComments', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[RoomComments] ( + [Id] bigint IDENTITY(1,1) NOT NULL, + [RoomId] bigint NOT NULL, + [CreatorAccountId] bigint NOT NULL, + [Comment] nvarchar(max) NOT NULL, + [CommentType] nvarchar(64) NOT NULL, + [PositionX] real NULL, + [PositionY] real NULL, + [PositionZ] real NULL, + [CreatedAt] datetime2 NOT NULL, + CONSTRAINT [PK_RoomComments] PRIMARY KEY CLUSTERED ([Id] ASC) + ); + + -- The two lookups are by room (the room page, and the admin panel listing a room's comments) + -- and by author (moderating one account's comments). + CREATE INDEX [IX_RoomComments_RoomId] ON [dbo].[RoomComments] ([RoomId]); + CREATE INDEX [IX_RoomComments_CreatorAccountId] ON [dbo].[RoomComments] ([CreatorAccountId]); +END"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.RoomComments', N'U') IS NOT NULL + DROP TABLE [dbo].[RoomComments];"); + } +} diff --git a/Server/Migrations/20260926120000_AddPlayerEventClubMetadata.cs b/Server/Migrations/20260926120000_AddPlayerEventClubMetadata.cs new file mode 100644 index 0000000..00256d1 --- /dev/null +++ b/Server/Migrations/20260926120000_AddPlayerEventClubMetadata.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260926120000_AddPlayerEventClubMetadata")] +public partial class AddPlayerEventClubMetadata : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.PlayerEvents', N'U') IS NOT NULL +BEGIN + IF COL_LENGTH('dbo.PlayerEvents', 'ClubId') IS NULL ALTER TABLE [PlayerEvents] ADD [ClubId] bigint NULL; + IF COL_LENGTH('dbo.PlayerEvents', 'IsMultiInstance') IS NULL ALTER TABLE [PlayerEvents] ADD [IsMultiInstance] bit NOT NULL DEFAULT 0; + IF COL_LENGTH('dbo.PlayerEvents', 'SupportMultiInstanceRoomChat') IS NULL ALTER TABLE [PlayerEvents] ADD [SupportMultiInstanceRoomChat] bit NOT NULL DEFAULT 0; + IF COL_LENGTH('dbo.PlayerEvents', 'DefaultBroadcastPermissions') IS NULL ALTER TABLE [PlayerEvents] ADD [DefaultBroadcastPermissions] int NOT NULL DEFAULT 0; + IF COL_LENGTH('dbo.PlayerEvents', 'CanRequestBroadcastPermissions') IS NULL ALTER TABLE [PlayerEvents] ADD [CanRequestBroadcastPermissions] int NOT NULL DEFAULT 0; + IF COL_LENGTH('dbo.PlayerEvents', 'TagsJson') IS NULL ALTER TABLE [PlayerEvents] ADD [TagsJson] nvarchar(max) NOT NULL DEFAULT N'[]'; +END"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.PlayerEvents', N'U') IS NOT NULL +BEGIN + IF COL_LENGTH('dbo.PlayerEvents', 'TagsJson') IS NOT NULL ALTER TABLE [PlayerEvents] DROP COLUMN [TagsJson]; + IF COL_LENGTH('dbo.PlayerEvents', 'CanRequestBroadcastPermissions') IS NOT NULL ALTER TABLE [PlayerEvents] DROP COLUMN [CanRequestBroadcastPermissions]; + IF COL_LENGTH('dbo.PlayerEvents', 'DefaultBroadcastPermissions') IS NOT NULL ALTER TABLE [PlayerEvents] DROP COLUMN [DefaultBroadcastPermissions]; + IF COL_LENGTH('dbo.PlayerEvents', 'SupportMultiInstanceRoomChat') IS NOT NULL ALTER TABLE [PlayerEvents] DROP COLUMN [SupportMultiInstanceRoomChat]; + IF COL_LENGTH('dbo.PlayerEvents', 'IsMultiInstance') IS NOT NULL ALTER TABLE [PlayerEvents] DROP COLUMN [IsMultiInstance]; + IF COL_LENGTH('dbo.PlayerEvents', 'ClubId') IS NOT NULL ALTER TABLE [PlayerEvents] DROP COLUMN [ClubId]; +END"); + } +} diff --git a/Server/Migrations/20260927120000_AddKeepsakes.cs b/Server/Migrations/20260927120000_AddKeepsakes.cs new file mode 100644 index 0000000..acc3441 --- /dev/null +++ b/Server/Migrations/20260927120000_AddKeepsakes.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20260927120000_AddKeepsakes")] +public partial class AddKeepsakes : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.KeepsakeInstances', N'U') IS NULL +BEGIN + CREATE TABLE [KeepsakeInstances] ( + [KeepsakeInstanceId] uniqueidentifier NOT NULL, + [RoomId] bigint NOT NULL, + [SubRoomId] bigint NULL, + [KeepsakeCategoryId] int NOT NULL, + [PlacedByAccountId] bigint NOT NULL, + [PlacedAt] datetime2 NOT NULL, + [Removed] bit NOT NULL DEFAULT 0, + CONSTRAINT [PK_KeepsakeInstances] PRIMARY KEY ([KeepsakeInstanceId]) + ); + CREATE INDEX [IX_KeepsakeInstances_RoomId] ON [KeepsakeInstances] ([RoomId], [Removed]); +END +IF OBJECT_ID(N'dbo.KeepsakeCollectionRecords', N'U') IS NULL +BEGIN + CREATE TABLE [KeepsakeCollectionRecords] ( + [KeepsakeCollectionRecordId] bigint IDENTITY(1,1) NOT NULL, + [AccountId] bigint NOT NULL, + [KeepsakeInstanceId] uniqueidentifier NOT NULL, + [KeepsakeCategoryId] int NOT NULL, + [CollectedAt] datetime2 NOT NULL, + CONSTRAINT [PK_KeepsakeCollectionRecords] PRIMARY KEY ([KeepsakeCollectionRecordId]) + ); + CREATE UNIQUE INDEX [IX_KeepsakeCollectionRecords_AccountId_KeepsakeInstanceId] + ON [KeepsakeCollectionRecords] ([AccountId], [KeepsakeInstanceId]); +END"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.KeepsakeCollectionRecords', N'U') IS NOT NULL DROP TABLE [KeepsakeCollectionRecords]; +IF OBJECT_ID(N'dbo.KeepsakeInstances', N'U') IS NOT NULL DROP TABLE [KeepsakeInstances];"); + } +} diff --git a/Server/Migrations/20260928000000_AddModerationStartAndNote.cs b/Server/Migrations/20260928000000_AddModerationStartAndNote.cs new file mode 100644 index 0000000..ab71548 --- /dev/null +++ b/Server/Migrations/20260928000000_AddModerationStartAndNote.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20260928000000_AddModerationStartAndNote")] + public partial class AddModerationStartAndNote : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH(N'dbo.Moderation', N'CreatedAt') IS NULL + ALTER TABLE [dbo].[Moderation] ADD [CreatedAt] datetime2 NOT NULL CONSTRAINT [DF_Moderation_CreatedAt] DEFAULT GETUTCDATE(); +IF COL_LENGTH(N'dbo.Moderation', N'ModeratorNote') IS NULL + ALTER TABLE [dbo].[Moderation] ADD [ModeratorNote] nvarchar(max) NULL;"); + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH(N'dbo.Moderation', N'ModeratorNote') IS NOT NULL ALTER TABLE [dbo].[Moderation] DROP COLUMN [ModeratorNote]; +IF COL_LENGTH(N'dbo.Moderation', N'CreatedAt') IS NOT NULL ALTER TABLE [dbo].[Moderation] DROP COLUMN [CreatedAt];"); + } + } +} diff --git a/Server/Migrations/20261002130000_AddAccountEmojiAndChatFavorite.cs b/Server/Migrations/20261002130000_AddAccountEmojiAndChatFavorite.cs new file mode 100644 index 0000000..1e0623b --- /dev/null +++ b/Server/Migrations/20261002130000_AddAccountEmojiAndChatFavorite.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + [Migration("20261002130000_AddAccountEmojiAndChatFavorite")] + public partial class AddAccountEmojiAndChatFavorite : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH(N'dbo.Profiles', N'displayEmoji') IS NULL + ALTER TABLE [dbo].[Profiles] ADD [displayEmoji] nvarchar(max) NULL; +IF COL_LENGTH(N'dbo.Chats', N'favoritedBy') IS NULL + ALTER TABLE [dbo].[Chats] ADD [favoritedBy] nvarchar(max) NULL;"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH(N'dbo.Chats', N'favoritedBy') IS NOT NULL ALTER TABLE [dbo].[Chats] DROP COLUMN [favoritedBy]; +IF COL_LENGTH(N'dbo.Profiles', N'displayEmoji') IS NOT NULL ALTER TABLE [dbo].[Profiles] DROP COLUMN [displayEmoji];"); + } + } +} diff --git a/Server/Migrations/20261003062000_AddMessageDismissal.cs b/Server/Migrations/20261003062000_AddMessageDismissal.cs new file mode 100644 index 0000000..7ed34ff --- /dev/null +++ b/Server/Migrations/20261003062000_AddMessageDismissal.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20261003062000_AddMessageDismissal")] +public class AddMessageDismissal : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) => + migrationBuilder.AddColumn("IsDismissed", "Messages", type: "bit", + nullable: false, defaultValue: false); + + protected override void Down(MigrationBuilder migrationBuilder) => + migrationBuilder.DropColumn("IsDismissed", "Messages"); +} diff --git a/Server/Migrations/20261008120000_AddPlayerEventBroadcastingRoomInstance.cs b/Server/Migrations/20261008120000_AddPlayerEventBroadcastingRoomInstance.cs new file mode 100644 index 0000000..18ccdb2 --- /dev/null +++ b/Server/Migrations/20261008120000_AddPlayerEventBroadcastingRoomInstance.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20261008120000_AddPlayerEventBroadcastingRoomInstance")] +public partial class AddPlayerEventBroadcastingRoomInstance : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.PlayerEvents', N'U') IS NOT NULL +BEGIN + IF COL_LENGTH('dbo.PlayerEvents', 'BroadcastingRoomInstanceId') IS NULL ALTER TABLE [PlayerEvents] ADD [BroadcastingRoomInstanceId] bigint NULL; +END"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.PlayerEvents', N'U') IS NOT NULL +BEGIN + IF COL_LENGTH('dbo.PlayerEvents', 'BroadcastingRoomInstanceId') IS NOT NULL ALTER TABLE [PlayerEvents] DROP COLUMN [BroadcastingRoomInstanceId]; +END"); + } +} diff --git a/Server/Migrations/20261008120000_AddWeeklyChallenges.cs b/Server/Migrations/20261008120000_AddWeeklyChallenges.cs new file mode 100644 index 0000000..98decde --- /dev/null +++ b/Server/Migrations/20261008120000_AddWeeklyChallenges.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +namespace RadicadeServer.Migrations; + +[DbContext(typeof(RecDBContext))] +[Migration("20261008120000_AddWeeklyChallenges")] +public class AddWeeklyChallenges : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) => migrationBuilder.CreateTable( + name: "WeeklyChallenges", + columns: table => new + { + PlayerId = table.Column(type: "bigint", nullable: false), + ChallengeMapId = table.Column(type: "int", nullable: false), + MapJson = table.Column(type: "nvarchar(max)", nullable: false), + RewardGiftDropId = table.Column(type: "bigint", nullable: true) + }, + constraints: table => table.PrimaryKey("PK_WeeklyChallenges", x => new { x.PlayerId, x.ChallengeMapId })); + + protected override void Down(MigrationBuilder migrationBuilder) => migrationBuilder.DropTable("WeeklyChallenges"); +} diff --git a/Server/Migrations/20261015120000_AddRoomJoinGiftFromPlayerId.cs b/Server/Migrations/20261015120000_AddRoomJoinGiftFromPlayerId.cs new file mode 100644 index 0000000..08d3420 --- /dev/null +++ b/Server/Migrations/20261015120000_AddRoomJoinGiftFromPlayerId.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + /// + /// Who a room's join box is from. The box already travelled with RecNet's + /// Gift.FromPlayerId, but no room could set it, so every join box arrived with a sender of + /// zero and the client had no name to put on it. + /// + [DbContext(typeof(RecDBContext))] + [Migration("20261015120000_AddRoomJoinGiftFromPlayerId")] + public partial class AddRoomJoinGiftFromPlayerId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH(N'dbo.Rooms', N'JoinGiftFromPlayerId') IS NULL + ALTER TABLE [Rooms] ADD [JoinGiftFromPlayerId] bigint NOT NULL DEFAULT 0;"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF COL_LENGTH(N'dbo.Rooms', N'JoinGiftFromPlayerId') IS NOT NULL + ALTER TABLE [Rooms] DROP COLUMN [JoinGiftFromPlayerId];"); + } + } +} diff --git a/Server/Migrations/20261016090000_AddRoomJoinGiftsTable.cs b/Server/Migrations/20261016090000_AddRoomJoinGiftsTable.cs new file mode 100644 index 0000000..56630c1 --- /dev/null +++ b/Server/Migrations/20261016090000_AddRoomJoinGiftsTable.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + /// + /// A room used to describe exactly one join gift box in its own Rooms row. This table is that + /// box's list: one row per box, so a room can hand out several. A room with no rows here keeps + /// its one box from the Rooms row, which is what every room had before this table existed. + /// + [DbContext(typeof(RecDBContext))] + [Migration("20261016090000_AddRoomJoinGiftsTable")] + public partial class AddRoomJoinGiftsTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.RoomJoinGifts', N'U') IS NULL +BEGIN + CREATE TABLE [RoomJoinGifts] ( + [RoomJoinGiftId] bigint NOT NULL IDENTITY(1,1), + [RoomId] bigint NOT NULL, + [SortOrder] int NOT NULL DEFAULT 0, + [Version] int NOT NULL DEFAULT 1, + [Enabled] bit NOT NULL DEFAULT 1, + [OncePerPlayer] bit NOT NULL DEFAULT 1, + [Title] nvarchar(max) NOT NULL DEFAULT N'Room Visit Gift', + [Message] nvarchar(max) NOT NULL DEFAULT N'Thanks for visiting!', + [AvatarItem] nvarchar(max) NOT NULL DEFAULT N'', + [AvatarItemType] int NOT NULL DEFAULT 0, + [EquipmentItem] nvarchar(max) NOT NULL DEFAULT N'', + [EquipmentModificationGuid] nvarchar(max) NOT NULL DEFAULT N'', + [ConsumableItem] nvarchar(max) NOT NULL DEFAULT N'', + [CurrencyType] int NOT NULL DEFAULT 1, + [CurrencyAmount] int NOT NULL DEFAULT 0, + [Xp] int NOT NULL DEFAULT 0, + [Context] int NOT NULL DEFAULT 2, + [Rarity] int NOT NULL DEFAULT 0, + [FromPlayerId] bigint NOT NULL DEFAULT 0, + CONSTRAINT [PK_RoomJoinGifts] PRIMARY KEY ([RoomJoinGiftId]) + ); +END + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_RoomJoinGifts_RoomId' AND object_id = OBJECT_ID(N'dbo.RoomJoinGifts')) + CREATE INDEX [IX_RoomJoinGifts_RoomId] ON [RoomJoinGifts] ([RoomId]);"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +IF OBJECT_ID(N'dbo.RoomJoinGifts', N'U') IS NOT NULL + DROP TABLE [RoomJoinGifts];"); + } + } +} diff --git a/Server/Migrations/RecDBContextModelSnapshot.cs b/Server/Migrations/RecDBContextModelSnapshot.cs new file mode 100644 index 0000000..cf93e10 --- /dev/null +++ b/Server/Migrations/RecDBContextModelSnapshot.cs @@ -0,0 +1,1801 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RadicadeServer.DB; + +#nullable disable + +namespace RadicadeServer.Migrations +{ + [DbContext(typeof(RecDBContext))] + partial class RecDBContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.Challenges.WeeklyChallengeState", b => + { + b.Property("PlayerId").HasColumnType("bigint"); + b.Property("ChallengeMapId").HasColumnType("int"); + b.Property("MapJson").IsRequired().HasColumnType("nvarchar(max)"); + b.Property("RewardGiftDropId").HasColumnType("bigint"); + b.HasKey("PlayerId", "ChallengeMapId"); + b.ToTable("WeeklyChallenges"); + }); + + modelBuilder.Entity("RadicadeServer.Admin.AdminSessionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2"); + + b.Property("PreviousTokenHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminSessions"); + }); + modelBuilder.Entity("RadicadeServer.Admin.AdminUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotpEnabled") + .HasColumnType("bit"); + + b.Property("TotpSecret") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.ToTable("AdminUsers"); + }); + + modelBuilder.Entity("RadicadeServer.Custom.CustomAddition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Files") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CustomContent"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Currency", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("DraculaSilver") + .HasColumnType("bigint"); + + b.Property("Gold") + .HasColumnType("bigint"); + + b.Property("LaserTagTickets") + .HasColumnType("bigint"); + + b.Property("RecTokens") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerAvatar", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Avatar") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentSlots") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Outfits") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.Local.PlayerSettings", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.PlayerUnlocks", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Consumables") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Equipment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Inventions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeys") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Unlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Accounts.RealPresence", b => + { + b.Property("playerId") + .HasColumnType("bigint"); + + b.Property("LoginLock") + .HasColumnType("nvarchar(max)"); + + b.Property("deviceClass") + .HasColumnType("int"); + + b.Property("errorCode") + .HasColumnType("int"); + + b.Property("isOnline") + .HasColumnType("bit"); + + b.Property("roomInstance") + .HasColumnType("bigint"); + + b.Property("statusVisibility") + .HasColumnType("int"); + + b.Property("vRMovementMode") + .HasColumnType("int"); + + b.HasKey("playerId"); + + b.ToTable("Presences"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Cached.CachedLogin", b => + { + b.Property("loginId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("loginId")); + + b.Property("accountId") + .HasColumnType("bigint"); + + b.Property("lastLoginTime") + .HasColumnType("datetime2"); + + b.Property("platform") + .HasColumnType("int"); + + b.Property("platformId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("requirePassword") + .HasColumnType("bit"); + + b.HasKey("loginId"); + + b.ToTable("CachedLogins"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.PlayerData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AppVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Birthday") + .HasColumnType("datetime2"); + + b.Property("CodeOfConductAccepted") + .HasColumnType("bit"); + + b.Property("Developer") + .HasColumnType("bit"); + + b.Property("DeviceClass") + .HasColumnType("int"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailVerified") + .HasColumnType("bit"); + + b.Property("EmailVerifyToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Hashword") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsJunior") + .HasColumnType("bit"); + + b.Property("JuniorState") + .HasColumnType("int"); + + b.Property("LastIpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameChanges") + .HasColumnType("int"); + + b.Property("Parent") + .HasColumnType("bigint"); + + b.Property("PasswordResetExpires") + .HasColumnType("datetime2"); + + b.Property("PasswordResetToken") + .HasColumnType("nvarchar(max)"); + + b.Property("PendingEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TokenVersion") + .HasColumnType("int"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Auth.Profile", b => + { + b.Property("accountId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("accountId")); + + b.Property("BioLocked") + .HasColumnType("bit"); + + b.Property("NameLocked") + .HasColumnType("bit"); + + b.Property("bio") + .HasColumnType("nvarchar(max)"); + + b.Property("createdAt") + .HasColumnType("datetime2"); + + b.Property("displayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isJunior") + .HasColumnType("bit"); + + b.Property("platforms") + .HasColumnType("int"); + + b.Property("profileImage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("role") + .HasColumnType("nvarchar(max)"); + + b.Property("username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("accountId"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.CDN.ImageDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("EventId") + .HasColumnType("bigint"); + + b.Property("ImageType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("PlayerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("GameConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.MainConfig", b => + { + b.Property("FUCKYOUMICROSOFT") + .HasColumnType("nvarchar(450)"); + + b.Property("AutoMicMutingConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CdnBaseUri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DailyObjectives") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectingReload") + .HasColumnType("bit"); + + b.Property("GiftDropStoreItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastRefresh_DailyObjectives") + .HasColumnType("datetime2"); + + b.Property("LevelProgressionMaps") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomCurrencyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomKeyConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ServerMaintenance") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShareBaseUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StorefrontConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FUCKYOUMICROSOFT"); + + b.ToTable("Config"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Config.ServerConfigVal", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.ToTable("ServerConfig"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.DB.PlaylistDB", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlaylistId")); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PlaylistId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventDB", b => + { + b.Property("PlayerEventId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("AttendeeCount") + .HasColumnType("int"); + + b.Property("BroadcastingRoomInstanceId") + .HasColumnType("bigint"); + + b.Property("CanRequestBroadcastPermissions") + .HasColumnType("int"); + + b.Property("ClubId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DefaultBroadcastPermissions") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsMultiInstance") + .HasColumnType("bit"); + + b.Property("ModerationState") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("TagsJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SupportMultiInstanceRoomChat") + .HasColumnType("bit"); + + b.HasKey("PlayerEventId"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Events.PlayerEventResponse", b => + { + b.Property("PlayerEventResponseId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PlayerEventResponseId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("PlayerEventResponseId"); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.GenericData", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AccountId"); + + b.ToTable("Gifts"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Images.CheeredImage", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("IsCheered") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("SavedImageId") + .HasColumnType("bigint"); + + b.HasKey("InteractionId"); + + b.ToTable("ImageInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionDB", b => + { + b.Property("InventionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InventionId")); + + b.Property("AICost") + .HasColumnType("int"); + + b.Property("AllowTrial") + .HasColumnType("bit"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorPermission") + .HasColumnType("int"); + + b.Property("CreatorPlayerId") + .HasColumnType("bigint"); + + b.Property("CurrentVersionNumber") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralPermission") + .HasColumnType("int"); + + b.Property("HideFromPlayer") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("IsAGInvention") + .HasColumnType("bit"); + + b.Property("IsCertifiedInvention") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumDownloads") + .HasColumnType("int"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomOrigin") + .HasColumnType("bigint"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InventionId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionPersonalDetails", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DetailId")); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("IsCheering") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("DetailId"); + + b.ToTable("InventionPersonalDetails"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Inventions.InventionVersion", b => + { + b.Property("VersionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("VersionId")); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChipsCost") + .HasColumnType("int"); + + b.Property("CloudVariablesCost") + .HasColumnType("int"); + + b.Property("InstantiationCost") + .HasColumnType("int"); + + b.Property("InventionId") + .HasColumnType("bigint"); + + b.Property("LightsCost") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("VersionId"); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Leaderboards.LeaderboardScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("StatChannel") + .HasColumnType("int"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "StatChannel", "PlayerId"); + + b.ToTable("LeaderboardScores"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.ChatThread", b => + { + b.Property("chatThreadId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("chatThreadId")); + + b.Property("chatThreadName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("lastReadMessageId") + .HasColumnType("bigint"); + + b.Property("messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("playerIds") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("chatThreadId"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Messages.Message", b => + { + b.Property("IsDismissed") + .HasColumnType("bit"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FromPlayerId") + .HasColumnType("bigint"); + + b.Property("PlayerEventId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SentTime") + .HasColumnType("datetime2"); + + b.Property("ToPlayerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.AccountCreationBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AccountCreationBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.GameBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GameBlocks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.HardwareBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAccountId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DetectionCategory") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("Evidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("OffenseNumber") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("HardwareBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.ModerationDB", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppealCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealContact") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolution") + .HasColumnType("nvarchar(max)"); + + b.Property("AppealResolvedAt") + .HasColumnType("datetime2"); + + b.Property("AppealSubmittedAt") + .HasColumnType("datetime2"); + + b.Property("AppealText") + .HasColumnType("nvarchar(max)"); + + b.Property("BanMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ModeratorNote") + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("ExpireDuration") + .HasColumnType("datetime2"); + + b.Property("IsBan") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Moderation"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.Reputation", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CheerCreative") + .HasColumnType("int"); + + b.Property("CheerCredit") + .HasColumnType("int"); + + b.Property("CheerGeneral") + .HasColumnType("int"); + + b.Property("CheerGreatHost") + .HasColumnType("int"); + + b.Property("CheerHelpful") + .HasColumnType("int"); + + b.Property("CheerSportsman") + .HasColumnType("int"); + + b.Property("IsCheerful") + .HasColumnType("bit"); + + b.Property("Noteriety") + .HasColumnType("float"); + + b.Property("ReportCount") + .HasColumnType("int"); + + b.Property("ReputationScore") + .HasColumnType("float"); + + b.Property("SelectedCheer") + .HasColumnType("int"); + + b.HasKey("AccountId"); + + b.ToTable("Reputations"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Moderation.VoteKickLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsForce") + .HasColumnType("bit"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("VoteKicks"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Progression.ProgressionData", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Progress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("XP") + .HasColumnType("int"); + + b.HasKey("PlayerId"); + + b.ToTable("Progression"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Relationships.RealRelationship", b => + { + b.Property("RelationshipId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RelationshipId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Favorited") + .HasColumnType("int"); + + b.Property("Ignored") + .HasColumnType("int"); + + b.Property("Muted") + .HasColumnType("int"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RelationshipType") + .HasColumnType("int"); + + b.HasKey("RelationshipId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.PlayerInteractionDB", b => + { + b.Property("InteractionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InteractionId")); + + b.Property("Cheered") + .HasColumnType("bit"); + + b.Property("CheeredAt") + .HasColumnType("datetime2"); + + b.Property("Favorited") + .HasColumnType("bit"); + + b.Property("FavoritedAt") + .HasColumnType("datetime2"); + + b.Property("LastVisted") + .HasColumnType("datetime2"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("Visited") + .HasColumnType("bit"); + + b.HasKey("InteractionId"); + + b.ToTable("RoomInteractions"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RestorableSubRoom", b => + { + b.Property("HistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("HistoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("SubRoomId") + .HasColumnType("bigint"); + + b.HasKey("HistoryId"); + + b.ToTable("SubRoomHistory"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomBan", b => + { + b.Property("BanId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("BanId")); + + b.Property("actorId") + .HasColumnType("bigint"); + + b.Property("bannedAt") + .HasColumnType("datetime2"); + + b.Property("bannedByName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("bannedPlayerId") + .HasColumnType("bigint"); + + b.Property("expiresAt") + .HasColumnType("datetime2"); + + b.Property("reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.HasKey("BanId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomData", b => + { + b.Property("RoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CloningAllowed") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorAccountId") + .HasColumnType("bigint"); + + b.Property("CustomWarning") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisableMicAutoMute") + .HasColumnType("bit"); + + b.Property("DisableRoomComments") + .HasColumnType("bit"); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDorm") + .HasColumnType("bit"); + + b.Property("IsRRO") + .HasColumnType("bit"); + + b.Property("JoinGiftAvatarItem") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftAvatarItemType") + .HasColumnType("int"); + + b.Property("JoinGiftConsumableItem") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftContext") + .HasColumnType("int"); + + b.Property("JoinGiftCurrencyAmount") + .HasColumnType("int"); + + b.Property("JoinGiftCurrencyType") + .HasColumnType("int"); + + b.Property("JoinGiftEnabled") + .HasColumnType("bit"); + + b.Property("JoinGiftEquipmentItem") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftEquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftFromPlayerId") + .HasColumnType("bigint"); + + b.Property("JoinGiftMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftOncePerPlayer") + .HasColumnType("bit"); + + b.Property("JoinGiftRarity") + .HasColumnType("int"); + + b.Property("JoinGiftTitle") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JoinGiftVersion") + .HasColumnType("int"); + + b.Property("JoinGiftXp") + .HasColumnType("int"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("int"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("MinLevel") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublishedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("SupportsJuniors") + .HasColumnType("bit"); + + b.Property("SupportsLevelVoting") + .HasColumnType("bit"); + + b.Property("SupportsMobile") + .HasColumnType("bit"); + + b.Property("SupportsQuest2") + .HasColumnType("bit"); + + b.Property("SupportsScreens") + .HasColumnType("bit"); + + b.Property("SupportsTeleportVR") + .HasColumnType("bit"); + + b.Property("SupportsVRLow") + .HasColumnType("bit"); + + b.Property("SupportsWalkVR") + .HasColumnType("bit"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WarningMask") + .HasColumnType("int"); + + b.Property("loadScreen") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RoomId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomInstance", b => + { + b.Property("roomInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("roomInstanceId")); + + b.Property("EncryptVoiceChat") + .HasColumnType("bit"); + + b.Property("clubId") + .HasColumnType("bigint"); + + b.Property("dataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("eventId") + .HasColumnType("bigint"); + + b.Property("isFull") + .HasColumnType("bit"); + + b.Property("isInProgress") + .HasColumnType("bit"); + + b.Property("isPrivate") + .HasColumnType("bit"); + + b.Property("location") + .HasColumnType("nvarchar(max)"); + + b.Property("maxCapacity") + .HasColumnType("int"); + + b.Property("name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRegionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("photonRoomId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("roomCode") + .HasColumnType("nvarchar(max)"); + + b.Property("roomId") + .HasColumnType("bigint"); + + b.Property("roomInstanceType") + .HasColumnType("int"); + + b.Property("subRoomId") + .HasColumnType("bigint"); + + b.HasKey("roomInstanceId"); + + b.ToTable("RoomInstances"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomKey", b => + { + b.Property("RoomKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoomKeyId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ReplicationId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoomKeyId"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomRole", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RoleId")); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InvitedRole") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.HasKey("RoleId"); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.RoomStats", b => + { + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("CheerCount") + .HasColumnType("int"); + + b.Property("FavoriteCount") + .HasColumnType("int"); + + b.Property("VisitCount") + .HasColumnType("int"); + + b.Property("VisitorCount") + .HasColumnType("int"); + + b.HasKey("RoomId"); + + b.ToTable("RoomStats"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Rooms.SubRoom", b => + { + b.Property("SubRoomId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubRoomId")); + + b.Property("Accessibility") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DataBlob") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSandbox") + .HasColumnType("bit"); + + b.Property("MaxPlayers") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoomId") + .HasColumnType("bigint"); + + b.Property("SavedByAccountId") + .HasColumnType("bigint"); + + b.Property("UnitySceneId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SubRoomId"); + + b.ToTable("SubRooms"); + }); + + modelBuilder.Entity("RadicadeServer.RecNet.Store.GiftDropDB", b => + { + b.Property("GiftDropId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GiftDropId")); + + b.Property("AvatarItemType") + .HasColumnType("int"); + + b.Property("AvatarItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConsumableItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Context") + .HasColumnType("int"); + + b.Property("CurrencyAmount") + .HasColumnType("int"); + + b.Property("CurrencyType") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EquipmentItems") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsQuery") + .HasColumnType("bit"); + + b.Property("ItemSetFriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ItemSetId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("PurchasableItemId") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("StorefrontId") + .HasColumnType("bigint"); + + b.Property("SubscribersOnly") + .HasColumnType("bit"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Unique") + .HasColumnType("bit"); + + b.Property("XPAmount") + .HasColumnType("int"); + + b.HasKey("GiftDropId"); + + b.ToTable("GiftItems"); + }); + +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Moderation/ModerationPolicy.cs b/Server/Moderation/ModerationPolicy.cs new file mode 100644 index 0000000..2611333 --- /dev/null +++ b/Server/Moderation/ModerationPolicy.cs @@ -0,0 +1,51 @@ +namespace RadicadeServer.Utils +{ + public static class ModerationPolicy + { + public const string VoiceModerationBanMethod = "Voice Moderation"; + public const string AutomaticTimeoutBanMethod = "Automatic Timeout"; + public const string ModeratorReportBanMethod = "Moderator Report"; + public const string TimeoutBanMethod = "Timeout"; + + public const string AutomaticVoiceModerationBanText = + "You have been banned as a result of our voice moderation system detecting language that violates our code of conduct."; + + public static readonly TimeSpan DefaultTimeoutDuration = TimeSpan.FromMinutes(60); + + public static readonly TimeSpan MinimumTimeoutDuration = TimeSpan.FromMinutes(60); + + public static readonly TimeSpan MaximumTimeoutDuration = TimeSpan.FromHours(24); + + public static bool IsTimeout(string? banMethod) => + Matches(banMethod, AutomaticTimeoutBanMethod) + || Matches(banMethod, ModeratorReportBanMethod) + || Matches(banMethod, TimeoutBanMethod); + + public static bool IsVoiceModeration(string? banMethod) => Matches(banMethod, VoiceModerationBanMethod); + + public static bool IssuesAppealCode(string? banMethod) => !IsTimeout(banMethod); + + public static bool RefusesAppeal(string? banMethod) => IsVoiceModeration(banMethod); + + public static TimeSpan ClampTimeout(TimeSpan duration) + { + if (duration < MinimumTimeoutDuration) return MinimumTimeoutDuration; + if (duration > MaximumTimeoutDuration) return MaximumTimeoutDuration; + return duration; + } + + public static DateTime TimeoutExpiry(DateTime startedAtUtc, TimeSpan? duration = null) => + startedAtUtc + ClampTimeout(duration ?? DefaultTimeoutDuration); + + public static DateTime ExtendedTimeoutExpiry(DateTime startedAtUtc, DateTime currentExpiryUtc, TimeSpan? duration = null) + { + var capped = startedAtUtc + MaximumTimeoutDuration; + var proposed = TimeoutExpiry(startedAtUtc, duration); + var expiry = proposed > currentExpiryUtc ? proposed : currentExpiryUtc; + return expiry > capped ? capped : expiry; + } + + private static bool Matches(string? banMethod, string expected) => + string.Equals(banMethod?.Trim(), expected, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/Server/Moderation/OkResponse.cs b/Server/Moderation/OkResponse.cs new file mode 100644 index 0000000..e641ae2 --- /dev/null +++ b/Server/Moderation/OkResponse.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Moderation +{ + public class OkResponse + { + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + } +} diff --git a/Server/Moderation/SanitizeAPI.cs b/Server/Moderation/SanitizeAPI.cs new file mode 100644 index 0000000..2109d59 --- /dev/null +++ b/Server/Moderation/SanitizeAPI.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Utils; +using System.Linq; + +namespace RadicadeServer.API +{ + [Route("api/sanitize")] + public class SanitizeAPI : Controller + { + [HttpPost("v1/isPure")] + public IsPureResponse GetIsPure([FromBody] IsPureRequest req) + { + if (req.Value == null) return new IsPureResponse { IsPure = false }; + return new IsPureResponse { IsPure = req.Value.IsPure() }; + } + + [HttpPost("v1")] + public string GetSanitized([FromBody] SanitizeStringRequest req) + { + if (req.Value == null) return ""; + return $"\"{req.Value.Sanitize()}\""; + } + + [HttpPost("v2/isAllowed/bulk")] + public Success IsAllowedBulk() + { + var values = ReadBulkValues(); + var arePure = values.Select(value => value.IsPure()).ToList(); + + return new Success(true, null, new { ArePure = arePure }); + } + + private List ReadBulkValues() + { + if (Request.HasFormContentType) + { + var form = Request.Form["Values"]; + if (form.Count > 0) return form.Where(value => value != null).Select(value => value!).ToList(); + } + + var query = Request.Query["Values"]; + if (query.Count > 0) return query.Where(value => value != null).Select(value => value!).ToList(); + + return new List(); + } + } +} diff --git a/Server/Moderation/VoiceChatRestriction.cs b/Server/Moderation/VoiceChatRestriction.cs new file mode 100644 index 0000000..11c0180 --- /dev/null +++ b/Server/Moderation/VoiceChatRestriction.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Moderation; + +namespace RadicadeServer.RecNet.Moderation +{ + public class VoiceChatRestriction + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long PlayerId { get; set; } + + public string Reason { get; set; } = ""; + + public ReportCategory Category { get; set; } + + public DateTime StartedAt { get; set; } = DateTime.UtcNow; + + public DateTime ExpiresAt { get; set; } + + public string IssuedBy { get; set; } = "Admin Panel"; + + public string? IssuedByUsername { get; set; } + + public string? ModeratorNote { get; set; } + + public DateTime? ClearedAt { get; set; } + + public string? ClearedBy { get; set; } + } +} diff --git a/Server/NameServer/NameServerResponse.cs b/Server/NameServer/NameServerResponse.cs new file mode 100644 index 0000000..421e9e7 --- /dev/null +++ b/Server/NameServer/NameServerResponse.cs @@ -0,0 +1,44 @@ +namespace RadicadeServer.RecNet +{ + public class NameServerResponse + { + public string Auth { get; set; } = null!; + public string AI { get; set; } = null!; + public string API { get; set; } = null!; + public string WWW { get; set; } = null!; + public string Cards { get; set; } = null!; + public string Notifications { get; set; } = null!; + public string Images { get; set; } = null!; + public string CDN { get; set; } = null!; + public string Commerce { get; set; } = null!; + public string Data { get; set; } = null!; + public string DataCollection { get; set; } = null!; + public string Discovery { get; set; } = null!; + public string Matchmaking { get; set; } = null!; + public string Storage { get; set; } = null!; + public string Chat { get; set; } = null!; + public string Geo { get; set; } = null!; + public string Leaderboard { get; set; } = null!; + public string Accounts { get; set; } = null!; + public string Link { get; set; } = null!; + public string Locker { get; set; } = null!; + public string Moderation { get; set; } = null!; + public string RoomComments { get; set; } = null!; + public string RoomieIntegrations { get; set; } = null!; + public string Clubs { get; set; } = null!; + public string Rooms { get; set; } = null!; + public string Econ { get; set; } = null!; + public string BugReporting { get; set; } = null!; + public string CMS { get; set; } = null!; + public string GameLogs { get; set; } = null!; + public string PlatformNotifications { get; set; } = null!; + public string StringsCDN { get; set; } = null!; + public string Strings { get; set; } = null!; + public string Lists { get; set; } = null!; + public string PlayerSettings { get; set; } = null!; + public string Studio { get; set; } = null!; + public string StudioCDN { get; set; } = null!; + public string Thorn { get; set; } = null!; + public string Videos { get; set; } = null!; + } +} diff --git a/Server/NameServer/NameSever.cs b/Server/NameServer/NameSever.cs new file mode 100644 index 0000000..785a57b --- /dev/null +++ b/Server/NameServer/NameSever.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.RecNet; + +namespace RadicadeServer.NS +{ + [Route("/")] + public class NameSever : Controller + { + [HttpGet("")] + [HttpGet("ns")] + [HttpGet("nameserver")] + public IActionResult GetNameServer() + { + return Ok(DBConfig.NameServer); + } + } +} diff --git a/Server/Notifications/Announcement.cs b/Server/Notifications/Announcement.cs new file mode 100644 index 0000000..9b787d9 --- /dev/null +++ b/Server/Notifications/Announcement.cs @@ -0,0 +1,20 @@ +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.RecNet.Notifications +{ + public class Announcement + { + public long AnnouncementId { get; set; } + public int AnnouncementStatus { get; set; } + public int AnnouncementType { get; set; } + public string Title { get; set; } = null!; + public string Body { get; set; } = null!; + public string ImageName { get; set; } = null!; + public bool IsMostRecent { get; set; } + public string LinkName { get; set; } = null!; + public int LinkType { get; set; } + public string LinkUri { get; set; } = null!; + public Platforms Platform { get; set; } + public DateTime CreatedAt { get; set; } + } +} diff --git a/Server/Notifications/AnnouncementBroadcast.cs b/Server/Notifications/AnnouncementBroadcast.cs new file mode 100644 index 0000000..cf659fc --- /dev/null +++ b/Server/Notifications/AnnouncementBroadcast.cs @@ -0,0 +1,18 @@ +namespace RadicadeServer.RecNet.Notifications +{ + public static class AnnouncementBroadcast + { + + public const string UpdateNotificationId = "AnnouncementUpdate"; + + public static Notification Update(Announcement announcement) + => new() { Id = UpdateNotificationId, Msg = announcement }; + + + + public const string DeleteNotificationId = "AnnouncementDelete"; + + public static Notification Delete(Announcement announcement) + => new() { Id = DeleteNotificationId, Msg = announcement }; + } +} diff --git a/Server/Notifications/HubClient.cs b/Server/Notifications/HubClient.cs new file mode 100644 index 0000000..b7243fa --- /dev/null +++ b/Server/Notifications/HubClient.cs @@ -0,0 +1,11 @@ +namespace RadicadeServer.RecNet.Notifications +{ + public class HubClient + { + public long AccountId { get; set; } + public string ConnectionId { get; set; } = string.Empty; + public List Subscriptions { get; set; } = new(); + public object SubscriptionsLock { get; } = new(); + public bool HasSubscriptions { get; set; } + } +} diff --git a/Server/Notifications/Notification.cs b/Server/Notifications/Notification.cs new file mode 100644 index 0000000..3cc1c36 --- /dev/null +++ b/Server/Notifications/Notification.cs @@ -0,0 +1,18 @@ +namespace RadicadeServer.RecNet.Notifications +{ + public class Notification + { + private object id = null!; + + public object Id + { + get => id is System.Enum value + ? System.Convert.ToInt64(value, System.Globalization.CultureInfo.InvariantCulture) + .ToString(System.Globalization.CultureInfo.InvariantCulture) + : id; + set => id = value; + } + + public object? Msg { get; set; } + } +} diff --git a/Server/Notifications/Notify.cs b/Server/Notifications/Notify.cs new file mode 100644 index 0000000..38f8f85 --- /dev/null +++ b/Server/Notifications/Notify.cs @@ -0,0 +1,482 @@ +using Microsoft.AspNetCore.SignalR; +using System.Globalization; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Notifications +{ + public static class Notify + { + public static IDbContextFactory DbFactory = null!; + public static IHubContext Context = null!; + public static List Clients = new List(); + + private static readonly System.Collections.Concurrent.ConcurrentDictionary _contexts = new(); + + public static void RegisterContext(HubCallerContext context) + { + _contexts[context.ConnectionId] = context; + } + + public static void ForgetContext(string connectionId) + { + _contexts.TryRemove(connectionId, out _); + } + + + + + + public static readonly object ClientsLock = new(); + + public static HubClient? GetClient(long accountId) + { + lock (ClientsLock) + return Clients.FirstOrDefault(n => n.AccountId == accountId); + } + + public static HubClient? GetConnectionClient(string connectionId) + { + lock (ClientsLock) + return Clients.FirstOrDefault(n => n.ConnectionId == connectionId); + } + + public static int ConnectedConnectionCount + { + get { lock (ClientsLock) return Clients.Count; } + } + + public static List ConnectedAccountIds() + { + lock (ClientsLock) + return Clients.Select(n => n.AccountId).Distinct().ToList(); + } + + public static List SnapshotClients() + { + lock (ClientsLock) + { + return Clients.ToList(); + } + } + + public static async Task SendMessage(long TargetId, Message msg, string? idFormOverride = null, + Action? prepareWithId = null) + { + if (DbFactory == null) + { + return; + } + + await using var ctx = await DbFactory.CreateDbContextAsync(); + + msg.SentTime = DateTime.UtcNow; + msg.ToPlayerId = TargetId; + + var originalId = msg.Id; + await ctx.Database.CreateExecutionStrategy().ExecuteAsync(async () => + { + await using var attempt = await DbFactory.CreateDbContextAsync(); + msg.Id = originalId; + await using var transaction = prepareWithId != null && attempt.Database.IsRelational() + ? await attempt.Database.BeginTransactionAsync() : null; + await attempt.Messages.AddAsync(msg); + await attempt.SaveChangesAsync(); + if (prepareWithId != null) + { + prepareWithId(msg); + await attempt.SaveChangesAsync(); + } + if (transaction != null) await transaction.CommitAsync(); + }); + + List clients; + lock (ClientsLock) + clients = Notify.Clients.Where(n => n.AccountId == TargetId).ToList(); + + var notif = new Notification { Id = NotificationType.MessageReceived, Msg = msg }; + + if (clients.Count == 0) + { + return; + } + + foreach (var client in clients) + await SendToConnection(client.ConnectionId, notif, idFormOverride); + } + + public static async Task SendUntilGoneAsync(long accountId, Notification message, TimeSpan window, TimeSpan interval, Notification? alternate = null) + { + if (window <= TimeSpan.Zero) window = TimeSpan.FromSeconds(8); + if (interval <= TimeSpan.Zero) interval = TimeSpan.FromSeconds(1); + + var deadline = DateTime.UtcNow + window; + var pushes = 0; + + while (true) + { + List clients; + lock (ClientsLock) + clients = Clients.Where(n => n.AccountId == accountId).ToList(); + + if (clients.Count == 0) + { + return pushes; + } + + pushes++; + + var frame = alternate != null && pushes % 2 == 0 ? alternate : message; + foreach (var client in clients) + await SendToConnection(client.ConnectionId, frame); + + if (DateTime.UtcNow >= deadline) + { + return pushes; + } + + await Task.Delay(interval); + } + } + + private static string PayloadProbe(Notification frame) + { + try + { + if (frame.Msg == null) return "null"; + if (frame.Msg is string s) return s.Length == 0 ? "empty string" : "string"; + return frame.Msg.GetType().Name switch + { + "JsonElement" => "object", + _ => frame.Msg.GetType().Name + }; + } + catch { return "unknown"; } + } + + public static Notification EmptyObjectPayload(NotificationType id) + => new() { Id = id, Msg = JsonDocument.Parse("{}").RootElement.Clone() }; + + public static Notification EmptyObjectPayload(string id) + => new() { Id = id, Msg = JsonDocument.Parse("{}").RootElement.Clone() }; + + public static void SendReliably(long accountId, Notification message, TimeSpan window, TimeSpan interval, Notification? alternate = null) + { + _ = Task.Run(async () => + { + try { await SendUntilGoneAsync(accountId, message, window, interval, alternate); } + catch (Exception) { } + }); + } + + public static int DisconnectAccount(long accountId, string reason) + { + List connectionIds; + lock (ClientsLock) + connectionIds = Clients.Where(n => n.AccountId == accountId).Select(n => n.ConnectionId).ToList(); + + var closed = 0; + foreach (var connectionId in connectionIds) + { + if (!_contexts.TryRemove(connectionId, out var context)) continue; + + try + { + context.Abort(); + closed++; + } + catch (Exception) + { + } + } + + return closed; + } + + public static async Task SendNotify(long AccountId, Notification Message) + { + List clients; + lock (ClientsLock) + clients = Notify.Clients.Where(n => n.AccountId == AccountId).ToList(); + + if (clients.Count == 0) + { + return; + } + + + + + + + + foreach (var client in clients) + await SendToConnection(client.ConnectionId, Message); + } + + + + + + + + public static async Task SendSelfAccountUpdate(long accountId) + { + if (DbFactory == null) + { + return; + } + + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var profile = await ctx.Profiles.FirstOrDefaultAsync(x => x.accountId == accountId); + if (profile == null) return; + + var plrData = await ctx.Players.FirstOrDefaultAsync(x => x.AccountId == accountId); + if (plrData == null) return; + + await SendNotify(accountId, new Notification + { + Id = "SelfAccountUpdate", + Msg = PlayerUtils.BuildSelfAccount(profile, plrData) + }); + } + + public static async Task BuildSelfPresenceAsync(RecDBContext ctx, long accountId) + { + var realPresence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == accountId); + if (realPresence == null) return null; + + RoomInstance? roomInstance = realPresence.roomInstance is > 0 + ? await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == realPresence.roomInstance) + : null; + + return await Presence.CreateAsync(ctx, realPresence, roomInstance); + } + + public static async Task SendGlobalNotify(Notification Message) + { + + + + + + + if (Notify.Context == null) + { + return; + } + await Notify.Context.Clients.All.SendAsync("Notification", Message.ToJson()); + } + + + + + private static async Task SendToConnection(string connectionId, Notification Message, string? idFormOverride = null) + { + if (Notify.Context == null) + { + return; + } + await Notify.Context.Clients.Client(connectionId).SendAsync("Notification", + idFormOverride == null ? Message.ToJson() : EnvelopeWithIdForm(Message, idFormOverride)); + + } + + public static string EnvelopeWithIdForm(Notification message, string idJson) + => $"{{\"Id\":{idJson},\"Msg\":{message.Msg?.ToJson() ?? "null"}}}"; + + public static string IdFormJson(string? form, NotificationType type) + { + var number = Convert.ToInt64(type, CultureInfo.InvariantCulture) + .ToString(CultureInfo.InvariantCulture); + + return (form ?? "string").Trim().ToLowerInvariant() switch + { + "number" => number, + "name" => JsonSerializer.Serialize(type.ToString()), + _ => JsonSerializer.Serialize(number), + }; + } + + public static async Task ReplayStoredCoachMessageAsync(long accountId, string? form) + { + if (DbFactory == null) return "no database configured"; + + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var stored = await ctx.Messages + .Where(m => m.ToPlayerId == accountId && m.Type == MessageType.CoachMessage) + .OrderByDescending(m => m.Id) + .FirstOrDefaultAsync(); + + if (stored == null) + return $"account {accountId} has no stored coach message - send one from the admin panel first, then replay it."; + + List clients; + lock (ClientsLock) + clients = Notify.Clients.Where(n => n.AccountId == accountId).ToList(); + + if (clients.Count == 0) + return $"account {accountId} has {connectedClause()} but no notify hub connection; nothing to push to. " + + "Check the [NotifyHub] connect line in server-console.log."; + + var notif = new Notification { Id = NotificationType.MessageReceived, Msg = stored }; + var idJson = IdFormJson(form, NotificationType.MessageReceived); + + foreach (var client in clients) + await SendToConnection(client.ConnectionId, notif, idJson); + + var wire = EnvelopeWithIdForm(notif, idJson); + + return $"replayed stored coach message {stored.Id} to account {accountId} on {clients.Count} connection(s).\n" + + $"Id token on the wire: {idJson}\n" + + $"envelope: {wire}\n" + + ExpectedClientReaction(idJson, NotificationType.MessageReceived); + } + + private static string connectedClause() + { + var accounts = ConnectedAccountIds(); + return accounts.Count == 0 + ? "no notify hub connections at all" + : $"{accounts.Count} account(s) connected ({string.Join(", ", accounts)})"; + } + + private static string ExpectedClientReaction(string idJson, NotificationType type) + { + var number = Convert.ToInt64(type, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture); + if (idJson == number) + return "expected: the client logs a hub-frame parse failure and handles nothing - this is NOT a fix."; + if (idJson.Trim('"') == type.ToString()) + return "expected: the client parses the frame, finds no handler under that name, and silently ignores it."; + return $"expected: the client handles it as {type} ({number}) - the message appears on the watch without a relog."; + } + + public static string WireId(object? id) + { + try { return JsonSerializer.Serialize(id); } + catch { return id?.ToString() ?? "null"; } + } + + public static async Task SendAccountUpdate(long accountId) + { + if (DbFactory == null) return; + + await using var ctx = await DbFactory.CreateDbContextAsync(); + var profile = await ctx.Profiles.FirstOrDefaultAsync(x => x.accountId == accountId); + if (profile == null) return; + + await SendNotify(accountId, new Notification { Id = "AccountUpdate", Msg = profile }); + await UpdateSubscriptions(accountId, Notifs.SubscriptionUpdateProfile, profile); + await SendSelfAccountUpdate(accountId); + } + + public static Task UpdateEveryone(long playerId) => UpdateSubscriptions(playerId); + + public static async Task UpdateSubscriptions(long playerId) + { + if (DbFactory == null) return; + + var subscribers = GetSubscriberConnections(playerId); + if (subscribers.Count == 0) return; + + var factory = DbFactory; + await Task.WhenAll(subscribers.Select(async subscriber => + { + try + { + await using var ctx = await factory.CreateDbContextAsync(); + await SendStatusTo(ctx, playerId, subscriber); + } + catch (Exception) + { + } + })); + } + + public static async Task UpdateSubscriptionsForClient(string connectionId) + { + if (DbFactory == null) return; + + var client = GetConnectionClient(connectionId); + if (client == null) return; + + long[] subscriptions; + lock (client.SubscriptionsLock) + subscriptions = client.HasSubscriptions ? client.Subscriptions.Distinct().ToArray() : Array.Empty(); + + if (subscriptions.Length == 0) return; + + await using var ctx = await DbFactory.CreateDbContextAsync(); + foreach (var playerId in subscriptions) + await SendStatusTo(ctx, playerId, client); + } + + public static async Task UpdateSubscriptions(long playerId, object id, object message) + { + var subscribers = GetSubscriberConnections(playerId); + var wireId = id is Notifs.SubscriptionUpdateProfile + ? "AccountUpdate" + : id; + var notification = new Notification { Id = wireId, Msg = message }; + if (notification.Id is "AccountUpdate") + await UpdateSubscriptions(playerId); + + foreach (var subscriber in subscribers) + await SendToConnection(subscriber.ConnectionId, notification); + } + + public static async Task SendToSubscribers(long playerId, Notification message) + { + foreach (var subscriber in GetSubscriberConnections(playerId)) + await SendToConnection(subscriber.ConnectionId, message); + } + + private static List GetSubscriberConnections(long playerId) + { + return SnapshotClients().Where(client => + { + lock (client.SubscriptionsLock) + return client.HasSubscriptions && client.Subscriptions.Contains(playerId); + }).ToList(); + } + + private static async Task SendStatusTo(RecDBContext ctx, long fromPlayerId, HubClient recipient) + { + var realPresence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == fromPlayerId); + if (realPresence == null) return; + + RoomInstance? roomInstance = realPresence.roomInstance is > 0 + ? await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == realPresence.roomInstance) + : null; + roomInstance = await RoomUtils.ForViewerAsync(roomInstance, recipient.AccountId); + + if (roomInstance != null) + { + roomInstance = roomInstance.Copy(); + roomInstance.photonRegion = string.Empty; + roomInstance.photonRoomId = string.Empty; + roomInstance.dataBlob = string.Empty; + roomInstance.roomCode = string.Empty; + } + + var playerPresence = await Presence.CreateAsync(ctx, realPresence, roomInstance); + playerPresence.ForViewer(recipient.AccountId); + await SendToConnection(recipient.ConnectionId, new Notification + { + Id = "PresenceUpdate", + Msg = playerPresence + }); + } + } +} diff --git a/Server/Notifications/NotifyHub.cs b/Server/Notifications/NotifyHub.cs new file mode 100644 index 0000000..5507c4c --- /dev/null +++ b/Server/Notifications/NotifyHub.cs @@ -0,0 +1,302 @@ +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Notifications +{ + public class NotifyHub : Hub + { + private static readonly string[] TokenCarriers = { "access_token", "accessToken", "token", "id_token" }; + + private RecDBContext db; + public NotifyHub(IHubContext hubContext, RecDBContext db) { + Notify.Context = hubContext; + this.db = db; + } + + + + + + private string? RawToken() + { + var httpContext = Context.GetHttpContext(); + if (httpContext == null) return null; + + foreach (var name in TokenCarriers) + { + var fromQuery = httpContext.Request.Query[name].ToString(); + if (!string.IsNullOrWhiteSpace(fromQuery)) return fromQuery; + } + + var header = httpContext.Request.Headers["Authorization"].ToString(); + if (!string.IsNullOrWhiteSpace(header)) return header; + + foreach (var name in TokenCarriers) + { + if (httpContext.Request.Cookies.TryGetValue(name, out var cookie) && !string.IsNullOrWhiteSpace(cookie)) + return cookie; + } + + return null; + } + + private long? GetAccountId() + { + var authenticatedAccount = Context.User?.GetUserId(); + if (authenticatedAccount != null) return authenticatedAccount; + + var raw = RawToken(); + return string.IsNullOrWhiteSpace(raw) ? null : raw.GetUserId(); + } + + private static JsonElement RawJson(string json) => JsonDocument.Parse(json).RootElement.Clone(); + + private async Task SendNotificationToCaller(string id, object msg) + { + var notification = new Notification { Id = id, Msg = msg }; + await Clients.Caller.SendAsync("Notification", notification.ToJson()); + } + + private static List ReadPlayerIds(object? update) + { + var ids = new List(); + + void Add(JsonElement element) + { + if (element.ValueKind == JsonValueKind.Number && element.TryGetInt64(out var single)) + { + ids.Add(single); + return; + } + + if (element.ValueKind == JsonValueKind.String && long.TryParse(element.GetString(), out single)) + { + ids.Add(single); + return; + } + + if (element.ValueKind != JsonValueKind.Array) return; + + foreach (var entry in element.EnumerateArray()) + { + if (entry.ValueKind == JsonValueKind.Number && entry.TryGetInt64(out var value)) + ids.Add(value); + else if (entry.ValueKind == JsonValueKind.String && long.TryParse(entry.GetString(), out value)) + ids.Add(value); + } + } + + try + { + if (update is SubscriptionUpdate subscription && subscription.PlayerIds != null) + { + ids.AddRange(subscription.PlayerIds); + return ids; + } + + if (update is not JsonElement json) return ids; + + if (json.ValueKind == JsonValueKind.Array) + { + Add(json); + return ids; + } + + if (json.ValueKind != JsonValueKind.Object) return ids; + + foreach (var property in json.EnumerateObject()) + { + if (!property.Name.Equals("PlayerIds", StringComparison.OrdinalIgnoreCase)) continue; + Add(property.Value); + } + } + catch { } + + return ids; + } + + [HubMethodName("SubscribeToPlayers")] + public Task Subscribe(object? update) => ApplySubscriptions(update); + + [HubMethodName("playerSubscriptions/v1/update")] + public Task SubscribeToPlayersV1(object? update) => ApplySubscriptions(update); + + private async Task ApplySubscriptions(object? update) + { + long? account = GetAccountId(); + if (account == null) return; + + var ids = ReadPlayerIds(update); + + var client = Notify.GetConnectionClient(Context.ConnectionId); + if (client != null) + { + lock (client.SubscriptionsLock) + { + client.Subscriptions = ids.Where(id => id > 0).Distinct().ToList(); + client.HasSubscriptions = true; + } + await Notify.UpdateSubscriptionsForClient(Context.ConnectionId); + } + } + + [HubMethodName("SubscribeTo")] + public Task SubscribeToChannels(object? update) => ApplyChannelSubscriptions(update, subscribing: true); + + [HubMethodName("UnsubscribeFrom")] + public Task UnsubscribeFromChannels(object? update) => ApplyChannelSubscriptions(update, subscribing: false); + + private Task ApplyChannelSubscriptions(object? update, bool subscribing) + { + var channels = ReadChannels(update); + if (channels.Count == 0) return Task.CompletedTask; + return Task.CompletedTask; + } + + private static List ReadChannels(object? update) + { + var channels = new List(); + + try + { + if (update is not JsonElement json || json.ValueKind != JsonValueKind.Object) return channels; + + foreach (var property in json.EnumerateObject()) + { + if (!property.Name.Equals("Channels", StringComparison.OrdinalIgnoreCase)) continue; + if (property.Value.ValueKind != JsonValueKind.Array) continue; + + foreach (var entry in property.Value.EnumerateArray()) + { + var channel = entry.ValueKind == JsonValueKind.String ? entry.GetString() : null; + if (!string.IsNullOrWhiteSpace(channel)) channels.Add(channel!); + } + } + } + catch { } + + return channels; + } + + [HubMethodName("heartbeat2")] + public Task Heartbeat() => AnswerHeartbeat(); + + [HubMethodName("heartbeat")] + public Task HeartbeatUnversioned() => AnswerHeartbeat(); + + private async Task AnswerHeartbeat() + { + var account = GetAccountId(); + if (account == null) return null; + + var presence = await Notify.BuildSelfPresenceAsync(db, account.Value); + if (presence == null) return null; + + await SendNotificationToCaller("PresenceUpdate", presence); + + return RawJson($"{{\"Id\":{account.Value},\"Msg\":{presence.ToJson()}}}"); + } + + public override async Task OnConnectedAsync() + { + var httpContext = Context.GetHttpContext(); + long? account = GetAccountId(); + var tokenCarrier = string.IsNullOrWhiteSpace(RawToken()) ? "none" : "present"; + + + if (account != null) + { + Notify.RegisterContext(Context); + + lock (Notify.ClientsLock) Notify.Clients.Add(new HubClient + { + AccountId = account.Value, + ConnectionId = Context.ConnectionId + }); + + var activePresence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == account.Value); + if (activePresence != null && !activePresence.isOnline && activePresence.roomInstance.HasValue) + { + activePresence.isOnline = true; + await db.SaveChangesAsync(); + await Notify.UpdateSubscriptions(account.Value); + } + + try { await Notify.SendSelfAccountUpdate(account.Value); } + catch (Exception) { } + + try + { + var presence = await Notify.BuildSelfPresenceAsync(db, account.Value); + if (presence != null) await SendNotificationToCaller("PresenceUpdate", presence); + } + catch (Exception) { } + + try + { + + var ban = await db.ResolveBanAsync(account.Value); + if (ban != null) + { + var notification = new Notification { Id = NotificationType.ModerationKick, Msg = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, account.Value) }; + await Clients.Caller.SendAsync("Notification", notification.ToJson()); + } + } catch (Exception) + { + } + + try + { + var pendingWarning = await db.GetPendingWarningAsync(account.Value); + if (pendingWarning != null) + { + await PlayerUtils.DeliverWarningAsync(account.Value, + PlayerUtils.BuildWarningBlockDetails(pendingWarning, (int)PlayerUtils.WarningScreenHold.TotalSeconds)); + } + } + catch (Exception) + { + } + } + + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var userId = GetAccountId(); + + lock (Notify.ClientsLock) Notify.Clients.RemoveAll(x => x.ConnectionId == Context.ConnectionId); + Notify.ForgetContext(Context.ConnectionId); + + if (userId != null) + { + bool stillConnected; + lock (Notify.ClientsLock) + stillConnected = Notify.Clients.Any(x => x.AccountId == userId.Value); + + if (!stillConnected) + { + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId.Value); + lock (Notify.ClientsLock) + stillConnected = Notify.Clients.Any(x => x.AccountId == userId.Value); + if (presence != null && !stillConnected) + { + presence.isOnline = false; + await db.SaveChangesAsync(); + await Notify.UpdateSubscriptions(userId.Value); + } + } + } + + await base.OnDisconnectedAsync(exception); + } + } +} diff --git a/Server/Notifications/NotifyHubInvocationFilter.cs b/Server/Notifications/NotifyHubInvocationFilter.cs new file mode 100644 index 0000000..8f350c2 --- /dev/null +++ b/Server/Notifications/NotifyHubInvocationFilter.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.SignalR; + +namespace RadicadeServer.API.Notifications +{ + public sealed class NotifyHubInvocationFilter : IHubFilter + { + public async ValueTask InvokeMethodAsync( + HubInvocationContext invocationContext, + Func> next) + { + try + { + return await next(invocationContext); + } + catch (Exception) + { + return null; + } + } + } +} diff --git a/Server/Notifications/NotifyHubRoutes.cs b/Server/Notifications/NotifyHubRoutes.cs new file mode 100644 index 0000000..32fe3b0 --- /dev/null +++ b/Server/Notifications/NotifyHubRoutes.cs @@ -0,0 +1,27 @@ +namespace RadicadeServer.API.Notifications +{ + public static class NotifyHubRoutes + { + public static readonly string[] Paths = + { + "/notify/hub/v1", + "/notify/hub", + "/notifyhub/v1", + "/hub/v1", + "/hub", + }; + + public static bool IsHubPath(string? path) + { + if (string.IsNullOrEmpty(path)) return false; + + foreach (var route in Paths) + { + if (path.Equals(route, StringComparison.OrdinalIgnoreCase)) return true; + if (path.StartsWith(route + "/", StringComparison.OrdinalIgnoreCase)) return true; + } + + return false; + } + } +} diff --git a/Server/Notifications/PlatformNotificationsAPI.cs b/Server/Notifications/PlatformNotificationsAPI.cs new file mode 100644 index 0000000..42e02aa --- /dev/null +++ b/Server/Notifications/PlatformNotificationsAPI.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.API.Notifications; + +[ApiController] +[Route("platformnotifications")] +public sealed class PlatformNotificationsAPI : ControllerBase +{ + [HttpGet("preferences"), Authorize] + public IActionResult GetPreferences() => Ok(new { email = true, push = true, notifications = true }); + + [HttpGet("accounts/{accountId:long}/receives/{notificationType}"), Authorize] + public IActionResult Receives(long accountId, string notificationType) => Ok(true); + + [HttpGet("crm/me/config"), Authorize] + public IActionResult GetCrmConfig() => Ok(new + { + Results = Array.Empty(), + TotalResults = 0L + }); + + [HttpGet("config/categories"), Authorize] + public IActionResult GetCategories() => Ok(new + { + Results = Array.Empty(), + TotalResults = 0L + }); +} diff --git a/Server/Notifications/RewardDiag.cs b/Server/Notifications/RewardDiag.cs new file mode 100644 index 0000000..ad94d3d --- /dev/null +++ b/Server/Notifications/RewardDiag.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Notifications +{ + public static class RewardDiag + { + private const int DefaultLogLines = 120; + + public static async Task DescribeAsync(RecDBContext db, long accountId, int logLines) + { + var lines = new System.Text.StringBuilder(); + + lines.AppendLine($"build {BuildInfo.Tag}; assembly published {BuildInfo.AssemblyBuiltUtc:u}; running since {BuildInfo.StartedUtc:u}"); + lines.AppendLine($"account {accountId}"); + + var progression = await db.Progression.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerId == accountId); + var json = progression?.GetProgressionJson(); + var rewards = json?.PendingGameRewards ?? new List(); + + var day = json == null ? null : RewardDay.For(json, DateTime.UtcNow); + if (day != null) + lines.AppendLine($"today: dayKey={day.Day} firstActivityPaid={day.FirstActivityPaid} " + + $"dailyChallengePaid={day.DailyChallengePaid} activityBoxes={day.ActivityBoxes}/{RewardDay.ActivityBoxesPerDay}"); + + lines.AppendLine($"reward selections kept: {rewards.Count} ({rewards.Count(r => !r.Consumed)} outstanding)"); + if (rewards.Count == 0) + lines.AppendLine(" (none - no reward has been requested for this account since the ledger was last reset)"); + + foreach (var reward in rewards.OrderByDescending(r => r.CreatedAt)) + { + var state = reward.Consumed + ? $"collected box={reward.ConsumedGiftDropId} at {reward.ConsumedAt:u}" + : "OUTSTANDING"; + lines.AppendLine($" selectionId={reward.RewardSelectionId} rewardType={reward.RewardType} " + + $"context={RewardTrace.Context(reward.GiftContext)} created={reward.CreatedAt:u} {state}"); + lines.AppendLine($" message=\"{reward.Message}\""); + lines.AppendLine($" drop1={RewardTrace.Box(reward.GiftDrop1)}"); + lines.AppendLine($" drop2={RewardTrace.Box(reward.GiftDrop2)}"); + lines.AppendLine($" drop3={RewardTrace.Box(reward.GiftDrop3)}"); + } + + var boxes = await StorefrontShared.GetPendingGiftsAsync(db, accountId); + lines.AppendLine($"pending gift boxes: {boxes.Count}"); + foreach (var box in boxes) + lines.AppendLine($" {RewardTrace.Box(box)} room={RroRewardPools.RoomForContext(box.Context) ?? ""}"); + + lines.AppendLine($"game-rewards.log tail (last {logLines} lines):"); + try + { + var path = System.IO.Path.Join(DBConfig.PersistentLogs, "game-rewards.log"); + if (!System.IO.File.Exists(path)) + lines.AppendLine(" (no reward log has been written yet - the reward routes have not been hit)"); + else + foreach (var line in System.IO.File.ReadLines(path).TakeLast(Math.Clamp(logLines, 1, 2000))) + lines.AppendLine(" " + line); + } + catch (Exception ex) + { + lines.AppendLine($" (the reward log could not be read: {ex.Message})"); + } + + return lines.ToString(); + } + } +} diff --git a/Server/Notifications/RewardPushDiag.cs b/Server/Notifications/RewardPushDiag.cs new file mode 100644 index 0000000..db13390 --- /dev/null +++ b/Server/Notifications/RewardPushDiag.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.API.Notifications +{ + public static class RewardPushDiag + { + public const string RegisteredName = "RewardSelectionReceived"; + + public static string EnumNumber => Convert.ToInt64(NotificationType.GiftPackageRewardSelectionReceived, + System.Globalization.CultureInfo.InvariantCulture) + .ToString(System.Globalization.CultureInfo.InvariantCulture); + + public static async Task ReplayPendingRewardSelectionAsync(RecDBContext db, long accountId, string? idForm) + { + var id = Resolve(idForm); + + var progression = await db.Progression.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerId == accountId); + var pending = progression?.GetProgressionJson()?.PendingGameRewards; + var reward = pending?.Where(r => !r.Consumed).OrderByDescending(r => r.CreatedAt).FirstOrDefault(); + + if (reward == null) + return $"account {accountId} has no pending reward selection - finish a game or a daily challenge first.\n"; + + var connections = Notify.SnapshotClients().Count(c => c.AccountId == accountId); + var notification = new Notification { Id = id, Msg = reward }; + await Notify.SendNotify(accountId, notification); + + var idJson = JsonSerializer.Serialize(id); + var lines = new System.Text.StringBuilder(); + lines.AppendLine($"account {accountId}: pushed reward selectionId={reward.RewardSelectionId} ({reward.RewardType}, \"{reward.Message}\") on {connections} connection(s)."); + lines.AppendLine($"Id token on the wire: {idJson}" + (id == RegisteredName ? " (the key the client registered a handler under)" : string.Empty)); + lines.AppendLine($"expected: {(id == EnumNumber ? "the client parses the frame, finds no handler for a bare number unless its own table has one, and handles nothing." : "the client's dispatcher finds a handler under this exact key and the selection screen opens - no relog.")}"); + lines.AppendLine($"envelope: {Notify.EnvelopeWithIdForm(notification, idJson)}"); + lines.AppendLine($"pending for this account now: {pending!.Count(r => !r.Consumed)} selection(s)."); + return lines.ToString(); + } + + private static string Resolve(string? idForm) => (idForm ?? "name").Trim().ToLowerInvariant() switch + { + "name" => RegisteredName, + "number" => EnumNumber, + "" => RegisteredName, + _ => idForm!.Trim(), + }; + } +} diff --git a/Server/Notifications/SubscriptionUpdate.cs b/Server/Notifications/SubscriptionUpdate.cs new file mode 100644 index 0000000..82dfae0 --- /dev/null +++ b/Server/Notifications/SubscriptionUpdate.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Notifications +{ + public class SubscriptionUpdate + { + public List PlayerIds { get; set; } = new(); + } +} diff --git a/Server/PlayerCheer/CheerPlayerRequest.cs b/Server/PlayerCheer/CheerPlayerRequest.cs new file mode 100644 index 0000000..b6758d0 --- /dev/null +++ b/Server/PlayerCheer/CheerPlayerRequest.cs @@ -0,0 +1,15 @@ +using RadicadeServer.RecNet.Enums.Moderation; + +namespace RadicadeServer.RecNet.Moderation +{ + public class CheerPlayerRequest + { + public long PlayerId { get; set; } + public CheerCategory Category { get; set; } + } + + public class SelectCheerRequest + { + public CheerCategory Category { get; set; } + } +} \ No newline at end of file diff --git a/Server/PlayerEvents/EventClock.cs b/Server/PlayerEvents/EventClock.cs new file mode 100644 index 0000000..effad99 --- /dev/null +++ b/Server/PlayerEvents/EventClock.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Events +{ + public static class EventClock + { + public static DateTime FromPostedTime(DateTime posted) => posted.Kind switch + { + DateTimeKind.Utc => posted, + DateTimeKind.Local => posted.ToUniversalTime(), + _ => TimeZoneInfo.ConvertTimeToUtc(posted, TimeZoneInfo.Local) + }; + + public static string ToWire(DateTime stored) => (stored.Kind == DateTimeKind.Local + ? stored.ToUniversalTime() + : stored) + .ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'", CultureInfo.InvariantCulture); + + public sealed class Json : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetString() is { } raw + ? DateTime.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind) + : default; + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + => writer.WriteStringValue(ToWire(value)); + } + } +} diff --git a/Server/PlayerEvents/EventResponse.cs b/Server/PlayerEvents/EventResponse.cs new file mode 100644 index 0000000..9d69cda --- /dev/null +++ b/Server/PlayerEvents/EventResponse.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.Events +{ + public class EventResponse + { + public long PlayerEventResponseId { get; set; } + public long PlayerEventId { get; set; } + public long PlayerId { get; set; } + + + [JsonConverter(typeof(EventClock.Json))] + public DateTime CreatedAt { get; set; } + public EventResponses Type { get; set; } + } +} diff --git a/Server/PlayerEvents/EventsAPI.cs b/Server/PlayerEvents/EventsAPI.cs new file mode 100644 index 0000000..85a207a --- /dev/null +++ b/Server/PlayerEvents/EventsAPI.cs @@ -0,0 +1,1259 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.Clubs; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using ReportCategory = RadicadeServer.RecNet.Enums.Moderation.ReportCategory; + +namespace RadicadeServer.API.Events +{ + [Route("api/playerevents")] + public class EventsAPI(RecDBContext db) : Controller + { + private const string DefaultEventImageName = "default"; + + private const int EventResultSuccess = 0; + + + + private const int EventResultBroadcastNotMultiInstance = 27; + + private static List ParseEventTags(string? tagsJson) + { + try + { + return JsonSerializer.Deserialize>(tagsJson ?? "[]") ?? []; + } + catch + { + return []; + } + } + + private static PlayerEventTag[] ParsePostedEventTags(Dictionary fields) + { + var raw = GetEventField(fields, "Tags", "tags"); + if (string.IsNullOrWhiteSpace(raw)) return []; + + try + { + using var document = JsonDocument.Parse(raw); + if (document.RootElement.ValueKind != JsonValueKind.Array) return []; + return document.RootElement.EnumerateArray() + .Select(entry => + { + if (entry.ValueKind == JsonValueKind.String) + return new PlayerEventTag { Tag = entry.GetString() ?? string.Empty }; + if (entry.ValueKind != JsonValueKind.Object) + return new PlayerEventTag(); + + var tag = entry.TryGetProperty("Tag", out var tagValue) || entry.TryGetProperty("tag", out tagValue) + ? tagValue.GetString() ?? string.Empty + : string.Empty; + var type = entry.TryGetProperty("Type", out var typeValue) || entry.TryGetProperty("type", out typeValue) + ? typeValue + : default; + return new PlayerEventTag + { + Tag = tag, + Type = type.ValueKind == JsonValueKind.Number && type.TryGetInt32(out var parsedType) ? parsedType : 0 + }; + }) + .Where(tag => !string.IsNullOrWhiteSpace(tag.Tag)) + .ToArray(); + } + catch + { + return []; + } + } + + private static PlayerEvent ToPlayerEvent(PlayerEventDB ev) + { + return new PlayerEvent + { + PlayerEventId = ev.PlayerEventId, + Name = ev.Name, + Description = ev.Description, + ImageName = string.IsNullOrWhiteSpace(ev.ImageName) ? DefaultEventImageName : ev.ImageName, + StartTime = ev.StartTime, + EndTime = ev.EndTime, + CreatorPlayerId = ev.CreatorPlayerId, + AttendeeCount = ev.AttendeeCount, + RoomId = ev.RoomId, + SubRoomId = ev.SubRoomId, + Accessibility = ev.Accessibility, + ClubId = ev.ClubId, + State = ev.ModerationState, + Tags = ParseEventTags(ev.TagsJson), + CreatedAt = ev.CreatedAt, + PlayerCount = ev.AttendeeCount, + IsFull = false, + IsMultiInstance = ev.IsMultiInstance, + SupportMultiInstanceRoomChat = ev.SupportMultiInstanceRoomChat, + DefaultBroadcastPermissions = ev.DefaultBroadcastPermissions, + CanRequestBroadcastPermissions = ev.CanRequestBroadcastPermissions, + BroadcastingRoomInstanceId = ev.BroadcastingRoomInstanceId + }; + } + + private static EventResponse ToEventResponse(PlayerEventResponse r) + { + return new EventResponse + { + PlayerEventResponseId = r.PlayerEventResponseId, + PlayerEventId = r.PlayerEventId, + PlayerId = r.PlayerId, + CreatedAt = r.CreatedAt, + Type = r.Type + }; + } + + private static object ToEventEnvelope(PlayerEventDB ev) + { + var playerEvent = ToPlayerEvent(ev); + return new Dictionary + { + ["PlayerEventId"] = playerEvent.PlayerEventId, + ["Id"] = playerEvent.PlayerEventId, + ["EventId"] = playerEvent.PlayerEventId, + ["CreatorPlayerId"] = playerEvent.CreatorPlayerId, + ["ImageName"] = playerEvent.ImageName, + ["RoomId"] = playerEvent.RoomId, + ["SubRoomId"] = playerEvent.SubRoomId, + ["ClubId"] = playerEvent.ClubId, + ["Name"] = playerEvent.Name, + ["Description"] = playerEvent.Description, + ["StartTime"] = EventClock.ToWire(playerEvent.StartTime), + ["EndTime"] = EventClock.ToWire(playerEvent.EndTime), + ["AttendeeCount"] = playerEvent.AttendeeCount, + ["Accessibility"] = playerEvent.Accessibility, + ["IsMultiInstance"] = playerEvent.IsMultiInstance, + ["SupportMultiInstanceRoomChat"] = playerEvent.SupportMultiInstanceRoomChat, + ["DefaultBroadcastPermissions"] = playerEvent.DefaultBroadcastPermissions, + ["CanRequestBroadcastPermissions"] = playerEvent.CanRequestBroadcastPermissions, + ["BroadcastingRoomInstanceId"] = playerEvent.BroadcastingRoomInstanceId, + ["Tags"] = playerEvent.Tags.Select(tag => new Dictionary + { + ["Tag"] = tag.Tag, + ["Type"] = tag.Type + }).ToArray(), + }; + } + + private static object ToEventResult(PlayerEventDB ev, int result = EventResultSuccess) => new + { + PlayerEvent = ToEventEnvelope(ev), + Result = result, + TagModifyResult = new { Result = result, Tags = Array.Empty() } + }; + + [HttpGet("v1/searchlive"), Authorize] + [HttpGet("v1/search"), Authorize] + [HttpGet("v2/search"), Authorize] + public async Task> SearchEvents([FromQuery] string? search = null, [FromQuery] string? query = null) + { + var events = await db.PlayerEvents.ToListAsync(); + var results = PlayerEventQueries.Search(events, search ?? query, DateTime.UtcNow); + + return results.Select(ToPlayerEvent).ToList(); + } + + [HttpGet("v1"), Authorize] + public async Task> GetBrowseFeed([FromQuery] int skip = 0, [FromQuery] int take = 50) + { + var now = DateTime.UtcNow; + var events = await db.PlayerEvents + .Where(x => x.EndTime > now) + .OrderBy(x => x.StartTime) + .Skip(Math.Max(0, skip)) + .Take(Math.Clamp(take, 0, 100)) + .ToListAsync(); + + return events.Select(ToPlayerEvent).ToList(); + } + + [HttpGet("v1/bulk"), Authorize] + public async Task> GetEventsBulk([FromQuery] List? id) + { + if (id == null || id.Count == 0) return []; + + var events = await db.PlayerEvents + .Where(x => id.Contains(x.PlayerEventId)) + .ToDictionaryAsync(x => x.PlayerEventId); + + return id.Where(events.ContainsKey).Select(e => ToPlayerEvent(events[e])).ToList(); + } + + [HttpGet("v1/creator/{playerId}"), Authorize] + public async Task> GetEventsByCreator(long playerId) + { + var created = await db.PlayerEvents + .Where(x => x.CreatorPlayerId == playerId) + .OrderByDescending(x => x.StartTime) + .ToListAsync(); + + return created.Select(ToPlayerEvent).ToList(); + } + + [HttpGet("v1/tagfilters"), Authorize] + public RoomFilters GetEventTagFilters() + { + var pinned = new List + { + "workshops", "celebration", "game", "meetup", "performance", + "coop", "grandopening", "class", "competition" + }; + var popular = new List + { + "workshops", "celebration", "class", "coop", "competition", + "game", "grandopening", "meetup", "performance" + }; + return new RoomFilters + { + PinnedFilters = pinned, + PopularFilters = popular, + TrendingFilters = null + }; + } + + [HttpGet("v1/all"), Authorize] + [HttpGet("v2/all"), Authorize] + [HttpGet("v2"), Authorize] + public async Task GetAllEvents() + { + var userId = User.GetUserId() ?? 0; + + var created = await db.PlayerEvents + .Where(x => x.CreatorPlayerId == userId) + .OrderByDescending(x => x.StartTime) + .ToListAsync(); + + return new AllEvents + { + Created = created.Select(ToPlayerEvent).ToList(), + Responses = [] + }; + } + + [HttpGet("v1/all/{playerId:long}"), Authorize] + [HttpGet("v2/all/{playerId:long}"), Authorize] + public async Task GetAllEventsForPlayer(long playerId) + { + var created = await db.PlayerEvents + .Where(x => x.CreatorPlayerId == playerId) + .OrderByDescending(x => x.StartTime) + .ToListAsync(); + + return new AllEvents + { + Created = created.Select(ToPlayerEvent).ToList(), + Responses = [] + }; + } + + [HttpGet("v1/created"), Authorize] + [HttpGet("v2/created"), Authorize] + public async Task> GetCreatedEvents() + { + var userId = User.GetUserId() ?? 0; + + var created = await db.PlayerEvents + .Where(x => x.CreatorPlayerId == userId) + .OrderByDescending(x => x.StartTime) + .ToListAsync(); + + return created.Select(ToPlayerEvent).ToList(); + } + + [HttpGet("v1/room/{roomId}"), Authorize] + public async Task> GetEventsInRoom(long roomId) + { + var events = await db.PlayerEvents.ToListAsync(); + return PlayerEventQueries.InRoom(events, roomId).Select(ToPlayerEvent).ToList(); + } + + [HttpGet("v1/clubs"), Authorize] + public async Task> GetEventsByClubs([FromQuery] List? id) + { + if (id == null || id.Count == 0) return []; + + var events = await db.PlayerEvents + .Where(x => x.ClubId != null && id.Contains(x.ClubId.Value)) + .OrderBy(x => x.StartTime) + .ToListAsync(); + return events.Select(ToPlayerEvent).ToList(); + } + + [HttpGet("v1/club/{clubId:long}"), Authorize] + [HttpGet("v1/club_events/{clubId:long}"), Authorize] + public async Task GetEventsByClub(long clubId) + { + var events = await db.PlayerEvents + .Where(x => x.ClubId == clubId) + .OrderBy(x => x.StartTime) + .ToListAsync(); + return Ok(new { Events = events.Select(ToPlayerEvent).ToList(), ContinuationToken = string.Empty }); + } + + [HttpGet("v1/events_by_club_ids/{clubIds}"), Authorize] + public IActionResult GetEventsByClubIdsLegacy(string clubIds) + { + return Ok(Array.Empty()); + } + + [HttpGet("v1/remote_player_event_list/{accountId}"), Authorize] + public IActionResult GetRemotePlayerEventList(long accountId) + { + return Ok(new + { + Events = Array.Empty(), + Responses = Array.Empty() + }); + } + + [HttpGet("v1/upcoming_events"), Authorize] + public async Task> GetUpcomingEvents() + { + var events = await db.PlayerEvents + .Where(x => x.EndTime > DateTime.UtcNow) + .OrderBy(x => x.StartTime) + .Take(50) + .ToListAsync(); + return events.Select(ToPlayerEvent).ToList(); + } + + [HttpGet("v1/filters"), Authorize] + public RoomFilters GetEventFilters() => GetEventTagFilters(); + + [HttpGet("v1/{eventId:long}"), Authorize] + [HttpGet("v1/event/{eventId}"), Authorize] + public async Task> GetEvent(long eventId) + { + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + return Ok(ToPlayerEvent(ev)); + } + + [HttpGet("v2/{eventId:long}"), Authorize] + public async Task GetEventV2(long eventId) + { + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + return Ok(ToEventResult(ev)); + } + + [HttpPost("v1/create"), Authorize] + [HttpPost("v1/event"), Authorize] + [HttpPost("v1/event/create"), Authorize] + public async Task> CreateEvent( + [FromForm] string name, + [FromForm] string? description, + [FromForm] long roomId, + [FromForm] DateTime? startTime, + [FromForm] DateTime? endTime, + [FromForm] string? imageName, + [FromForm] Accessibility? accessibility) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + if (string.IsNullOrWhiteSpace(name)) + return BadRequest(new { error = "Event name is required." }); + if (roomId <= 0) + return BadRequest(new { error = "A valid room id is required." }); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) + return BadRequest(new { error = "Room does not exist." }); + + var now = DateTime.UtcNow; + var ev = new PlayerEventDB + { + Name = name.Trim(), + Description = description ?? string.Empty, + ImageName = string.IsNullOrWhiteSpace(imageName) ? string.Empty : imageName, + StartTime = EventClock.FromPostedTime(startTime ?? now), + EndTime = EventClock.FromPostedTime(endTime ?? now.AddHours(2)), + CreatorPlayerId = userId.Value, + AttendeeCount = 1, + RoomId = roomId, + Accessibility = accessibility ?? Accessibility.Public, + CreatedAt = now + }; + + await db.PlayerEvents.AddAsync(ev); + await db.SaveChangesAsync(); + + + await db.PlayerEventResponses.AddAsync(new PlayerEventResponse + { + PlayerEventId = ev.PlayerEventId, + PlayerId = userId.Value, + Type = EventResponses.Yes + }); + await db.SaveChangesAsync(); + + await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Created(ev)); + + return Ok(ToPlayerEvent(ev)); + } + + [HttpPost("v2"), Authorize] + public async Task CreateEventV2() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var fields = await ReadEventFieldsAsync(); + + var name = GetEventField(fields, "Name", "name"); + var roomId = GetEventLong(fields, "RoomId", "roomId") ?? 0; + if (string.IsNullOrWhiteSpace(name) || roomId <= 0) + return BadRequest(new { error = "Invalid event request." }); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) + return BadRequest(new { error = "Room does not exist." }); + + var now = DateTime.UtcNow; + var ev = new PlayerEventDB + { + Name = name.Trim(), + Description = GetEventField(fields, "Description", "description") ?? string.Empty, + ImageName = GetEventField(fields, "ImageName", "imageName") ?? string.Empty, + StartTime = ParseEventDate(GetEventField(fields, "StartTime", "startTime"), now), + EndTime = ParseEventDate(GetEventField(fields, "EndTime", "endTime"), now.AddHours(2)), + CreatorPlayerId = userId.Value, + AttendeeCount = 0, + RoomId = roomId, + SubRoomId = GetEventLong(fields, "SubRoomId", "subRoomId"), + ClubId = GetEventLong(fields, "ClubId", "clubId"), + Accessibility = ParseEventAccessibility(GetEventField(fields, "Accessibility", "accessibility")), + IsMultiInstance = ParseEventBoolean(GetEventField(fields, "IsMultiInstance", "isMultiInstance")), + SupportMultiInstanceRoomChat = ParseEventBoolean(GetEventField(fields, "SupportMultiInstanceRoomChat", "supportMultiInstanceRoomChat")), + DefaultBroadcastPermissions = GetEventInt(fields, "DefaultBroadcastPermissions", "defaultBroadcastPermissions") ?? 0, + CanRequestBroadcastPermissions = GetEventInt(fields, "CanRequestBroadcastPermissions", "canRequestBroadcastPermissions") ?? 0, + TagsJson = JsonSerializer.Serialize(ParsePostedEventTags(fields)), + CreatedAt = now + }; + + await db.PlayerEvents.AddAsync(ev); + await db.SaveChangesAsync(); + + await db.PlayerEventResponses.AddAsync(new PlayerEventResponse + { + PlayerEventId = ev.PlayerEventId, + PlayerId = userId.Value, + Type = EventResponses.Yes + }); + ev.AttendeeCount = 1; + await db.SaveChangesAsync(); + + + + await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Created(ev)); + + return Ok(ToEventResult(ev)); + } + + [HttpPost("v2/{eventId:long}"), Authorize] + [HttpPut("v2/{eventId:long}/accessibility"), Authorize] + [HttpPut("v2/{eventId:long}/club"), Authorize] + [HttpPut("v2/{eventId:long}/description"), Authorize] + [HttpPut("v2/{eventId:long}/image"), Authorize] + [HttpPut("v2/{eventId:long}/multiinstance"), Authorize] + [HttpPut("v2/{eventId:long}/name"), Authorize] + [HttpPut("v2/{eventId:long}/room"), Authorize] + [HttpPut("v2/{eventId:long}/tags"), Authorize] + [HttpPut("v2/{eventId:long}/time"), Authorize] + public async Task UpdateEventV2(long eventId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + if (ev.CreatorPlayerId != userId) return Forbid(); + + var fields = await ReadEventFieldsAsync(); + + var name = GetEventField(fields, "Name", "name"); + if (name != null) ev.Name = name.Trim(); + + if (fields.TryGetValue("Description", out var description)) ev.Description = description; + + if (fields.TryGetValue("ImageName", out var imageName)) ev.ImageName = imageName; + + var startTime = GetEventField(fields, "StartTime", "startTime"); + if (startTime != null) ev.StartTime = ParseEventDate(startTime, ev.StartTime); + + var endTime = GetEventField(fields, "EndTime", "endTime"); + if (endTime != null) ev.EndTime = ParseEventDate(endTime, ev.EndTime); + + var roomId = GetEventLong(fields, "RoomId", "roomId"); + if (roomId is > 0 && await db.Rooms.AnyAsync(x => x.RoomId == roomId)) ev.RoomId = roomId.Value; + + if (fields.TryGetValue("SubRoomId", out var subRoomIdRaw) && + TryParseNullableEventId(subRoomIdRaw, out var subRoomId)) + ev.SubRoomId = subRoomId; + + if (fields.TryGetValue("ClubId", out var clubIdRaw) && + TryParseNullableEventId(clubIdRaw, out var clubId)) + ev.ClubId = clubId; + + + var accessibility = GetEventField(fields, "Accessibility", "accessibility"); + if (accessibility != null) ev.Accessibility = ParseEventAccessibility(accessibility); + + var multiInstance = GetEventField(fields, "IsMultiInstance", "isMultiInstance"); + if (multiInstance != null) ev.IsMultiInstance = ParseEventBoolean(multiInstance); + + var roomChat = GetEventField(fields, "SupportMultiInstanceRoomChat", "supportMultiInstanceRoomChat"); + if (roomChat != null) ev.SupportMultiInstanceRoomChat = ParseEventBoolean(roomChat); + + var defaultBroadcastPermissions = GetEventInt(fields, "DefaultBroadcastPermissions", "defaultBroadcastPermissions"); + if (defaultBroadcastPermissions != null) ev.DefaultBroadcastPermissions = defaultBroadcastPermissions.Value; + + var canRequestBroadcastPermissions = GetEventInt(fields, "CanRequestBroadcastPermissions", "canRequestBroadcastPermissions"); + if (canRequestBroadcastPermissions != null) ev.CanRequestBroadcastPermissions = canRequestBroadcastPermissions.Value; + + if (fields.ContainsKey("Tags")) + ev.TagsJson = JsonSerializer.Serialize(ParsePostedEventTags(fields)); + + var updatePath = Request?.Path.Value?.TrimEnd('/').Split('/').LastOrDefault()?.ToLowerInvariant(); + switch (updatePath) + { + case "accessibility": + if (GetEventField(fields, "Accessibility", "accessibility") is { } accessibilityValue) + ev.Accessibility = ParseEventAccessibility(accessibilityValue); + break; + case "club": + if (fields.TryGetValue("ClubId", out var clubValue) && TryParseNullableEventId(clubValue, out var parsedClubId)) + ev.ClubId = parsedClubId; + break; + case "description": + if (fields.TryGetValue("Description", out var descriptionValue)) ev.Description = descriptionValue; + break; + case "image": + if (fields.TryGetValue("ImageName", out var imageValue)) ev.ImageName = imageValue; + break; + case "multiinstance": + if (GetEventField(fields, "IsMultiInstance", "isMultiInstance") is { } multiInstanceValue) + ev.IsMultiInstance = ParseEventBoolean(multiInstanceValue); + if (GetEventField(fields, "SupportMultiInstanceRoomChat", "supportMultiInstanceRoomChat") is { } multiChatValue) + ev.SupportMultiInstanceRoomChat = ParseEventBoolean(multiChatValue); + if (GetEventInt(fields, "DefaultBroadcastPermissions", "defaultBroadcastPermissions") is { } defaultPermissions) + ev.DefaultBroadcastPermissions = defaultPermissions; + if (GetEventInt(fields, "CanRequestBroadcastPermissions", "canRequestBroadcastPermissions") is { } requestPermissions) + ev.CanRequestBroadcastPermissions = requestPermissions; + break; + case "name": + if (fields.TryGetValue("Name", out var eventName)) ev.Name = eventName.Trim(); + break; + case "room": + var updatedRoomId = GetEventLong(fields, "RoomId", "roomId"); + if (updatedRoomId is > 0 && + await db.Rooms.AnyAsync(x => x.RoomId == updatedRoomId.Value)) + ev.RoomId = updatedRoomId.Value; + if (fields.TryGetValue("SubRoomId", out var subRoomValue) && TryParseNullableEventId(subRoomValue, out var parsedSubRoomId)) + ev.SubRoomId = parsedSubRoomId; + break; + case "tags": + if (GetEventField(fields, "Tags", "tags") != null) + ev.TagsJson = JsonSerializer.Serialize(ParsePostedEventTags(fields)); + break; + case "time": + if (GetEventField(fields, "StartTime", "startTime") is { } startValue) + ev.StartTime = ParseEventDate(startValue, ev.StartTime); + if (GetEventField(fields, "EndTime", "endTime") is { } endValue) + ev.EndTime = ParseEventDate(endValue, ev.EndTime); + break; + } + + await db.SaveChangesAsync(); + + await PlayerEventNotifications.PushAsync(ev.CreatorPlayerId, PlayerEventNotifications.Updated(ev)); + + return Ok(ToEventResult(ev)); + } + + [HttpPost("v1/respond"), Authorize] + public async Task RespondToEvent() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var body = await ReadJsonBodyAsync(); + if (body == null) return BadRequest(); + + var eventId = GetBodyLong(body.Value, "PlayerEventId", "playerEventId"); + var typeRaw = GetBodyLong(body.Value, "Type", "type"); + if (eventId == null || typeRaw == null) return BadRequest(); + + if (!Enum.IsDefined(typeof(EventResponses), (int)typeRaw.Value) || + (EventResponses)(int)typeRaw.Value is EventResponses.None or EventResponses.Pending) + return BadRequest(); + + var response = await SetEventResponse((long)eventId, userId.Value, (EventResponses)(int)typeRaw.Value); + if (response == null) return NotFound(); + + return Ok(ToEventResult(response.Value.ev)); + } + + [HttpPost("v1/event/{eventId}/response"), Authorize] + public async Task> SetResponse(long eventId, [FromForm] EventResponses response) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + if (!Enum.IsDefined(typeof(EventResponses), response) || response is EventResponses.None or EventResponses.Pending) + return BadRequest(); + + var result = await SetEventResponse(eventId, userId.Value, response); + if (result == null) return NotFound(); + + return Ok(ToEventResponse(result.Value.response)); + } + + private async Task<(PlayerEventResponse response, PlayerEventDB ev)?> SetEventResponse( + long eventId, long playerId, EventResponses type) + { + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return null; + + var existing = await db.PlayerEventResponses + .FirstOrDefaultAsync(x => x.PlayerEventId == eventId && x.PlayerId == playerId); + + var hasInvitation = existing?.Type == EventResponses.Pending || + await db.Messages.AsNoTracking().AnyAsync(message => + message.ToPlayerId == playerId && + message.PlayerEventId == eventId && + message.Type == MessageType.PlayerEventInvitation); + if (ev.Accessibility == Accessibility.Private && + ev.CreatorPlayerId != playerId && + !hasInvitation) + return null; + + if (existing == null) + { + existing = new PlayerEventResponse + { + PlayerEventId = eventId, + PlayerId = playerId, + Type = type + }; + await db.PlayerEventResponses.AddAsync(existing); + } + else + { + existing.Type = type; + } + + await db.SaveChangesAsync(); + + ev.AttendeeCount = await db.PlayerEventResponses + .CountAsync(x => x.PlayerEventId == eventId && x.Type == EventResponses.Yes); + + await db.SaveChangesAsync(); + return (existing, ev); + } + + [HttpPost("v2/rsvp/{eventId:long}"), Authorize] + public async Task> RespondToEventV2(long eventId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var result = await SetEventResponse(eventId, userId.Value, EventResponses.Yes); + if (result == null) return NotFound(); + + return Ok(ToEventResponse(result.Value.response)); + } + + [HttpPost("v1/broadcast"), Authorize] + [HttpPut("v1/broadcast"), Authorize] + public async Task BroadcastRoomInstance() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var fields = await ReadEventFieldsAsync(); + var eventId = GetEventLong(fields, "PlayerEventId", "playerEventId", "EventId", "eventId", "Id", "id") ?? 0; + if (eventId <= 0) eventId = await ReadEventIdFromRequestAsync(); + if (eventId <= 0) return BadRequest(new { error = "PlayerEventId is required." }); + + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + + if (ev.CreatorPlayerId != userId && !await db.IsDeveloper(userId.Value)) + return Forbid(); + + + long? roomInstanceId = null; + var rawInstance = GetEventField(fields, + "RoomInstanceId", "roomInstanceId", "BroadcastingRoomInstanceId", "broadcastingRoomInstanceId", + "BroadcastRoomInstanceId", "broadcastRoomInstanceId"); + if (!string.IsNullOrWhiteSpace(rawInstance) && + !string.Equals(rawInstance, "null", StringComparison.OrdinalIgnoreCase)) + { + if (!long.TryParse(rawInstance, out var parsedInstance) || parsedInstance <= 0) + return BadRequest(new { error = "RoomInstanceId must be a positive id or null." }); + roomInstanceId = parsedInstance; + } + + if (roomInstanceId.HasValue) + { + + + + if (!ev.IsMultiInstance) + return Ok(ToEventResult(ev, EventResultBroadcastNotMultiInstance)); + + var instance = await db.RoomInstances + .FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId.Value); + if (instance == null || instance.roomId != ev.RoomId) + return Ok(ToEventResult(ev, EventResultBroadcastNotMultiInstance)); + } + + ev.BroadcastingRoomInstanceId = roomInstanceId; + await db.SaveChangesAsync(); + + + + await PlayerEventNotifications.PushToEventInstancesAsync(db, ev); + + return Ok(ToEventResult(ev)); + } + + [HttpPost("v1/deleteResponse"), Authorize] + [HttpDelete("v1/deleteResponse"), Authorize] + public async Task DeleteEventResponse() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var fields = await ReadEventFieldsAsync(); + var eventId = GetEventLong(fields, "PlayerEventId", "playerEventId", "EventId", "eventId", "Id", "id") ?? 0; + if (eventId <= 0) eventId = await ReadEventIdFromRequestAsync(); + if (eventId <= 0) return BadRequest(new { error = "PlayerEventId is required." }); + + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + + var existing = await db.PlayerEventResponses + .FirstOrDefaultAsync(x => x.PlayerEventId == eventId && x.PlayerId == userId.Value); + if (existing != null) + { + db.PlayerEventResponses.Remove(existing); + await db.SaveChangesAsync(); + + ev.AttendeeCount = await db.PlayerEventResponses + .CountAsync(x => x.PlayerEventId == eventId && x.Type == EventResponses.Yes); + await db.SaveChangesAsync(); + } + + return Ok(ToEventResult(ev)); + } + + [HttpPost("v1/report"), Authorize] + public async Task ReportEvent() + { + var reporterId = User.GetUserId(); + if (reporterId == null) return Unauthorized(); + + var body = await ReadJsonBodyAsync(); + var eventId = body == null ? null : GetBodyLong(body.Value, "PlayerEventId", "playerEventId"); + if (eventId == null) + return Ok(new { success = false, error = "PlayerEventId is required" }); + + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) + return Ok(new { success = false, error = "No such event" }); + + int category = 0; + string? details = null; + if (body != null) + { + if (body.Value.TryGetProperty("ReportCategory", out var catRaw) && + long.TryParse(catRaw.ToString(), out var catParsed)) + category = (int)catParsed; + if (body.Value.TryGetProperty("Details", out var detailsRaw) && + detailsRaw.ValueKind == JsonValueKind.String) + details = detailsRaw.GetString(); + } + + return Ok(new { success = true, error = string.Empty }); + } + + [HttpPost("v1/bulkInvite"), Authorize] + public async Task BulkInvite() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var fields = await ReadEventFieldsAsync(); + var eventIdRaw = GetEventField(fields, "PlayerEventId", "playerEventId", "EventId", "eventId"); + if (!long.TryParse(eventIdRaw, out var eventId) || eventId <= 0) + return BadRequest(new { error = "PlayerEventId is required." }); + + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + + + + + + + if (!await CanInviteToEventAsync(ev, userId.Value)) + return StatusCode(StatusCodes.Status403Forbidden, new Dictionary + { + ["error"] = "That event belongs to another player.", + ["success"] = false, + ["value"] = null, + ["PlayerEventId"] = ev.PlayerEventId, + ["CreatorPlayerId"] = ev.CreatorPlayerId + }); + + var invitedPlayerIds = await ReadInvitedPlayerIdsAsync(); + if (invitedPlayerIds.Count == 0) + return BadRequest(new { error = "At least one invited player is required." }); + + var failedInvites = new List(); + + + var invitedPlayers = new List(); + var validPlayerIds = invitedPlayerIds + .Where(playerId => playerId > 0 && playerId != userId.Value) + .Distinct() + .ToList(); + var profiles = await db.Profiles.AsNoTracking() + .Where(profile => validPlayerIds.Contains(profile.accountId)) + .Select(profile => profile.accountId) + .ToListAsync(); + var existingResponses = await db.PlayerEventResponses + .Where(response => response.PlayerEventId == eventId && validPlayerIds.Contains(response.PlayerId)) + .ToDictionaryAsync(response => response.PlayerId); + + foreach (var playerId in invitedPlayerIds.Distinct()) + { + if (playerId <= 0 || playerId == userId.Value || !profiles.Contains(playerId)) + { + failedInvites.Add(new { PlayerId = playerId }); + continue; + } + + if (!existingResponses.ContainsKey(playerId)) + { + existingResponses[playerId] = new PlayerEventResponse + { + PlayerEventId = eventId, + PlayerId = playerId, + Type = EventResponses.Pending + }; + await db.PlayerEventResponses.AddAsync(existingResponses[playerId]); + invitedPlayers.Add(playerId); + } + else if (existingResponses[playerId].Type is not (EventResponses.Yes or EventResponses.Pending)) + { + existingResponses[playerId].Type = EventResponses.Pending; + } + } + + await db.SaveChangesAsync(); + + foreach (var playerId in invitedPlayers) + await PlayerEventNotifications.PushAsync(playerId, + PlayerEventNotifications.ResponseChanged(ev, existingResponses[playerId])); + + foreach (var playerId in validPlayerIds.Where(profiles.Contains)) + { + await Notify.SendMessage(playerId, new Message + { + FromPlayerId = userId.Value, + Type = MessageType.PlayerEventInvitation, + PlayerEventId = eventId + }); + } + + return Ok(new Dictionary + { + ["FailedInvites"] = failedInvites, + ["Result"] = 0 + }); + } + + private async Task CanInviteToEventAsync(PlayerEventDB ev, long userId) + { + if (ev.CreatorPlayerId == userId) return true; + + if (await db.PlayerEventResponses + .AnyAsync(x => x.PlayerEventId == ev.PlayerEventId && x.PlayerId == userId)) + return true; + + if (ev.ClubId is > 0) + { + var club = await ClubStore.GetAsync(db, ev.ClubId.Value); + if (club != null && ClubStore.GetMembershipType(club, userId) >= ClubMembershipTypes.Coowner) + return true; + } + + return !await db.Profiles.AnyAsync(x => x.accountId == ev.CreatorPlayerId); + } + + private async Task> ReadInvitedPlayerIdsAsync() + { + var ids = new List(); + if (Request.Body.CanSeek) Request.Body.Position = 0; + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var field in form.Where(field => field.Key.Equals("InvitedPlayerIds", StringComparison.OrdinalIgnoreCase) || + field.Key.Equals("InvitedPlayerIds[]", StringComparison.OrdinalIgnoreCase))) + { + foreach (var value in field.Value) + if (long.TryParse(value, out var id)) ids.Add(id); + } + return ids; + } + + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + if (Request.Body.CanSeek) Request.Body.Position = 0; + try + { + using var document = JsonDocument.Parse(body); + if (document.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var property in document.RootElement.EnumerateObject() + .Where(property => property.Name.Equals("InvitedPlayerIds", StringComparison.OrdinalIgnoreCase) || + property.Name.Equals("InvitedPlayerIds[]", StringComparison.OrdinalIgnoreCase))) + { + if (property.Value.ValueKind != JsonValueKind.Array) continue; + foreach (var value in property.Value.EnumerateArray()) + if (long.TryParse(value.ToString(), out var id)) ids.Add(id); + } + } + } + catch (JsonException) { } + return ids; + } + + [HttpGet("v1/event/{eventId}/responses"), Authorize] + [HttpGet("v1/{eventId:long}/responses"), Authorize] + public async Task>> GetResponses(long eventId) + { + if (eventId == -1) return Ok(Array.Empty()); + + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + + var responses = await db.PlayerEventResponses + .Where(x => x.PlayerEventId == eventId) + .ToListAsync(); + + return Ok(responses.Select(ToEventResponse).ToList()); + } + + [HttpGet("v1/-1/responses"), Authorize] + public IActionResult GetLegacyPlaceholderResponses() => Ok(Array.Empty()); + + [HttpGet("v1/event/{eventId}/attendees"), Authorize] + [HttpGet("v1/{eventId:long}/attendees"), Authorize] + public async Task>> GetAttendees(long eventId) + { + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + + var attendees = await db.PlayerEventResponses + .Where(x => x.PlayerEventId == eventId && x.Type == EventResponses.Yes) + .Select(x => x.PlayerId) + .ToListAsync(); + + return Ok(attendees); + } + + [HttpPost("v2/delete/{eventId:long}"), Authorize] + [HttpDelete("v2/delete/{eventId:long}"), Authorize] + [HttpPost("v2/delete"), Authorize] + [HttpDelete("v2/delete"), Authorize] + [HttpPost("v2/{eventId:long}/delete"), Authorize] + [HttpDelete("v2/{eventId:long}/delete"), Authorize] + public async Task DeleteEventV2(long eventId = 0) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var id = eventId > 0 ? eventId : await ReadEventIdFromRequestAsync(); + if (id <= 0) return NotFound(); + + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == id); + + if (ev != null && ev.CreatorPlayerId != userId) return Forbid(); + + if (ev == null) + { + await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Deleted(id)); + return Ok(ToEventResult(AlreadyGone(id, userId.Value))); + } + + await DeleteEventRow(ev); + + await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Deleted(ev.PlayerEventId)); + + return Ok(ToEventResult(ev)); + } + + private static PlayerEventDB AlreadyGone(long eventId, long creatorPlayerId) => new() + { + PlayerEventId = eventId, + Name = string.Empty, + Description = string.Empty, + ImageName = string.Empty, + CreatorPlayerId = creatorPlayerId, + TagsJson = "[]" + }; + + private async Task ReadEventIdFromRequestAsync() + { + foreach (var key in new[] { "PlayerEventId", "playerEventId", "EventId", "eventId", "Id", "id" }) + if (Request.Query.TryGetValue(key, out var query) && + long.TryParse(query.FirstOrDefault(), out var queryId) && queryId > 0) + return queryId; + + if (Request.ContentLength is > 0) + { + try + { + Request.EnableBuffering(); + var fields = await ReadEventFieldsAsync(); + var bodyId = GetEventLong(fields, "PlayerEventId", "playerEventId", "EventId", "eventId", "Id", "id") ?? 0; + Request.Body.Position = 0; + if (bodyId > 0) return bodyId; + } + catch + { + try { Request.Body.Position = 0; } catch { } + } + } + + return 0; + } + + [HttpDelete("v1/event/{eventId}"), Authorize] + [HttpDelete("v1/{eventId:long}"), Authorize] + public async Task DeleteEvent(long eventId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); + if (ev == null) return NotFound(); + + if (ev.CreatorPlayerId != userId) + return Forbid(); + + await DeleteEventRow(ev); + + await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Deleted(ev.PlayerEventId)); + + return Ok(); + } + + private async Task DeleteEventRow(PlayerEventDB ev) + { + var responses = await db.PlayerEventResponses.Where(x => x.PlayerEventId == ev.PlayerEventId).ToListAsync(); + db.PlayerEventResponses.RemoveRange(responses); + db.PlayerEvents.Remove(ev); + await db.SaveChangesAsync(); + } + + private async Task> ReadEventFieldsAsync() + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + Request.EnableBuffering(); + + foreach (var kv in Request.Query) + fields[kv.Key] = kv.Value.ToString(); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var kv in form) + fields[kv.Key] = kv.Value.ToString(); + if (Request.Body.CanSeek) Request.Body.Position = 0; + return fields; + } + + if (Request.ContentLength == 0) return fields; + + try + { + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + if (Request.Body.CanSeek) Request.Body.Position = 0; + if (string.IsNullOrWhiteSpace(body)) return fields; + + var trimmed = body.TrimStart(); + if (trimmed.StartsWith("{", StringComparison.Ordinal)) + { + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in doc.RootElement.EnumerateObject()) + { + fields[prop.Name] = prop.Value.ValueKind == JsonValueKind.String + ? prop.Value.GetString() ?? "" + : prop.Value.ToString(); + } + } + + if (doc.RootElement.TryGetProperty("PlayerEvent", out var nested) && + nested.ValueKind == JsonValueKind.Object) + { + foreach (var prop in nested.EnumerateObject()) + { + if (fields.ContainsKey(prop.Name)) continue; + fields[prop.Name] = prop.Value.ValueKind == JsonValueKind.String + ? prop.Value.GetString() ?? "" + : prop.Value.ToString(); + } + } + } + } + catch + { + } + + return fields; + } + + private async Task ReadJsonBodyAsync() + { + if (Request.ContentLength is not > 0) return null; + + try + { + using var reader = new StreamReader(Request.Body); + var body = await reader.ReadToEndAsync(); + if (string.IsNullOrWhiteSpace(body)) return null; + + using var doc = JsonDocument.Parse(body); + return doc.RootElement.Clone(); + } + catch + { + return null; + } + } + + private async Task ConsumeRequestBodyAsync() + { + try + { + if (Request.HasFormContentType) + { + await Request.ReadFormAsync(); + } + else if (Request.ContentLength is > 0) + { + using var reader = new StreamReader(Request.Body); + await reader.ReadToEndAsync(); + } + } + catch + { + } + } + + private static long? GetBodyLong(JsonElement body, params string[] names) + { + foreach (var name in names) + { + if (!body.TryGetProperty(name, out var value)) continue; + if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var number)) + return number; + if (value.ValueKind == JsonValueKind.String && + long.TryParse(value.GetString(), out var parsed)) + return parsed; + } + return null; + } + + private static string? GetEventField(Dictionary fields, params string[] names) + { + foreach (var name in names) + { + if (fields.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)) + return value; + } + return null; + } + + private static long? GetEventLong(Dictionary fields, params string[] names) + { + var raw = GetEventField(fields, names); + return long.TryParse(raw, out var value) ? value : null; + } + + private static bool TryParseNullableEventId(string raw, out long? value) + { + if (string.IsNullOrWhiteSpace(raw) || string.Equals(raw, "null", StringComparison.OrdinalIgnoreCase)) + { + value = null; + return true; + } + + if (long.TryParse(raw, out var parsed) && parsed > 0) + { + value = parsed; + return true; + } + + value = null; + return false; + } + + private static int? GetEventInt(Dictionary fields, params string[] names) + { + var raw = GetEventField(fields, names); + return int.TryParse(raw, out var value) ? value : null; + } + + private static bool ParseEventBoolean(string? raw) + => bool.TryParse(raw, out var value) && value; + + private static DateTime ParseEventDate(string? raw, DateTime fallback) + { + if (string.IsNullOrWhiteSpace(raw)) return fallback; + return DateTime.TryParse(raw, out var parsed) ? EventClock.FromPostedTime(parsed) : fallback; + } + + private static Accessibility ParseEventAccessibility(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return Accessibility.Public; + + if (int.TryParse(raw, out var intValue) && Enum.IsDefined(typeof(Accessibility), intValue)) + return (Accessibility)intValue; + + return raw.Trim().ToLowerInvariant() switch + { + "public" => Accessibility.Public, + "private" => Accessibility.Private, + "unlisted" or "unlised" => Accessibility.Unlised, + _ => Accessibility.Public + }; + } + } +} diff --git a/Server/PlayerEvents/PlayerEvent.cs b/Server/PlayerEvents/PlayerEvent.cs new file mode 100644 index 0000000..fd155e6 --- /dev/null +++ b/Server/PlayerEvents/PlayerEvent.cs @@ -0,0 +1,51 @@ +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.Events +{ + public class PlayerEvent + { + public long PlayerEventId { get; set; } + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public string ImageName { get; set; } = null!; + + + + [JsonConverter(typeof(EventClock.Json))] + public DateTime StartTime { get; set; } + + [JsonConverter(typeof(EventClock.Json))] + public DateTime EndTime { get; set; } + public long CreatorPlayerId { get; set; } + public int AttendeeCount { get; set; } + public long RoomId { get; set; } + public long? SubRoomId { get; set; } + public Accessibility Accessibility { get; set; } + + public long? ClubId { get; set; } + public int State { get; set; } = 0; + public List Tags { get; set; } = []; + + [JsonConverter(typeof(EventClock.Json))] + public DateTime CreatedAt { get; set; } + + public int PlayerCount { get; set; } + public bool IsFull { get; set; } + + public bool IsMultiInstance { get; set; } = false; + public bool SupportMultiInstanceRoomChat { get; set; } = false; + public int DefaultBroadcastPermissions { get; set; } = 0; + public int CanRequestBroadcastPermissions { get; set; } = 0; + + + + public long? BroadcastingRoomInstanceId { get; set; } + } + + public class PlayerEventTag + { + public string Tag { get; set; } = string.Empty; + public int Type { get; set; } + } +} diff --git a/Server/PlayerEvents/PlayerEventDB.cs b/Server/PlayerEvents/PlayerEventDB.cs new file mode 100644 index 0000000..f52ff05 --- /dev/null +++ b/Server/PlayerEvents/PlayerEventDB.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.Events +{ + public class PlayerEventDB + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long PlayerEventId { get; set; } + + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public string ImageName { get; set; } = ""; + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public long CreatorPlayerId { get; set; } + public int AttendeeCount { get; set; } + public long RoomId { get; set; } + public long? ClubId { get; set; } + public long? SubRoomId { get; set; } + public bool IsMultiInstance { get; set; } + public bool SupportMultiInstanceRoomChat { get; set; } + public int DefaultBroadcastPermissions { get; set; } + public int CanRequestBroadcastPermissions { get; set; } + + + + + + public long? BroadcastingRoomInstanceId { get; set; } + + public string TagsJson { get; set; } = "[]"; + + public Accessibility Accessibility { get; set; } + + + + + public int ModerationState { get; set; } = 0; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Server/PlayerEvents/PlayerEventNotifications.cs b/Server/PlayerEvents/PlayerEventNotifications.cs new file mode 100644 index 0000000..3192917 --- /dev/null +++ b/Server/PlayerEvents/PlayerEventNotifications.cs @@ -0,0 +1,126 @@ +using RadicadeServer.API.Notifications; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.RecNet.Events +{ + public static class PlayerEventNotifications + { + public static async Task PushToEventInstancesAsync(RecDBContext db, PlayerEventDB ev) + { + var audienceIds = await ( + from presence in db.Presences.AsNoTracking() + join instance in db.RoomInstances.AsNoTracking() + on presence.roomInstance equals (long?)instance.roomInstanceId + where presence.isOnline && instance.eventId == ev.PlayerEventId + select presence.playerId).ToListAsync(); + var update = Updated(ev); + foreach (var recipientId in audienceIds.Append(ev.CreatorPlayerId).Distinct()) + await PushAsync(recipientId, update); + } + + public static Notification Created(PlayerEventDB ev) => new() + { + Id = NotificationType.PlayerEventCreated, + Msg = Payload(ev) + }; + + public static Notification Updated(PlayerEventDB ev) => new() + { + Id = NotificationType.PlayerEventUpdated, + Msg = Payload(ev) + }; + + public static Notification Deleted(long playerEventId) => new() + { + Id = NotificationType.PlayerEventDeleted, + Msg = new Dictionary + { + ["playerEventId"] = playerEventId, + ["PlayerEventId"] = playerEventId + } + }; + + public static Notification ResponseChanged(PlayerEventDB ev, PlayerEventResponse response) => new() + { + Id = NotificationType.PlayerEventResponseChanged, + Msg = new Dictionary + { + ["PlayerEvent"] = Payload(ev), + ["PlayerEventResponse"] = new Dictionary + { + ["PlayerEventResponseId"] = response.PlayerEventResponseId, + ["PlayerEventId"] = response.PlayerEventId, + ["PlayerId"] = response.PlayerId, + ["CreatedAt"] = response.CreatedAt, + ["Type"] = (int)response.Type + } + } + }; + + public static Dictionary Payload(PlayerEventDB ev) + { + var tags = ParseTags(ev.TagsJson); + + return new Dictionary + { + ["tags"] = tags.Select(tag => new Dictionary + { + ["tag"] = tag.Tag, + ["type"] = tag.Type + }).ToArray(), + ["playerEventId"] = ev.PlayerEventId, + ["creatorPlayerId"] = ev.CreatorPlayerId, + ["roomId"] = ev.RoomId, + ["subRoomId"] = ev.SubRoomId, + ["clubId"] = ev.ClubId, + ["name"] = ev.Name, + ["description"] = ev.Description, + + + ["imageName"] = string.IsNullOrWhiteSpace(ev.ImageName) ? DefaultImageName : ev.ImageName, + + + ["startTime"] = EventClock.ToWire(ev.StartTime), + ["endTime"] = EventClock.ToWire(ev.EndTime), + ["attendeeCount"] = ev.AttendeeCount, + ["accessibility"] = (int)ev.Accessibility, + ["isMultiInstance"] = ev.IsMultiInstance, + ["supportMultiInstanceRoomChat"] = ev.SupportMultiInstanceRoomChat, + ["defaultBroadcastPermissions"] = ev.DefaultBroadcastPermissions, + ["canRequestBroadcastPermissions"] = ev.CanRequestBroadcastPermissions, + ["broadcastingRoomInstanceId"] = ev.BroadcastingRoomInstanceId + }; + } + + public static async Task PushAsync(long accountId, Notification frame) + { + try + { + await Notify.SendNotify(accountId, frame); + } + catch (Exception ex) + { + Console.WriteLine($"[Events] pushing {Notify.WireId(frame.Id)} to account {accountId} failed: {ex.Message}"); + } + } + + private const string DefaultImageName = "default"; + + private static List ParseTags(string? tagsJson) + { + if (string.IsNullOrWhiteSpace(tagsJson)) return []; + + try + { + return System.Text.Json.JsonSerializer.Deserialize>(tagsJson) ?? []; + } + catch + { + return []; + } + } + } +} diff --git a/Server/PlayerEvents/PlayerEventQueries.cs b/Server/PlayerEvents/PlayerEventQueries.cs new file mode 100644 index 0000000..9e0da91 --- /dev/null +++ b/Server/PlayerEvents/PlayerEventQueries.cs @@ -0,0 +1,34 @@ +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Events; + +namespace RadicadeServer.API.Events +{ + public static class PlayerEventQueries + { + public static List Search(IEnumerable source, string? term, DateTime now) + { + var results = source + .Where(x => x.Accessibility == Accessibility.Public && x.EndTime > now) + .OrderByDescending(x => x.StartTime) + .ToList(); + + var trimmed = (term ?? string.Empty).Trim(); + if (trimmed.Length == 0) + return results; + + return results + .Where(x => + x.Name.Contains(trimmed, StringComparison.OrdinalIgnoreCase) || + (x.Description ?? string.Empty).Contains(trimmed, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + public static List InRoom(IEnumerable source, long roomId) + { + return source + .Where(x => x.RoomId == roomId) + .OrderByDescending(x => x.StartTime) + .ToList(); + } + } +} diff --git a/Server/PlayerEvents/PlayerEventResponse.cs b/Server/PlayerEvents/PlayerEventResponse.cs new file mode 100644 index 0000000..fd3b1a0 --- /dev/null +++ b/Server/PlayerEvents/PlayerEventResponse.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.Events +{ + + + + + public class PlayerEventResponse + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long PlayerEventResponseId { get; set; } + + public long PlayerEventId { get; set; } + public long PlayerId { get; set; } + public EventResponses Type { get; set; } = EventResponses.Pending; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Server/PlayerEvents/RbObjective.cs b/Server/PlayerEvents/RbObjective.cs new file mode 100644 index 0000000..03f568b --- /dev/null +++ b/Server/PlayerEvents/RbObjective.cs @@ -0,0 +1,12 @@ +using RadicadeServer.RecNet.Enums.Challenges; +using RadicadeServer.RecNet.Progression.Objectives; + +namespace Rubidium2018.Utils.Objectives +{ + public interface RbObjective + { + private static ObjectiveTypes[] AllowedTypes() { return []; } + public static ObjectiveTypes GetRandomObjectiveType() { return ObjectiveTypes.Default; } + public static Objective CreateRandomObjective() { return null!; } + } +} diff --git a/Server/PlayerEvents/RbPresetDailys.cs b/Server/PlayerEvents/RbPresetDailys.cs new file mode 100644 index 0000000..7c8cc78 --- /dev/null +++ b/Server/PlayerEvents/RbPresetDailys.cs @@ -0,0 +1,46 @@ +using RadicadeServer.RecNet.Enums.Challenges; +using RadicadeServer.RecNet.Progression.Objectives; + +namespace Rubidium2018.Utils.Objectives +{ + public class RbPresetDailys : RbObjective + { + private static ObjectiveTypes[] DisallowedTypes = { + ObjectiveTypes.AllDailyObjectives, + ObjectiveTypes.FirstSessionOfDay, + ObjectiveTypes.NUX_AllPunchcardObjectives, + ObjectiveTypes.NUX_PunchcardObjective, + ObjectiveTypes.DailyObjective3, + ObjectiveTypes.DailyObjective2, + ObjectiveTypes.DailyObjective1, + ObjectiveTypes.OOBE_GoToActivity, + ObjectiveTypes.OOBE_GoToLockerRoom, + ObjectiveTypes.OOBE_FinishActivity, + ObjectiveTypes.CreateACustomRoom, + ObjectiveTypes.Default + }; + + private static ObjectiveTypes[] AllowedTypes() + { + var allowed = Enum.GetValues(typeof(ObjectiveTypes)).Cast().Where(o => !DisallowedTypes.Contains(o)).ToArray(); + return allowed; + } + + public static ObjectiveTypes GetRandomObjectiveTypes() + { + var allowed = AllowedTypes(); + return allowed[Random.Shared.Next(allowed.Length)]; + } + + public static Objective CreateRandomObjective() + { + var ObjectiveTypes = GetRandomObjectiveTypes(); + return new Objective + { + type = ObjectiveTypes, + score = ((int)ObjectiveTypes >= 100) ? 3 : 1, + xp = 200 + }; + } + } +} diff --git a/Server/PlayerReporting/BlockDetails.cs b/Server/PlayerReporting/BlockDetails.cs new file mode 100644 index 0000000..dc4fff9 --- /dev/null +++ b/Server/PlayerReporting/BlockDetails.cs @@ -0,0 +1,49 @@ +using RadicadeServer.RecNet.Enums.Moderation; + +namespace RadicadeServer.RecNet.Moderation +{ + public class BlockDetails + { + public int Duration { get; set; } + + public long GameSessionId { get; set; } + public long PlayerIdReporter { get; set; } + + public bool IsBan { get; set; } + public bool IsHostKick { get; set; } + + public bool IsWarning { get; set; } + + public DateTime? TimeoutStartedAt { get; set; } + + public string Message { get; set; } = string.Empty; + + public ReportCategory ReportCategory { get; set; } + + public string VoteKickReason { get; set; } = string.Empty; + + public int? BannedUntil { get; set; } + + + public string DisplayReason { get; set; } = string.Empty; + + + public string BanType { get; set; } = string.Empty; + + + public bool IsVoiceModAutoban { get; set; } + + public string? ModeratorNote { get; set; } + + + + + + + + + public string BanAppealCode { get; set; } = string.Empty; + + public string BanAppealText { get; set; } = string.Empty; + } +} diff --git a/Server/PlayerReporting/ContentReportAPI.cs b/Server/PlayerReporting/ContentReportAPI.cs new file mode 100644 index 0000000..502ad82 --- /dev/null +++ b/Server/PlayerReporting/ContentReportAPI.cs @@ -0,0 +1,292 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.Discord.Webhooks; + +namespace RadicadeServer.API.Moderation; + + + + + + + + + + + + + +public class ContentReportAPI : Controller +{ + private readonly RecDBContext db; + + public ContentReportAPI(RecDBContext dbContext) { + db = dbContext; + } + + [HttpPost("api/inventions/v1/report"), Authorize] + [HttpPost("api/inventions/v2/report"), Authorize] + public Task ReportInvention() => HandleReport("Invention", + new[] { "InventionId", "inventionId", "Id", "id", "TargetId", "targetId", "ReporteeId", "reporteeId" }, + async targetId => + { + var invention = await db.Inventions.AsNoTracking().FirstOrDefaultAsync(x => x.InventionId == targetId); + return invention == null ? null : (OwnerId: invention.CreatorPlayerId, TargetName: invention.Name); + }); + + [HttpPost("api/rooms/v1/report"), Authorize] + [HttpPost("api/rooms/v2/report"), Authorize] + public Task ReportRoom() => HandleReport("Room", + new[] { "RoomId", "roomId", "Id", "id", "TargetId", "targetId", "ReporteeId", "reporteeId" }, + async targetId => + { + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == targetId); + return room == null ? null : (OwnerId: room.CreatorAccountId, TargetName: room.Name); + }); + + [HttpPost("api/clubs/v1/report"), Authorize] + [HttpPost("api/clubs/v2/report"), Authorize] + [HttpPost("api/clubreporting/v1/report"), Authorize] + public Task ReportClub() => HandleReport("Club", + new[] { "ClubId", "clubId", "Id", "id", "TargetId", "targetId", "ReporteeId", "reporteeId" }, + _ => Task.FromResult<(long OwnerId, string TargetName)?>(null)); + + [HttpPost("api/board/v1/report"), Authorize] + [HttpPost("api/board/v2/report"), Authorize] + [HttpPost("api/communityboard/v1/report"), Authorize] + [HttpPost("api/communityboard/v2/report"), Authorize] + public Task ReportBoard() => HandleReport("Board", + new[] { "BoardId", "boardId", "PostId", "postId", "CommentId", "commentId", "Id", "id", "TargetId", "targetId" }, + _ => Task.FromResult<(long OwnerId, string TargetName)?>(null)); + + [HttpPost("api/playerevents/v2/report"), Authorize] + [HttpPost("api/events/v1/report"), Authorize] + [HttpPost("api/events/v2/report"), Authorize] + public Task ReportEvent() => HandleReport("Event", + new[] { "EventId", "eventId", "PlayerEventId", "playerEventId", "Id", "id", "TargetId", "targetId", "ReporteeId", "reporteeId" }, + async targetId => + { + var ev = await db.PlayerEvents.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerEventId == targetId); + return ev == null ? null : (OwnerId: ev.CreatorPlayerId, TargetName: ev.Name); + }); + + private async Task HandleReport( + string reportType, + string[] idFieldNames, + Func> resolveTarget) + { + var reporterId = User.GetUserId(); + if (reporterId == null) return Unauthorized(); + + var fields = await ReadFieldsAsync(); + + var targetId = GetLong(fields, idFieldNames); + + long reportedId = 0; + string targetName = ""; + if (targetId.HasValue) + { + var resolved = await resolveTarget(targetId.Value); + if (resolved.HasValue) + { + reportedId = resolved.Value.OwnerId; + targetName = resolved.Value.TargetName; + } + else + { + + + + targetName = $"{reportType} {targetId.Value}"; + } + } + + + + + + return await SubmitReport(reportType, targetId, targetName, reportedId, reporterId.Value, fields); + } + + [HttpPost("api/customAvatarItems/v1/{customAvatarItemId:guid}/report"), Authorize] + public async Task ReportCustomAvatarItem(Guid customAvatarItemId) + { + var reporterId = User.GetUserId(); + if (reporterId == null) return Unauthorized(); + + var fields = await ReadFieldsAsync(); + + var item = await db.CustomAvatarItems.AsNoTracking() + .FirstOrDefaultAsync(x => x.CustomAvatarItemId == customAvatarItemId); + + return await SubmitReport( + "CustomAvatarItem", + null, + item?.Name ?? $"Custom Avatar Item {customAvatarItemId}", + item?.CreatorAccountId ?? 0, + reporterId.Value, + fields); + } + + private async Task SubmitReport( + string reportType, + long? targetId, + string targetName, + long reportedId, + long reporterId, + Dictionary fields) + { + var reason = GetField(fields, "Reason", "reason", "ReportReason", "reportReason") ?? ""; + var details = GetField(fields, "Details", "details", "Description", "description", "Message", "message", "Text", "text") ?? ""; + var category = ParseCategory(fields); + var roomId = GetLong(fields, "RoomId", "roomId") ?? 0; + + if (reportedId > 0 && reportedId != reporterId) + { + var reputation = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == reportedId); + if (reputation == null) + { + reputation = new Reputation { AccountId = reportedId }; + await db.Reputations.AddAsync(reputation); + } + reputation.ReportCount += 1; + await db.SaveChangesAsync(); + } + + AdminPanelAPI.AddModerationReport(reporterId, reportedId, reason, details, category, roomId, + reportType, targetId, targetName); + + ReportDiscord.SendAsync(WebhookConfig.ReportsUrl, $"🚩 {reportType} Report", + new DiscordPayload + { + Title = $"{reportType} Report", + ThumbnailUrl = WebhookImages.ForReport(reportType) ?? "", + Color = 0xF0B232, + Fields = new List + { + new() { Name = "Report Type", Value = reportType, Inline = true }, + new() { Name = "Reporter", Value = reporterId.ToString(), Inline = true }, + new() { Name = "Reported Player", Value = reportedId > 0 ? reportedId.ToString() : "Unknown", Inline = true }, + new() { Name = "Target", Value = targetId.HasValue ? $"{targetName} ({targetId})" : targetName, Inline = true }, + new() { Name = "Category", Value = category.ToString(), Inline = true }, + new() { Name = "Room", Value = roomId.ToString(), Inline = true }, + new() { Name = "Reason", Value = reason, Inline = false }, + new() { Name = "Details", Value = details, Inline = false } + }, + TimestampUtc = DateTime.UtcNow.ToString("O") + }); + + return Ok(new OkResponse { Success = true }); + } + + + + + + + private async Task> ReadFieldsAsync() + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var kv in Request.Query) + fields[kv.Key] = kv.Value.ToString(); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var kv in form) + fields[kv.Key] = kv.Value.ToString(); + return fields; + } + + if (Request.ContentLength is not > 0) return fields; + + try + { + using var reader = new StreamReader(Request.Body); + var body = await reader.ReadToEndAsync(); + if (string.IsNullOrWhiteSpace(body)) return fields; + + var trimmed = body.TrimStart(); + if (trimmed.StartsWith("{", StringComparison.Ordinal)) + { + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in doc.RootElement.EnumerateObject()) + { + fields[prop.Name] = prop.Value.ValueKind == JsonValueKind.String + ? prop.Value.GetString() ?? "" + : prop.Value.ToString(); + } + } + } + else if (trimmed.Contains('=')) + { + + foreach (var pair in body.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var eq = pair.IndexOf('='); + if (eq < 0) + { + fields[Uri.UnescapeDataString(pair)] = ""; + continue; + } + var key = Uri.UnescapeDataString(pair.Substring(0, eq)); + var value = Uri.UnescapeDataString(pair.Substring(eq + 1)); + fields[key] = value; + } + } + } + catch + { + + + } + + return fields; + } + + private static string? GetField(Dictionary fields, params string[] names) + { + foreach (var name in names) + { + if (fields.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)) + return value; + } + return null; + } + + private static long? GetLong(Dictionary fields, params string[] names) + { + var raw = GetField(fields, names); + if (raw == null) return null; + if (long.TryParse(raw, out var value)) return value; + + + var digits = new string(raw.Where(char.IsDigit).ToArray()); + return long.TryParse(digits, out value) ? value : null; + } + + private static ReportCategory ParseCategory(Dictionary fields) + { + var raw = GetField(fields, "ReportCategory", "reportCategory", "Category", "category", "Reason", "reason", "ReportReason", "reportReason"); + if (raw == null) return ReportCategory.Misc; + + if (int.TryParse(raw, out var intValue) && Enum.IsDefined(typeof(ReportCategory), intValue)) + return (ReportCategory)intValue; + + if (Enum.TryParse(raw, ignoreCase: true, out var parsed)) + return parsed; + + return ReportCategory.Misc; + } +} diff --git a/Server/PlayerReporting/Hile.cs b/Server/PlayerReporting/Hile.cs new file mode 100644 index 0000000..beca57b --- /dev/null +++ b/Server/PlayerReporting/Hile.cs @@ -0,0 +1,17 @@ +namespace RadicadeServer.RecNet.Moderation +{ + public enum HileType + { + Obcured = 0, + Time = 1, + Inject = 2, + GiftCount = 3, + Engine = 4 + } + + public class Hile + { + public string Message { get; set; } = string.Empty; + public HileType Type { get; set; } + } +} diff --git a/Server/PlayerReporting/HileAPI.cs b/Server/PlayerReporting/HileAPI.cs new file mode 100644 index 0000000..9bb3ec3 --- /dev/null +++ b/Server/PlayerReporting/HileAPI.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.RecNet.Moderation; + +namespace RadicadeServer.API.Moderation +{ + [Route("api/PlayerReporting/v1/")] + public class HileAPI : Controller + { + + public HileAPI() { + } + + [HttpPost("hile")] + public IActionResult PostHile([FromForm] Hile hile) + { + + var body = Environment.GetEnvironmentVariable("RADICADE_HILE_BODY") ?? "ok-true"; + switch (body) + { + case "ok-false": + return Ok(false); + case "nocontent": + return NoContent(); + case "servererror": + return StatusCode(500); + default: + return Ok(true); + } + } + } +} diff --git a/Server/PlayerReporting/IsPureResponse.cs b/Server/PlayerReporting/IsPureResponse.cs new file mode 100644 index 0000000..49e2567 --- /dev/null +++ b/Server/PlayerReporting/IsPureResponse.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Moderation +{ + public class IsPureResponse + { + public bool IsPure { get; set; } + } +} diff --git a/Server/PlayerReporting/PlayerReportingAPI.cs b/Server/PlayerReporting/PlayerReportingAPI.cs new file mode 100644 index 0000000..143b331 --- /dev/null +++ b/Server/PlayerReporting/PlayerReportingAPI.cs @@ -0,0 +1,1010 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.Utils; +using RadicadeServer.Discord.Webhooks; + +namespace RadicadeServer.API.Moderation +{ + public class CreateReportRequest + { + public long PlayerIdReported { get; set; } + public ReportCategory ReportCategory { get; set; } + public string Details { get; set; } = string.Empty; + public string HeightReporter { get; set; } = string.Empty; + public string? HeightReported { get; set; } + public long RoomId { get; set; } + } + + public class VoteKickRequest + { + public long PlayerId { get; set; } + public bool Response { get; set; } + public long GameSessionId { get; set; } + + + + public string Reason { get; set; } = string.Empty; + + + + public KickReason? ReportCategory { get; set; } + } + + public class KickFromEventRequest + { + public long PlayerId { get; set; } + public long GameSessionId { get; set; } + public string Response { get; set; } = string.Empty; + } + + public class InstantKickRequest + { + public long PlayerId { get; set; } + public long GameSessionId { get; set; } + + + + public string Reason { get; set; } = string.Empty; + } + + public class CreateWarningRequest + { + public long WarnedPlayerId { get; set; } + public ReportCategory ReportCategory { get; set; } + public string? DisplayReason { get; set; } + public string? ModeratorNote { get; set; } + } + + [Route("api/PlayerReporting/")] + public class PlayerReportingAPI : Controller + { + private readonly RecDBContext db; + + private const int VoteKickThreshold = 3; + private const int AutomaticTimeoutReportThreshold = 5; + + + private static readonly TimeSpan KickDuration = TimeSpan.FromMinutes(15); + + + + + private sealed class VoteKickState + { + public HashSet Voters { get; } = new(); + public string Reason { get; set; } = string.Empty; + } + + private static readonly ConcurrentDictionary<(long GameSessionId, long PlayerId), VoteKickState> _voteKicks = new(); + + public PlayerReportingAPI(RecDBContext dbContext) { + db = dbContext; + } + + + + + + + + + + + private static readonly string[] PlayerIdAliases = + { "PlayerId", "playerId", "PlayerID", "playerID", "Player_Id", "player_id", + "PlayerIds", "playerIds", + "PlayerIdReported", "playerIdReported", "ReportedPlayerId", "reportedPlayerId", + "TargetPlayerId", "targetPlayerId", "TargetPlayer", "targetPlayer", + "TargetId", "targetId", "VictimId", "victimId", "KickedPlayerId", "kickedPlayerId", + "OffendingPlayerId", "offendingPlayerId", "UserId", "userId", "Id", "id" }; + + private static readonly string[] ReasonAliases = + { "Reason", "reason", "Details", "details", "Message", "message" }; + + + + + private static string? LookupKickField(IFormCollection? form, JsonNode? node, string key, + IQueryCollection? query = null, RouteValueDictionary? route = null) + { + if (route != null && route.TryGetValue(key, out var routeValue) && routeValue != null) + return routeValue.ToString(); + + if (query != null && query.TryGetValue(key, out var queryValue)) + return queryValue.FirstOrDefault(); + + if (form != null) + { + + + foreach (var formKey in form.Keys) + { + if (string.Equals(formKey, key, StringComparison.OrdinalIgnoreCase)) + return form[formKey].ToString(); + } + } + return LookupJsonRecursive(node, key); + } + + + + + private static string? LookupJsonRecursive(JsonNode? node, string key) + { + if (node is JsonObject obj) + { + foreach (var kvp in obj) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + if (kvp.Value is JsonArray array) + return array.Count > 0 ? array[0]?.ToString() : null; + return kvp.Value?.ToString(); + } + } + foreach (var kvp in obj) + { + var found = LookupJsonRecursive(kvp.Value, key); + if (found != null) return found; + } + } + else if (node is JsonArray arr) + { + foreach (var item in arr) + { + var found = LookupJsonRecursive(item, key); + if (found != null) return found; + } + } + return null; + } + + private static long ScanForPlayerId(JsonNode? node) + { + if (node is JsonValue bare && long.TryParse(bare.ToString(), out var bareId)) + return bareId; + + if (node is JsonArray array) + foreach (var entry in array) + if (entry is JsonValue value && long.TryParse(value.ToString(), out var arrayId) && arrayId > 0) + return arrayId; + + if (node is not JsonObject obj) return 0; + + foreach (var pair in obj) + { + if (pair.Key.Contains("player", StringComparison.OrdinalIgnoreCase) && + long.TryParse(pair.Value?.ToString(), out var named) && named > 0) + return named; + } + + foreach (var pair in obj) + { + var nested = ScanForPlayerId(pair.Value); + if (nested > 0) return nested; + } + + return 0; + } + + private static T BindKickBody(IFormCollection? form, JsonNode? node, + IQueryCollection? query = null, RouteValueDictionary? route = null) where T : class, new() + { + var result = new T(); + var idProperty = typeof(T).GetProperty("PlayerId"); + if (idProperty != null) + { + long playerId = 0; + foreach (var alias in PlayerIdAliases) + { + var raw = LookupKickField(form, node, alias, query, route); + if (!string.IsNullOrWhiteSpace(raw) && long.TryParse(raw, out playerId)) + break; + playerId = 0; + } + + if (playerId <= 0) playerId = ScanForPlayerId(node); + + if (playerId <= 0) + { + var text = LookupKickField(form, node, "Reason") + ?? LookupKickField(form, node, "reason") + ?? LookupKickField(form, node, "Details") + ?? LookupKickField(form, node, "Message"); + if (!string.IsNullOrWhiteSpace(text)) + { + var m = System.Text.RegularExpressions.Regex.Match(text, @"\((\d+)\)\s*$"); + if (m.Success && long.TryParse(m.Groups[1].Value, out var embedded)) + playerId = embedded; + } + } + + if (playerId > 0) idProperty.SetValue(result, playerId); + } + + var reasonProperty = typeof(T).GetProperty("Reason"); + if (reasonProperty != null) + { + foreach (var alias in ReasonAliases) + { + var raw = LookupKickField(form, node, alias, query, route); + if (!string.IsNullOrWhiteSpace(raw)) + { + reasonProperty.SetValue(result, raw); + break; + } + } + } + + foreach (var property in typeof(T).GetProperties()) + { + if (property == idProperty || property == reasonProperty || property.GetSetMethod() == null) + continue; + var raw = LookupKickField(form, node, property.Name, query, route); + if (string.IsNullOrWhiteSpace(raw)) continue; + try + { + var converted = System.ComponentModel.TypeDescriptor + .GetConverter(property.PropertyType) + .ConvertFromInvariantString(raw); + if (converted != null) property.SetValue(result, converted); + } + catch { } + } + return result; + } + + private async Task ReadKickBodyAsync(string endpoint) where T : class, new() + { + var query = Request?.Query; + var route = RouteData?.Values; + + if (Request!.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + return BindKickBody(form, null, query, route); + } + + using var reader = new StreamReader(Request.Body); + var json = await reader.ReadToEndAsync(); + + if (string.IsNullOrWhiteSpace(json)) return BindKickBody(null, null, query, route); + try + { + return BindKickBody(null, JsonNode.Parse(json), query, route); + } + catch + { + return BindKickBody(null, null, query, route); + } + } + + private static readonly string[] WarningStaffRoles = { "moderator", "developer" }; + + private static Dictionary WarningEnvelope(bool success, string error) => new() + { + ["Success"] = success, + ["success"] = success, + ["Error"] = error, + ["error"] = error, + ["Message"] = error, + }; + + [HttpPost("/api/playerwarnings"), Authorize] + public async Task CreateWarning() + { + var request = await ReadKickBodyAsync("createWarning"); + var moderatorId = User.GetUserId(); + if (moderatorId == null) + return Unauthorized(WarningEnvelope(false, "Unauthorized")); + + var moderator = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == moderatorId.Value); + if (moderator == null || !WarningStaffRoles.Any(role => moderator.HasRole(role))) + { + return StatusCode(StatusCodes.Status403Forbidden, WarningEnvelope(false, "Forbidden")); + } + + if (request.WarnedPlayerId <= 0) + return BadRequest(WarningEnvelope(false, "WarnedPlayerId is required")); + + var displayReason = string.IsNullOrWhiteSpace(request.DisplayReason) ? null : request.DisplayReason.Trim(); + var moderatorNote = string.IsNullOrWhiteSpace(request.ModeratorNote) ? null : request.ModeratorNote.Trim(); + + var warning = await IssueWarningAsync(db, request.WarnedPlayerId, request.ReportCategory, + displayReason, moderatorNote, moderatorId.Value); + + if (warning == null) + { + AdminActionDiscord.LogAction($"Warn Player (awaiting decision): {request.WarnedPlayerId}", + moderator.username ?? moderatorId.Value.ToString(), + $"@{moderator.username} warned developer {request.WarnedPlayerId} ({request.ReportCategory}) - parked until they accept or deny" + + (displayReason == null ? string.Empty : $": {displayReason}")); + + return Ok(WarningEnvelope(true, string.Empty)); + } + + AdminActionDiscord.LogAction($"Warn Player: {warning.WarnedPlayerId}", + moderator.username ?? moderatorId.Value.ToString(), + $"@{moderator.username} warned player {warning.WarnedPlayerId} ({warning.ReportCategory})" + + (displayReason == null ? string.Empty : $": {displayReason}") + + (moderatorNote == null ? string.Empty : $" | note: {moderatorNote}")); + + return Ok(WarningEnvelope(true, string.Empty)); + } + + public static async Task IssueWarningAsync( + RecDBContext db, + long warnedPlayerId, + ReportCategory reportCategory, + string? displayReason, + string? moderatorNote, + long moderatorPlayerId, + bool devDecisionResolved = false) + { + var warning = new PlayerWarning + { + ModeratorPlayerId = moderatorPlayerId, + WarnedPlayerId = warnedPlayerId, + ReportCategory = reportCategory, + DisplayReason = displayReason, + ModeratorNote = moderatorNote, + CreatedAt = DateTime.UtcNow + }; + + var hold = (int)PlayerUtils.WarningScreenHold.TotalSeconds; + var prompt = PlayerUtils.BuildWarningBlockDetails(warning, hold); + prompt.ModeratorNote = moderatorNote; + + if (!devDecisionResolved && + await PlayerUtils.DeferForDeveloperAsync(db, warnedPlayerId, PlayerUtils.DeveloperDecisionKind.Warning, prompt)) + { + return null; + } + + await db.PlayerWarnings.AddAsync(warning); + await db.SaveChangesAsync(); + + await PlayerUtils.EjectToDormAsync(db, warnedPlayerId); + + await PlayerUtils.DeliverWarningAsync(warnedPlayerId, PlayerUtils.BuildWarningBlockDetails(warning, hold)); + + return warning; + } + + [HttpPost("/api/playerwarnings/acknowledge"), Authorize] + public async Task AcknowledgeWarning() + { + var playerId = User.GetUserId(); + if (playerId == null) + return Unauthorized(WarningEnvelope(false, "Unauthorized")); + + var pending = await db.PlayerWarnings + .Where(x => x.WarnedPlayerId == playerId.Value && x.AcknowledgedAt == null) + .ToListAsync(); + + if (pending.Count == 0) + { + return Ok(WarningEnvelope(true, string.Empty)); + } + + var acknowledgedAt = DateTime.UtcNow; + foreach (var warning in pending) + warning.AcknowledgedAt = acknowledgedAt; + + await db.SaveChangesAsync(); + + await PlayerUtils.ReleaseModerationHoldAsync(playerId.Value, await db.IsDeveloper(playerId.Value)); + return Ok(WarningEnvelope(true, string.Empty)); + } + + [HttpPost("v1/moderationBlockDetails"), Authorize] + public Task GetBlockDetailsPost() => GetBlockDetails(); + + private async Task EmptyOrWarningBlockDetailsAsync(long playerId) + { + var pending = await db.GetPendingWarningAsync(playerId); + + return pending == null ? CreateEmptyBlockDetails() : PlayerUtils.BuildWarningBlockDetails(pending); + } + + [HttpGet("v1/moderationBlockDetails"), Authorize] + public async Task GetBlockDetails() + { + var userId = User.GetUserId(); + + + + + var ban = await db.ResolveBanAsync(userId!.Value, Request.GetClientIp()); + + + + + + + + + + + + + if (ban == null) + return Ok(await EmptyOrWarningBlockDetailsAsync(userId!.Value)); + + var remaining = ban.ExpiryUtc - DateTime.UtcNow; + + if (remaining <= TimeSpan.Zero) + return Ok(await EmptyOrWarningBlockDetailsAsync(userId!.Value)); + + bool isPermanent = ban.IsPermanent; + int duration = isPermanent ? int.MaxValue : (int)remaining.TotalSeconds; + int? bannedUntil = null; + if (isPermanent) + bannedUntil = int.MaxValue; + else + { + var unix = new DateTimeOffset(DateTime.SpecifyKind(ban.ExpiryUtc, DateTimeKind.Utc), TimeSpan.Zero) + .ToUnixTimeSeconds(); + if (unix <= int.MaxValue) + bannedUntil = (int)unix; + } + var message = ban.Reason ?? ""; + + if (ModerationPolicy.IsTimeout(ban.BanMethod)) + message = GetTimeoutDisplayReason(ban.BanMethod, message, ban.Category); + + + var relatedUsername = await PlayerUtils.RelatedAccountUsernameAsync(db, ban.BannedAccountId, userId!.Value); + if (relatedUsername != null) + message = message + $"\nRelated account: @{relatedUsername}"; + + var appealCode = ban.AppealCode ?? string.Empty; + string appealText = appealCode.Length > 0 ? PlayerUtils.BuildBanAppealText(appealCode) : string.Empty; + + + + + + + var appealPending = false; + if (ban.Kind == "Moderation") + { + appealPending = await db.Moderation.AsNoTracking() + .AnyAsync(x => x.PlayerId == userId!.Value && x.IsBan && x.AppealText != null && x.AppealResolution == null); + } + + if (appealPending) + { + appealCode = string.Empty; + appealText = PlayerUtils.PendingAppealText; + } + else + { + var cooldownError = await PlayerUtils.GetAppealCooldownErrorAsync(db, userId!.Value); + if (cooldownError != null) + { + appealCode = string.Empty; + appealText = cooldownError; + } + else if (appealCode.Length == 0 && ban.Kind == "Moderation" + && ModerationPolicy.IssuesAppealCode(ban.BanMethod)) + { + appealCode = PlayerUtils.GenerateAppealCode(); + var moderation = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == userId.Value && x.IsBan); + if (moderation != null) + { + moderation.AppealCode = appealCode; + await db.SaveChangesAsync(); + } + appealText = PlayerUtils.BuildBanAppealText(appealCode); + } + } + + + if (!ModerationPolicy.IssuesAppealCode(ban.BanMethod)) + { + appealCode = string.Empty; + appealText = string.Empty; + } + + return Ok(PlayerUtils.ApplyClientBanKind(new BlockDetails + { + Duration = duration, + IsBan = true, + IsHostKick = false, + Message = message, + PlayerIdReporter = 1, + ReportCategory = ban.Category, + VoteKickReason = "", + BannedUntil = bannedUntil, + DisplayReason = message, + TimeoutStartedAt = ban.StartedAtUtc, + BanAppealCode = appealCode, + BanAppealText = appealText + }, ban.BanMethod)); + } + + public static bool ShouldApplyAutomaticTimeout(int reportCount, bool reporterIsModerator) => + !reporterIsModerator && reportCount > AutomaticTimeoutReportThreshold; + + public static string GetTimeoutDisplayReason(string? banMethod, string? storedReason, ReportCategory category) + { + var reason = (storedReason ?? string.Empty).Trim(); + if (string.Equals(banMethod, "Moderator Report", StringComparison.OrdinalIgnoreCase)) + { + var moderatorReason = string.IsNullOrWhiteSpace(reason) + || reason.Equals("You were reported by a moderator.", StringComparison.OrdinalIgnoreCase) + ? "a moderator report" + : reason.TrimEnd('.'); + return $"You have received a timeout. {moderatorReason}."; + } + + const string prefix = "You have received a timeout due to a high number of reports for "; + if (reason.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + reason = reason[prefix.Length..].Trim().TrimEnd('.'); + else if (string.IsNullOrWhiteSpace(reason) || reason.Length > 80 + || reason.Contains('.') || reason.Contains('!') || reason.Contains('?')) + reason = GetAutomaticTimeoutReason(category); + + return $"You have received a timeout due to a high number of reports for {reason}."; + } + + public static string GetAutomaticTimeoutReason(ReportCategory category) => category switch + { + ReportCategory.CoC_Underage => "Underage behavior", + ReportCategory.CoC_Sexual => "Sexual behavior", + ReportCategory.CoC_Discrimination => "Discrimination", + ReportCategory.CoC_Trolling => "Trolling", + ReportCategory.CoC_NameOrProfile => "Inappropriate name or profile", + ReportCategory.InappropriateClothing => "Inappropriate clothing", + ReportCategory.Unknown => "Reported by players", + _ => category.ToString() + }; + + private static object CreateEmptyBlockDetails() => new + { + ReportCategory = 0, + Duration = 0, + GameSessionId = 0, + Message = "" + }; + + [HttpPost("v1/deviceId"), AllowAnonymous] + public async Task PostDeviceId([FromForm] DeviceId deviceId) + { + var userId = User.GetUserId(); + if (!userId.HasValue) + { + if (string.IsNullOrWhiteSpace(Request.Headers.Authorization.ToString())) return Ok(); + return Unauthorized(); + } + + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId.Value); + if (player == null) return NotFound(); + + player.DeviceId = deviceId.newDeviceId; + await db.SaveChangesAsync(); + + return Ok(); + } + + [HttpPost("v3/create"), Authorize] + public async Task> CreateReport([FromForm] CreateReportRequest request) + { + if (request.PlayerIdReported <= 0 || !Enum.IsDefined(typeof(ReportCategory), request.ReportCategory)) + return BadRequest(new OkResponse { Success = false, Message = "A valid player and report category are required." }); + + var reporterId = User.GetUserId(); + + + + + + var reputation = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == request.PlayerIdReported); + if (reputation == null) + { + reputation = new Reputation { AccountId = request.PlayerIdReported }; + await db.Reputations.AddAsync(reputation); + } + + var reporter = reporterId.HasValue && reporterId!.Value != request.PlayerIdReported + ? await db.Profiles.FirstOrDefaultAsync(x => x.accountId == reporterId!.Value) + : null; + var isValidPlayerReport = reporter != null; + var reporterIsModerator = reporter?.HasRole("moderator") == true; + if (isValidPlayerReport && !reporterIsModerator) + reputation.ReportCount += 1; + + + + + + + + + + + + if (reporterIsModerator) + { + var timeoutStartedAt = DateTime.UtcNow; + var timeoutExpiry = ModerationPolicy.TimeoutExpiry(timeoutStartedAt); + var reportMsg = string.IsNullOrWhiteSpace(request.Details) + ? "You were reported by a moderator." + : request.Details; + + var existingModeration = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == request.PlayerIdReported && x.IsBan); + if (existingModeration == null) + { + + await db.Moderation.AddAsync(new ModerationDB + { + PlayerId = request.PlayerIdReported, + IsBan = true, + Message = reportMsg, + Category = request.ReportCategory, + ExpireDuration = timeoutExpiry, + CreatedAt = timeoutStartedAt, + BanMethod = "Moderator Report", + AppealCode = null + }); + + reputation.ApplyBanPenalty(permanent: false, cheating: false); + + var blockDetails = PlayerUtils.BuildBlockDetails(true, request.ReportCategory, GetTimeoutDisplayReason("Moderator Report", reportMsg, request.ReportCategory), timeoutExpiry, reporterId: reporterId!.Value); + blockDetails.TimeoutStartedAt = timeoutStartedAt; + blockDetails.BanType = "Timeout"; + await PlayerUtils.KickOrPromptAsync(db, request.PlayerIdReported, blockDetails); + + await AdminActionDiscord.LogBanAsync(db, request.PlayerIdReported, + bannedBy: reporter?.username ?? $"Moderator {reporterId}", + causedByReports: true, + reason: reportMsg, + category: request.ReportCategory, + expiry: timeoutExpiry, + isModerator: true); + } + else if (existingModeration.ExpireDuration < timeoutExpiry) + { + existingModeration.IsBan = true; + existingModeration.Message = reportMsg; + existingModeration.Category = request.ReportCategory; + + existingModeration.ExpireDuration = ModerationPolicy.ExtendedTimeoutExpiry( + existingModeration.CreatedAt, existingModeration.ExpireDuration, timeoutExpiry - timeoutStartedAt); + existingModeration.BanMethod = "Moderator Report"; + existingModeration.AppealCode = null; + reputation.ApplyBanPenalty(permanent: false, cheating: false); + + var blockDetails = PlayerUtils.BuildBlockDetails(true, request.ReportCategory, GetTimeoutDisplayReason("Moderator Report", reportMsg, request.ReportCategory), timeoutExpiry, reporterId: reporterId!.Value); + blockDetails.TimeoutStartedAt = timeoutStartedAt; + blockDetails.BanType = "Timeout"; + await PlayerUtils.KickOrPromptAsync(db, request.PlayerIdReported, blockDetails); + + await AdminActionDiscord.LogBanAsync(db, request.PlayerIdReported, + bannedBy: reporter?.username ?? $"Moderator {reporterId}", + causedByReports: true, + reason: reportMsg, + category: request.ReportCategory, + expiry: timeoutExpiry, + isModerator: true); + } + + AdminPanelAPI.AddModerationReport( + reporterId!.Value, + request.PlayerIdReported, + reportMsg, + request.Details, + request.ReportCategory, + request.RoomId); + } + + if (isValidPlayerReport && ShouldApplyAutomaticTimeout(reputation.ReportCount, reporterIsModerator)) + { + var autoBanStartedAt = DateTime.UtcNow; + var autoBanExpiry = ModerationPolicy.TimeoutExpiry(autoBanStartedAt); + var autoBanReason = GetAutomaticTimeoutReason(request.ReportCategory); + var autoBanMessage = $"You have received a timeout due to a high number of reports for {autoBanReason}."; + + var existingModeration = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == request.PlayerIdReported && x.IsBan); + if (existingModeration == null) + { + + + + + await db.Moderation.AddAsync(new ModerationDB + { + PlayerId = request.PlayerIdReported, + IsBan = true, + Message = autoBanMessage, + Category = request.ReportCategory, + ExpireDuration = autoBanExpiry, + CreatedAt = autoBanStartedAt, + BanMethod = "Automatic Timeout", + AppealCode = null + }); + + reputation.ApplyBanPenalty(permanent: false, cheating: false); + var blockDetails = PlayerUtils.BuildBlockDetails(true, request.ReportCategory, autoBanMessage, autoBanExpiry); + blockDetails.TimeoutStartedAt = autoBanStartedAt; + + blockDetails.BanType = "Timeout"; + await PlayerUtils.KickOrPromptAsync(db, request.PlayerIdReported, blockDetails); + + + + var autoBanReporter = reporter; + await AdminActionDiscord.LogBanAsync(db, request.PlayerIdReported, + bannedBy: $"Reports ({autoBanReporter?.username ?? reporterId.ToString()})", + causedByReports: true, + reason: autoBanMessage, + category: request.ReportCategory, + expiry: autoBanExpiry, + isModerator: false); + } + else if (existingModeration.ExpireDuration < autoBanExpiry) + { + + existingModeration.IsBan = true; + existingModeration.Message = autoBanMessage; + existingModeration.Category = request.ReportCategory; + + existingModeration.ExpireDuration = ModerationPolicy.ExtendedTimeoutExpiry( + existingModeration.CreatedAt, existingModeration.ExpireDuration, autoBanExpiry - autoBanStartedAt); + existingModeration.BanMethod = "Automatic Timeout"; + existingModeration.AppealCode = null; + reputation.ApplyBanPenalty(permanent: false, cheating: false); + + var blockDetails = PlayerUtils.BuildBlockDetails(true, request.ReportCategory, autoBanMessage, autoBanExpiry); + blockDetails.TimeoutStartedAt = autoBanStartedAt; + blockDetails.BanType = "Timeout"; + await PlayerUtils.KickOrPromptAsync(db, request.PlayerIdReported, blockDetails); + + + var autoBanReporter2 = reporter; + await AdminActionDiscord.LogBanAsync(db, request.PlayerIdReported, + bannedBy: $"Reports ({autoBanReporter2?.username ?? reporterId.ToString()})", + causedByReports: true, + reason: autoBanMessage, + category: request.ReportCategory, + expiry: autoBanExpiry, + isModerator: false); + } + } + + await db.SaveChangesAsync(); + + + ReportDiscord.SendAsync(WebhookConfig.ReportsUrl, "🚩 Player Report", + new DiscordPayload + { + Title = "Player Report", + ThumbnailUrl = WebhookImages.PlayerReport, + Color = 0xF0B232, + Fields = new List + { + new() { Name = "Reporter", Value = reporterId?.ToString() ?? "Unknown", Inline = true }, + new() { Name = "Reported Player", Value = request.PlayerIdReported.ToString(), Inline = true }, + new() { Name = "Category", Value = request.ReportCategory.ToString(), Inline = true }, + new() { Name = "Room", Value = request.RoomId.ToString(), Inline = true }, + new() { Name = "Details", Value = request.Details, Inline = false } + }, + TimestampUtc = DateTime.UtcNow.ToString("O") + }); + + return Ok(new OkResponse { Success = true }); + } + + [HttpPost("v2/voteToKick"), Authorize] + public async Task> VoteToKick() + { + var request = await ReadKickBodyAsync("voteToKick"); + var voterId = User.GetUserId(); + if (voterId == null) return Unauthorized(); + + if (request.PlayerId <= 0) + return BadRequest(new OkResponse { Success = false, Message = "Missing player id." }); + + var key = (request.GameSessionId, request.PlayerId); + + if (request.PlayerId == voterId.Value) + { + await ResolveDeveloperDecisionAsync(request.PlayerId, request.Response); + return Ok(new OkResponse { Success = true }); + } + + var state = _voteKicks.GetOrAdd(key, _ => new VoteKickState()); + + bool thresholdReached; + lock (state.Voters) + { + + + if (!string.IsNullOrWhiteSpace(request.Reason) && string.IsNullOrWhiteSpace(state.Reason)) + state.Reason = request.Reason.Trim(); + + + + var before = state.Voters.Count; + if (request.Response) + state.Voters.Add(voterId.Value); + else + state.Voters.Remove(voterId.Value); + + thresholdReached = before < VoteKickThreshold && state.Voters.Count >= VoteKickThreshold; + } + + if (thresholdReached) + { + var reason = string.IsNullOrWhiteSpace(state.Reason) ? "Kicked by player vote" : state.Reason; + + + + + var category = ReportCategory.VoteKick; + if (request.ReportCategory.HasValue && Enum.IsDefined(typeof(KickReason), request.ReportCategory.Value)) + category = (ReportCategory)(int)request.ReportCategory.Value; + else + { + var known = Defaults.GetVoteKickReasons() + .FirstOrDefault(r => string.Equals(r.Reason, state.Reason, StringComparison.OrdinalIgnoreCase)); + if (known != null) category = (ReportCategory)(int)known.ReportCategory; + } + + await ApplyKick(request.PlayerId, category, reason); + _voteKicks.TryRemove(key, out _); + } + + return Ok(new OkResponse { Success = true }); + } + + [HttpPost("v1/kickFromEvent"), Authorize] + public async Task> KickFromEvent() + { + var request = await ReadKickBodyAsync("kickFromEvent"); + var requesterId = User.GetUserId(); + + if (request.PlayerId <= 0) + return BadRequest(new OkResponse { Success = false, Message = "Missing player id." }); + + + + await ApplyKick(request.PlayerId, ReportCategory.Misc, string.IsNullOrWhiteSpace(request.Response) ? "Kicked from event" : request.Response); + + return Ok(new OkResponse { Success = true }); + } + + [HttpPost("v1/instantKick"), Authorize] + public async Task> InstantKick() + { + var request = await ReadKickBodyAsync("instantKick"); + var requesterId = User.GetUserId(); + + + + + if (request.PlayerId <= 0) + return BadRequest(new OkResponse { Success = false, Message = "Missing player id." }); + + var reason = string.IsNullOrWhiteSpace(request.Reason) ? "Kicked instantly" : request.Reason.Trim(); + await ApplyKick(request.PlayerId, ReportCategory.VoteKick, reason, force: true); + + return Ok(new OkResponse { Success = true }); + } + + private async Task ResolveDeveloperDecisionAsync(long playerId, bool accepted) + { + var decision = PlayerUtils.TakeDeveloperDecision(playerId); + if (decision == null) + { + return; + } + + if (!accepted) + { + AdminActionDiscord.LogAction($"Developer denied {decision.Kind}: {playerId}", + playerId.ToString(), + $"Developer {playerId} denied the parked {decision.Kind} ({decision.Reason}); nothing applied."); + return; + } + + if (decision.Kind == PlayerUtils.DeveloperDecisionKind.Warning) + { + var warning = await IssueWarningAsync(db, playerId, decision.Category, decision.DisplayReason, + decision.ModeratorNote, decision.IssuedBy, devDecisionResolved: true); + + AdminActionDiscord.LogAction($"Warn Player (accepted): {playerId}", + decision.IssuedBy.ToString(), + $"Developer {playerId} accepted the warning ({decision.Category})" + + (decision.DisplayReason == null ? string.Empty : $": {decision.DisplayReason}")); + + return; + } + + await ApplyKick(playerId, decision.Category, decision.Reason, devDecisionResolved: true); + } + + private async Task ApplyKick(long playerId, ReportCategory category, string message, bool force = false, bool devDecisionResolved = false) + { + if (!devDecisionResolved && + await PlayerUtils.DeferForDeveloperAsync(db, playerId, PlayerUtils.DeveloperDecisionKind.Kick, + PlayerUtils.BuildBlockDetails(true, category, message, DateTime.UtcNow.Add(KickDuration)))) + { + return; + } + + var moderation = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == playerId); + var expires = DateTime.UtcNow.Add(KickDuration); + + if (moderation == null) + { + await db.Moderation.AddAsync(new ModerationDB + { + PlayerId = playerId, + IsBan = true, + Message = message, + Category = category, + ExpireDuration = expires + }); + } + else if (moderation.ExpireDuration < expires) + { + + moderation.IsBan = true; + moderation.Message = message; + moderation.Category = category; + moderation.ExpireDuration = expires; + } + + + + + + if (category == ReportCategory.VoteKick) + { + await db.VoteKicks.AddAsync(new VoteKickLog + { + PlayerId = playerId, + Reason = message, + Category = category, + IsForce = force, + CreatedAt = DateTime.UtcNow, + ExpiresAt = expires + }); + } + + await db.SaveChangesAsync(); + + + + + + + + await PlayerUtils.KickOrPromptAsync(db, playerId, PlayerUtils.BuildBlockDetails(true, category, message, expires), devDecisionResolved: true); + } + + [HttpGet("v1/voteToKickReasons")] + [EnableCors("AdminPanel")] + public ActionResult> GetVoteKickReasons() + { + return Ok(Defaults.GetVoteKickReasons()); + } + } +} diff --git a/Server/PlayerReporting/SanitizeStringRequest.cs b/Server/PlayerReporting/SanitizeStringRequest.cs new file mode 100644 index 0000000..40724e7 --- /dev/null +++ b/Server/PlayerReporting/SanitizeStringRequest.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Moderation +{ + public class IsPureRequest + { + public string Value { get; set; } = null!; + } + public class SanitizeStringRequest + { + public int ReplacementChar { get; set; } + public string Value { get; set; } = null!; + } +} diff --git a/Server/PlayerReputation/Reputation.cs b/Server/PlayerReputation/Reputation.cs new file mode 100644 index 0000000..db917cd --- /dev/null +++ b/Server/PlayerReputation/Reputation.cs @@ -0,0 +1,180 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Moderation; + +namespace RadicadeServer.RecNet.Moderation +{ + public class Reputation + { + + + + + + + + + + + + public const int CheerBadgeThreshold = 5; + + + + + + public const double DefaultReputation = 10.0; + + + + + + + + public const double NoterietyPerCheer = 0.5; + public const double NoterietyPerSubscriber = 0.5; + + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long AccountId { get; set; } + public double Noteriety { get; set; } + + + + + + + public double ReputationScore { get; set; } = DefaultReputation; + + + + + + + public int ReportCount { get; set; } + public int CheerGeneral { get; set; } + public int CheerHelpful { get; set; } + public int CheerCreative { get; set; } + public int CheerGreatHost { get; set; } + public int CheerSportsman { get; set; } + public int CheerCredit { get; set; } + public int SelectedCheer { get; set; } + public bool IsCheerful { get; set; } + + + + + + + [NotMapped] + public int TotalCheers => + CheerGeneral + CheerHelpful + CheerCreative + CheerGreatHost + CheerSportsman + CheerCredit; + + + + + + [NotMapped] + public string Trust => ReputationScore switch + { + >= 8.0 => "Trusted", + >= 6.0 => "Neutral", + _ => "Not Trusted" + }; + + public int GetCheerCount(CheerCategory category) => category switch + { + CheerCategory.General => CheerGeneral, + CheerCategory.Helpful => CheerHelpful, + CheerCategory.Creative => CheerCreative, + CheerCategory.GreatHost => CheerGreatHost, + CheerCategory.Sportsman => CheerSportsman, + CheerCategory.Credit => CheerCredit, + _ => 0 + }; + + + + + + + + public void AddCheer(CheerCategory category) + { + switch (category) + { + case CheerCategory.General: CheerGeneral++; break; + case CheerCategory.Helpful: CheerHelpful++; break; + case CheerCategory.Creative: CheerCreative++; break; + case CheerCategory.GreatHost: CheerGreatHost++; break; + case CheerCategory.Sportsman: CheerSportsman++; break; + case CheerCategory.Credit: CheerCredit++; break; + } + + Noteriety += NoterietyPerCheer; + IsCheerful = CheerBadgeEarned; + } + + + + + + public void AddSubscriber() + { + Noteriety += NoterietyPerSubscriber; + } + + + + + + + public void ApplyBanPenalty(bool permanent, bool cheating) + { + double penalty = permanent ? 4.0 : (cheating ? 1.0 : 0.5); + ReputationScore = Math.Max(0.0, ReputationScore - penalty); + } + + + + + + + + + + public Reputation ForClient() + { + return new Reputation + { + AccountId = AccountId, + Noteriety = Math.Min(0.05, Noteriety / 100.0), + ReputationScore = ReputationScore, + + + + ReportCount = ReportCount, + CheerGeneral = CheerGeneral, + CheerHelpful = CheerHelpful, + CheerCreative = CheerCreative, + CheerGreatHost = CheerGreatHost, + CheerSportsman = CheerSportsman, + CheerCredit = CheerCredit, + SelectedCheer = SelectedCheer, + + + + IsCheerful = CheerBadgeEarned + }; + } + + + + + + + + [NotMapped] + public bool CheerBadgeEarned => TotalCheers >= CheerBadgeThreshold; + + } +} diff --git a/Server/PlayerReputation/ReputationAPI.cs b/Server/PlayerReputation/ReputationAPI.cs new file mode 100644 index 0000000..a898a3b --- /dev/null +++ b/Server/PlayerReputation/ReputationAPI.cs @@ -0,0 +1,288 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.API.Notifications; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Moderation +{ + [Route("api/playerReputation")] + public class ReputationAPI : Controller + { + RecDBContext db; + public ReputationAPI(RecDBContext context) { + this.db = context; + } + + [HttpGet("v1/{id}")] + public IActionResult GetReputation(long id) + { + var reputation = db.Reputations.FirstOrDefault((x) => x.AccountId == id); + reputation ??= new Reputation { AccountId = id }; + return Ok(CreateClientReputation(reputation.ForClient())); + } + + [HttpPost("v1/bulk")] + public IActionResult GetBulkReputation([FromForm] long[] ids) + { + var reputations = new List(); + foreach (var id in ids) + { + var reputation = db.Reputations.FirstOrDefault((x) => x.AccountId == id); + reputation ??= new Reputation { AccountId = id }; + reputations.Add(CreateClientReputation(reputation.ForClient())); + } + return Ok(reputations); + } + + [HttpGet("v2/bulk")] + public IActionResult GetBulkReputationV2([FromQuery] long[] id) + { + var reputations = new List(); + foreach (var i in id) + { + var reputation = db.Reputations.FirstOrDefault((x) => x.AccountId == i); + reputation ??= new Reputation { AccountId = i }; + reputations.Add(CreateClientReputation(reputation.ForClient())); + } + return Ok(reputations); + } + + private static ClientReputation CreateClientReputation(Reputation reputation) => new() + { + Id = reputation.AccountId, + AccountId = reputation.AccountId, + + + + + IsCheerful = true, + Noteriety = (float)reputation.Noteriety, + SelectedCheer = reputation.SelectedCheer, + + + + CheerCredit = reputation.CheerCredit > 0 ? reputation.CheerCredit : 20, + + + + CheerGeneral = reputation.CheerGeneral, + CheerHelpful = reputation.CheerHelpful, + CheerCreative = reputation.CheerCreative, + CheerGreatHost = reputation.CheerGreatHost, + CheerSportsman = reputation.CheerSportsman + }; + + private sealed class ClientReputation + { + public long Id { get; set; } + [JsonPropertyName("accountId")] + public long AccountId { get; set; } + public bool IsCheerful { get; set; } + public float Noteriety { get; set; } + public int SelectedCheer { get; set; } + public int CheerCredit { get; set; } + public int CheerGeneral { get; set; } + public int CheerHelpful { get; set; } + public int CheerCreative { get; set; } + public int CheerGreatHost { get; set; } + public int CheerSportsman { get; set; } + } + + + + + + + + private static readonly CheerCategory[] AwardableCheers = + { + CheerCategory.General, CheerCategory.Helpful, CheerCategory.Sportsman, + CheerCategory.GreatHost, CheerCategory.Creative + }; + + [HttpPost("~/api/PlayerCheer/v1/create")] + [HttpPost("v1/cheer"), Authorize] + public async Task> CheerPlayer() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var request = await ReadRequestValuesAsync(); + var values = request.Values; + var playerId = ReadLong(values, "playeridto", "playerid", "player", "targetplayerid", "accountid", "id"); + var category = ReadInt(values, "category", "cheercategory", "cheer", "value"); + + if (playerId == null || category == null) + { + return BadRequest(Success.Error("No player or cheer category was given.")); + } + + if (playerId == userId) return BadRequest(Success.Error("You can't cheer yourself.")); + + var cheer = (CheerCategory)category.Value; + if (!AwardableCheers.Contains(cheer)) + return BadRequest(Success.Error($"Unknown cheer category {category.Value}.")); + + var reputation = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (reputation == null) + { + + + reputation = new Reputation { AccountId = playerId.Value }; + await db.Reputations.AddAsync(reputation); + } + + reputation.AddCheer(cheer); + + await db.SaveChangesAsync(); + + + + + + await Notify.SendMessage(playerId.Value, new Message + { + FromPlayerId = userId.Value, + Type = MessageType.PlayerCheer, + Data = category.Value.ToString() + }); + + await Notify.SendAccountUpdate(playerId.Value); + await SendReputationUpdate(playerId.Value, reputation); + + return Ok(Success.Ok); + } + + private async Task<(Dictionary Values, string Raw)> ReadRequestValuesAsync() + { + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var (key, value) in Request.RouteValues) + if (value != null) values[key] = value.ToString() ?? string.Empty; + + foreach (var (key, value) in Request.Query) + values[key] = value.ToString(); + + Request.EnableBuffering(); + + string raw; + using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true)) + raw = (await reader.ReadToEndAsync()).Trim(); + + Request.Body.Position = 0; + + if (raw.Length == 0) return (values, raw); + + if (raw.StartsWith('{')) + { + try + { + if (JsonNode.Parse(raw) is JsonObject json) + { + foreach (var (key, value) in json) + values[key] = value?.ToString() ?? string.Empty; + return (values, raw); + } + } + catch (JsonException) { } + } + + if (raw.Contains('=')) + { + foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var parts = pair.Split('=', 2); + if (parts.Length == 2) + values[parts[0]] = Uri.UnescapeDataString(parts[1]); + } + return (values, raw); + } + + values["category"] = raw; + return (values, raw); + } + + private static int? ReadInt(Dictionary values, params string[] keys) + { + foreach (var key in keys) + if (values.TryGetValue(key, out var raw) && int.TryParse(raw, out var parsed)) + return parsed; + return null; + } + + private static long? ReadLong(Dictionary values, params string[] keys) + { + foreach (var key in keys) + if (values.TryGetValue(key, out var raw) && long.TryParse(raw, out var parsed)) + return parsed; + return null; + } + + + + + + + [HttpPost("~/api/PlayerCheer/v1/SetSelectedCheer")] + [HttpPost("v1/selectedCheer"), HttpPost("v1/selectCheer"), + HttpPost("v1/selectedCheer/{category:int}"), HttpPut("v1/selectedCheer"), Authorize] + public async Task> SetSelectedCheer() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var request = await ReadRequestValuesAsync(); + var category = ReadInt(request.Values, "category", "cheercategory", "cheer", "value", "selectedcheer"); + if (category == null) + { + return BadRequest(Success.Error("No cheer category was given.")); + } + + var reputation = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == userId); + if (reputation == null) + { + reputation = new Reputation + { + AccountId = userId.Value, + IsCheerful = true, + CheerCredit = 20 + }; + await db.Reputations.AddAsync(reputation); + } + + reputation.SelectedCheer = category.Value; + await db.SaveChangesAsync(); + + await Notify.SendAccountUpdate(userId.Value); + await SendReputationUpdate(userId.Value, reputation); + + return Ok(Success.Ok); + } + + /// + /// The frame the cheer counters and the cheer badge are built from, pushed under + /// "ReputationUpdate" - the key the 2023 client registers a handler for. The account update + /// only carries the profile, so without this the cheer a player was just handed, and the + /// badge it unlocked, stayed invisible on the receiving client until their next login + /// refetched the reputation, which is what "not updated instantly" looked like. + /// + private static Task SendReputationUpdate(long accountId, Reputation reputation) => + Notify.SendNotify(accountId, new Notification + { + Id = "ReputationUpdate", + Msg = CreateClientReputation(reputation.ForClient()) + }); + } +} diff --git a/Server/PlayerWarnings/PlayerWarning.cs b/Server/PlayerWarnings/PlayerWarning.cs new file mode 100644 index 0000000..9c0ae3f --- /dev/null +++ b/Server/PlayerWarnings/PlayerWarning.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Moderation; + +namespace RadicadeServer.RecNet.Moderation +{ + public class PlayerWarning + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long ModeratorPlayerId { get; set; } + + public long WarnedPlayerId { get; set; } + + public ReportCategory ReportCategory { get; set; } + + public string? DisplayReason { get; set; } + + public string? ModeratorNote { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime? AcknowledgedAt { get; set; } + } +} diff --git a/Server/Players/AccountsAPI.cs b/Server/Players/AccountsAPI.cs new file mode 100644 index 0000000..700afa3 --- /dev/null +++ b/Server/Players/AccountsAPI.cs @@ -0,0 +1,1441 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Identity.Client; +using System; +using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Web; +using RadicadeServer.API.Notifications; +using RadicadeServer.Auth; +using RadicadeServer.Clubs; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Radicade.Mail; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Accounts +{ + [Route("accounts/")] + public class AccountsAPI : Controller + { + RecDBContext db; + public AccountsAPI(RecDBContext context) + { + this.db = context; + } + + + + + + + + [HttpGet("namegen/options")] + public object GetNameGenerationOptions() + { + return new + { + adjectives = new[] + { + "Adventurous", "Amazing", "Brave", "Bright", "Cheerful", "Clever", "Cool", + "Daring", "Energetic", "Fast", "Friendly", "Funny", "Golden", "Happy", + "Jolly", "Kind", "Lively", "Lucky", "Mighty", "Mystic", "Nimble", "Playful", + "Quick", "Radiant", "Rainbow", "Rustic", "Silky", "Smart", "Sneaky", + "Sparkling", "Steady", "Sunny", "Super", "Swift", "Tidy", "Tiny", + "Victorious", "Wacky", "Wild", "Zany", "Zippy" + }, + nouns = new[] + { + "Badger", "Bear", "Buffalo", "Camel", "Cheetah", "Deer", "Dolphin", "Eagle", + "Elk", "Falcon", "Ferret", "Fox", "Gecko", "Hawk", "Hedgehog", "Jaguar", + "Koala", "Lemur", "Lion", "Lynx", "Marmot", "Meerkat", "Newt", "Ocelot", + "Otter", "Panda", "Penguin", "Pony", "Quail", "Rabbit", "Raccoon", "Raven", + "Skunk", "Sloth", "Squirrel", "Tiger", "Turtle", "Vulture", "Walrus", "Wolf" + } + }; + } + + [HttpGet("parentalcontrol/me"), Authorize] + public IActionResult GetParentalControls() + { + return Ok(new + { + accountId = User.GetUserId(), + disallowInAppPurchases = false, + requirePasswordForPurchases = false + }); + } + + [HttpGet("accountprivacysettings/{accountId:long}"), Authorize] + public async Task GetAccountPrivacySettings(long accountId) + { + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId); + if (profile == null) return NotFound(); + + return Ok(BuildPrivacySettings(profile)); + } + + private static Dictionary BuildPrivacySettings(Profile profile) => new() + { + ["accountId"] = profile.accountId, + ["isJunior"] = profile.isJunior, + ["profileVisibility"] = 0, + ["whoCanContactMe"] = 0, + ["whoCanInviteMe"] = 0, + ["whoCanJoinMe"] = 0, + ["whoCanSeeMyActivity"] = 0, + ["showOnlineStatus"] = true, + ["allowFriendRequests"] = true, + ["isRecentHistoryVisible"] = profile.isRecentHistoryVisible, + ["isrecenthistoryvisible"] = profile.isRecentHistoryVisible + }; + + [HttpPut("accountprivacysettings/recenthistoryvisibility"), Authorize] + public async Task SetRecentHistoryVisibility() + { + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == User.GetUserId()); + if (profile == null) return NotFound(); + + var body = await ReadRawBodyAsync(); + var value = ReadBoolFromPayload(body, PrivacyVisibilityKeys); + if (value == null) + { + return BadRequest(new Success(false, "Invalid recent history visibility value.", null)); + } + + profile.isRecentHistoryVisible = value.Value; + await db.SaveChangesAsync(); + return Ok(BuildPrivacySettings(profile)); + } + + private static readonly string[] PrivacyVisibilityKeys = + [ + "isRecentHistoryVisible", "isrecenthistoryvisible", "recentHistoryVisibility", + "recenthistoryvisibility", "value", "isVisible", "visible" + ]; + + private async Task ReadRawBodyAsync() + { + try + { + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true); + var raw = await reader.ReadToEndAsync(); + Request.Body.Position = 0; + return raw ?? string.Empty; + } + catch (Exception) + { + return string.Empty; + } + } + + private bool? ReadBoolFromPayload(string body, string[] keys) + { + var trimmed = body?.Trim() ?? string.Empty; + + if (trimmed.StartsWith('{')) + { + try + { + using var doc = JsonDocument.Parse(trimmed); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var property in doc.RootElement.EnumerateObject()) + if (keys.Contains(property.Name, StringComparer.OrdinalIgnoreCase)) + { + var named = ParseBoolElement(property.Value); + if (named != null) return named; + } + + var booleanProperties = doc.RootElement.EnumerateObject() + .Where(property => property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False) + .ToList(); + if (booleanProperties.Count == 1) + return booleanProperties[0].Value.GetBoolean(); + } + } + catch (JsonException) + { + } + } + + if (trimmed.Contains('=')) + { + foreach (var pair in trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var kv = pair.Split('=', 2); + var key = Uri.UnescapeDataString(kv[0]).Trim(); + if (!keys.Contains(key, StringComparer.OrdinalIgnoreCase)) continue; + var value = ParseBoolText(kv.Length > 1 ? Uri.UnescapeDataString(kv[1]) : null); + if (value != null) return value; + } + } + + var bare = ParseBoolText(trimmed); + if (bare != null) return bare; + + foreach (var key in keys) + { + if (Request.Query.TryGetValue(key, out var queryValue)) + { + var value = ParseBoolText(queryValue.FirstOrDefault()); + if (value != null) return value; + } + } + + return null; + } + + private static bool? ParseBoolElement(JsonElement element) => element.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Number when element.TryGetInt64(out var number) => number != 0, + JsonValueKind.String => ParseBoolText(element.GetString()), + _ => null + }; + + private static bool? ParseBoolText(string? text) + { + if (string.IsNullOrWhiteSpace(text)) return null; + var trimmed = text.Trim().Trim('"'); + if (bool.TryParse(trimmed, out var parsed)) return parsed; + if (trimmed is "1") return true; + if (trimmed is "0") return false; + if (trimmed.Equals("yes", StringComparison.OrdinalIgnoreCase)) return true; + if (trimmed.Equals("no", StringComparison.OrdinalIgnoreCase)) return false; + return null; + } + + [HttpGet("account/me"), Authorize] + public async Task GetProfile() + { + var userId = User.GetUserId(); + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return null; + + + + + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (plrData == null) return null; + + return PlayerUtils.BuildSelfAccount(profile, plrData); + } + + [HttpPost("account/me"), Authorize] + [HttpPut("account/me"), Authorize] + public async Task UpdateAccount() + { + var userId = User.GetUserId(); + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (profile == null || plrData == null) return null; + + var body = await ReadRawBodyAsync(); + + var raw = ReadBirthdayFromPayload(body); + string? rejection = null; + if (!string.IsNullOrWhiteSpace(raw)) rejection = await ApplyBirthdayAsync(raw, plrData); + + var rawUsername = ReadUsernameFromPayload(body); + string? usernameRejection = null; + if (!string.IsNullOrWhiteSpace(rawUsername)) usernameRejection = await ApplyUsernameAsync(rawUsername, profile); + + return PlayerUtils.BuildSelfAccount(profile, plrData); + } + + [HttpPost("account/create")] + [HttpPost("/account/create")] + [HttpPost("/api/accounts/account/create")] + public async Task CreateAccount([FromForm] AccountCreationData acc) + { + if (acc == null) return new Success(false, "No account details were sent.", null); + + if (string.IsNullOrEmpty(acc.platformId)) acc.platformId = Request.Form["platform_id"].ToString(); + if (string.IsNullOrEmpty(acc.deviceId)) acc.deviceId = Request.Form["device_id"].ToString(); + + + + acc.platformId = acc.platformId?.Trim() ?? string.Empty; + acc.deviceId = acc.deviceId?.Trim() ?? string.Empty; + acc.username = acc.username?.Trim(); + + if (acc.platformId.Length == 0) return new Success(false, "A platform id is required.", null); + if (acc.deviceId.Length == 0) return new Success(false, "A device id is required.", null); + + var isWhitelisted = await AuthShared.IsWhitelisted(acc.platformId); + if (!isWhitelisted) return new Success(false, "Error", "You are not whitelisted."); + + var platformAuth = Request.Form["platform_auth"].ToString(); + if (string.IsNullOrWhiteSpace(platformAuth)) platformAuth = Request.Form["platformAuth"].ToString(); + + var proof = await PlatformIdentity.ProveAsync(acc.platform, acc.platformId, + Request.Headers["x-steam-ticket"].ToString(), platformAuth, HttpContext.RequestAborted); + if (!proof.Verified) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Account] create refused: platform {acc.platform} identity " + + $"{acc.platformId} device {acc.deviceId} from {Request.GetClientIp()}: {proof.Detail} " + + $"(platform_auth {(string.IsNullOrWhiteSpace(platformAuth) ? "absent" : "present")}, " + + $"x-steam-ticket {(Request.Headers.ContainsKey("x-steam-ticket") ? "present" : "absent")})"); + return new Success(false, "Error", proof.Detail); + } + + if (proof.SteamId != null) + { + var standing = await PlatformIdentity.CheckSteamStandingAsync(proof.SteamId, HttpContext.RequestAborted); + if (standing != null) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Account] create refused: Steam account {proof.SteamId} " + + $"is not in good standing: {standing}"); + return new Success(false, "Error", standing); + } + } + + var clientIp = Request.GetClientIp(); + + if (await db.IsIpBanned(clientIp)) + return new Success(false, "Error", "This IP address is banned from creating new accounts."); + + if (await db.IsAccountCreationBlocked(clientIp)) + return new Success(false, "Error", "This IP address is blocked from creating new accounts."); + + if (await db.IsHardwareBanned(acc.deviceId)) + return new Success(false, "Error", "This device is banned."); + + + + + using var identity = await RegistrationGuard.LockIdentityAsync( + RegistrationGuard.IdentityKey(acc.platform.ToString(), acc.platformId, acc.deviceId)); + + var adoptable = await PlayerUtils.FindAdoptableAccountForDeviceAsync(db, acc.deviceId); + if (adoptable != null) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Account] create adopted this device's account {adoptable.AccountId} " + + $"for platform {acc.platform} identity {acc.platformId}"); + return await PlayerUtils.AdoptDeviceAccountAsync(db, adoptable.AccountId, acc, markStarted: true); + } + + return await PlayerUtils.CreateAccount(acc, clientIp, accountCreationInProgress: true); + } + + [HttpGet("account/search")] + public async Task GetByName([FromQuery] string name) + { + var profiles = await db.Profiles + .Where(x => x.displayName.Contains(name) || x.username.Contains(name)) + .ToListAsync(); + + return profiles.Select(p => (object)new Dictionary + { + ["accountId"] = p.accountId, + ["profileImage"] = p.profileImage, + ["isJunior"] = p.isJunior, + ["platforms"] = p.platforms, + ["username"] = p.username, + ["displayName"] = p.displayName, + ["createdAt"] = p.createdAt, + ["personalPronouns"] = p.personalPronouns, + ["identityFlags"] = p.identityFlags, + ["displayEmoji"] = (object?)null, + }).ToArray(); + } + + [HttpGet("account/{who}/bio"), Authorize] + public async Task GetBioFromId(string who) + { + + long? userId; + if (who == "me") + userId = User.GetUserId(); + else + userId = (await db.Profiles.FirstOrDefaultAsync(x => x.username == who))?.accountId; + if (userId == null) + userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; + + if (userId == null) return new BioResponse { accountId = 0, bio = string.Empty }; + + var bio = (await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId))?.bio; + return new BioResponse { accountId = (long)userId!, bio = bio ?? string.Empty }; + } + [HttpPut("account/me/bio"), Authorize] + public async Task SetBio(string bio) + { + var userId = User.GetUserId(); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return new Success(false, "Invalid profile.", null); + + + if (profile.BioLocked) return new Success(false, "Your bio is locked and cannot be changed.", null); + + if (!bio.IsPure()) return new Success(false, "Bio contains offensive language.", null); + + profile.bio = bio; + + await db.SaveChangesAsync(); + + await SendAccountUpdate((long)userId!); + + return new Success(true, null, bio); + } + [HttpPut("account/me/displayname"), Authorize] + public async Task SetDisplayName(string displayName) + { + var userId = User.GetUserId(); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return new Success(false, "Invalid profile.", null); + + + if (profile.NameLocked) return new Success(false, "Your name is locked and cannot be changed.", null); + + displayName = displayName?.Trim() ?? string.Empty; + + if (displayName.Length == 0) return new Success(false, "Your name cannot be empty.", null); + + if (!displayName.IsPure()) return new Success(false, "Name contains offensive language.", null); + + if (await IsNameTakenAsync((long)userId!, displayName)) + return new Success(false, "That name is already taken.", null); + + profile.displayName = displayName; + + if (await IsChoosingTheirAccountNameAsync((long)userId!)) + { + var previous = profile.username; + profile.username = displayName; + } + + await db.SaveChangesAsync(); + + await SendAccountUpdate((long)userId!); + + return new Success(true, null, displayName); + } + + private Task IsChoosingTheirAccountNameAsync(long accountId) => + PlayerUtils.IsChoosingTheirAccountNameAsync(db, accountId); + [HttpPut("account/me/username"), Authorize] + public async Task SetUsername(string username) + { + var userId = User.GetUserId(); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return new Success(false, "Invalid profile.", null); + + + if (profile.NameLocked) return new Success(false, "Your name is locked and cannot be changed.", null); + + username = username?.Trim() ?? string.Empty; + + if (username.Length == 0) return new Success(false, "Your username cannot be empty.", null); + + + + + var chosen = UsernameRules.Parse(username, out var rejection); + if (chosen == null) return new Success(false, rejection ?? "Your username cannot be empty.", null); + + if (await IsNameTakenAsync((long)userId!, chosen)) + return new Success(false, "That username is already taken.", null); + + var previousUsername = profile.username; + var displayNameWasTheUsername = string.IsNullOrWhiteSpace(profile.displayName) + || string.Equals(profile.displayName, previousUsername, StringComparison.Ordinal); + var stillChoosing = await IsChoosingTheirAccountNameAsync((long)userId!); + + profile.username = chosen; + + + + + if (stillChoosing || displayNameWasTheUsername) profile.displayName = chosen; + + await db.SaveChangesAsync(); + + if (stillChoosing) + await PlayerUtils.RefreshAccountCreationMarkersAsync(db, (long)userId!, usernameChosen: true); + + await SendAccountUpdate((long)userId!); + + return new Success(true, null, username); + } + + [HttpPut("account/me/personalpronouns"), Authorize] + public Task SetPersonalPronouns() => + SetProfileIdentityValue(true, "pronounFlags", "PronounFlags", "pronouns", "Pronouns", "personalPronouns", "PersonalPronouns", "value", "Value"); + + [HttpPut("account/me/identityflags"), Authorize] + public Task SetIdentityFlags() => + SetProfileIdentityValue(false, "identityFlags", "IdentityFlags", "value", "Value"); + + private async Task SetProfileIdentityValue(bool isPronouns, params string[] keys) + { + var p = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == User.GetUserId()); + if (p == null) return new Success(false, "Invalid profile.", null); + + var body = await ReadRawBodyAsync(); + var raw = ReadStringFromPayload(body, keys); + if (!int.TryParse(raw, out var value) || value < 0) + return new Success(false, "A non-negative integer value is required.", null); + + if (isPronouns) p.personalPronouns = value; + else p.identityFlags = value; + await db.SaveChangesAsync(); + await SendAccountUpdate(p.accountId); + return new Success(true, null, value); + } + + [HttpPut("account/me/emoji"), Authorize] + [HttpPost("account/me/emoji"), Authorize] + public async Task SetDisplayEmoji() + { + var userId = User.GetUserId(); + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (profile == null || plrData == null) return null; + + var body = await ReadRawBodyAsync(); + var emoji = ReadStringFromPayload(body, "displayEmoji", "DisplayEmoji", "emoji", "Emoji", "value", "Value"); + if (emoji == null) + { + return PlayerUtils.BuildSelfAccount(profile, plrData); + } + + profile.displayEmoji = string.IsNullOrWhiteSpace(emoji) ? null : emoji.Trim(); + await db.SaveChangesAsync(); + await SendAccountUpdate(profile.accountId); + return PlayerUtils.BuildSelfAccount(profile, plrData); + } + private async Task IsNameTakenAsync(long accountId, string name) => + await db.Profiles.AnyAsync(x => x.accountId != accountId && + (x.username == name || x.displayName == name)); + + [HttpPut("account/me/profileimage"), Authorize] + public async Task SetProfileImage(string imageName) + { + var userId = User.GetUserId(); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return new Success(false, "Invalid profile.", null); + + profile.profileImage = imageName; + + await db.SaveChangesAsync(); + + await SendAccountUpdate((long)userId!); + + return new Success(true, null, imageName); + } + + private static readonly string[] ImageNameKeys = + { + "imageName", "ImageName", "image", "Image", "bannerImage", "BannerImage" + }; + + private async Task ReadImageNameAsync(string? bound) + { + if (!string.IsNullOrWhiteSpace(bound)) return bound.Trim(); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var key in ImageNameKeys) + { + var value = form[key].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(value)) return value.Trim(); + } + + // The client's own field name is not guaranteed to be one of the spellings above, + // so a form that carries exactly one piece of text still names the banner. + var named = form + .Where(field => !string.IsNullOrWhiteSpace(field.Value.ToString())) + .Select(field => field.Value.ToString().Trim()) + .Distinct() + .ToList(); + if (named.Count == 1) return named[0]; + + return null; + } + + // A chunked body has no Content-Length at all, so the old "is not > 0" guard refused + // every name sent that way. Only a body that is explicitly empty is skipped now. + if (Request.ContentLength == 0) return null; + + try + { + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + if (Request.Body.CanSeek) Request.Body.Position = 0; + if (string.IsNullOrWhiteSpace(body)) return null; + + return ReadImageNameFromBody(body); + } + catch + { + return null; + } + } + + private static string? ReadImageNameFromBody(string body) + { + var trimmed = body.Trim(); + + if (trimmed.StartsWith('{')) + { + using var document = JsonDocument.Parse(trimmed); + if (document.RootElement.ValueKind != JsonValueKind.Object) return null; + + foreach (var key in ImageNameKeys) + { + if (!document.RootElement.TryGetProperty(key, out var element)) continue; + if (element.ValueKind != JsonValueKind.String) continue; + + var value = element.GetString(); + if (!string.IsNullOrWhiteSpace(value)) return value.Trim(); + } + + // One string member under a name we do not know is still the image name; two or + // more and the payload is something else, so nothing is guessed. + var strings = document.RootElement.EnumerateObject() + .Where(property => property.Value.ValueKind == JsonValueKind.String) + .Select(property => property.Value.GetString()) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(value => value!.Trim()) + .Distinct() + .ToList(); + + return strings.Count == 1 ? strings[0] : null; + } + + if (trimmed.StartsWith("\"")) + { + var value = JsonSerializer.Deserialize(trimmed); + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } + + if (trimmed.Contains('=')) + { + foreach (var pair in trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var kv = pair.Split('=', 2); + if (kv.Length < 2) continue; + if (!ImageNameKeys.Contains(Uri.UnescapeDataString(kv[0]).Trim(), StringComparer.OrdinalIgnoreCase)) + continue; + + var value = Uri.UnescapeDataString(kv[1]).Trim(); + if (!string.IsNullOrWhiteSpace(value)) return value; + } + + // Form encoded, but nothing in it names an image: not a name to store. + return null; + } + + return trimmed; + } + + [HttpPut("account/me/bannerimage"), Authorize] + public async Task SetBannerImage(string? imageName) + { + var userId = User.GetUserId(); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + if (profile == null) return NotFound(new Success(false, "Invalid profile.", null)); + + var resolved = await ReadImageNameAsync(imageName); + if (string.IsNullOrWhiteSpace(resolved)) + { + return BadRequest(new Success(false, "An image name is required.", null)); + } + + profile.bannerImage = resolved; + + await db.SaveChangesAsync(); + + await SendAccountUpdate((long)userId!); + + return Ok(new Success(true, null, resolved)); + } + + [HttpPost("account/me/email"), Authorize] + public async Task SetEmail() + { + var userId = User.GetUserId(); + + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (plrData == null) return new Success(false, "Invalid profile.", null); + + + + + string? email = null; + try + { + using var reader = new StreamReader(Request.Body); + var raw = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(raw)) + { + if (raw.TrimStart().StartsWith("{")) + { + using var doc = JsonDocument.Parse(raw); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var key in new[] { "email", "newEmail", "new_email" }) + { + if (doc.RootElement.TryGetProperty(key, out var el) && el.ValueKind == JsonValueKind.String) + { + email = el.GetString(); + break; + } + } + } + } + else + { + var form = HttpUtility.ParseQueryString(raw); + email = form["email"] ?? form["newEmail"] ?? form["new_email"]; + } + } + } + catch + { + + } + + if (string.IsNullOrWhiteSpace(email)) email = Request.Form["email"].ToString(); + if (string.IsNullOrWhiteSpace(email)) email = Request.Query["email"].ToString(); + + if (string.IsNullOrWhiteSpace(email)) return new Success(false, "Email is required.", null); + + email = email.Trim(); + if (email.Length > 254 || !email.Contains('@')) + return new Success(false, "Invalid email address.", null); + + + if (string.Equals(plrData.Email, email, StringComparison.OrdinalIgnoreCase) && plrData.EmailVerified) + return new Success(true, null, email); + + + + if (await IsEmailTaken(email, excludeAccountId: (long)userId!)) + return new Success(false, "This email is already registered to another account.", null); + + + + var emailLimitError = CheckEmailVerificationLimit(userId); + if (emailLimitError != null) return new Success(false, emailLimitError, null); + + + + + + var isResend = string.Equals(plrData.PendingEmail, email, StringComparison.OrdinalIgnoreCase); + plrData.PendingEmail = email; + plrData.EmailVerified = false; + if (!isResend || string.IsNullOrWhiteSpace(plrData.EmailVerifyToken)) + plrData.EmailVerifyToken = Guid.NewGuid().ToString("N"); + await db.SaveChangesAsync(); + + await SendAccountUpdate((long)userId!); + SendVerificationEmail(plrData); + + return new Success(true, null, email); + } + + + + + + + + private async Task IsEmailTaken(string email, long excludeAccountId) + { + var candidates = await db.Players + .Where(x => x.AccountId != excludeAccountId) + .Select(x => new { x.Email, x.PendingEmail }) + .ToListAsync(); + return candidates.Any(x => + !string.IsNullOrWhiteSpace(x.Email) && string.Equals(x.Email, email, StringComparison.OrdinalIgnoreCase) || + !string.IsNullOrWhiteSpace(x.PendingEmail) && string.Equals(x.PendingEmail, email, StringComparison.OrdinalIgnoreCase)); + } + + + + + + [HttpGet("account/me/email"), Authorize] + public async Task GetEmail() + { + var userId = User.GetUserId(); + var plrData = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == userId); + if (plrData == null) return NotFound(); + + return Ok(new + { + email = plrData.Email, + verified = plrData.EmailVerified, + + + pendingEmail = plrData.PendingEmail, + pendingVerified = false + }); + } + + + + + + [HttpPost("account/me/email/resend"), Authorize] + public async Task ResendEmail() + { + var userId = User.GetUserId(); + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (plrData == null) return new Success(false, "Invalid profile.", null); + + + + var pending = plrData.PendingEmail; + if (string.IsNullOrWhiteSpace(pending) || !pending.Contains('@')) + return new Success(false, "No email address pending verification.", null); + + if (plrData.EmailVerified) + return new Success(true, null, plrData.Email); + + if (string.IsNullOrWhiteSpace(plrData.EmailVerifyToken)) + { + plrData.EmailVerifyToken = Guid.NewGuid().ToString("N"); + await db.SaveChangesAsync(); + } + + + + var emailLimitError = CheckEmailVerificationLimit(userId); + if (emailLimitError != null) return new Success(false, emailLimitError, null); + + SendVerificationEmail(plrData); + return new Success(true, null, pending); + } + + + + + + + [HttpGet("account/me/email/verify")] + public async Task VerifyEmail([FromQuery] string code) + { + var (success, message, email) = await TryVerifyEmail(code); + return Content(VerificationPage(success, message), "text/html"); + } + + + + + + [HttpGet("account/me/email/verify/result")] + public async Task VerifyEmailResult([FromQuery] string code) + { + var (success, message, email) = await TryVerifyEmail(code); + return Ok(new + { + success, + message, + email + }); + } + + private async Task<(bool Success, string Message, string? Email)> TryVerifyEmail(string code) + { + if (string.IsNullOrWhiteSpace(code)) + return (false, "This verification link is missing its code.", null); + + var plrData = await db.Players.FirstOrDefaultAsync(x => x.EmailVerifyToken == code); + if (plrData == null) + return (false, "This verification link is invalid or has already been used.", null); + + + + var pending = plrData.PendingEmail; + if (string.IsNullOrWhiteSpace(pending)) + return (false, "There is no email waiting to be verified on this account.", null); + + + + + if (await IsEmailTaken(pending, plrData.AccountId)) + return (false, "This email is already registered to another account and can no longer be verified.", null); + + plrData.Email = pending; + plrData.PendingEmail = null; + plrData.EmailVerified = true; + plrData.EmailVerifyToken = null; + await db.SaveChangesAsync(); + + await SendAccountUpdate(plrData.AccountId); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == plrData.AccountId); + var atName = profile != null && !string.IsNullOrWhiteSpace(profile.username) ? " @" + profile.username : ""; + return (true, $"{plrData.Email} is now verified on your Radicade account{atName}. You can close this page and return to the game.", plrData.Email); + } + + + + + + + + + [HttpPost("account/me/birthday"), Authorize] + [HttpPut("account/me/birthday"), Authorize] + public async Task SetBirthday() + { + var userId = User.GetUserId(); + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (plrData == null) return new Success(false, "Invalid profile.", null); + + var raw = await ReadBirthdayTextAsync(); + + if (string.IsNullOrWhiteSpace(raw)) return new Success(false, "Birthday is required.", null); + + var error = await ApplyBirthdayAsync(raw, plrData); + if (error != null) return new Success(false, error, null); + + return new Success(true, null, plrData.Birthday!.Value.ToString("yyyy-MM-dd")); + } + + private async Task ApplyBirthdayAsync(string raw, PlayerData plrData) + { + var birthday = PlayerUtils.ParseBirthday(raw); + if (birthday == null) return "Invalid birthday."; + + plrData.Birthday = birthday; + plrData.IsJunior = PlayerUtils.IsJuniorByBirthday(birthday); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == plrData.AccountId); + if (profile != null) profile.isJunior = plrData.IsJunior; + await db.SaveChangesAsync(); + + await SendAccountUpdate(plrData.AccountId); + + return null; + } + + private async Task ApplyUsernameAsync(string raw, Profile profile) + { + + + var chosen = UsernameRules.Parse(raw, out var rejection); + if (chosen == null) return rejection ?? "A name is required."; + + var alreadyItsName = string.Equals(chosen, profile.username, StringComparison.Ordinal); + if (!alreadyItsName) + { + + + using var nameGate = await RegistrationGuard.LockNameAsync(chosen); + if (await PlayerUtils.IsNameTakenAsync(db, chosen)) return "That name is already taken."; + + profile.username = chosen; + profile.displayName = chosen; + await db.SaveChangesAsync(); + } + + + + + + if (await PlayerUtils.IsChoosingTheirAccountNameAsync(db, profile.accountId)) + await PlayerUtils.RefreshAccountCreationMarkersAsync(db, profile.accountId, usernameChosen: true); + + if (!alreadyItsName) await SendAccountUpdate(profile.accountId); + return null; + } + + private static string? ReadBirthdayPartsFromJson(string body) + { + var trimmed = body?.TrimStart() ?? string.Empty; + if (!trimmed.StartsWith('{')) return null; + + try + { + using var doc = JsonDocument.Parse(trimmed); + if (doc.RootElement.ValueKind != JsonValueKind.Object) return null; + + var fromFlat = BirthdayParts(doc.RootElement); + if (fromFlat != null) return fromFlat; + + foreach (var property in doc.RootElement.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.Object) continue; + var nested = BirthdayParts(property.Value); + if (nested != null) return nested; + } + } + catch { } + + return null; + } + + private static string? BirthdayParts(JsonElement element) + { + int? year = null, month = null, day = null; + + foreach (var property in element.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.Number || !property.Value.TryGetInt32(out var value)) continue; + + var name = property.Name; + if (year == null && name.Equals("year", StringComparison.OrdinalIgnoreCase)) year = value; + else if (month == null && (name.Equals("month", StringComparison.OrdinalIgnoreCase) || name.Equals("birthdayMonth", StringComparison.OrdinalIgnoreCase))) month = value; + else if (day == null && (name.Equals("day", StringComparison.OrdinalIgnoreCase) || name.Equals("birthdayDay", StringComparison.OrdinalIgnoreCase))) day = value; + } + + if (year == null || month == null || day == null) return null; + + var candidateYear = Math.Clamp(year.Value, 1, 9999); + var clampedMonth = Math.Clamp(month.Value, 1, 12); + var clampedDay = Math.Clamp(day.Value, 1, DateTime.DaysInMonth(candidateYear, clampedMonth)); + + return new DateTime(candidateYear, clampedMonth, clampedDay).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + } + + private string? ReadUsernameFromPayload(string body) + { + var trimmed = body?.TrimStart() ?? string.Empty; + + if (trimmed.StartsWith('{')) + { + try + { + using var doc = JsonDocument.Parse(trimmed); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var property in doc.RootElement.EnumerateObject()) + if (property.Value.ValueKind == JsonValueKind.String && + string.Equals(property.Name, "username", StringComparison.OrdinalIgnoreCase)) + return property.Value.GetString(); + + foreach (var property in doc.RootElement.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.Object) continue; + foreach (var nested in property.Value.EnumerateObject()) + if (nested.Value.ValueKind == JsonValueKind.String && + string.Equals(nested.Name, "username", StringComparison.OrdinalIgnoreCase)) + return nested.Value.GetString(); + } + } + } + catch { } + } + else if (!string.IsNullOrWhiteSpace(trimmed)) + { + var posted = HttpUtility.ParseQueryString(trimmed)["username"]; + if (!string.IsNullOrWhiteSpace(posted)) return posted; + } + + var query = Request.Query["username"].ToString(); + if (!string.IsNullOrWhiteSpace(query)) return query; + + if (Request.HasFormContentType) + { + var form = Request.Form["username"].ToString(); + if (!string.IsNullOrWhiteSpace(form)) return form; + } + + return null; + } + + private async Task ReadBirthdayTextAsync() => ReadBirthdayFromPayload(await ReadRawBodyAsync()); + + private static readonly string[] BirthdayKeys = + [ + "birthday", "birthdayUTC", "birthdayUtc", "dateOfBirth", "dateofbirth", + "dob", "birthdate", "birthDate" + ]; + + private string? ReadStringFromPayload(string body, params string[] keys) + { + var trimmed = body?.Trim() ?? string.Empty; + if (trimmed.StartsWith('{')) + { + try + { + using var doc = JsonDocument.Parse(trimmed); + foreach (var key in keys) + { + if (!doc.RootElement.TryGetProperty(key, out var value)) continue; + if (value.ValueKind == JsonValueKind.String) return value.GetString(); + if (value.ValueKind == JsonValueKind.Number) return value.GetRawText(); + if (value.ValueKind == JsonValueKind.Null) return string.Empty; + } + } + catch { } + } + + foreach (var key in keys) + { + if (Request.Query.TryGetValue(key, out var queryValue) && queryValue.Count > 0) + return queryValue[0]; + } + + if (Request.HasFormContentType) + { + foreach (var key in keys) + { + if (Request.Form.TryGetValue(key, out var formValue) && formValue.Count > 0) + return formValue[0]; + } + } + + return null; + } + + private string? ReadBirthdayFromPayload(string body) + { + var raw = ReadBirthdayFromJson(body); + if (!string.IsNullOrWhiteSpace(raw)) return raw; + + var parts = ReadBirthdayPartsFromJson(body); + if (!string.IsNullOrWhiteSpace(parts)) return parts; + + if (!string.IsNullOrWhiteSpace(body) && !body.TrimStart().StartsWith('{')) + { + var form = HttpUtility.ParseQueryString(body); + foreach (var key in BirthdayKeys) + { + raw = form[key]; + if (!string.IsNullOrWhiteSpace(raw)) return raw; + } + + var loose = BirthdayUnderAnUnknownName( + form.AllKeys.Where(key => key != null).Select(key => new KeyValuePair(key!, form[key]))); + if (!string.IsNullOrWhiteSpace(loose)) return loose; + } + + foreach (var key in BirthdayKeys) + { + raw = Request.Query[key].ToString(); + if (!string.IsNullOrWhiteSpace(raw)) return raw; + } + + var looseQuery = BirthdayUnderAnUnknownName( + Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value.ToString()))); + if (!string.IsNullOrWhiteSpace(looseQuery)) return looseQuery; + + if (Request.HasFormContentType) + { + foreach (var key in BirthdayKeys) + { + raw = Request.Form[key].ToString(); + if (!string.IsNullOrWhiteSpace(raw)) return raw; + } + + return BirthdayUnderAnUnknownName( + Request.Form.Select(pair => new KeyValuePair(pair.Key, pair.Value.ToString()))); + } + + return null; + } + + private static bool LooksLikeBirthdayName(string name) + { + var cleaned = new string(name.Where(char.IsLetterOrDigit).ToArray()).ToLowerInvariant(); + return cleaned.Contains("birthday") || cleaned is "dob" or "dateofbirth" or "birthdate"; + } + + private static string? BirthdayUnderAnUnknownName(IEnumerable> fields) + { + foreach (var field in fields) + { + if (string.IsNullOrWhiteSpace(field.Key) || string.IsNullOrWhiteSpace(field.Value)) continue; + if (!LooksLikeBirthdayName(field.Key)) continue; + if (PlayerUtils.ParseBirthday(field.Value) == null) continue; + return field.Value; + } + + return null; + } + + private static string? ReadBirthdayFromJson(string body, int depth = 0) + { + var trimmed = body?.TrimStart() ?? string.Empty; + if (!trimmed.StartsWith('{')) return null; + + try + { + using var doc = JsonDocument.Parse(trimmed); + if (doc.RootElement.ValueKind != JsonValueKind.Object) return null; + + foreach (var property in doc.RootElement.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.String) continue; + if (!BirthdayKeys.Contains(property.Name, StringComparer.OrdinalIgnoreCase)) continue; + return property.Value.GetString(); + } + + foreach (var property in doc.RootElement.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.String) continue; + if (!LooksLikeBirthdayName(property.Name)) continue; + var candidate = property.Value.GetString(); + if (PlayerUtils.ParseBirthday(candidate) == null) continue; + return candidate; + } + + if (depth >= 1) return null; + + foreach (var property in doc.RootElement.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.Object) continue; + + var nested = ReadBirthdayFromJson(property.Value.GetRawText(), depth + 1); + if (!string.IsNullOrWhiteSpace(nested)) return nested; + } + } + catch { } + + return null; + } + + private static string DescribeFields(string body) + { + var trimmed = body?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(trimmed)) return ""; + if (!trimmed.StartsWith('{')) return $"{trimmed.Length} bytes, not a JSON object"; + + try + { + using var doc = JsonDocument.Parse(trimmed); + var names = doc.RootElement.EnumerateObject().Select(x => x.Name).Take(25).ToList(); + return names.Count == 0 ? "" : string.Join(", ", names); + } + catch + { + return "unparseable JSON"; + } + } + + + + + + + [HttpPost("account/me/acceptcoc"), Authorize] + public async Task AcceptCodeOfConduct() + { + var userId = User.GetUserId(); + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (plrData == null) return new Success(false, "Invalid profile.", null); + + plrData.CodeOfConductAccepted = true; + await db.SaveChangesAsync(); + + if (await PlayerUtils.IsChoosingTheirAccountNameAsync(db, (long)userId!)) + await PlayerUtils.RefreshAccountCreationMarkersAsync(db, (long)userId!); + + await SendAccountUpdate((long)userId!); + + return new Success(true, null, null); + } + + + + + + + + [HttpGet("account/me/haspassword"), Authorize] + public async Task GetHasPassword() + { + var userId = User.GetUserId(); + if (userId == null) return null; + + var user = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == userId.Value); + if (user == null) return null; + + return !string.IsNullOrEmpty(user.Hashword); + } + + + + + + + + + + + [HttpPost("account/me/changepassword"), Authorize] + public async Task ChangePassword([FromForm] string oldPassword, [FromForm] string newPassword) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Not signed in.", null); + + if (string.IsNullOrWhiteSpace(newPassword)) return new Success(false, "New password is required.", null); + if (newPassword.Length < AuthAPI.MinPasswordLength) return new Success(false, $"Password must be at least {AuthAPI.MinPasswordLength} characters.", null); + + var user = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId.Value); + if (user == null) return new Success(false, "User does not exist.", null); + + + + if (!string.IsNullOrWhiteSpace(user.Hashword)) + { + if (string.IsNullOrEmpty(oldPassword)) + return new Success(false, "Current password is required.", null); + var hasher = new PasswordHasher(); + var check = hasher.VerifyHashedPassword(user, user.Hashword, oldPassword); + if (check != PasswordVerificationResult.Success && check != PasswordVerificationResult.SuccessRehashNeeded) + return new Success(false, "Current password is incorrect.", null); + } + + user.Hashword = new PasswordHasher().HashPassword(user, newPassword); + await db.SaveChangesAsync(); + + + + if (await PlayerUtils.IsChoosingTheirAccountNameAsync(db, userId.Value)) + await PlayerUtils.RefreshAccountCreationMarkersAsync(db, userId.Value); + + await SendAccountUpdate(userId.Value); + + return new Success(true, null, null); + } + + + + + + + private string? CheckEmailVerificationLimit(long? userId) + { + return null; + } + + private void SendVerificationEmail(PlayerData plrData) + { + if (string.IsNullOrWhiteSpace(plrData.EmailVerifyToken)) return; + + + + var address = plrData.PendingEmail; + if (string.IsNullOrWhiteSpace(address)) return; + + + + var profile = db.Profiles.FirstOrDefault(x => x.accountId == plrData.AccountId); + var atName = profile != null && !string.IsNullOrWhiteSpace(profile.username) + ? " @" + System.Net.WebUtility.HtmlEncode(profile.username) + "" + : ""; + + var link = $"https://radicade.app/verify?code={plrData.EmailVerifyToken}"; + var html = + "
" + + "\"Radicade\"" + + "

Confirm your Radicade email

" + + "

Hi there,

" + + "

We received a request to set " + System.Net.WebUtility.HtmlEncode(address) + " as the email on your Radicade account" + atName + ". Confirm it's you by clicking the button below:

" + + "

Verify my email

" + + "

If you didn't request this, you can safely ignore this email.

"; + + _ = RadicadeMail.SendAsync(address, "Confirm your email - Radicade", html); + } + + private static string VerificationPage(bool success, string message) + { + var color = success ? "#16a34a" : "#dc2626"; + return "Radicade Email Verification" + + "" + + "
" + + "
" + (success ? "✅" : "⚠️") + "
" + + "

" + (success ? "Email verified" : "Verification failed") + "

" + + "

" + message + "

" + + "
"; + } + + [HttpGet("account/bulk")] + public async Task> GetBulk([FromQuery] int[] id) + { + List result = new List(); + foreach (int i in id) + { + Profile? profile = await db.Profiles.FirstOrDefaultAsync((prf) => prf.accountId == i); + result.Add(profile ?? BuildMissingProfile(i)); + } + return result; + } + + [HttpGet("account/{id}")] + public async Task GetAccount(long id) + { + Profile? profile = await db.Profiles.FirstOrDefaultAsync((prf) => prf.accountId == id); + return profile ?? BuildMissingProfile(id); + } + + [HttpGet("account/{id}/clubs")] + public async Task GetClubsForAccount(long id) + { + var clubs = await ClubStore.LoadAsync(db); + return Ok(clubs.Where(x => x.ownerPlayerId == id) + .Select(x => ClubStore.BuildClub(x, User.GetUserId() ?? 0)) + .ToArray()); + } + + private static Profile BuildMissingProfile(long accountId) + { + var name = $"Player{accountId}"; + return new Profile + { + accountId = accountId, + profileImage = "DefaultProfileImage", + isJunior = false, + platforms = 1, + username = name, + displayName = name, + bio = "", + createdAt = DateTime.UtcNow + }; + } + + public async Task SendAccountUpdate(long userId) + { + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); + var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == userId); + if (profile == null) return; + if (plrData == null) return; + + await Notify.SendNotify(userId, new Notification + { + Id = "AccountUpdate", + Msg = profile + }); + + await Notify.UpdateSubscriptions(userId, Notifs.SubscriptionUpdateProfile, profile); + + await Notify.SendNotify(userId, new Notification + { + Id = "SelfAccountUpdate", + Msg = PlayerUtils.BuildSelfAccount(profile, plrData) + }); + } + } +} diff --git a/Server/Players/AdministrativeData.cs b/Server/Players/AdministrativeData.cs new file mode 100644 index 0000000..4c3c08a --- /dev/null +++ b/Server/Players/AdministrativeData.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Accounts +{ + public class AdministrativeData + { + public DateTime LastLoginTime { get; set; } + public DateTime JoinData { get; set; } + public int PermissionFactors { get; set; } + public int TrustFactor { get; set; } + } +} diff --git a/Server/Players/AuthSecurityMonitor.cs b/Server/Players/AuthSecurityMonitor.cs new file mode 100644 index 0000000..19c5bee --- /dev/null +++ b/Server/Players/AuthSecurityMonitor.cs @@ -0,0 +1,122 @@ +using System.Collections.Concurrent; + +namespace RadicadeServer.Utils +{ + public static class AuthSecurityMonitor + { + public static readonly TimeSpan Window = TimeSpan.FromMinutes(10); + + public const int AccountFailureAlertThreshold = 8; + + public const int SourceSprayAccountThreshold = 12; + + public const int SourceFailureAlertThreshold = 40; + + public const int MaxProgressiveDelayMs = 3000; + + private static readonly ConcurrentDictionary AccountFailures = new(); + private static readonly ConcurrentDictionary SourceFailures = new(); + + public static int RecordFailedLogin(long? accountId, string? ip) + { + var now = DateTime.UtcNow; + var accountCount = 0; + + if (accountId is > 0) + { + var counter = AccountFailures.GetOrAdd(accountId.Value, _ => new Counter()); + accountCount = counter.Record(now, Window); + if (accountCount == AccountFailureAlertThreshold) + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security alert: {accountCount} failed sign-ins for account {accountId.Value} within {Window.TotalMinutes:0} minutes - for staff review only, no automatic action taken"); + } + + if (!string.IsNullOrWhiteSpace(ip)) + { + var source = SourceFailures.GetOrAdd(ip!, _ => new SourceCounter()); + var (failures, accounts) = source.Record(now, Window, accountId); + if (accounts == SourceSprayAccountThreshold) + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security alert: suspected password spraying from {ip} - failed sign-ins for {accounts} different accounts within {Window.TotalMinutes:0} minutes"); + else if (failures == SourceFailureAlertThreshold) + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security alert: {failures} failed sign-ins from {ip} within {Window.TotalMinutes:0} minutes"); + } + + return accountCount; + } + + public static int ProgressiveDelayMs(int failureCount) => + failureCount <= 3 ? 0 : Math.Min(MaxProgressiveDelayMs, (failureCount - 3) * 300); + + public static bool IsSourceThrottled(string? ip) + { + if (string.IsNullOrWhiteSpace(ip)) return false; + if (!SourceFailures.TryGetValue(ip!, out var source)) return false; + return source.DistinctAccounts(DateTime.UtcNow, Window) >= SourceSprayAccountThreshold; + } + + public static void RecordSuccessfulLogin(long accountId, string? ip, bool newDevice, bool newIp) + { + if (accountId > 0) AccountFailures.TryRemove(accountId, out _); + + if (newDevice || newIp) + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security note: account {accountId} signed in from a {(newDevice ? "new device" : "new IP")} - informational only, this never bans or locks by itself"); + } + + public static void NoteCredentialChange(long accountId, string kind) => + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] security note: {kind} for account {accountId} - verify it was requested by the account owner"); + + public static void Reset() + { + AccountFailures.Clear(); + SourceFailures.Clear(); + } + + private sealed class Counter + { + private readonly object gate = new(); + private readonly List hits = new(); + + public int Record(DateTime now, TimeSpan window) + { + lock (gate) + { + hits.RemoveAll(t => now - t > window); + hits.Add(now); + return hits.Count; + } + } + } + + private sealed class SourceCounter + { + private readonly object gate = new(); + private readonly List<(DateTime At, long AccountId)> hits = new(); + + public (int Failures, int Accounts) Record(DateTime now, TimeSpan window, long? accountId) + { + lock (gate) + { + hits.RemoveAll(h => now - h.At > window); + hits.Add((now, accountId ?? 0)); + return (hits.Count, Distinct()); + } + } + + public int DistinctAccounts(DateTime now, TimeSpan window) + { + lock (gate) + { + hits.RemoveAll(h => now - h.At > window); + return Distinct(); + } + } + + private int Distinct() + { + var set = new HashSet(); + foreach (var hit in hits) + if (hit.AccountId > 0) set.Add(hit.AccountId); + return set.Count; + } + } + } +} diff --git a/Server/Players/BioResponse.cs b/Server/Players/BioResponse.cs new file mode 100644 index 0000000..028614f --- /dev/null +++ b/Server/Players/BioResponse.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Accounts +{ + public class BioResponse + { + public long accountId { get; set; } + public string bio { get; set; } = null!; + } +} diff --git a/Server/Players/Currency.cs b/Server/Players/Currency.cs new file mode 100644 index 0000000..788213d --- /dev/null +++ b/Server/Players/Currency.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Accounts +{ + public class Currency + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long AccountId { get; set; } + + public long RecTokens { get; set; } = 0; + public long LaserTagTickets { get; set; } = 0; + public long Gold { get; set; } = 0; + public long DraculaSilver { get; set; } = 0; + } +} diff --git a/Server/Players/FirstTimeUtils.cs b/Server/Players/FirstTimeUtils.cs new file mode 100644 index 0000000..55c9812 --- /dev/null +++ b/Server/Players/FirstTimeUtils.cs @@ -0,0 +1,371 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Extensions; +using System; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Progression; + +namespace RadicadeServer.Utils +{ + public static class FirstTimeUtils + { + + + public const string AutoMicMutingConfig = "{\"MicSpamVolumeThreshold\":1.125,\"MicVolumeSampleInterval\":0.25,\"MicVolumeSampleRollingWindowLength\":7,\"MicSpamSamplePercentageForWarning\":0.8,\"MicSpamSamplePercentageForWarningToEnd\":0.2,\"MicSpamSamplePercentageForForceMute\":0.8,\"MicSpamSamplePercentageForForceMuteToEnd\":0.2,\"MicSpamWarningStateVolumeMultiplier\":0.25}"; + public const string StorefrontConfig = "{\"MinPlayerLevelForGifting\":1,\"LatestStoreBadgeDateTime\":\"2025-11-13T17:27:21.5641043Z\"}"; + public const string LevelProgressionMap = "[{\"Level\":0,\"RequiredXp\":0,\"GiftDropId\":null},{\"Level\":1,\"RequiredXp\":10,\"GiftDropId\":3288},{\"Level\":2,\"RequiredXp\":10,\"GiftDropId\":3293},{\"Level\":3,\"RequiredXp\":10,\"GiftDropId\":3288},{\"Level\":4,\"RequiredXp\":20,\"GiftDropId\":3293},{\"Level\":5,\"RequiredXp\":20,\"GiftDropId\":3288},{\"Level\":6,\"RequiredXp\":20,\"GiftDropId\":3293},{\"Level\":7,\"RequiredXp\":20,\"GiftDropId\":3288},{\"Level\":8,\"RequiredXp\":20,\"GiftDropId\":3293},{\"Level\":9,\"RequiredXp\":20,\"GiftDropId\":3288},{\"Level\":10,\"RequiredXp\":20,\"GiftDropId\":3293},{\"Level\":11,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":12,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":13,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":14,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":15,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":16,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":17,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":18,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":19,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":20,\"RequiredXp\":45,\"GiftDropId\":3293},{\"Level\":21,\"RequiredXp\":115,\"GiftDropId\":3293},{\"Level\":22,\"RequiredXp\":115,\"GiftDropId\":3292},{\"Level\":23,\"RequiredXp\":115,\"GiftDropId\":3293},{\"Level\":24,\"RequiredXp\":115,\"GiftDropId\":3292},{\"Level\":25,\"RequiredXp\":115,\"GiftDropId\":3293},{\"Level\":26,\"RequiredXp\":115,\"GiftDropId\":3292},{\"Level\":27,\"RequiredXp\":115,\"GiftDropId\":3293},{\"Level\":28,\"RequiredXp\":115,\"GiftDropId\":3292},{\"Level\":29,\"RequiredXp\":115,\"GiftDropId\":3293},{\"Level\":30,\"RequiredXp\":115,\"GiftDropId\":3292},{\"Level\":31,\"RequiredXp\":360,\"GiftDropId\":3291},{\"Level\":32,\"RequiredXp\":360,\"GiftDropId\":3292},{\"Level\":33,\"RequiredXp\":360,\"GiftDropId\":3292},{\"Level\":34,\"RequiredXp\":360,\"GiftDropId\":3292},{\"Level\":35,\"RequiredXp\":360,\"GiftDropId\":3291},{\"Level\":36,\"RequiredXp\":360,\"GiftDropId\":3292},{\"Level\":37,\"RequiredXp\":360,\"GiftDropId\":3292},{\"Level\":38,\"RequiredXp\":360,\"GiftDropId\":3292},{\"Level\":39,\"RequiredXp\":360,\"GiftDropId\":3292},{\"Level\":40,\"RequiredXp\":360,\"GiftDropId\":3291},{\"Level\":41,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":42,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":43,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":44,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":45,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":46,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":47,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":48,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":49,\"RequiredXp\":1080,\"GiftDropId\":3291},{\"Level\":50,\"RequiredXp\":1080,\"GiftDropId\":3290},{\"Level\":51,\"RequiredXp\":270,\"GiftDropId\":33363},{\"Level\":52,\"RequiredXp\":270,\"GiftDropId\":3292},{\"Level\":53,\"RequiredXp\":297,\"GiftDropId\":33462},{\"Level\":54,\"RequiredXp\":297,\"GiftDropId\":3291},{\"Level\":55,\"RequiredXp\":297,\"GiftDropId\":18152},{\"Level\":56,\"RequiredXp\":356,\"GiftDropId\":3292},{\"Level\":57,\"RequiredXp\":356,\"GiftDropId\":33466},{\"Level\":58,\"RequiredXp\":356,\"GiftDropId\":3291},{\"Level\":59,\"RequiredXp\":356,\"GiftDropId\":3314},{\"Level\":60,\"RequiredXp\":356,\"GiftDropId\":3290},{\"Level\":61,\"RequiredXp\":428,\"GiftDropId\":3292},{\"Level\":62,\"RequiredXp\":428,\"GiftDropId\":33338},{\"Level\":63,\"RequiredXp\":428,\"GiftDropId\":3292},{\"Level\":64,\"RequiredXp\":428,\"GiftDropId\":33463},{\"Level\":65,\"RequiredXp\":428,\"GiftDropId\":4463},{\"Level\":66,\"RequiredXp\":428,\"GiftDropId\":3291},{\"Level\":67,\"RequiredXp\":428,\"GiftDropId\":3880},{\"Level\":68,\"RequiredXp\":428,\"GiftDropId\":3292},{\"Level\":69,\"RequiredXp\":428,\"GiftDropId\":33464},{\"Level\":70,\"RequiredXp\":428,\"GiftDropId\":33329},{\"Level\":71,\"RequiredXp\":556,\"GiftDropId\":3292},{\"Level\":72,\"RequiredXp\":556,\"GiftDropId\":3025},{\"Level\":73,\"RequiredXp\":556,\"GiftDropId\":3291},{\"Level\":74,\"RequiredXp\":556,\"GiftDropId\":33305},{\"Level\":75,\"RequiredXp\":556,\"GiftDropId\":33467},{\"Level\":76,\"RequiredXp\":556,\"GiftDropId\":3292},{\"Level\":77,\"RequiredXp\":556,\"GiftDropId\":33465},{\"Level\":78,\"RequiredXp\":556,\"GiftDropId\":3291},{\"Level\":79,\"RequiredXp\":556,\"GiftDropId\":33202},{\"Level\":80,\"RequiredXp\":556,\"GiftDropId\":33051},{\"Level\":81,\"RequiredXp\":778,\"GiftDropId\":3291},{\"Level\":82,\"RequiredXp\":778,\"GiftDropId\":33335},{\"Level\":83,\"RequiredXp\":778,\"GiftDropId\":3292},{\"Level\":84,\"RequiredXp\":778,\"GiftDropId\":33281},{\"Level\":85,\"RequiredXp\":778,\"GiftDropId\":33203},{\"Level\":86,\"RequiredXp\":778,\"GiftDropId\":3291},{\"Level\":87,\"RequiredXp\":778,\"GiftDropId\":33468}]"; + public const string DailyObjectives = "[[{\"type\":1030,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":1033,\"score\":10,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":2000,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null}],[{\"type\":601,\"score\":2,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":32,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":6,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null}],[{\"type\":301,\"score\":2,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":302,\"score\":2,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":6,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null}],[{\"type\":701,\"score\":2,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":702,\"score\":20,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":32,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null}],[{\"type\":1010,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":1013,\"score\":10,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":6,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null}],[{\"type\":500,\"score\":2,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":502,\"score\":20,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":2000,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null}],[{\"type\":1020,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":1023,\"score\":10,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null},{\"type\":32,\"score\":1,\"xp\":200,\"descriptionOverride\":null,\"tooltipOverride\":null}]]"; + public const string NamedImages = "[{\"FriendlyImageName\":\"DormRoomBucket\",\"ImageName\":\"n/67gesMhbOkCd3-qO1cKkIg\",\"StartTime\":\"2018-09-27T18:00:00Z\",\"EndTime\":\"2222-02-22T22:22:00Z\"},{\"FriendlyImageName\":\"ControlSchemeTutorial_Creator\",\"ImageName\":\"n/877acd69ecf44493a21021fadb5f77e4\",\"StartTime\":\"2023-05-23T00:12:00Z\",\"EndTime\":\"2030-05-23T00:12:00Z\"},{\"FriendlyImageName\":\"UI_Menu_Token_Image_10\",\"ImageName\":\"n/54094cbd9d0b455fb949266d1679a834\",\"StartTime\":\"2025-06-02T07:00:00Z\",\"EndTime\":\"2099-06-02T07:00:00Z\"},{\"FriendlyImageName\":\"UI_Menu_Token_Image_25\",\"ImageName\":\"n/4e4e496810ae4bbcb61564c34df450b6\",\"StartTime\":\"2025-06-02T07:00:00Z\",\"EndTime\":\"2099-06-02T07:00:00Z\"},{\"FriendlyImageName\":\"UI_Menu_Token_Image_50\",\"ImageName\":\"n/ca1c08334a7a422b941ef2987b636edf\",\"StartTime\":\"2025-06-02T07:00:00Z\",\"EndTime\":\"2099-06-02T07:00:00Z\"},{\"FriendlyImageName\":\"UI_Menu_Token_Image_100\",\"ImageName\":\"n/979ea0d3d9e9487cb7ca56c37c7ece6b\",\"StartTime\":\"2025-06-02T07:00:00Z\",\"EndTime\":\"2099-06-02T07:00:00Z\"},{\"FriendlyImageName\":\"MintFloatieBetterThumbnail\",\"ImageName\":\"n/23998eb5a1714dcc94a97b9b6ea82d50\",\"StartTime\":\"2025-07-02T23:24:00Z\",\"EndTime\":\"2026-07-02T23:24:00Z\"},{\"FriendlyImageName\":\"IceCreamHatBetterThumbnail\",\"ImageName\":\"n/aec858dad86b42238338da60a5071dc2\",\"StartTime\":\"2025-07-02T23:25:00Z\",\"EndTime\":\"2026-07-02T23:25:00Z\"},{\"FriendlyImageName\":\"IceCreamShirtBetterThumbnail\",\"ImageName\":\"n/55a94ecff89543b0bef248bc1b232906\",\"StartTime\":\"2025-07-02T23:25:00Z\",\"EndTime\":\"2026-07-02T23:25:00Z\"},{\"FriendlyImageName\":\"IceCreamPantsBetterThumbnail\",\"ImageName\":\"n/3cd34681c224408e9f1a8d29ed3efd66\",\"StartTime\":\"2025-07-02T23:26:00Z\",\"EndTime\":\"2026-07-02T23:26:00Z\"},{\"FriendlyImageName\":\"IceCreamGlovesBetterThumbnail\",\"ImageName\":\"n/a68d8ae9938045ee895028f0be46e141\",\"StartTime\":\"2025-07-02T23:26:00Z\",\"EndTime\":\"2026-07-02T23:26:00Z\"}]"; + public static readonly string LoadingScreen = LoadingScreenImages.Tips.ToJson(); + public const string GiftDropStoreItems = "[{\"PurchasableItemId\":2597201,\"Type\":0,\"IsFeatured\":true,\"Prices\":[{\"CurrencyType\":2,\"Price\":100}],\"GiftDrops\":[{\"GiftDropId\":0,\"ItemSetId\":0,\"ItemSetFriendlyName\":\"rawdata\",\"FriendlyName\":\"rawdata\",\"Tooltip\":\"rawdata\",\"IsQuery\":false,\"Unique\":false,\"SubscribersOnly\":false,\"ConsumableItemDesc\":\"\",\"AvatarItemDesc\":\"274cb9b2-2f59-47ea-9a8d-a5b656d148c6,,,\",\"AvatarItemType\":0,\"EquipmentPrefabName\":\"\",\"EquipmentModificationGuid\":\"\",\"CurrencyType\":0,\"Currency\":0,\"Xp\":0,\"Platform\":0,\"PlatformsToSpawnOn\":0,\"Context\":0,\"Rarity\":0}]}]"; + public const string CommunityBoard = "{\"FeaturedPlayer\":{\"Id\":1,\"TitleOverride\":\"Community Star\",\"UrlOverride\":\"\"},\"FeaturedRoomGroup\":{\"FeaturedRoomGroupId\":1,\"Name\":\"Top Picks\",\"Rooms\":[{\"RoomId\":2,\"RoomName\":\"Rec Center\",\"ImageName\":\"\"},{\"RoomId\":4,\"RoomName\":\"Golden Trophy\",\"ImageName\":\"\"}]},\"CurrentAnnouncement\":{\"Message\":\"Welcome to Radicade!\",\"MoreInfoUrl\":\"\"},\"InstagramImages\":null,\"Videos\":null}"; + public const string CurrentFeatured = "{\"FeaturedRoomGroupId\":0,\"Name\":\"RecRoomOriginals\",\"Rooms\":[{\"RoomId\":2,\"RoomName\":\"RecCenter\",\"ImageName\":\"RecCenter\"}]}"; + + + private static Dictionary Images = new(); + + private static void ParseImagesFile() + { + if (!File.Exists(DBConfig.DefaultImagesFile)) + { + return; + } + + string readFile = File.ReadAllText(DBConfig.DefaultImagesFile); + string[] keys = readFile.Split('\n', StringSplitOptions.RemoveEmptyEntries); + foreach (var key in keys) + { + if (key.StartsWith("#")) continue; + if (string.IsNullOrEmpty(key)) continue; + + string[] values = key.Split('=', 2, StringSplitOptions.TrimEntries); + if (values.Length != 2) continue; + + Images[values[0]] = values[1]; + } + } + + public async static Task DownloadImages() + { + if (Images.Count == 0) + ParseImagesFile(); + + HttpClient httpClient = new HttpClient + { + + Timeout = TimeSpan.FromSeconds(30) + }; + foreach (var image in Images.Keys) + { + string filePath = Path.Join(DBConfig.CDNImages, image); + string url = Images[image]; + + if (File.Exists(filePath)) continue; + + using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + if (!response.IsSuccessStatusCode) + { + continue; + } + + await using var stream = await response.Content.ReadAsStreamAsync(); + await using var fileStream = File.Create(filePath); + + await stream.CopyToAsync(fileStream); + } + httpClient.Dispose(); + } + + public async static Task AddCoach(this RecDBContext ctx) + { + Profile profile = new Profile(); + + profile.platforms = 1; + profile.username = "Coach"; + profile.profileImage = "default"; + profile.displayName = profile.username; + profile.isJunior = false; + profile.createdAt = DateTime.Now; + + await ctx.Profiles.AddAsync(profile); + await ctx.SaveChangesAsync(); + + + PlayerAvatar playerAvatar = new PlayerAvatar(); + playerAvatar.Avatar = DBConfig.DefaultAvatar; + playerAvatar.Outfits = "[]"; + playerAvatar.AccountId = profile.accountId; + + + Reputation reputation = new Reputation(); + reputation.AccountId = profile.accountId; + + + PlayerSettings settings = new PlayerSettings(); + settings.AccountId = profile.accountId; + settings.Settings = DBConfig.DefaultSettings2023; + + + ProgressionData progression = new ProgressionData(); + progression.PlayerId = profile.accountId; + progression.XP = 0; + progression.Level = 99; + progression.Progress = new ProgressionJson().ToJson(); + + + RealPresence presence = new RealPresence(); + presence.playerId = profile.accountId; + presence.deviceClass = DeviceClass.Unknown; + presence.statusVisibility = StatusVisibility.Public; + presence.vRMovementMode = VRMovementModes.TELEPORT; + presence.roomInstance = null; + presence.LoginLock = null; + + + Currency currency = new Currency(); + currency.AccountId = profile.accountId; + + + PlayerUnlocks unlocks = new PlayerUnlocks(); + unlocks.AccountId = profile.accountId; + + + CachedLogin cachedLogin = new CachedLogin(); + + cachedLogin.accountId = profile.accountId; + cachedLogin.platform = Platforms.All; + cachedLogin.platformId = "0"; + cachedLogin.lastLoginTime = DateTime.Now; + + + PlayerData playerData = new PlayerData(); + playerData.AccountId = profile.accountId; + playerData.Phone = ""; + playerData.Birthday = DateTime.MinValue; + playerData.Parent = null; + playerData.AppVersion = DBConfig.GameVersion; + playerData.DeviceClass = DeviceClass.Unknown; + playerData.NameChanges = 99; + playerData.Email = ""; + playerData.Hashword = ""; + playerData.JuniorState = JuniorState.BasedOnBirthday; + playerData.Visibility = StatusVisibility.Public; + + + await ctx.CachedLogins.AddAsync(cachedLogin); + await ctx.Avatars.AddAsync(playerAvatar); + await ctx.Reputations.AddAsync(reputation); + await ctx.PlayerSettings.AddAsync(settings); + await ctx.Presences.AddAsync(presence); + await ctx.Currency.AddAsync(currency); + await ctx.Unlocks.AddAsync(unlocks); + await ctx.Players.AddAsync(playerData); + await ctx.Progression.AddAsync(progression); + await ctx.SaveChangesAsync(); + } + + public async static Task AddServerConfig(this RecDBContext ctx) + { + + await ctx.ServerConfig.AddAsync(new ServerConfigVal("AccountCreate.RequireSteamTicket", "false")); + + + await ctx.ServerConfig.AddAsync(new ServerConfigVal("WhitelistOnly", "false")); + await ctx.ServerConfig.AddAsync(new ServerConfigVal("SteamWhitelist", "")); + + await ctx.ServerConfig.AddAsync(new ServerConfigVal("Announcement", "[]")); + + await ctx.ServerConfig.AddAsync(new ServerConfigVal("NamedImages", NamedImages)); + await ctx.ServerConfig.AddAsync(new ServerConfigVal("LoadingScreenTipData", LoadingScreen)); + + await ctx.ServerConfig.AddAsync(new ServerConfigVal("CommunityBoard", CommunityBoard)); + await ctx.ServerConfig.AddAsync(new ServerConfigVal("CurrentFeaturedRooms", CurrentFeatured)); + + await ctx.ServerConfig.AddAsync(new ServerConfigVal("Filters", "rro")); + + await ctx.ServerConfig.AddAsync(new ServerConfigVal("CharadesWords", "Dog;Cat;Bird;Fish;Monkey;Elephant;Dinosaur;Robot;Superhero;Pirate;Cowboy;Astronaut;Chef;Doctor;Police Officer;Firefighter;Teacher;Baby;Zombie;Ghost;Brushing Teeth;Taking a Shower;Eating Spaghetti;Making a Sandwich;Riding a Bike;Swimming;Running;Sleeping;Dancing;Singing;Fishing;Cooking;Driving a Car;Flying a Plane;Basketball;Soccer;Guitar;Taking a Selfie;Opening a Present;Walking a Dog;Chicken;LEGO;Banana Peel;Bees;Fly;Elevator;Phone;Jar;Spider;Dinosaur;Invisible Enemy;Wind;Mosquito;Falling;Reflection;Sneaking;Glass Door;Carrying Boxes;Revolving Door;Floor Is Lava;Magician;Detective;Ninja;Vampire;Werewolf;King;Queen;Knight;Scientist;Construction Worker;Photographer;Rock Star;News Reporter;Secret Agent;Treasure Hunter;Time Traveler;Supervillain;Circus Performer;Archaeologist;Lifeguard;Raccoon;Penguin;Pizza;Grandma;Alien;Wi-Fi;Police Officer;Superhero;Job;Horse;Fish;Ghost;Vampire;Monkey;Astronaut;Wizard;Shark;Tiny Door")); + await ctx.ServerConfig.AddAsync(new ServerConfigVal("CharadesAprilFoolsWords", "Prank;Joke;Fool;Trick;Clown;Jester;Comedian;Confetti;Balloon;Pie;Banana;Chicken;Spider;Bug;Snake;Mustache;Disguise;Mask;Costume;Surprise;Laughter;Sneak;Hide;Scare;Troll;Mischief;Chaos;Fake;Lie;Trap;Decoy;Magic;Wizard;Detective;Thief;Rubber Chicken;Whoopee Cushion;Whistle;Air Horn;Party;Celebration;Secret;Shock;Confusion;Scream;Giggle;Smile;Prankster;Foolish;Trickster;Gotcha;April Fools")); + await ctx.ServerConfig.AddAsync(new ServerConfigVal("Icebreakers", "Favorite Game;First Game;Favorite Room;Funniest Gaming Moment;Dream Game Feature;Favorite Platform;Favorite Game Genre;Worst Game;Favorite Soundtrack;Favorite Video Game World;Favorite Food;Weirdest Food;Favorite Animal;Weirdest Dream;Dream Skill;Useless Talent;Favorite Meal;Favorite Movie;Favorite TV Show;Favorite Song;Favorite Holiday;Morning Person;Night Owl;Dream Vacation;Dream Job;Favorite Season;Favorite Hobby;Favorite Sport;Favorite Color;Favorite Memory;Flying;Invisibility;Unlimited Money;Free Time;Underwater;Space;Internet;Video Games;Rewind;Pause;Future;Past;Luck;Intelligence;Theme Park;Video Game;Tiny Version;Giant Version;Loading Screen;Username;NPC;Avatar;Food Name;Superpower;Aliens;Pet;Dumbest Argument;Dream House;Dream Car;Favorite Character;Favorite YouTuber;Favorite Musician;Favorite Book;Favorite App;Favorite Website;Favorite Holiday;Favorite Game Character;Favorite Snack;Favorite Drink;Favorite Place;Favorite School Subject;Favorite Sport;Favorite Meme;Favorite Emoji;Favorite Childhood Game;Dream Pet;Dream Superpower;Dream Adventure;Best Friend;Biggest Fear;Hidden Talent;Bucket List;Perfect Day;Favorite Weather;Favorite Time of Day;Favorite Season;Favorite Number;Favorite Letter;Favorite Fruit;Favorite Candy;Favorite Restaurant;Favorite Pizza Topping;Favorite Ice Cream;Favorite Cereal;Favorite Breakfast;Favorite Fast Food;Favorite Dessert;Favorite Vegetable;Favorite Cartoon;Favorite Superhero;Favorite Villain;Favorite Movie Character;Favorite Game Developer")); + + await ctx.ServerConfig.AddAsync(new ServerConfigVal("HighRanks", "1")); + + } + + public async static Task AddConfig(this RecDBContext ctx) + { + + MainConfig mainConfig = new MainConfig(); + + mainConfig.ShareBaseUrl = DBConfig.NameServer.WWW + "{0}"; + mainConfig.CdnBaseUri = DBConfig.NameServer.CDN; + mainConfig.LevelProgressionMaps = LevelProgressionMap; + mainConfig.AutoMicMutingConfig = AutoMicMutingConfig; + mainConfig.StorefrontConfig = StorefrontConfig; + mainConfig.DailyObjectives = DailyObjectives; + mainConfig.ServerMaintenance = "{\"StartsInMinutes\":0}"; + mainConfig.RoomKeyConfig = "{\"MaxKeysPerRoom\":500}"; + mainConfig.RoomCurrencyConfig = "{}"; + mainConfig.GiftDropStoreItems = GiftDropStoreItems; + mainConfig.FUCKYOUMICROSOFT = Guid.NewGuid().ToString(); + + await ctx.Config.AddAsync(mainConfig); + } + + public async static Task AddDefaultRooms(this RecDBContext ctx) + { + + await AddDormRoom(ctx); + + await DefaultRooms.EnsureAsync(ctx); + } + + public async static Task AddDormRoom(this RecDBContext ctx) + { + + SubRoom dormSubRoom = new SubRoom(); + dormSubRoom.DataBlob = ""; + dormSubRoom.IsSandbox = true; + dormSubRoom.MaxPlayers = 20; + dormSubRoom.Accessibility = RecNet.Enums.Accessibility.Private; + dormSubRoom.Name = "Home"; + dormSubRoom.UnitySceneId = "76d98498-60a1-430c-ab76-b54a29b7a163"; + dormSubRoom.DataBlob = ""; + + RoomStats roomStats = new RoomStats(); + + RoomData dormRoom = new RoomData(); + dormRoom.Name = "DormRoom"; + dormRoom.ImageName = DormRoomImageName; + dormRoom.Description = "Your private room."; + dormRoom.CreatorAccountId = 1; + dormRoom.IsRRO = true; + dormRoom.IsDorm = true; + dormRoom.Accessibility = RecNet.Enums.Accessibility.Private; + dormRoom.CreatedAt = DateTime.Now; + dormRoom.State = RoomState.Active; + dormRoom.CloningAllowed = true; + dormRoom.Tags = StringTools.CreateTagString(new List { new ItemTag { Type = TagType.General, Tag = "dormroom" } }); + + await ctx.Rooms.AddAsync(dormRoom); + await ctx.SaveChangesAsync(); + + dormSubRoom.RoomId = dormRoom.RoomId; + roomStats.RoomId = dormRoom.RoomId; + + await ctx.SubRooms.AddAsync(dormSubRoom); + await ctx.RoomStats.AddAsync(roomStats); + await ctx.SaveChangesAsync(); + } + + public const string DormRoomImageName = "Dorm"; + + public async static Task NormalizeDormRoomImagesAsync(this RecDBContext ctx) + { + var unsetNames = new[] { "", "default.png", DBConfig.DefaultRoomImage }; + + var dorms = await ctx.Rooms.Where(room => room.IsDorm).ToListAsync(); + var missing = dorms + .Where(room => unsetNames.Any(unset => string.Equals(unset, room.ImageName?.Trim() ?? string.Empty, StringComparison.OrdinalIgnoreCase))) + .ToList(); + if (missing.Count == 0) + return; + + foreach (var dorm in missing) + dorm.ImageName = DormRoomImageName; + + await ctx.SaveChangesAsync(); + } + + + public async static Task AddGameConfigs(this RecDBContext ctx) + { + + + await ctx.GameConfig.AddAsync(new ConfigVal("Screens.ForceVerification", "1")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("SafePlaySFXAudioSourceSounds", "true")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("Backtrace.config", "0|0.05|0|20|50|1|0|0|1000")); + await ctx.GameConfig.AddAsync(new ConfigVal("Backtrace.enabledLikelihood", "1.0|1.0|1.0|1.0|1.0|1.0|1.0|1.0")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("RRUI.UseRRUIHomeScreen", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("RRUI.UseNewProfile", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("UseRRUIEventsScreen", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("ClickOnName.MaxRaycastDistance", "5")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.ScreenSharing.CompressionQualityMultiplier", "1.0")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.ScreenSharing.RefreshHzMultiplier", "30.0")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.ScreenSharing.ResolutionMultiplier", "1.0")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.ScreenSharing.EnableZipCompression", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.ScreenSharing.MaxFragmentBytes", "15360")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.UseDeveloperMode", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.Circuits.MaxChipResources", "3000")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.Persistence.MaxCloudVariables", "100")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.EnableRRUIConfigMenu", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.ForbiddenSavedSpawnableTools", "")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.RoomSavingEnabled", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.InventionSavingEnabled", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.MaxChipsVisible", "5000")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.MaxChipsUnculled", "200")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.IgnoreInkCost", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.Persistence.Enabled", "true")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.Persistence.AutosaveIntervalSeconds", "300")); + await ctx.GameConfig.AddAsync(new ConfigVal("UGC.EnableInventionsInBetaRooms", "true")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("Rendering.DisableSrpBatcher", "")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("splitTestSoftOverrides", "")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("RoomDetails.PhotoRollEnabled", "true")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("runningNetworkTimeout", "30")); + await ctx.GameConfig.AddAsync(new ConfigVal("loadingNetworkTimeout", "180")); + + await ctx.GameConfig.AddAsync(new ConfigVal("SynchronizedField.RemoveDefaultEntries", "true")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("Rewards.SelectionTimeout", "1800")); + await ctx.GameConfig.AddAsync(new ConfigVal("Rewards.UseRewardSelection", "true")); + + + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Creative.Query", "^CreativeCampus | ^RecRoomGallery")); + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Creative.Title", "Creative Campus")); + + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Featured.Query", "#featured")); + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Featured.Title", "Featured")); + + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Quests.Query", "#quest #rro")); + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Quests.Title", "QUESTS")); + + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Shooters.Query", "#import")); + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Shooters.Title", "Import Rooms!")); + + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Sports.Query", "#sport #rro | #pvp #rro")); + await ctx.GameConfig.AddAsync(new ConfigVal("Door.Sports.Title", "SPORTS & PVP")); + + await ctx.GameConfig.AddAsync(new ConfigVal("Door.StuntRunner.Query", "#stuntrunner #community")); + await ctx.GameConfig.AddAsync(new ConfigVal("Door.StuntRunner.Title", "COMMUNITY")); + } + } +} diff --git a/Server/Players/LinkAPI.cs b/Server/Players/LinkAPI.cs new file mode 100644 index 0000000..bd1dfec --- /dev/null +++ b/Server/Players/LinkAPI.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.API.Link; + +[Route("link")] +public class LinkAPI : Controller +{ + [HttpGet("actionlink/{code}")] + public IActionResult ConsumeActionLink(string code) + { + if (string.Equals(code?.Trim(), "COACH", StringComparison.OrdinalIgnoreCase)) + return Ok(new { Success = true, Code = "COACH", InfluencerAccountId = 1L }); + return Ok(new { Success = false, Code = code?.Trim() ?? string.Empty, InfluencerAccountId = 0L }); + } + private static readonly HashSet KnownDeepLinks = new(StringComparer.OrdinalIgnoreCase) + { + "recroom://testlink", + "recroom://solus", + "recroom://winterwonderland", + "recroom://reccenter", + "recroom://testforcat" + }; + + [HttpPost("actionlink")] + public async Task> CheckActionLink() + { + string? linkId = null; + + using (var reader = new StreamReader(Request.Body)) + { + var body = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(body)) + { + try + { + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in doc.RootElement.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.String && + (prop.Name.Equals("shortLinkId", StringComparison.OrdinalIgnoreCase) || + prop.Name.Equals("code", StringComparison.OrdinalIgnoreCase) || + prop.Name.Equals("link", StringComparison.OrdinalIgnoreCase) || + prop.Name.Equals("deepLink", StringComparison.OrdinalIgnoreCase) || + prop.Name.Equals("fullLink", StringComparison.OrdinalIgnoreCase) || + prop.Name.Equals("roomId", StringComparison.OrdinalIgnoreCase))) + { + linkId = prop.Value.GetString(); + break; + } + } + } + } + catch (JsonException) + { + } + } + } + + if (linkId == null && Request.HasFormContentType) + linkId = Request.Form["shortLinkId"].ToString(); + + if (!string.IsNullOrWhiteSpace(linkId)) + { + var normalized = linkId.Trim(); + if (!normalized.StartsWith("recroom://", StringComparison.OrdinalIgnoreCase)) + normalized = $"recroom://{normalized}"; + if (KnownDeepLinks.Contains(normalized)) + return Ok(0); + } + + return Ok(0); + } +} diff --git a/Server/Players/Player.cs b/Server/Players/Player.cs new file mode 100644 index 0000000..c72b6d5 --- /dev/null +++ b/Server/Players/Player.cs @@ -0,0 +1,30 @@ +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Moderation; + +namespace RadicadeServer.RecNet.Accounts +{ + public class Player + { + public long Id { get; set; } + public string Username { get; set; } = null!; + public string DisplayName { get; set; } = null!; + public string Bio { get; set; } = null!; + public int XP { get; set; } + public int Level { get; set; } + public bool Developer { get; set; } + public bool Verified { get; set; } + public bool CanReceiveInvites { get; set; } + public string ProfileImageName { get; set; } = null!; + public bool JuniorProfile { get; set; } + public bool ForceJuniorImages { get; set; } + public bool PendingJunior { get; set; } + public bool HasBirthday { get; set; } + + + public bool HasAcceptedCodeOfConduct { get; set; } + public bool AvoidJuniors { get; set; } + public Reputation PlayerReputation { get; set; } = null!; + public List PlatformIds { get; set; } = null!; + public AdministrativeData AdministrativeData { get; set; } = null!; + } +} diff --git a/Server/Players/PlayerAvatar.cs b/Server/Players/PlayerAvatar.cs new file mode 100644 index 0000000..1066485 --- /dev/null +++ b/Server/Players/PlayerAvatar.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; + +namespace RadicadeServer.RecNet.Accounts.Local +{ + public class PlayerAvatar + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long AccountId { get; set; } + public string Avatar { get; set; } = null!; + public string Outfits { get; set; } = null!; + + + + + + public string EquipmentSlots { get; set; } = "[]"; + + public Avatar.Avatar? DeserializeAvatar() + { + return Avatar.FromJson(); + } + public string SerializeAvatar(Avatar.Avatar avatar) + { + Avatar = avatar.ToJson(); + return Avatar; + } + + public List? DeserializeOutfits() + { + return Outfits.FromJson>(); + } + } +} diff --git a/Server/Players/PlayerHeartbeat.cs b/Server/Players/PlayerHeartbeat.cs new file mode 100644 index 0000000..f7e4081 --- /dev/null +++ b/Server/Players/PlayerHeartbeat.cs @@ -0,0 +1,14 @@ +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.RecNet.Accounts +{ + public class PlayerHeartbeat + { + public long playerId { get; set; } + public StatusVisibility statusVisibility { get; set; } + public DeviceClass deviceClass { get; set; } + public RoomInstance? roomInstance { get; set; } + public bool isOnline { get; set; } + } +} diff --git a/Server/Players/PlayerSettings.cs b/Server/Players/PlayerSettings.cs new file mode 100644 index 0000000..049d349 --- /dev/null +++ b/Server/Players/PlayerSettings.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.Extensions; + +namespace RadicadeServer.RecNet.Accounts.Local +{ + public class PlayerSettings + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long AccountId { get; set; } + public string Settings { get; set; } = "[]"; + + public List DeserializeSettings() + { + if (string.IsNullOrWhiteSpace(Settings)) + return new List(); + + return Settings.FromJson>() ?? new List(); + } + } +} diff --git a/Server/Players/PlayerUnlocks.cs b/Server/Players/PlayerUnlocks.cs new file mode 100644 index 0000000..025cd64 --- /dev/null +++ b/Server/Players/PlayerUnlocks.cs @@ -0,0 +1,62 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; + +namespace RadicadeServer.RecNet.Accounts +{ + public class PlayerUnlocks + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long AccountId { get; set; } + + public string AvatarItems { get; set; } = DBConfig.DefaultAvatarUnlocks; + public string Consumables { get; set; } = DBConfig.DefaultConsumableUnlocks; + public string Equipment { get; set; } = DBConfig.DefaultEquipmentUnlocks; + + public string Inventions { get; set; } = "[]"; + public string RoomKeys { get; set; } = "[]"; + + public List GetAvatarItems() + { + if (string.IsNullOrWhiteSpace(this.AvatarItems) || this.AvatarItems == "null") + return new List(); + return JsonSerializer.Deserialize>(this.AvatarItems) ?? new List(); + } + public void AddAvatarItem(AvatarItem avatarItem) + { + var items = GetAvatarItems(); + items.Add(avatarItem); + this.AvatarItems = items.ToJson(); + } + + public List GetEquipment() + { + return JsonSerializer.Deserialize>(this.Equipment) ?? new List(); + } + public void AddEquipment(Equipment equipment) + { + var items = GetEquipment(); + items.Add(equipment); + this.Equipment = items.ToJson(); + } + + public List GetConsumables() + { + if (string.IsNullOrWhiteSpace(this.Consumables) || this.Consumables == "null") + return new List(); + return JsonSerializer.Deserialize>(this.Consumables) ?? new List(); + } + public void AddConsumable(Consumable consumable) + { + var consumables = GetConsumables(); + consumables.Add(consumable); + this.Consumables = JsonSerializer.Serialize(consumables); + } + public void SetConsumables(List consumables) + { + this.Consumables = JsonSerializer.Serialize(consumables ?? new List()); + } + } +} diff --git a/Server/Players/PlayerUtils.cs b/Server/Players/PlayerUtils.cs new file mode 100644 index 0000000..4cef7a3 --- /dev/null +++ b/Server/Players/PlayerUtils.cs @@ -0,0 +1,1541 @@ +using Azure; +using System.Collections.Concurrent; +using System.Globalization; +using System.Security.Cryptography; +using System.Text.Json; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Auth.Enums; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Utils +{ + public static class PlayerUtils + { + public static IDbContextFactory DbFactory = null!; + + private const int MaxPlatformIdLength = 64; + + private const int MaxDeviceIdLength = 256; + + private const int MaxPasswordLength = 128; + + private const int UsernameAttempts = 12; + + public static async Task CreateAccount(AccountCreationData acc, string clientIp = "", bool accountCreationInProgress = false) + { + if (acc == null) return new Success(false, "No account details were sent.", null); + + + if (!TryReadRegistrationFields(acc, out var rejection, out var platformId, out var deviceId, + out var birthday, out var email, out var avatar, out var password)) + { + return new Success(false, rejection, null); + } + + + + + await using var db = await DbFactory.CreateDbContextAsync(); + + + + + var chosenUsername = UsernameRules.Parse(acc.username, out var usernameRejection); + if (usernameRejection != null) return new Success(false, usernameRejection, null); + + + + + + + bool usernameChosen = chosenUsername != null; + string? username = null; + IDisposable? acquiredNameGate = null; + if (usernameChosen) + { + acquiredNameGate = await RegistrationGuard.LockNameAsync(chosenUsername!); + if (!await IsNameTakenAsync(db, chosenUsername!)) username = chosenUsername; + } + else + { + for (var attempt = 0; attempt < UsernameAttempts && username == null; attempt++) + { + var generated = await GenerateFreeUsernameAsync(db); + if (generated == null) break; + acquiredNameGate = await RegistrationGuard.LockNameAsync(generated); + if (await IsNameTakenAsync(db, generated)) + { + acquiredNameGate.Dispose(); + acquiredNameGate = null; + continue; + } + username = generated; + } + } + + if (username == null) + { + acquiredNameGate?.Dispose(); + return new Success(false, usernameChosen + ? "That name is already taken." + : "Could not find an available name. Please choose one.", null); + } + + using var nameGate = acquiredNameGate; + + if (email.Length > 0 && await db.Players.AnyAsync(x => x.Email.ToLower() == email.ToLower())) + { + + email = string.Empty; + } + + + + + + + + + + + + Profile? profile = null; + try + { + await db.Database.CreateExecutionStrategy().ExecuteAsync(async () => + { + await using var attemptDb = await DbFactory.CreateDbContextAsync(); + await using var transaction = attemptDb.Database.IsRelational() + ? await attemptDb.Database.BeginTransactionAsync() + : null; + + profile = await InsertNewAccountAsync(attemptDb, new NewAccountRows( + platform: acc.platform, + platformId: platformId, + deviceId: deviceId, + clientIp: clientIp, + deviceClass: acc.deviceClass, + username: username, + usernameChosen: usernameChosen, + password: password, + birthday: birthday, + email: email, + avatar: avatar, + accountCreationInProgress: accountCreationInProgress)); + + if (transaction != null) await transaction.CommitAsync(); + }); + } + catch (Exception ex) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Registration] account insert failed: {ex.GetType().Name}: {ex.Message}"); + return new Success(false, "Account creation failed. Nothing was saved - please try again.", null); + } + + if (profile == null) + { + + + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Registration] the account insert finished without producing a profile"); + return new Success(false, "Account creation failed. Nothing was saved - please try again.", null); + } + + + + + try + { + await RoomUtils.GetOrCreateDorm(profile.accountId); + } + catch (Exception ex) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Registration] account {profile.accountId} dorm threw: {ex.GetType().Name}: {ex.Message}"); + } + + RegistrationGuard.Record(clientIp, deviceId); + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Registration] created account {profile.accountId} " + + $"('{profile.username}', platform={acc.platform}, {RegistrationGuard.Describe(clientIp, deviceId)}, " + + $"nameChosen={usernameChosen}, password={password != null}, junior={profile.isJunior})"); + + return new Success(true, "", profile); + } + + private sealed record NewAccountRows( + Platforms platform, + string platformId, + string deviceId, + string clientIp, + DeviceClass deviceClass, + string username, + bool usernameChosen, + string? password, + DateTime? birthday, + string email, + string? avatar, + bool accountCreationInProgress); + + private static async Task InsertNewAccountAsync(RecDBContext db, NewAccountRows rows) + { + var now = DateTime.Now; + var hasher = new PasswordHasher(); + + var profile = new Profile + { + platforms = PlatformMask(rows.platform), + username = rows.username, + profileImage = "DefaultProfileImage", + displayName = rows.username, + isJunior = IsJuniorByBirthday(rows.birthday), + createdAt = now + }; + + await db.Profiles.AddAsync(profile); + await db.SaveChangesAsync(); + + var accountId = profile.accountId; + + var playerAvatar = new PlayerAvatar + { + Avatar = rows.avatar ?? DBConfig.DefaultAvatar, + Outfits = "[]", + AccountId = accountId + }; + + var reputation = new Reputation + { + AccountId = accountId + }; + + var settings = new PlayerSettings + { + AccountId = accountId, + Settings = rows.accountCreationInProgress + ? DBConfig.AccountCreationInProgressSettings2023(rows.usernameChosen, rows.password != null) + : DBConfig.NewAccountSettings2023 + }; + + var progression = new ProgressionData() + { + PlayerId = accountId, + XP = 0, + Level = 1, + Progress = new ProgressionJson().ToJson() + }; + + var presence = new RealPresence + { + playerId = accountId, + deviceClass = rows.deviceClass, + statusVisibility = StatusVisibility.Public, + vRMovementMode = VRMovementModes.TELEPORT + }; + + var currency = new Currency + { + AccountId = accountId + }; + + var unlocks = new PlayerUnlocks + { + AccountId = accountId, + AvatarItems = DBConfig.DefaultAvatarUnlocks + }; + + var cachedLogin = new CachedLogin + { + accountId = accountId, + platform = rows.platform, + platformId = rows.platformId, + lastLoginTime = now + }; + + var playerData = new PlayerData + { + AccountId = accountId, + Phone = "", + Birthday = rows.birthday, + Parent = null, + AppVersion = DBConfig.GameVersion, + DeviceClass = rows.deviceClass, + NameChanges = 99, + Email = rows.email, + Hashword = rows.password == null ? "" : hasher.HashPassword(new PlayerData(), rows.password), + + + IsJunior = IsJuniorByBirthday(rows.birthday), + JuniorState = JuniorState.BasedOnBirthday, + Visibility = StatusVisibility.Public, + DeviceId = rows.deviceId, + LastIpAddress = rows.clientIp ?? "" + }; + + await db.AddRangeAsync( + cachedLogin, + playerAvatar, + reputation, + settings, + presence, + currency, + unlocks, + playerData, + progression + ); + + await db.SaveChangesAsync(); + + + + + if (rows.accountCreationInProgress || rows.usernameChosen || rows.password != null) + await RefreshAccountCreationMarkersAsync(db, accountId, rows.usernameChosen); + + return profile; + } + + private static bool TryReadRegistrationFields(AccountCreationData acc, out string? rejection, + out string platformId, out string deviceId, out DateTime? birthday, out string email, + out string? avatar, out string? password) + { + rejection = null; + platformId = string.Empty; + deviceId = string.Empty; + birthday = null; + email = string.Empty; + avatar = null; + password = null; + + + + + platformId = acc.platformId?.Trim() ?? string.Empty; + if (platformId.Length == 0) { rejection = "A platform id is required."; return false; } + if (platformId.Length > MaxPlatformIdLength || !IsSafeIdentity(platformId)) + { + rejection = "That platform id is not valid."; + return false; + } + + deviceId = acc.deviceId?.Trim() ?? string.Empty; + if (deviceId.Length == 0) { rejection = "A device id is required."; return false; } + if (deviceId.Length > MaxDeviceIdLength || !IsSafeIdentity(deviceId)) + { + rejection = "That device id is not valid."; + return false; + } + + password = string.IsNullOrEmpty(acc.password) ? null : acc.password; + if (password != null) + { + if (password.Length < RadicadeServer.Auth.AuthAPI.MinPasswordLength) + { + rejection = $"Passwords must be at least {RadicadeServer.Auth.AuthAPI.MinPasswordLength} characters."; + return false; + } + if (password.Length > MaxPasswordLength) + { + rejection = $"Passwords must be {MaxPasswordLength} characters or fewer."; + return false; + } + } + + var postedBirthday = acc.birthday ?? acc.birthdayUTC; + birthday = ReadBirthday(acc.birthday, acc.birthdayUTC); + if (birthday == null && postedBirthday != null) + { + + + rejection = "That birthday is not valid."; + return false; + } + + email = ReadEmail(acc.email); + avatar = ReadAvatar(acc.avatar ?? acc.avatarJson); + return true; + } + + private static bool IsSafeIdentity(string value) + { + foreach (var character in value) + { + if (char.IsAsciiLetterOrDigit(character)) continue; + if (character is ':' or '-' or '_' or '.' or '|') continue; + return false; + } + return true; + } + + public static int PlatformMask(Platforms platform) => + platform == Platforms.All ? 1 : 1 << (int)platform; + + private static async Task GenerateFreeUsernameAsync(RecDBContext db) + { + for (var attempt = 0; attempt < UsernameAttempts; attempt++) + { + var candidate = UsernameRules.Generate(); + if (UsernameRules.Rejection(candidate) != null) continue; + if (await IsNameTakenAsync(db, candidate)) continue; + return candidate; + } + return null; + } + + public static async Task RefreshAccountCreationMarkersAsync(RecDBContext db, long accountId, bool? usernameChosen = null) + { + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == accountId); + if (player == null) return; + + var settings = await db.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == accountId); + if (settings == null) + { + settings = new PlayerSettings + { + AccountId = accountId, + Settings = DBConfig.AccountCreationInProgressSettings2023(false, false) + }; + db.PlayerSettings.Add(settings); + } + + var values = settings.DeserializeSettings(); + SetSettingFlag(values, "Recroom.AccountCreation.HasStarted", true); + SetSettingFlag(values, "Recroom.AccountCreation.HasChosenUsername", + usernameChosen ?? HasSettingFlag(values, "Recroom.AccountCreation.HasChosenUsername")); + SetSettingFlag(values, "Recroom.AccountCreation.HasCreatedPassword", !string.IsNullOrWhiteSpace(player.Hashword)); + SetSettingFlag(values, "Recroom.AccountCreation.AcceptedCodeOfConduct", player.CodeOfConductAccepted); + settings.Settings = values.ToJson(); + + await db.SaveChangesAsync(); + } + + public static async Task HasFinishedAccountCreationAsync(RecDBContext db, long accountId) + { + var settings = await db.PlayerSettings.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == accountId); + return settings != null && HasSettingFlag(settings.DeserializeSettings(), "Recroom.AccountCreation.HasFinished"); + } + + public static bool HasSettingFlag(List values, string key) => + values.Any(setting => string.Equals(setting.Key, key, StringComparison.OrdinalIgnoreCase) && + string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); + + public static void SetSettingFlag(List values, string key, bool value) + { + var existing = values.FirstOrDefault(setting => string.Equals(setting.Key, key, StringComparison.OrdinalIgnoreCase)); + if (existing == null) + values.Add(new Setting { Key = key, Value = value ? "True" : "False" }); + else + existing.Value = value ? "True" : "False"; + } + + public static async Task FindAdoptableAccountForDeviceAsync(RecDBContext db, string? deviceId) + { + if (string.IsNullOrWhiteSpace(deviceId)) return null; + + var finished = await FinishedAccountIdsAsync(db); + return await db.Players + .Where(player => player.DeviceId == deviceId && !finished.Contains(player.AccountId)) + .OrderByDescending(player => player.AccountId) + .FirstOrDefaultAsync(); + } + + private static async Task> FinishedAccountIdsAsync(RecDBContext db) + { + var settings = await db.PlayerSettings.AsNoTracking().ToListAsync(); + var finished = new HashSet(); + foreach (var row in settings) + { + if (row.DeserializeSettings().Any(setting => + string.Equals(setting.Key, "Recroom.AccountCreation.HasFinished", StringComparison.OrdinalIgnoreCase) && + string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase))) + { + finished.Add(row.AccountId); + } + } + return finished; + } + + public static async Task AdoptDeviceAccountAsync(RecDBContext db, long accountId, AccountCreationData acc, bool markStarted) + { + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == accountId); + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == accountId); + if (profile == null || player == null) return new Success(false, "Missing account data.", null); + + if (!await db.CachedLogins.AnyAsync(login => login.accountId == accountId && + login.platform == acc.platform && login.platformId == acc.platformId)) + { + db.CachedLogins.Add(new CachedLogin + { + accountId = accountId, + platform = acc.platform, + platformId = acc.platformId, + lastLoginTime = DateTime.Now + }); + } + + if (!string.IsNullOrWhiteSpace(acc.deviceId)) player.DeviceId = acc.deviceId; + + if (markStarted) + { + var settings = await db.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == accountId); + if (settings == null) + { + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = accountId, + Settings = DBConfig.AccountCreationInProgressSettings2023(false, false) + }); + } + else + { + var values = settings.DeserializeSettings(); + var started = values.FirstOrDefault(x => + string.Equals(x.Key, "Recroom.AccountCreation.HasStarted", StringComparison.OrdinalIgnoreCase)); + if (started == null) values.Add(new Setting { Key = "Recroom.AccountCreation.HasStarted", Value = "True" }); + else started.Value = "True"; + settings.Settings = values.ToJson(); + } + } + + await db.SaveChangesAsync(); + return new Success(true, "", profile); + } + + public static string? ReadChosenUsername(string? posted) => UsernameRules.Parse(posted, out _); + + private static DateTime? ReadBirthday(DateTime? birthday, DateTime? birthdayUTC) => + ValidBirthday(AsUtc(birthday ?? birthdayUTC)); + + public static DateTime? ReadBirthday(DateTime? posted) => ValidBirthday(AsUtc(posted)); + + public static DateTime? ParseBirthday(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + + if (!DateTime.TryParse( + raw, + CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, + out var parsed)) + return null; + + return ValidBirthday(parsed); + } + + private static DateTime? AsUtc(DateTime? value) + { + if (value == null) return null; + + return value.Value.Kind switch + { + DateTimeKind.Utc => value.Value, + DateTimeKind.Local => value.Value.ToUniversalTime(), + _ => DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) + }; + } + + private static DateTime? ValidBirthday(DateTime? value) + { + if (value == null) return null; + if (value.Value.Year < 1900 || value.Value > DateTime.UtcNow) return null; + + return value; + } + + private static string ReadEmail(string? posted) + { + var email = posted?.Trim() ?? string.Empty; + return email.Length <= 254 && email.Contains('@') ? email : string.Empty; + } + + private static string? ReadAvatar(string? posted) + { + if (string.IsNullOrWhiteSpace(posted)) return null; + + try + { + var avatar = posted.FromJson(); + return avatar == null ? null : avatar.ToJson(); + } + catch (JsonException) + { + return null; + } + } + + public static async Task IsNameTakenAsync(RecDBContext db, string name) + { + + + + var key = UsernameRules.ComparisonKey(name); + return await db.Profiles.AnyAsync(x => x.username.ToUpper() == key || x.displayName.ToUpper() == key); + } + + + + + + + + public sealed record PlayerBanInfo( + string Kind, + string Reason, + DateTime ExpiryUtc, + string Signature, + ReportCategory Category = ReportCategory.Misc, + long? BannedAccountId = null, + string? AppealCode = null, + DateTime? StartedAtUtc = null, + string? BanMethod = null, + string? ModeratorNote = null) + { + public bool IsPermanent => ExpiryUtc >= DateTime.MaxValue.AddYears(-10); + } + + + + + + + + public static async Task ResolveBanAsync(this RecDBContext ctx, long accountId, string? requestIp = null) + { + var player = await ctx.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == accountId); + + + var existingModeration = await ctx.Moderation.FirstOrDefaultAsync(x => x.PlayerId == accountId && x.IsBan); + if (existingModeration != null) + { + if (existingModeration.ExpireDuration < DateTime.UtcNow) + { + ctx.Moderation.Remove(existingModeration); + await ctx.SaveChangesAsync(); + } + else + { + var now = DateTime.UtcNow; + var relatedHardwareBan = player != null && !string.IsNullOrWhiteSpace(player.DeviceId) + ? await ctx.HardwareBans.FirstOrDefaultAsync(x => x.DeviceId == player.DeviceId + && (!x.ExpiresAt.HasValue || x.ExpiresAt.Value >= now)) + : null; + return new PlayerBanInfo("Moderation", existingModeration.Message ?? "", existingModeration.ExpireDuration, + "mod:" + existingModeration.Id, existingModeration.Category, + BannedAccountId: relatedHardwareBan?.BannedAccountId, AppealCode: existingModeration.AppealCode, + StartedAtUtc: existingModeration.CreatedAt, BanMethod: existingModeration.BanMethod, + ModeratorNote: existingModeration.ModeratorNote); + } + } + + + if (player != null && !string.IsNullOrWhiteSpace(player.DeviceId)) + { + var hardwareBan = await ctx.HardwareBans.FirstOrDefaultAsync(x => x.DeviceId == player.DeviceId); + if (hardwareBan != null) + { + if (hardwareBan.ExpiresAt.HasValue && hardwareBan.ExpiresAt.Value < DateTime.UtcNow) + { + + ctx.HardwareBans.Remove(hardwareBan); + await ctx.SaveChangesAsync(); + } + else + { + return new PlayerBanInfo("Hardware", hardwareBan.Reason ?? "", hardwareBan.ExpiresAt ?? DateTime.MaxValue, + "hw:" + hardwareBan.Id, ReportCategory.Misc, hardwareBan.BannedAccountId, + StartedAtUtc: hardwareBan.CreatedAt, BanMethod: "Hardware Ban"); + } + } + } + + + + var candidateIps = new List(); + if (player != null && !string.IsNullOrWhiteSpace(player.LastIpAddress)) + candidateIps.Add(player.LastIpAddress); + if (!string.IsNullOrWhiteSpace(requestIp) && !candidateIps.Contains(requestIp)) + candidateIps.Add(requestIp); + + foreach (var ip in candidateIps) + { + var ipBan = await ctx.IpBans.FirstOrDefaultAsync(x => x.IpAddress == ip); + if (ipBan == null) continue; + if (ipBan.ExpiresAt.HasValue && ipBan.ExpiresAt.Value < DateTime.UtcNow) + { + + ctx.IpBans.Remove(ipBan); + await ctx.SaveChangesAsync(); + continue; + } + return new PlayerBanInfo("Ip", ipBan.Reason ?? "", ipBan.ExpiresAt ?? DateTime.MaxValue, "ip:" + ip, + StartedAtUtc: ipBan.CreatedAt, BanMethod: "IP Ban"); + } + + return null; + } + + public static async Task IsBanned(this RecDBContext ctx, long id) + { + + return await ctx.ResolveBanAsync(id) != null; + } + + + + + + + + public static async Task IsDeveloper(this RecDBContext ctx, long accountId) + { + var profile = await ctx.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId); + return profile != null && profile.HasRole("developer"); + } + + + + + + + + + + + public static async Task KickOrPromptAsync(RecDBContext db, long accountId, BlockDetails details, bool devDecisionResolved = false) + { + if (!devDecisionResolved && await db.IsDeveloper(accountId)) + { + await Notify.SendNotify(accountId, new Notification { Id = NotificationType.ModerationKick, Msg = details }); + return false; + } + + await EjectToDormAsync(db, accountId); + await Notify.SendNotify(accountId, new Notification { Id = NotificationType.ModerationKick, Msg = details }); + return true; + } + + + + + + + + + public static readonly TimeSpan WarningScreenHold = TimeSpan.FromSeconds(60); + + public static readonly TimeSpan WarningPromptLeadIn = TimeSpan.FromMilliseconds(500); + + public static Task HasPendingWarningAsync(this RecDBContext ctx, long playerId) => + ctx.PlayerWarnings.AnyAsync(x => x.WarnedPlayerId == playerId && x.AcknowledgedAt == null); + + public static Task GetPendingWarningAsync(this RecDBContext ctx, long playerId) => + ctx.PlayerWarnings.AsNoTracking() + .Where(x => x.WarnedPlayerId == playerId && x.AcknowledgedAt == null) + .OrderBy(x => x.Id) + .FirstOrDefaultAsync(); + + public static BlockDetails BuildWarningBlockDetails(PlayerWarning warning, int holdSeconds = 0) => new() + { + Duration = holdSeconds, + GameSessionId = 0, + IsBan = false, + IsHostKick = false, + IsWarning = true, + Message = string.IsNullOrWhiteSpace(warning.DisplayReason) ? DefaultWarningMessage : warning.DisplayReason!, + DisplayReason = warning.DisplayReason ?? string.Empty, + PlayerIdReporter = warning.ModeratorPlayerId, + ReportCategory = warning.ReportCategory, + VoteKickReason = string.Empty, + TimeoutStartedAt = warning.CreatedAt + }; + + public const string DefaultWarningMessage = "You have been warned by a moderator."; + + public static BlockDetails BuildClearedBlockDetails() => new() + { + Duration = 0, + GameSessionId = 0, + PlayerIdReporter = 0, + IsBan = false, + IsHostKick = false, + IsWarning = false, + Message = string.Empty, + DisplayReason = string.Empty, + VoteKickReason = string.Empty, + ReportCategory = ReportCategory.Unknown, + TimeoutStartedAt = null + }; + + public static async Task ReleaseModerationHoldAsync(long playerId, bool isDeveloper = false) + { + var cleared = BuildClearedBlockDetails(); + + for (int attempt = 1; attempt <= 2; attempt++) + { + try + { + if (!isDeveloper) + { + await Notify.SendNotify(playerId, new Notification + { + Id = NotificationType.ModerationKick, + Msg = cleared + }); + } + + await Notify.SendNotify(playerId, new Notification + { + Id = NotificationType.ModerationUpdateRequired, + Msg = cleared + }); + } + catch (Exception) + { + } + + if (attempt == 1) + { + try { await Task.Delay(TimeSpan.FromSeconds(1)); } + catch { } + } + } + } + + public static async Task DeliverWarningAsync(long playerId, BlockDetails details) + { + try + { + await Notify.SendNotify(playerId, new Notification + { + Id = NotificationType.ModerationUpdateRequired, + Msg = details + }); + } + catch (Exception) + { + } + + if (details.Duration > 0) + { + try { await Task.Delay(WarningPromptLeadIn); } + catch { } + } + + try + { + await Notify.SendNotify(playerId, new Notification + { + Id = NotificationType.ModerationKick, + Msg = details + }); + } + catch (Exception) + { + } + } + + public static async Task BlockedByPendingWarningAsync(RecDBContext db, long playerId, bool targetIsOwnDorm, string targetDescription) + { + try + { + if (targetIsOwnDorm) return null; + + var warning = await db.GetPendingWarningAsync(playerId); + if (warning == null) return null; + + var dormInstance = await PinToDormAsync(db, playerId); + + await DeliverWarningAsync(playerId, BuildWarningBlockDetails(warning, (int)WarningScreenHold.TotalSeconds)); + + return dormInstance == null + ? MatchResponse.Error(JoinStatus.BlockedFromRoom) + : new MatchResponse { roomInstance = dormInstance, errorCode = JoinStatus.Success }; + } + catch (Exception) + { + return null; + } + } + + + public enum DeveloperDecisionKind + { + Kick, + Warning + } + + public sealed record DeveloperDecision( + DeveloperDecisionKind Kind, + ReportCategory Category, + string Reason, + string? DisplayReason, + string? ModeratorNote, + long IssuedBy, + DateTime RequestedAt); + + private static readonly ConcurrentDictionary _pendingDeveloperDecisions = new(); + + public static readonly TimeSpan DeveloperDecisionWindow = TimeSpan.FromMinutes(5); + + public static async Task DeferForDeveloperAsync(RecDBContext db, long playerId, DeveloperDecisionKind kind, BlockDetails details) + { + if (!await db.IsDeveloper(playerId)) return false; + + var decision = new DeveloperDecision( + kind, + details.ReportCategory, + string.IsNullOrWhiteSpace(details.VoteKickReason) ? details.Message : details.VoteKickReason, + string.IsNullOrWhiteSpace(details.DisplayReason) ? details.Message : details.DisplayReason, + details.ModeratorNote, + details.PlayerIdReporter, + DateTime.UtcNow); + + if (_pendingDeveloperDecisions.TryGetValue(playerId, out var stale)) + { + var expired = DateTime.UtcNow - stale.RequestedAt > DeveloperDecisionWindow; + var unreachable = Notify.GetClient(playerId) == null; + + if (expired || unreachable) + { + _pendingDeveloperDecisions.TryRemove(playerId, out _); + } + } + + var parked = _pendingDeveloperDecisions.GetOrAdd(playerId, decision); + if (!ReferenceEquals(parked, decision)) + { + return true; + } + + if (Notify.GetClient(playerId) == null) + { + return true; + } + + try + { + await Notify.SendNotify(playerId, new Notification { Id = NotificationType.ModerationKick, Msg = details }); + } + catch (Exception) + { + } + + return true; + } + + public static DeveloperDecision? TakeDeveloperDecision(long playerId) + { + if (!_pendingDeveloperDecisions.TryRemove(playerId, out var decision)) return null; + + if (DateTime.UtcNow - decision.RequestedAt > DeveloperDecisionWindow) + { + return null; + } + + return decision; + } + + public static string GenerateAppealCode() + { + const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + return RandomNumberGenerator.GetString(chars, 8); + } + + + + + + + public static string AppealUrl() + { + return "https://support.radicade.app/new/ban-appeal/"; + } + + + + + + public static string BuildBanAppealText(string appealCode) + { + return $"Appeal your ban at {AppealUrl()} with code: {appealCode}"; + } + + + + + + + + public static string PendingAppealText => + "Your ban appeal has been submitted and is pending review by a moderator. You will be notified when it is accepted or denied."; + + public const string SupportSiteAppealText = "Submitted through the support site."; + + + public static readonly TimeSpan AppealCooldownDuration = TimeSpan.FromDays(30); + + public const string AppealCooldownPrefix = "You cannot file another ban appeal for: "; + + + + + + + + + + + public static async Task GetAppealCooldownErrorAsync(RecDBContext db, long playerId) + { + var now = DateTime.UtcNow; + var resolved = await db.Moderation.AsNoTracking() + .Where(x => x.PlayerId == playerId + && (x.AppealResolution == "Rejected" + || x.AppealResolution == "Reduced" + || x.AppealResolution == "Extended" + || x.AppealResolution == "Reviewed") + && x.AppealResolvedAt.HasValue + && x.AppealResolvedAt.Value > now.Subtract(AppealCooldownDuration)) + .OrderByDescending(x => x.AppealResolvedAt) + .FirstOrDefaultAsync(); + + if (resolved?.AppealResolvedAt == null) return null; + var remaining = resolved.AppealResolvedAt.Value.Add(AppealCooldownDuration) - now; + return $"{AppealCooldownPrefix}{FormatAppealCooldown(remaining)}"; + } + + + + + + + public static async Task<(string? Code, string? CooldownError)> GetAppealForNewBanAsync(RecDBContext db, long playerId) + { + var cooldownError = await GetAppealCooldownErrorAsync(db, playerId); + return cooldownError == null ? (GenerateAppealCode(), null) : (null, cooldownError); + } + + + public static string FormatAppealCooldown(TimeSpan remaining) + { + if (remaining <= TimeSpan.Zero) return "0 minutes"; + if (remaining.TotalDays >= 1) + { + var days = (int)Math.Floor(remaining.TotalDays); + var hours = (int)remaining.TotalHours % 24; + var dayText = $"{days} day{(days == 1 ? "" : "s")}"; + return hours > 0 ? $"{dayText}, {hours} hour{(hours == 1 ? "" : "s")}" : dayText; + } + if (remaining.TotalHours >= 1) + { + var hours = (int)Math.Ceiling(remaining.TotalHours); + return $"{hours} hour{(hours == 1 ? "" : "s")}"; + } + var mins = Math.Max(1, (int)Math.Ceiling(remaining.TotalMinutes)); + return $"{mins} minute{(mins == 1 ? "" : "s")}"; + } + + public static async Task RelatedAccountUsernameAsync(RecDBContext db, long? bannedAccountId, long accountId) + { + if (!bannedAccountId.HasValue || bannedAccountId.Value == accountId) return null; + var profile = await db.Profiles.AsNoTracking() + .FirstOrDefaultAsync(x => x.accountId == bannedAccountId.Value); + return profile?.username; + } + + public static async Task BuildBanBlockDetailsAsync(RecDBContext db, PlayerBanInfo ban, long accountId) + => ApplyClientBanKind( + BuildBlockDetails(true, ban.Category, ban.Reason, ban.ExpiryUtc, + appealCode: ban.AppealCode, startedAtUtc: ban.StartedAtUtc, + relatedUsername: await RelatedAccountUsernameAsync(db, ban.BannedAccountId, accountId)), + ban.BanMethod); + + public static BlockDetails ApplyClientBanKind(BlockDetails details, string? banMethod) + { + if (ModerationPolicy.IsVoiceModeration(banMethod)) + { + details.BanType = "VoiceModeration"; + details.IsVoiceModAutoban = true; + + details.BanAppealText = AccountLock.NotAppealableMessage; + } + else if (ModerationPolicy.IsTimeout(banMethod)) + { + details.BanType = "Timeout"; + details.IsVoiceModAutoban = false; + details.BanAppealCode = string.Empty; + details.BanAppealText = string.Empty; + } + else + { + details.BanType = "Moderation"; + details.IsVoiceModAutoban = false; + } + + return details; + } + + public static BlockDetails BuildBlockDetails( + bool isBan, + ReportCategory category, + string? message, + DateTime? expiryUtc = null, + long reporterId = 1, + bool isHostKick = false, + string? appealCode = null, + string? appealError = null, + DateTime? startedAtUtc = null, + string? relatedUsername = null) + { + + + + + + + bool isPermanent = expiryUtc.HasValue && (expiryUtc.Value >= DateTime.MaxValue.AddYears(-10) + || (expiryUtc.Value - DateTime.UtcNow).TotalDays > 365 * 10); + + int duration = 0; + int? bannedUntil = null; + if (expiryUtc.HasValue) + { + var remaining = expiryUtc.Value - DateTime.UtcNow; + if (remaining > TimeSpan.Zero && !isPermanent) + duration = (int)remaining.TotalSeconds; + if (isPermanent) + duration = int.MaxValue; + else + { + var unix = new DateTimeOffset(DateTime.SpecifyKind(expiryUtc.Value, DateTimeKind.Utc), TimeSpan.Zero) + .ToUnixTimeSeconds(); + if (unix <= int.MaxValue) + bannedUntil = (int)unix; + } + } + var reason = message ?? string.Empty; + + if (!string.IsNullOrWhiteSpace(relatedUsername)) + reason = reason + $"\nRelated account: @{relatedUsername}"; + var code = appealCode ?? string.Empty; + return new BlockDetails + { + Duration = duration, + GameSessionId = 0, + PlayerIdReporter = reporterId, + IsBan = isBan, + IsHostKick = isHostKick, + Message = reason, + ReportCategory = category, + VoteKickReason = isHostKick ? string.Empty : reason, + BannedUntil = bannedUntil, + DisplayReason = reason, + BanType = isBan ? "Moderation" : string.Empty, + TimeoutStartedAt = isBan ? startedAtUtc : null, + BanAppealCode = code, + BanAppealText = code.Length > 0 + ? BuildBanAppealText(code) + : (appealError ?? string.Empty) + }; + } + + + + + + public static string GetClientIp(this HttpRequest request) + { + try + { + static string NormalizeIp(string ip) + { + if (string.IsNullOrWhiteSpace(ip)) return ""; + ip = ip.Trim(); + + if (ip.StartsWith("::ffff:", StringComparison.OrdinalIgnoreCase)) + ip = ip.Substring("::ffff:".Length); + return ip; + } + + static bool IsLoopback(string ip) + { + if (string.IsNullOrWhiteSpace(ip)) return false; + return ip == "::1" || ip == "127.0.0.1" + || ip.StartsWith("::ffff:127.", StringComparison.OrdinalIgnoreCase) + || ip.StartsWith("127.", StringComparison.Ordinal); + } + + + + + + + + var cfConnecting = NormalizeIp(request.Headers["CF-Connecting-IP"].FirstOrDefault() ?? ""); + if (!string.IsNullOrWhiteSpace(cfConnecting)) + return cfConnecting; + + var remoteIp = request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""; + if (IsLoopback(remoteIp)) + { + var forwarded = request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(forwarded)) + { + var last = NormalizeIp(forwarded.Split(',').LastOrDefault()?.Trim() ?? ""); + if (!string.IsNullOrWhiteSpace(last)) + return last; + } + } + return NormalizeIp(remoteIp); + } + catch + { + return ""; + } + } + + + + + + + public static async Task IsIpBanned(this RecDBContext ctx, string ipAddress) + { + if (string.IsNullOrWhiteSpace(ipAddress)) return false; + + var ipBan = await ctx.IpBans.FirstOrDefaultAsync(x => x.IpAddress == ipAddress); + if (ipBan == null) return false; + + if (ipBan.ExpiresAt.HasValue && ipBan.ExpiresAt.Value < DateTime.UtcNow) + { + ctx.IpBans.Remove(ipBan); + await ctx.SaveChangesAsync(); + return false; + } + + return true; + } + + + + + + public static async Task IsHardwareBanned(this RecDBContext ctx, string deviceId) + { + if (string.IsNullOrWhiteSpace(deviceId)) return false; + + var hardwareBan = await ctx.HardwareBans.FirstOrDefaultAsync(x => x.DeviceId == deviceId); + if (hardwareBan == null) return false; + + if (hardwareBan.ExpiresAt.HasValue && hardwareBan.ExpiresAt.Value < DateTime.UtcNow) + { + ctx.HardwareBans.Remove(hardwareBan); + await ctx.SaveChangesAsync(); + return false; + } + + return true; + } + + + + + + + public static async Task IsAccountCreationBlocked(this RecDBContext ctx, string ipAddress) + { + if (string.IsNullOrWhiteSpace(ipAddress)) return false; + + var block = await ctx.AccountCreationBlocks.FirstOrDefaultAsync(x => x.IpAddress == ipAddress); + if (block == null) return false; + + if (block.ExpiresAt.HasValue && block.ExpiresAt.Value < DateTime.UtcNow) + { + ctx.AccountCreationBlocks.Remove(block); + await ctx.SaveChangesAsync(); + return false; + } + + return true; + } + + + + + + + public static string FormatBanTime(TimeSpan timeRemaining) + { + if (timeRemaining.TotalDays > 365 * 10) + return "This ban is permanent."; + + var parts = new List(); + if (timeRemaining.Days > 0) + parts.Add($"{timeRemaining.Days} Days"); + if (timeRemaining.Hours > 0) + parts.Add($"{timeRemaining.Hours} Hours"); + if (timeRemaining.Minutes > 0) + parts.Add($"{timeRemaining.Minutes} Minutes"); + if (timeRemaining.Seconds > 0) + parts.Add($"{timeRemaining.Seconds} Seconds"); + + return parts.Count > 0 ? string.Join(", ", parts) : "0 Seconds"; + } + + + + + + + public static string BanKickMessage(string reason, DateTime expiryUtc) + { + return expiryUtc >= DateTime.MaxValue.AddYears(-10) + ? $"{reason}\nThis ban is permanent." + : $"{reason}\nBan expires in: {FormatBanTime(expiryUtc - DateTime.UtcNow)}"; + } + + + + + + + + + + + public static async Task EjectToDormAsync(RecDBContext db, long playerId) + { + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence == null || !presence.roomInstance.HasValue) return; + + await PinToDormAsync(db, playerId); + } + + public static async Task PinToDormAsync(RecDBContext db, long playerId) + { + try + { + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId); + if (presence == null) + { + return null; + } + + if (presence.roomInstance.HasValue) + { + var currentInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance.Value); + if (currentInstance != null) + { + var currentRoom = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == currentInstance.roomId); + if (currentRoom != null && currentRoom.IsDorm && currentRoom.CreatorAccountId == playerId) + return currentInstance; + } + } + + var dorm = await RoomUtils.GetOrCreateDorm(playerId); + RoomInstance? dormInstance = null; + if (dorm != null) + dormInstance = await MatchmakingUtils.CreateRoomInstance(dorm.RoomId, true, null, null, null); + if (dormInstance == null) + { + return null; + } + + presence.roomInstance = dormInstance.roomInstanceId; + presence.errorCode = JoinStatus.Success; + presence.isOnline = true; + MatchmakingUtils.MarkInRoom(playerId); + + await db.SaveChangesAsync(); + + await Notify.SendNotify(playerId, new Notification + { + Id = "PresenceUpdate", + Msg = await Presence.CreateAsync(db, presence, dormInstance) + }); + + return dormInstance; + } + catch (Exception) + { + return null; + } + } + + + + + + + + + + + + + + public static bool IsJuniorByBirthday(DateTime? birthday) + { + if (birthday == null || birthday.Value.Year <= 1900) return false; + var ageYears = (DateTime.UtcNow - birthday.Value).TotalDays / 365.2425; + return ageYears < 13; + } + + private static bool HasBirthday(PlayerData plrData) => + plrData.Birthday != null && plrData.Birthday.Value.Year > 1900; + + public static SelfAccount BuildSelfAccount(Profile profile, PlayerData plrData) + { + bool hasBirthday = HasBirthday(plrData); + + + + + + + + var juniorState = plrData.JuniorState == JuniorState.ModerationOverride + ? JuniorState.ModerationOverride + : hasBirthday ? JuniorState.BasedOnBirthday : JuniorState.PendingOverride; + + return new SelfAccount + { + juniorState = juniorState, + accountId = profile.accountId, + parentAccountId = plrData.Parent, + availableUsernameChanges = 99, + bio = profile.bio, + displayName = profile.displayName, + displayEmoji = profile.displayEmoji, + personalPronouns = profile.personalPronouns, + identityFlags = profile.identityFlags, + + + + email = plrData.Email ?? "", + pendingEmail = plrData.PendingEmail ?? "", + emailVerified = plrData.EmailVerified, + createdAt = profile.createdAt, + + + + isJunior = profile.isJunior || plrData.JuniorState == JuniorState.ModerationOverride || IsJuniorByBirthday(plrData.Birthday), + phone = plrData.Phone ?? "", + platforms = profile.platforms, + profileImage = profile.profileImage, + bannerImage = profile.bannerImage, + username = profile.username, + hasBirthday = hasBirthday, + + + birthdayUTC = hasBirthday ? AsBirthdayUtc(plrData.Birthday) : null, + hasAcceptedCodeOfConduct = plrData.CodeOfConductAccepted, + + + + newPlayer = IsNewPlayer(plrData) + }; + } + + public static bool IsNewPlayer(PlayerData plrData) => + !(plrData.Birthday != null && plrData.Birthday.Value.Year > 1900) + || !plrData.CodeOfConductAccepted + || string.IsNullOrWhiteSpace(plrData.Hashword); + + public static async Task IsChoosingTheirAccountNameAsync(RecDBContext db, long accountId) + { + var settings = await db.PlayerSettings.AsNoTracking() + .FirstOrDefaultAsync(x => x.AccountId == accountId); + if (settings == null) return false; + + var list = settings.DeserializeSettings(); + bool SettingIs(string key, string value) => list.Any(setting => + string.Equals(setting.Key, key, StringComparison.OrdinalIgnoreCase) && + string.Equals(setting.Value, value, StringComparison.OrdinalIgnoreCase)); + + return SettingIs("Recroom.AccountCreation.HasStarted", "true") && + !SettingIs("Recroom.AccountCreation.HasFinished", "true"); + } + + private static DateTime? AsBirthdayUtc(DateTime? birthday) + { + if (birthday == null) return null; + + return birthday.Value.Kind == DateTimeKind.Utc + ? birthday.Value + : DateTime.SpecifyKind(birthday.Value, DateTimeKind.Utc); + } + + public static async Task GetPlayer(this RecDBContext ctx, long id) + { + Player player = new Player(); + + ProgressionData? progressionData = await ctx.Progression.FirstOrDefaultAsync(x => x.PlayerId == id); + if (progressionData == null) return null; + + Reputation? reputation = await ctx.Reputations.FirstOrDefaultAsync(x => x.AccountId == id); + if (reputation == null) return null; + + Profile? profile = await ctx.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (profile == null) return null; + + PlayerData? plrData = await ctx.Players.FirstOrDefaultAsync(x => x.AccountId == id); + if (plrData == null) return null; + + player.Id = id; + player.XP = LevelUpGiftService.XpIntoCurrentLevel(progressionData); + player.Level = LevelUpGiftService.DisplayLevel(progressionData.Level); + player.Username = profile.username; + player.DisplayName = profile.displayName; + player.Developer = profile.HasRole("developer"); + player.AvoidJuniors = false; + player.CanReceiveInvites = true; + player.ForceJuniorImages = false; + + + player.HasBirthday = HasBirthday(plrData); + player.HasAcceptedCodeOfConduct = plrData.CodeOfConductAccepted; + + + + + + + + player.PendingJunior = plrData.JuniorState != JuniorState.ModerationOverride && + !player.HasBirthday; + player.JuniorProfile = IsJuniorByBirthday(plrData.Birthday); + player.Bio = profile.bio!; + player.ProfileImageName = profile.profileImage; + + + player.PlayerReputation = reputation.ForClient(); + player.PlatformIds = new List { }; + player.AdministrativeData = new AdministrativeData + { + LastLoginTime = DateTime.Now, + JoinData = profile.createdAt, + PermissionFactors = 0, + TrustFactor = 100 + }; + + return player; + } + } +} diff --git a/Server/Players/PlayersV1.cs b/Server/Players/PlayersV1.cs new file mode 100644 index 0000000..e8fa939 --- /dev/null +++ b/Server/Players/PlayersV1.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Players.V1 +{ + [Route("api/players/v1")] + public class PlayersV1(RecDBContext db) : Controller + { + [HttpGet("progression/{id}")] + public ProgressionResponse? GetProgression(long id) + { + var data = db.Progression.FirstOrDefault((x) => x.PlayerId == id); + data ??= new ProgressionData { PlayerId = id }; + return new ProgressionResponse(data); + } + + [HttpPost("progression/bulk")] + public async Task?> GetBulkProgression([FromForm] int[] Ids) + { + var res = new List(); + foreach (int id in Ids) + { + var data = await db.Progression.FirstOrDefaultAsync((x) => x.PlayerId == id); + data ??= new ProgressionData { PlayerId = id }; + res.Add(new ProgressionResponse(data)); + } + return res; + } + + + private const string PhotoTaggingKey = "playerPhotoTaggingSetting"; + + [HttpGet("playerPhotoTaggingSetting"), Authorize] + public async Task GetPhotoTaggingSetting() + { + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(); + + var settings = await db.PlayerSettings.AsNoTracking() + .FirstOrDefaultAsync(x => x.AccountId == userId); + var value = settings?.DeserializeSettings() + ?.FirstOrDefault(x => string.Equals(x.Key, PhotoTaggingKey, StringComparison.OrdinalIgnoreCase)) + ?.Value; + + return Ok(int.TryParse(value, out var parsed) ? parsed : 0); + } + + [HttpPut("playerPhotoTaggingSetting"), Authorize] + [HttpPost("playerPhotoTaggingSetting"), Authorize] + public async Task SetPhotoTaggingSetting() + { + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(); + + int? setting = null; + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var key in new[] { "Setting", "setting" }) + { + if (int.TryParse(form[key], out var parsed)) + { + setting = parsed; + break; + } + } + } + else + { + try + { + using var doc = await System.Text.Json.JsonDocument.ParseAsync(Request.Body); + var root = doc.RootElement; + foreach (var key in new[] { "Setting", "setting" }) + { + if (root.ValueKind == System.Text.Json.JsonValueKind.Object && + root.TryGetProperty(key, out var prop) && + prop.ValueKind == System.Text.Json.JsonValueKind.Number && + prop.TryGetInt32(out var parsed)) + { + setting = parsed; + break; + } + } + } + catch (System.Text.Json.JsonException) { } + } + + var settings = await db.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == userId); + if (settings == null) + { + settings = new PlayerSettings { AccountId = userId.Value, Settings = "[]" }; + await db.PlayerSettings.AddAsync(settings); + } + + var actualSettings = settings.DeserializeSettings(); + var found = actualSettings.FirstOrDefault(x => + string.Equals(x.Key, PhotoTaggingKey, StringComparison.OrdinalIgnoreCase)); + + if (setting.HasValue) + { + if (found != null) + found.Value = setting.Value.ToString(); + else + actualSettings.Add(new Setting { Key = PhotoTaggingKey, Value = setting.Value.ToString() }); + + settings.Settings = actualSettings.ToJson(); + await db.SaveChangesAsync(); + } + + var storedValue = actualSettings + .FirstOrDefault(x => string.Equals(x.Key, PhotoTaggingKey, StringComparison.OrdinalIgnoreCase)) + ?.Value; + return Ok(int.TryParse(storedValue, out var stored) ? stored : 0); + } + } +} diff --git a/Server/Players/PlayersV2.cs b/Server/Players/PlayersV2.cs new file mode 100644 index 0000000..efec3c4 --- /dev/null +++ b/Server/Players/PlayersV2.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Royale; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Players.V2 +{ + [Route("api/players/v2")] + public class PlayersV2(RecDBContext db) : Controller + { + [HttpGet("progression/bulk")] + public async Task?> GetBulkProgressionV2([FromQuery] int[] id) + { + var res = new List(); + foreach (int i in id) + { + var data = await db.Progression.FirstOrDefaultAsync((x) => x.PlayerId == i); + data ??= new ProgressionData { PlayerId = i }; + res.Add(new ProgressionResponse(data)); + } + return res; + } + + [HttpPost("objectives"), Authorize] + public async Task>> CompleteObjectives([FromBody] List? objectives) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + if (objectives == null) return BadRequest(); + + int additionalXp = objectives + .Where(o => o.AdditionalXp > 0) + .Sum(o => o.AdditionalXp); + + var strings = new List(); + await RoyaleShared.AwardXpAsync(db, userId.Value, additionalXp, "Objective", strings); + + var stored = await db.Progression.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerId == userId.Value) + ?? new ProgressionData { PlayerId = userId.Value }; + + return Ok(new List { new(stored) }); + } + } + + public class CompleteObjectivesDTO + { + public int ObjectiveType { get; set; } + public int AdditionalXp { get; set; } + public bool InParty { get; set; } + } +} diff --git a/Server/Players/Presence.cs b/Server/Players/Presence.cs new file mode 100644 index 0000000..adacbd0 --- /dev/null +++ b/Server/Players/Presence.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; + +namespace RadicadeServer.RecNet.Accounts +{ + public class Presence + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long playerId { get; set; } + public bool isOnline { get; set; } + public DeviceClass deviceClass { get; set; } + public StatusVisibility statusVisibility { get; set; } + public VRMovementModes vRMovementMode { get; set; } + public JoinStatus errorCode { get; set; } + public RoomInstance? roomInstance { get; set; } + public string appVersion { get; set; } = DBConfig.GameVersion; + + public Presence ForViewer(long? viewerId) + { + if (viewerId != playerId && (!isOnline || statusVisibility == StatusVisibility.Offline)) + { + isOnline = false; + roomInstance = null; + } + return this; + } + + public static async Task CreateAsync(RecDBContext db, RealPresence presence, RoomInstance? instance) + { + var version = await db.Players.Where(player => player.AccountId == presence.playerId) + .Select(player => player.AppVersion).FirstOrDefaultAsync(); + return new Presence(presence, instance) + { + appVersion = string.IsNullOrWhiteSpace(version) ? DBConfig.GameVersion : version + }; + } + + public Presence(RealPresence presence, RoomInstance? instance) + { + this.playerId = presence.playerId; + this.deviceClass = presence.deviceClass; + this.isOnline = presence.isOnline; + this.statusVisibility = presence.statusVisibility; + + if (instance != null) + { + instance.eventId ??= 0; + instance.clubId ??= 0; + instance.roomCode ??= string.Empty; + instance.location ??= string.Empty; + } + + this.roomInstance = instance; + this.vRMovementMode = presence.vRMovementMode; + this.errorCode = presence.errorCode; + } + } +} diff --git a/Server/Players/PublicPlayerAPI.cs b/Server/Players/PublicPlayerAPI.cs new file mode 100644 index 0000000..ea8ec22 --- /dev/null +++ b/Server/Players/PublicPlayerAPI.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; + +namespace RadicadeServer.API.Public +{ + [Route("p/players")] + public class PublicPlayerAPI(RecDBContext db) : Controller + { + [HttpGet("online")] + public async Task>> GetAllOnlinePlayers() + { + var list = new List(); + var presences = await db.Presences.Where(x => x.isOnline).ToListAsync(); + foreach (var p in presences) + { + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == p.playerId); + if (player != null) + list.Add($"{player.displayName} (@{player.username})"); + } + return Json(list); + } + } +} diff --git a/Server/Players/RealPresence.cs b/Server/Players/RealPresence.cs new file mode 100644 index 0000000..6c4e814 --- /dev/null +++ b/Server/Players/RealPresence.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Rooms; + +namespace RadicadeServer.RecNet.Accounts +{ + public class RealPresence + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long playerId { get; set; } + public bool isOnline { get; set; } + public DeviceClass deviceClass { get; set; } + public StatusVisibility statusVisibility { get; set; } + public VRMovementModes vRMovementMode { get; set; } + public string? LoginLock { get; set; } + public long? roomInstance { get; set; } + public JoinStatus errorCode { get; set; } + public string appVersion = DBConfig.GameVersion; + } +} diff --git a/Server/Players/SelfAccount.cs b/Server/Players/SelfAccount.cs new file mode 100644 index 0000000..cc8d3cb --- /dev/null +++ b/Server/Players/SelfAccount.cs @@ -0,0 +1,55 @@ +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Players; + +namespace RadicadeServer.RecNet.Accounts +{ + + public class SelfAccount : Profile + { + public string email { get; set; } = null!; + + + + + + + + public string pendingEmail { get; set; } = null!; + + + + + + public bool emailVerified { get; set; } + public string phone { get; set; } = null!; + + + + + + + + + + + public JuniorState juniorState { get; set; } + public long? parentAccountId { get; set; } + public int availableUsernameChanges { get; set; } + + + public bool hasBirthday { get; set; } + + public DateTime? birthdayUTC { get; set; } + + + public bool hasAcceptedCodeOfConduct { get; set; } + + + + + + + + public bool newPlayer { get; set; } + } +} diff --git a/Server/Players/Setting.cs b/Server/Players/Setting.cs new file mode 100644 index 0000000..6d6b9d4 --- /dev/null +++ b/Server/Players/Setting.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Accounts.Local +{ + public class Setting + { + public long Id { get; set; } + [JsonPropertyName("accountId")] + public long AccountId { get; set; } + public string Key { get; set; } = null!; + public string Value { get; set; } = null!; + } +} diff --git a/Server/Players/UsernameRules.cs b/Server/Players/UsernameRules.cs new file mode 100644 index 0000000..8930b60 --- /dev/null +++ b/Server/Players/UsernameRules.cs @@ -0,0 +1,65 @@ +namespace RadicadeServer.Utils +{ + public static class UsernameRules + { + public const int MinLength = 3; + public const int MaxLength = 24; + + private static readonly string[] ReservedNames = + [ + "me", "you", "recroom", "recnet", "radicade", "againstgravity", + "system", "admin", "administrator", "moderator", "mod", "support", + "staff", "official", "developer", "owner", "root" + ]; + + public static string? Normalize(string? posted) + { + if (string.IsNullOrWhiteSpace(posted)) return null; + + var name = posted.Trim().TrimStart('@').Trim(); + return name.Length == 0 ? null : name; + } + + public static string? Parse(string? posted, out string? rejection) + { + rejection = null; + + var name = Normalize(posted); + if (name == null) return null; + + rejection = Rejection(name); + return rejection == null ? name : null; + } + + public static string? Rejection(string name) + { + if (name.Length < MinLength) + return $"Names must be at least {MinLength} characters."; + if (name.Length > MaxLength) + return $"Names must be {MaxLength} characters or fewer."; + + bool hasLetter = false; + foreach (var character in name) + { + if (char.IsAsciiLetter(character)) { hasLetter = true; continue; } + if (char.IsAsciiDigit(character)) continue; + return "Names can only use letters and numbers."; + } + + if (!hasLetter) + return "Names must contain at least one letter."; + + if (ReservedNames.Any(reserved => string.Equals(reserved, name, StringComparison.OrdinalIgnoreCase))) + return "That name is reserved. Please pick another one."; + + if (!name.IsPure()) + return "That name is not allowed. Please pick another one."; + + return null; + } + + public static string ComparisonKey(string name) => name.ToUpperInvariant(); + + public static string Generate() => RecNet.Generators.GenerateUsername(); + } +} diff --git a/Server/PlayersBanned/AccountCreationBlock.cs b/Server/PlayersBanned/AccountCreationBlock.cs new file mode 100644 index 0000000..3185788 --- /dev/null +++ b/Server/PlayersBanned/AccountCreationBlock.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Moderation +{ + + + + + + public class AccountCreationBlock + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + public string IpAddress { get; set; } = ""; + public string Reason { get; set; } = ""; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? ExpiresAt { get; set; } + } +} diff --git a/Server/PlayersBanned/AccountLock.cs b/Server/PlayersBanned/AccountLock.cs new file mode 100644 index 0000000..137e187 --- /dev/null +++ b/Server/PlayersBanned/AccountLock.cs @@ -0,0 +1,242 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.Utils +{ + public sealed record AccountLockResult(bool Success, bool Changed, string? Error = null); + + public static class AccountLock + { + public const string BanMethod = "Account Lock"; + + public const string Reason = + "Moderator (Account lock. Please submit a ticket with us to discuss. https://support.radicade.app/new/account-support/)"; + + public const string SupportUrl = "https://support.radicade.app"; + + public const string ErrorCode = "ACCOUNT_PERMALOCKED"; + + public const string NotAppealableMessage = "This ban cannot be appealed"; + + public static bool IsLockBan(ModerationDB ban) => + string.Equals(ban.BanMethod, BanMethod, StringComparison.OrdinalIgnoreCase); + + public const string FallbackUsername = "Account"; + + public static string LoginErrorMessage(string? username) + { + var name = (username ?? string.Empty).Trim(); + if (name.StartsWith("@", StringComparison.Ordinal)) name = name[1..].Trim(); + if (string.IsNullOrWhiteSpace(name)) name = FallbackUsername; + return $"@{name}: This account has been locked for security reasons. Contact us at {SupportUrl}."; + } + + public static Task FindActiveLockAsync(RecDBContext db, long accountId) => + db.Moderation.AsNoTracking() + .Where(x => x.PlayerId == accountId + && x.IsBan + && x.BanMethod == BanMethod + && x.ExpireDuration > DateTime.UtcNow) + .OrderByDescending(x => x.Id) + .FirstOrDefaultAsync(); + + public static async Task IsLockedAsync(RecDBContext db, long accountId) + { + try + { + return await FindActiveLockAsync(db, accountId) != null; + } + catch (Exception ex) + { + Log($"could not read lock state for account {accountId} - failing closed ({ex.GetType().Name})"); + return true; + } + } + + public static async Task ResolveUsernameAsync(RecDBContext db, long accountId) + { + try + { + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId); + return profile?.username; + } + catch + { + return null; + } + } + + public static async Task ApplyAsync(RecDBContext db, long accountId, string? staff, string? caseRef) + { + bool changed; + try + { + var existing = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == accountId && x.IsBan); + var note = NormalizeCase(caseRef); + + if (existing == null) + { + existing = new ModerationDB + { + PlayerId = accountId, + IsBan = true, + Category = ReportCategory.Moderator, + Message = Reason, + BanMethod = BanMethod, + ExpireDuration = DateTime.MaxValue, + CreatedAt = DateTime.UtcNow, + ModeratorNote = note + }; + await db.Moderation.AddAsync(existing); + changed = true; + } + else + { + changed = !IsAlreadyLocked(existing) || note != existing.ModeratorNote; + existing.IsBan = true; + existing.Category = ReportCategory.Moderator; + existing.Message = Reason; + existing.BanMethod = BanMethod; + existing.ExpireDuration = DateTime.MaxValue; + existing.ModeratorNote = note ?? existing.ModeratorNote; + } + + await db.SaveChangesAsync(); + } + catch (Exception ex) + { + Log($"failed to persist account lock for {accountId}: {ex.GetType().Name}"); + AdminActionDiscord.LogAction("Account Lock", StaffName(staff), + $"FAILED to lock account {accountId}: the permanent ban could not be saved ({ex.GetType().Name})."); + return new AccountLockResult(false, false, "The lock could not be saved. Nothing was changed."); + } + + var failures = new List(); + await EnforceOnlineAsync(db, accountId, failures); + + var detail = $"Locked account {accountId}. Category: {ReportCategory.Moderator}. Method: {BanMethod}. Reason: {Reason}"; + if (!string.IsNullOrWhiteSpace(caseRef)) detail += $" Case: {caseRef.Trim()}"; + detail += failures.Count == 0 + ? " Result: ban persisted, online state handled, new sign-ins refused." + : $" Result: ban persisted; partial failures - {string.Join("; ", failures)}."; + AdminActionDiscord.LogAction("Account Lock", StaffName(staff), detail); + + return new AccountLockResult(true, changed); + } + + public static async Task RemoveAsync(RecDBContext db, long accountId, string? staff, string? caseRef) + { + ModerationDB? existing; + try + { + existing = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == accountId && x.IsBan); + } + catch (Exception ex) + { + Log($"failed to read ban state before unlock for {accountId}: {ex.GetType().Name}"); + return new AccountLockResult(false, false, "The ban state could not be read."); + } + + if (existing == null) + return new AccountLockResult(false, false, "This account does not have an active ban."); + + if (!string.Equals(existing.BanMethod, BanMethod, StringComparison.OrdinalIgnoreCase)) + return new AccountLockResult(false, false, + $"The active ban is '{existing.BanMethod}', not an account lock. Refusing to remove an unrelated ban."); + + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId); + try + { + db.Moderation.Remove(existing); + await db.SaveChangesAsync(); + } + catch (Exception ex) + { + Log($"failed to persist account unlock for {accountId}: {ex.GetType().Name}"); + AdminActionDiscord.LogAction("Account Unlock", StaffName(staff), + $"FAILED to unlock account {accountId}: the ban row could not be removed ({ex.GetType().Name}). The account is still locked."); + return new AccountLockResult(false, false, "The unlock could not be saved. The account is still locked."); + } + + var detail = $"Unlocked account {accountId}: the account lock was removed; hardware/IP bans and unrelated records were left untouched."; + if (!string.IsNullOrWhiteSpace(caseRef)) detail += $" Case: {caseRef.Trim()}"; + AdminActionDiscord.LogAction("Account Unlock", StaffName(staff), detail); + + await BestEffortAsync(() => Notify.SendNotify(accountId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile })); + await BestEffortAsync(() => PlayerUtils.ReleaseModerationHoldAsync(accountId)); + + return new AccountLockResult(true, true); + } + + private static bool IsAlreadyLocked(ModerationDB ban) => + ban.IsBan + && ban.Category == ReportCategory.Moderator + && ban.Message == Reason + && string.Equals(ban.BanMethod, BanMethod, StringComparison.OrdinalIgnoreCase) + && ban.ExpireDuration >= DateTime.MaxValue.AddYears(-10); + + private static string? NormalizeCase(string? caseRef) + { + var trimmed = (caseRef ?? string.Empty).Trim(); + return trimmed.Length == 0 ? null : trimmed; + } + + private static string StaffName(string? staff) => + string.IsNullOrWhiteSpace(staff) ? "Unknown" : staff.Trim(); + + private static async Task EnforceOnlineAsync(RecDBContext db, long accountId, List failures) + { + bool online; + try + { + var liveClient = Notify.GetClient(accountId) != null; + var presence = await db.Presences.AsNoTracking().FirstOrDefaultAsync(x => x.playerId == accountId); + online = liveClient || presence?.isOnline == true; + } + catch (Exception ex) + { + failures.Add($"online-state check failed ({ex.GetType().Name})"); + return; + } + + if (!online) return; + + try { MatchmakingUtils.ClearInRoom(accountId); } + catch (Exception ex) { failures.Add($"clearing the in-room session failed ({ex.GetType().Name})"); } + + var startedAt = DateTime.UtcNow; + try + { + var lockBan = await db.Moderation.AsNoTracking() + .FirstOrDefaultAsync(x => x.PlayerId == accountId && x.IsBan && x.BanMethod == BanMethod); + if (lockBan != null) startedAt = lockBan.CreatedAt; + } + catch { } + + try { await PlayerUtils.EjectToDormAsync(db, accountId); } + catch (Exception ex) { failures.Add($"dorm transition failed ({ex.GetType().Name})"); } + + try + { + var details = PlayerUtils.BuildBlockDetails(true, ReportCategory.Moderator, Reason, DateTime.MaxValue, startedAtUtc: startedAt); + await Notify.SendNotify(accountId, new Notification { Id = NotificationType.ModerationKick, Msg = details }); + } + catch (Exception ex) { failures.Add($"moderation notification failed ({ex.GetType().Name})"); } + } + + private static async Task BestEffortAsync(Func action) + { + try { await action(); } + catch { } + } + + private static void Log(string message) => + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] {message}"); + } +} diff --git a/Server/PlayersBanned/BanStatusMonitor.cs b/Server/PlayersBanned/BanStatusMonitor.cs new file mode 100644 index 0000000..c460716 --- /dev/null +++ b/Server/PlayersBanned/BanStatusMonitor.cs @@ -0,0 +1,249 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.Utils +{ + + + + + + + + + + + + + + public class BanStatusMonitor : BackgroundService + { + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(15); + private readonly IDbContextFactory _dbFactory; + + + + private static readonly System.Collections.Concurrent.ConcurrentDictionary LastSeenBan = new(); + private static readonly System.Collections.Concurrent.ConcurrentDictionary LastSeenRoom = new(); + + private static readonly System.Collections.Concurrent.ConcurrentDictionary LastSeenVoiceRestriction = new(); + + public BanStatusMonitor(IDbContextFactory dbFactory) { + _dbFactory = dbFactory; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + await CheckConnectedPlayersAsync(stoppingToken); + } + catch (Exception) + { + } + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task CheckConnectedPlayersAsync(CancellationToken ct) + { + var clients = Notify.SnapshotClients(); + if (clients.Count == 0) return; + + var accountIds = clients.Select(c => c.AccountId).Distinct().ToList(); + + await using var db = await _dbFactory.CreateDbContextAsync(ct); + + + var players = await db.Players.AsNoTracking() + .Where(x => accountIds.Contains(x.AccountId)).ToListAsync(ct); + var presences = await db.Presences.AsNoTracking() + .Where(x => accountIds.Contains(x.playerId)).ToListAsync(ct); + var moderationBans = await db.Moderation + .Where(x => x.IsBan && x.BanMethod != AccountLock.BanMethod && accountIds.Contains(x.PlayerId)) + .ToListAsync(ct); + var hardwareBans = await db.HardwareBans.ToListAsync(ct); + var ipBans = await db.IpBans.ToListAsync(ct); + var voiceRestrictions = await db.VoiceChatRestrictions.AsNoTracking() + .Where(x => accountIds.Contains(x.PlayerId) && x.ClearedAt == null && x.ExpiresAt > DateTime.UtcNow) + .ToListAsync(ct); + + + + var now = DateTime.UtcNow; + bool cleaned = false; + foreach (var m in moderationBans.Where(m => m.ExpireDuration < now).ToList()) + { + db.Moderation.Remove(m); + moderationBans.Remove(m); + cleaned = true; + } + foreach (var h in hardwareBans.Where(h => h.ExpiresAt.HasValue && h.ExpiresAt.Value < now).ToList()) + { + db.HardwareBans.Remove(h); + hardwareBans.Remove(h); + cleaned = true; + } + foreach (var i in ipBans.Where(i => i.ExpiresAt.HasValue && i.ExpiresAt.Value < now).ToList()) + { + db.IpBans.Remove(i); + ipBans.Remove(i); + cleaned = true; + } + if (cleaned) await db.SaveChangesAsync(ct); + + var connected = new HashSet(accountIds); + foreach (var client in clients) + { + var id = client.AccountId; + var player = players.FirstOrDefault(p => p.AccountId == id); + if (player == null) continue; + + + var moderation = moderationBans.FirstOrDefault(m => m.PlayerId == id); + var hwBan = !string.IsNullOrWhiteSpace(player.DeviceId) + ? hardwareBans.FirstOrDefault(h => h.DeviceId == player.DeviceId) + : null; + var ipBan = ResolveIpBan(ipBans, player); + + string? signature = null; + string? reason = null; + DateTime? startedAt = null; + DateTime expiry = DateTime.MaxValue; + var banCategory = ReportCategory.Misc; + if (moderation != null) + { + signature = "mod:" + moderation.Id; + reason = moderation.Message ?? ""; + startedAt = moderation.CreatedAt; + expiry = moderation.ExpireDuration; + banCategory = moderation.Category; + } + else if (hwBan != null) + { + signature = "hw:" + hwBan.Id; + reason = hwBan.Reason ?? ""; + startedAt = hwBan.CreatedAt; + expiry = hwBan.ExpiresAt ?? DateTime.MaxValue; + } + else if (ipBan != null) + { + signature = "ip:" + ipBan.IpAddress; + reason = ipBan.Reason ?? ""; + startedAt = ipBan.CreatedAt; + expiry = ipBan.ExpiresAt ?? DateTime.MaxValue; + } + + var presence = presences.FirstOrDefault(p => p.playerId == id); + var last = LastSeenBan.GetValueOrDefault(id); + var roomKey = presence?.roomInstance ?? -1; + var lastRoom = LastSeenRoom.GetValueOrDefault(id, long.MinValue); + + if (signature != null) + { + var banChanged = last != signature; + var roomChanged = lastRoom != roomKey; + + LastSeenBan[id] = signature; + LastSeenRoom[id] = roomKey; + + if (banChanged || roomChanged) + { + if (!await db.IsDeveloper(id)) + await KickIfInRoomAsync(db, id, reason ?? "", expiry, startedAt, hwBan?.BannedAccountId, banChanged, banCategory); + } + } + else if (last != null) + { + LastSeenBan.TryRemove(id, out _); + LastSeenRoom.TryRemove(id, out _); + + await PlayerUtils.ReleaseModerationHoldAsync(id, await db.IsDeveloper(id)); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (profile != null) + await Notify.SendNotify(id, new Notification { Id = NotificationType.RefreshLogin, Msg = profile }); + } + + + var restriction = voiceRestrictions.FirstOrDefault(r => r.PlayerId == id); + var restrictionSignature = restriction == null + ? null + : $"vr:{restriction.Id}:{restriction.ExpiresAt.Ticks}"; + var known = LastSeenVoiceRestriction.TryGetValue(id, out var lastRestriction) + ? lastRestriction + : null; + if (known != restrictionSignature) + { + if (restrictionSignature == null) + LastSeenVoiceRestriction.TryRemove(id, out _); + else + LastSeenVoiceRestriction[id] = restrictionSignature; + + await VoiceChatRestrictions.PushAsync(id, restriction); + } + } + + + + foreach (var key in LastSeenBan.Keys) + { + if (!connected.Contains(key)) + LastSeenBan.TryRemove(key, out _); + LastSeenRoom.TryRemove(key, out _); + } + + foreach (var key in LastSeenVoiceRestriction.Keys) + { + if (!connected.Contains(key)) + LastSeenVoiceRestriction.TryRemove(key, out _); + } + } + + private static IpBan? ResolveIpBan(List ipBans, PlayerData player) + { + if (string.IsNullOrWhiteSpace(player.LastIpAddress)) return null; + return ipBans.FirstOrDefault(b => b.IpAddress == player.LastIpAddress); + } + + private async Task KickIfInRoomAsync(RecDBContext db, long accountId, string reason, DateTime expiry, DateTime? startedAt = null, long? relatedAccountId = null, bool banChanged = false, ReportCategory category = ReportCategory.Misc) + { + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == accountId); + + if (presence?.roomInstance != null) + { + var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance); + var room = roomInstance != null ? await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId) : null; + bool isOwnDorm = room != null && room.IsDorm && room.CreatorAccountId == accountId; + + if (isOwnDorm) + { + + bool relatedBan = relatedAccountId.HasValue && relatedAccountId.Value != accountId; + if (!relatedBan || !banChanged) return; + } + else + { + + await PlayerUtils.EjectToDormAsync(db, accountId); + } + } + + + await Notify.SendNotify(accountId, new Notification + { + Id = NotificationType.ModerationKick, + Msg = PlayerUtils.BuildBlockDetails(true, category, reason, expiry, startedAtUtc: startedAt, + relatedUsername: await PlayerUtils.RelatedAccountUsernameAsync(db, relatedAccountId, accountId)) + }); + } + } +} diff --git a/Server/PlayersBanned/DeviceId.cs b/Server/PlayersBanned/DeviceId.cs new file mode 100644 index 0000000..5a6e4a8 --- /dev/null +++ b/Server/PlayersBanned/DeviceId.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Moderation +{ + public class DeviceId + { + public string oldDeviceId { get; set; } = null!; + public string newDeviceId { get; set; } = null!; + } +} diff --git a/Server/PlayersBanned/GameBlock.cs b/Server/PlayersBanned/GameBlock.cs new file mode 100644 index 0000000..4de170e --- /dev/null +++ b/Server/PlayersBanned/GameBlock.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Moderation +{ + + + + + + + + + + + public class GameBlock + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + public string IpAddress { get; set; } = ""; + public string Reason { get; set; } = ""; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? ExpiresAt { get; set; } + } +} diff --git a/Server/PlayersBanned/GameBlockMonitor.cs b/Server/PlayersBanned/GameBlockMonitor.cs new file mode 100644 index 0000000..c383e09 --- /dev/null +++ b/Server/PlayersBanned/GameBlockMonitor.cs @@ -0,0 +1,116 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; + +namespace RadicadeServer.Utils +{ + + + + + + + + + + + public static class GameBlockMonitor + { + private static readonly object Lock = new(); + private static readonly SemaphoreSlim RefreshGate = new(1, 1); + private static Dictionary Active = new(); + private static DateTime LastRefresh = DateTime.MinValue; + private const double RefreshIntervalSeconds = 10; + + + + + + public static async Task IsBlockedAsync(IDbContextFactory factory, string ipAddress) + { + if (string.IsNullOrWhiteSpace(ipAddress)) return false; + + DateTime lastRefresh; + lock (Lock) lastRefresh = LastRefresh; + if ((DateTime.UtcNow - lastRefresh).TotalSeconds > RefreshIntervalSeconds) + await RefreshAsync(factory); + + lock (Lock) + { + if (Active.TryGetValue(NormalizeIp(ipAddress), out var expiresAt)) + return !expiresAt.HasValue || expiresAt.Value > DateTime.UtcNow; + return false; + } + } + + + + + + public static void Invalidate() + { + lock (Lock) LastRefresh = DateTime.MinValue; + } + + public static async Task RefreshAsync(IDbContextFactory factory) + { + await RefreshGate.WaitAsync(); + try + { + + DateTime lastRefresh; + lock (Lock) lastRefresh = LastRefresh; + if ((DateTime.UtcNow - lastRefresh).TotalSeconds <= RefreshIntervalSeconds) + return; + + try + { + await using var db = await factory.CreateDbContextAsync(); + var now = DateTime.UtcNow; + var rows = await db.GameBlocks.AsNoTracking().ToListAsync(); + + + + var expired = rows.Where(b => b.ExpiresAt.HasValue && b.ExpiresAt.Value < now).ToList(); + if (expired.Count > 0) + { + db.GameBlocks.RemoveRange(expired); + await db.SaveChangesAsync(); + } + + var active = new Dictionary(); + foreach (var row in rows.Where(b => !expired.Contains(b))) + active[NormalizeIp(row.IpAddress)] = row.ExpiresAt; + + lock (Lock) + { + Active = active; + LastRefresh = DateTime.UtcNow; + } + } + catch + { + + + lock (Lock) LastRefresh = DateTime.UtcNow; + } + } + finally + { + RefreshGate.Release(); + } + } + + + + + + + public static string NormalizeIp(string ip) + { + ip = (ip ?? "").Trim(); + if (ip.StartsWith("::ffff:", StringComparison.OrdinalIgnoreCase)) + ip = ip.Substring("::ffff:".Length); + return ip; + } + } +} diff --git a/Server/PlayersBanned/HardwareBan.cs b/Server/PlayersBanned/HardwareBan.cs new file mode 100644 index 0000000..ad1b4f0 --- /dev/null +++ b/Server/PlayersBanned/HardwareBan.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Moderation +{ + + + + + + + public class HardwareBan + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + public string DeviceId { get; set; } = ""; + public string Reason { get; set; } = ""; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? ExpiresAt { get; set; } + + + + + + + + + public long? BannedAccountId { get; set; } + + + + + + + + public int OffenseNumber { get; set; } + + + public string DetectionCategory { get; set; } = ""; + + + public string Evidence { get; set; } = ""; + + + public int DurationMinutes { get; set; } + } +} diff --git a/Server/PlayersBanned/IpBan.cs b/Server/PlayersBanned/IpBan.cs new file mode 100644 index 0000000..6400aae --- /dev/null +++ b/Server/PlayersBanned/IpBan.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Moderation +{ + + + + + + + + + public class IpBan + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + public string IpAddress { get; set; } = ""; + public string Reason { get; set; } = ""; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? ExpiresAt { get; set; } + } +} diff --git a/Server/PlayersBanned/ModerationDB.cs b/Server/PlayersBanned/ModerationDB.cs new file mode 100644 index 0000000..21a5a8c --- /dev/null +++ b/Server/PlayersBanned/ModerationDB.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Moderation; + +namespace RadicadeServer.RecNet.Moderation +{ + public class ModerationDB + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + public DateTime ExpireDuration { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public string Message { get; set; } = null!; + public string? ModeratorNote { get; set; } + public ReportCategory Category { get; set; } + public bool IsBan { get; set; } + public long PlayerId { get; set; } + + + + + + + public string BanMethod { get; set; } = "None"; + + + + + + + public string? AppealCode { get; set; } + + + public string? AppealText { get; set; } + + + public string? AppealContact { get; set; } + + + public DateTime? AppealSubmittedAt { get; set; } + + + public string? AppealResolution { get; set; } + + + + public DateTime? AppealResolvedAt { get; set; } + } +} diff --git a/Server/PlayersBanned/TokenRevocation.cs b/Server/PlayersBanned/TokenRevocation.cs new file mode 100644 index 0000000..67fc3eb --- /dev/null +++ b/Server/PlayersBanned/TokenRevocation.cs @@ -0,0 +1,58 @@ +using System.Collections.Concurrent; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; + +namespace RadicadeServer.Utils +{ + + + + + + + + + + + + + + public static class TokenRevocation + { + private static readonly ConcurrentDictionary Versions = new(); + + + + + public static async Task IsValidAsync(IServiceProvider services, long accountId, int tokenVersion) + { + if (Versions.TryGetValue(accountId, out var cached)) + return cached == tokenVersion; + + var factory = services.GetRequiredService>(); + await using var db = await factory.CreateDbContextAsync(); + var current = await db.Players + .AsNoTracking() + .Where(p => p.AccountId == accountId) + .Select(p => (int?)p.TokenVersion) + .FirstOrDefaultAsync(); + + var stored = current ?? 0; + Versions[accountId] = stored; + return stored == tokenVersion; + } + + + + + + + + + + public static void Bump(long accountId, int newVersion) + { + Versions.TryRemove(accountId, out _); + } + } +} diff --git a/Server/PlayersBanned/VoteKickLog.cs b/Server/PlayersBanned/VoteKickLog.cs new file mode 100644 index 0000000..e97a707 --- /dev/null +++ b/Server/PlayersBanned/VoteKickLog.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Moderation; + +namespace RadicadeServer.RecNet.Moderation +{ + + + + + + + + + + + public class VoteKickLog + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + public long PlayerId { get; set; } + public string Reason { get; set; } = ""; + public ReportCategory Category { get; set; } + public bool IsForce { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime ExpiresAt { get; set; } + } +} diff --git a/Server/PlayersBanned/VoteKickReason.cs b/Server/PlayersBanned/VoteKickReason.cs new file mode 100644 index 0000000..2d284a8 --- /dev/null +++ b/Server/PlayersBanned/VoteKickReason.cs @@ -0,0 +1,30 @@ +namespace RadicadeServer.RecNet.Moderation +{ + public enum KickReason + { + Moderator = -1, + Unknown, + DEPRECATED_MicrophoneAbuse, + Harassment, + Cheating, + DEPRECATED_ImmatureBehavior, + AFK, + Misc, + Underage, + VoteKick = 10, + MisleadingPurchases, + CoC_Underage = 100, + CoC_Sexual, + CoC_Discrimination, + CoC_Trolling, + CoC_NameOrProfile, + InappropriateClothing = 200, + IssuingInaccurateReports = 1000 + } + + public class VoteKickReason + { + public string Reason { get; set; } = null!; + public KickReason ReportCategory { get; set; } + } +} diff --git a/Server/Program.cs b/Server/Program.cs new file mode 100644 index 0000000..cbfc5f3 --- /dev/null +++ b/Server/Program.cs @@ -0,0 +1,1554 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.IdentityModel.Tokens; +using SixLabors.ImageSharp.Web.DependencyInjection; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.Json; +using RadicadeServer; +using RadicadeServer.API.Notifications; +using RadicadeServer.API.Relationships; +using RadicadeServer.Admin; +using RadicadeServer.Auth; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RecNet.Storefront; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.Discord.Webhooks; +using RadicadeServer.Utils; + + + + + + +const string SplitTestHardOverrides = ""; +const string LocalHttpsCertPassword = "radicade-local-dev-cert"; + + + + + + +var builder = WebApplication.CreateBuilder(args); +DBConfig.LoadConfiguration(builder.Configuration); +WebhookConfig.Load(builder.Configuration, builder.Environment.ContentRootPath); + + +builder.Logging.ClearProviders(); +builder.Logging.AddSimpleConsole(options => +{ + options.SingleLine = true; + options.TimestampFormat = "yyyy-MM-dd HH:mm:ss "; +}); +builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning); +builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Infrastructure", LogLevel.Warning); +builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Query", LogLevel.Error); +var listenUrls = builder.Configuration["Radicade:ListenUrls"]; +if (!string.IsNullOrWhiteSpace(listenUrls)) +{ + builder.WebHost.UseUrls(listenUrls.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)); +} +else if (builder.Environment.IsProduction()) +{ + builder.WebHost.UseUrls("http://0.0.0.0:3030"); +} +else +{ + builder.WebHost.UseUrls("https://localhost:3030"); +} + +if (builder.Environment.IsProduction()) +{ + var missingSecrets = new List(); + if (string.IsNullOrWhiteSpace(DBConfig.AdminApiKey)) missingSecrets.Add("Radicade:AdminApiKey"); + if (string.IsNullOrWhiteSpace(DBConfig.JwtKey)) missingSecrets.Add("Radicade:JwtKey"); + if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey)) missingSecrets.Add("Radicade:SteamApiKey"); + + if (missingSecrets.Count > 0) + { + throw new InvalidOperationException( + "Missing required production secrets: " + string.Join(", ", missingSecrets) + + ". Set these as environment variables (e.g. Radicade__SteamApiKey) or in a secrets manager " + + "— never commit real values into appsettings.Production.json."); + } +} +else +{ + if (string.IsNullOrWhiteSpace(DBConfig.JwtKey)) + { + DBConfig.JwtKey = "radicade-local-development-jwt-key-change-before-production"; + } + + if (string.IsNullOrWhiteSpace(DBConfig.AdminApiKey)) + { + DBConfig.AdminApiKey = "radicade-local-development-admin-key"; + } + + if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey) && + string.IsNullOrWhiteSpace(builder.Configuration["Radicade:TrustUnverifiedSteamIdentity"])) + { + DBConfig.TrustUnverifiedSteamIdentity = true; + } +} + +if (!Directory.Exists(DBConfig.CDNImages)) + Directory.CreateDirectory(DBConfig.CDNImages); +if (!Directory.Exists(DBConfig.CDNStorage)) + Directory.CreateDirectory(DBConfig.CDNStorage); +if (!Directory.Exists(DBConfig.CDNVideos)) + Directory.CreateDirectory(DBConfig.CDNVideos); +if (!Directory.Exists(DBConfig.CDNCustom)) + Directory.CreateDirectory(DBConfig.CDNCustom); +if (!Directory.Exists(DBConfig.PersistentLogs)) + Directory.CreateDirectory(DBConfig.PersistentLogs); +var dataProtectionKeys = Path.Join(DBConfig.PublicData, "Persistent", "DataProtection-Keys"); +if (!Directory.Exists(dataProtectionKeys)) + Directory.CreateDirectory(dataProtectionKeys); +var localHttpsCertPath = Path.Join(DBConfig.PublicData, "Persistent", "localhost-rsa-aes-devcert.pfx"); +if (builder.Environment.IsDevelopment()) +{ + EnsureLocalHttpsCertificate(localHttpsCertPath, LocalHttpsCertPassword); + var localHttpsCertificate = LoadLocalHttpsCertificateFromStore( + InstallLocalHttpsCertificate(localHttpsCertPath, LocalHttpsCertPassword)); + builder.WebHost.ConfigureKestrel(options => + { + options.ConfigureHttpsDefaults(httpsOptions => + { + httpsOptions.ServerCertificate = localHttpsCertificate; + httpsOptions.SslProtocols = System.Security.Authentication.SslProtocols.Tls12; + }); + }); +} + +DBConfig.DebugKey = Guid.NewGuid().AltGuid() + Guid.NewGuid().AltGuid(); +File.WriteAllText(Path.Join(DBConfig.PersistentLogs, "debugkey.txt"), DBConfig.DebugKey); + +DBConfig.LoadFiles(); + + + + +SteamWhitelist.Load(); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +if (string.IsNullOrWhiteSpace(connectionString)) + throw new InvalidOperationException("Missing ConnectionStrings:DefaultConnection. Set ConnectionStrings__DefaultConnection on the VPS or add it to local appsettings.Development.json."); + +builder.Services.AddDbContextFactory( + options => options.UseSqlServer(connectionString, sql => { sql.EnableRetryOnFailure(3); }) + .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)) + ); + +var key = Encoding.ASCII.GetBytes(DBConfig.JwtKey); +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddJwtBearer(options => +{ + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + + if (!string.IsNullOrWhiteSpace(accessToken) && + RadicadeServer.API.Notifications.NotifyHubRoutes.IsHubPath(path.Value)) + { + context.Token = accessToken; + } + + return Task.CompletedTask; + }, + + + + + + OnTokenValidated = async context => + { + var principal = context.Principal; + if (principal == null) return; + + var userIdClaim = principal.FindFirst("userId"); + var versionClaim = principal.FindFirst("tver"); + if (userIdClaim == null || versionClaim == null) return; + + if (long.TryParse(userIdClaim.Value, out var userId) && + int.TryParse(versionClaim.Value, out var tokenVersion) && + !await TokenRevocation.IsValidAsync(context.HttpContext.RequestServices, userId, tokenVersion)) + { + context.Fail("Session revoked. Please sign in again."); + } + }, + OnAuthenticationFailed = context => + { + return Task.CompletedTask; + }, + OnChallenge = context => + { + return Task.CompletedTask; + } + }; +}); + + +builder.Services.AddImageSharp(options => +{ + options.OnParseCommandsAsync = context => + { + var commands = context.Commands; + + if (commands.TryGetValue("cropSquare", out var cropValue) && + cropValue == "1" && + commands.TryGetValue("width", out var widthValue) && + int.TryParse(widthValue, out int width)) + { + commands["height"] = width.ToString(); + commands["rmode"] = "crop"; + } + + return Task.CompletedTask; + }; +}); +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeys)); +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.PropertyNamingPolicy = null; + options.JsonSerializerOptions.WriteIndented = true; +}); +builder.Services.Configure(options => +{ + options.MultipartBodyLengthLimit = 256L * 1024 * 1024; +}); +builder.Services.AddSignalR(options => Microsoft.AspNetCore.SignalR.HubOptionsExtensions + .AddFilter(options)); +builder.Services.AddSingleton(); +builder.Services.AddHttpClient(); + + + + +builder.Services.AddHostedService(); + + +var isDevelopment = builder.Environment.IsDevelopment(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAllOrigins", builder => + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + + + + + + options.AddPolicy("AdminPanel", builder => + { + var adminOrigins = new List(); + if (!string.IsNullOrWhiteSpace(DBConfig.AdminPanelUrl)) + adminOrigins.Add(DBConfig.AdminPanelUrl); + + if (adminOrigins.Count > 0) + { + builder.WithOrigins(adminOrigins.ToArray()) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + } + else + { + builder.WithOrigins("http://localhost:0") + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + } + + if (isDevelopment) + { + builder.SetIsOriginAllowed(origin => + { + if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri)) + return false; + return uri.Host is "localhost" or "127.0.0.1" or "[::1]"; + }); + } + }); +}); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.Migrate(); + await EnsureSubRoomUnityAssetColumn(dbContext); + await EnsureRoomJoinGiftColumns(dbContext); + await EnsureRoomDeveloperOwnedColumn(dbContext); + await EnsurePlayerEventColumns(dbContext); + await EnsureVoiceChatRestrictionTable(dbContext); + await EnsureValidSceneLocations(dbContext); + await EnsureCatalogTables(dbContext); + StageShowdownRoomContent(); + await EnsureProfileIdentityColumns(dbContext); + if (DBConfig.TestInfluencerAccountIds.Count > 0) + { + var testProfiles = await dbContext.Profiles + .Where(profile => DBConfig.TestInfluencerAccountIds.Contains(profile.accountId)) + .ToListAsync(); + foreach (var profile in testProfiles) + { + if (!profile.HasRole("influencer")) + profile.role = string.IsNullOrWhiteSpace(profile.role) ? "influencer" : $"{profile.role},influencer"; + } + if (testProfiles.Any(profile => profile.HasRole("influencer"))) + await dbContext.SaveChangesAsync(); + } + await RemoveBlanketCoOwners(dbContext); + + await SyncAdminAccounts(dbContext); + + if (!dbContext.Profiles.Any()) + { + await dbContext.AddCoach(); + await dbContext.AddGameConfigs(); + await dbContext.AddServerConfig(); + await dbContext.AddConfig(); + await dbContext.AddDefaultRooms(); + } + + if (!await dbContext.Rooms.AnyAsync(room => room.IsDorm)) + { + await dbContext.AddDormRoom(); + } + + await dbContext.NormalizeDormRoomImagesAsync(); + + await DefaultRooms.EnsureAsync(dbContext); + + + + + + + await FirstTimeUtils.DownloadImages(); + + LoadingScreenImages.EnsureInstalled(); + + await EnsureRequiredGameConfigs(dbContext); + await RepairAvatarBootData(dbContext); + await EnsureReferenceGameConfigs(dbContext); + await RepairSplitTestConfigs(dbContext); + await DisableCommunityBoardHowToBuildLink(dbContext); + await RepairPlayerSettingsBootData(dbContext); + await RepairNullProfileImages(dbContext); + + StorefrontCatalog.Load(); + + + + + + if (SteamWhitelist.CreatedEmptyConfig) + { + var wlOnly = await dbContext.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "WhitelistOnly"); + var wl = await dbContext.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "SteamWhitelist"); + + if (wl != null && !string.IsNullOrWhiteSpace(wl.Value)) + { + SteamWhitelist.SetSteamIds(wl.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } + + + + if (wlOnly != null && wlOnly.Value.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + SteamWhitelist.Enabled = true; + } + } + + await MatchmakingRuntimeState.ResetAsync(dbContext); + + MainConfig.ActiveConfig = await dbContext.Config.FirstOrDefaultAsync(); +} + +static void StageShowdownRoomContent() +{ + const string saveName = "Showdown.room"; + const string bundleName = "Showdown_0_1.assetbundle"; + var candidates = new[] + { + Path.GetFullPath(Path.Combine("..", "RoomImports", "Showdown")), + Path.GetFullPath(Path.Combine("..", "room-imports", "Showdown")), + Path.GetFullPath(Path.Combine("RoomImports", "Showdown")), + Path.GetFullPath(Path.Combine("room-imports", "Showdown")), + }; + var sourceDir = candidates.FirstOrDefault(Directory.Exists) ?? candidates[0]; + var storage = DBConfig.CDNStorage; + Directory.CreateDirectory(storage); + var saveSource = Path.Combine(sourceDir, saveName); + var bundleSource = Path.Combine(sourceDir, bundleName); + if (!File.Exists(saveSource) || !File.Exists(bundleSource)) + { + return; + } + File.Copy(saveSource, Path.Combine(storage, saveName), overwrite: true); + File.Copy(bundleSource, Path.Combine(storage, bundleName), overwrite: true); +} + +static async Task SyncAdminAccounts(RecDBContext dbContext) +{ + var configured = DBConfig.AdminAccounts; + var existing = await dbContext.AdminUsers.ToListAsync(); + + if (configured.Count == 0) + { + return; + } + + foreach (var account in configured) + { + var user = existing.FirstOrDefault(x => string.Equals(x.Username, account.Username, StringComparison.OrdinalIgnoreCase)); + if (user == null) + { + dbContext.AdminUsers.Add(new AdminUser + { + Username = account.Username, + PasswordHash = AdminSecurity.HashPassword(account.Password), + CreatedAt = DateTime.UtcNow + }); + await dbContext.SaveChangesAsync(); + continue; + } + + if (!AdminSecurity.VerifyPassword(user.PasswordHash, account.Password)) + { + user.PasswordHash = AdminSecurity.HashPassword(account.Password); + await dbContext.SaveChangesAsync(); + await AdminSession.RevokeAllAsync(dbContext, user.Id); + } + else if (AdminSecurity.NeedsRehash(user.PasswordHash)) + { + user.PasswordHash = AdminSecurity.HashPassword(account.Password); + await dbContext.SaveChangesAsync(); + } + } + + var removed = existing + .Where(user => !configured.Any(account => string.Equals(account.Username, user.Username, StringComparison.OrdinalIgnoreCase))) + .ToList(); + if (removed.Count == 0) return; + + foreach (var user in removed) + { + await AdminSession.RevokeAllAsync(dbContext, user.Id); + } + dbContext.AdminUsers.RemoveRange(removed); + await dbContext.SaveChangesAsync(); +} + +static async Task RemoveBlanketCoOwners(RecDBContext dbContext) +{ + const string marker = "Config.BlanketCoOwnersRemoved"; + if (await dbContext.GameConfig.AnyAsync(x => x.Key == marker)) return; + + var accounts = new List { 2 }; + accounts.AddRange(DBConfig.TestCoOwnerAccountIds); + accounts = accounts.Distinct().ToList(); + + var rows = await dbContext.RoomRoles + .Where(role => accounts.Contains(role.AccountId) && role.Role == RoleType.CoOwner) + .ToListAsync(); + + if (rows.Count > 0) + { + dbContext.RoomRoles.RemoveRange(rows); + await dbContext.SaveChangesAsync(); + } + + dbContext.GameConfig.Add(new ConfigVal(marker, "1")); + await dbContext.SaveChangesAsync(); +} + +static async Task EnsureProfileIdentityColumns(RecDBContext dbContext) +{ + await dbContext.Database.ExecuteSqlRawAsync(@" +IF COL_LENGTH('dbo.Profiles', 'personalPronouns') IS NULL + ALTER TABLE [Profiles] ADD [personalPronouns] int NOT NULL CONSTRAINT [DF_Profiles_personalPronouns] DEFAULT 0; +IF COL_LENGTH('dbo.Profiles', 'identityFlags') IS NULL + ALTER TABLE [Profiles] ADD [identityFlags] int NOT NULL CONSTRAINT [DF_Profiles_identityFlags] DEFAULT 0; +-- Profile.bannerImage / isRecentHistoryVisible. Added by a migration too, but a +-- production database that drifted (see EnsurePlayerEventColumns) would reach the +-- model before the column exists, and every Profiles query would then fail with an +-- invalid-column-name error. Idempotent, so the migration and this repair can coexist. +IF COL_LENGTH('dbo.Profiles', 'bannerImage') IS NULL + ALTER TABLE [Profiles] ADD [bannerImage] nvarchar(max) NULL; +IF COL_LENGTH('dbo.Profiles', 'isRecentHistoryVisible') IS NULL + ALTER TABLE [Profiles] ADD [isRecentHistoryVisible] bit NOT NULL CONSTRAINT [DF_Profiles_isRecentHistoryVisible] DEFAULT 1;"); +} + +static async Task EnsureSubRoomUnityAssetColumn(RecDBContext dbContext) +{ + await dbContext.Database.ExecuteSqlRawAsync(@" +IF COL_LENGTH('dbo.SubRooms', 'UnityAssetId') IS NULL + ALTER TABLE [SubRooms] ADD [UnityAssetId] nvarchar(max) NOT NULL CONSTRAINT [DF_SubRooms_UnityAssetId] DEFAULT N'';"); +} + +static async Task RepairNullProfileImages(RecDBContext dbContext) +{ + var broken = await dbContext.Profiles + .Where(p => p.profileImage == null || p.profileImage == "") + .ToListAsync(); + if (broken.Count == 0) + return; + + foreach (var profile in broken) + profile.profileImage = "DefaultProfileImage"; + await dbContext.SaveChangesAsync(); +} + +RoomUtils.DbFactory = app.Services.GetRequiredService>(); +MatchmakingUtils.DbFactory = app.Services.GetRequiredService>(); +Notify.DbFactory = app.Services.GetRequiredService>(); +Notify.Context = app.Services.GetRequiredService>(); +RelationshipsShared.DbFactory = app.Services.GetRequiredService>(); +PlayerUtils.DbFactory = app.Services.GetRequiredService>(); +AuthShared.DbFactory = app.Services.GetRequiredService>(); + + + + + + + + + + + + + +app.Use(async (context, next) => +{ + var path = context.Request.Path.Value ?? ""; + if (!path.StartsWith("/admin/", StringComparison.OrdinalIgnoreCase) && + !string.Equals(path, "/admin", StringComparison.OrdinalIgnoreCase)) + { + var ip = context.Request.GetClientIp(); + if (!string.IsNullOrWhiteSpace(ip)) + { + var dbFactory = context.RequestServices.GetRequiredService>(); + if (await GameBlockMonitor.IsBlockedAsync(dbFactory, ip)) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + await context.Response.WriteAsJsonAsync(new + { + error = "game_blocked", + message = "This IP address is blocked from connecting to the game servers." + }); + return; + } + } + } + + await next(); +}); + + + + + + + +app.Use(async (context, next) => +{ + var path = context.Request.Path.Value ?? ""; + if (path.Contains("//")) + { + var collapsed = System.Text.RegularExpressions.Regex.Replace(path, "/{2,}", "/"); + context.Request.Path = collapsed; + } + + await next(); +}); + + + + + +app.UseRouting(); + + + + + + +app.UseCors("AllowAllOrigins"); + + +app.Use(async (context, next) => +{ + var path = context.Request.Path.Value ?? ""; + if (path.StartsWith("/images/", StringComparison.OrdinalIgnoreCase)) + { + context.Response.Headers.Append("Access-Control-Allow-Origin", "*"); + context.Response.Headers.Append("Access-Control-Allow-Methods", "GET, OPTIONS"); + context.Response.Headers.Append("Access-Control-Allow-Headers", "*"); + } + + await next(); +}); + + + + + + +app.Use(async (context, next) => +{ + var path = context.Request.Path.Value ?? ""; + if (path.StartsWith("/admin/", StringComparison.OrdinalIgnoreCase) && + !path.StartsWith("/admin/auth", StringComparison.OrdinalIgnoreCase)) + { + var existingKey = context.Request.Query["key"].ToString(); + if (string.IsNullOrEmpty(existingKey) || !existingKey.IsAdminKey()) + { + void Promote() + { + var query = new Dictionary(); + foreach (var kv in context.Request.Query) + query[kv.Key] = kv.Value; + query["key"] = DBConfig.AdminApiKey; + context.Request.Query = new QueryCollection(query); + } + + if (context.Request.Cookies.TryGetValue(RadicadeServer.Auth.AdminSession.CookieName, out var sessionToken)) + { + var dbFactory = context.RequestServices.GetRequiredService>(); + await using var authDb = await dbFactory.CreateDbContextAsync(); + var (valid, _) = await RadicadeServer.Auth.AdminSession.ValidateAsync(authDb, sessionToken); + if (valid) Promote(); + } + } + } + + await next(); +}); + +app.Use(async (context, next) => +{ + var started = Stopwatch.GetTimestamp(); + + var originalBody = context.Response.Body; + var countingBody = new CountingStream(originalBody); + context.Response.Body = countingBody; + try + { + await next(); + } + catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {context.Request.Method} {context.Request.Path}{context.Request.QueryString} -> client disconnected mid-response (len={countingBody.BytesWritten})"); + } + catch (Exception ex) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] unhandled {context.Request.Method} {context.Request.Path}{context.Request.QueryString}: {ex}"); + if (!context.Response.HasStarted && !context.RequestAborted.IsCancellationRequested) + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsJsonAsync(new { error = "internal_server_error" }); + } + } + finally + { + var elapsed = Stopwatch.GetElapsedTime(started).TotalMilliseconds; + var bodyPreview = countingBody.BytesWritten <= 8 + ? " body=" + System.Text.Encoding.UTF8.GetString(countingBody.Peek) + : string.Empty; + Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {context.Request.Method} {context.Request.Path}{context.Request.QueryString} -> {context.Response.StatusCode} ({elapsed:0.0} ms) len={countingBody.BytesWritten} ct={context.Response.ContentType}{bodyPreview}"); + } +}); + +app.UseMiddleware(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.Use(async (context, next) => +{ + var diagnosticPaths = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "/matchmaking/player", + "/matchmaking/player/login", + "/matchmaking/player/exclusivelogin", + "/leaderboard/GetRanks", + "/leaderboard/GetPlayerRank", + "/leaderboard/GetNearbyScores", + "/leaderboard/leaderboard/GetRanks", + "/leaderboard/leaderboard/GetPlayerRank", + "/leaderboard/leaderboard/GetNearbyScores", + "/econ/api/checklist/v1/current", + "/econ/api/objectives/v1/myprogress", + "/api/players/v2/progression/bulk", + "/accounts/account/bulk", + "/api/playerReputation/v2/bulk", + "/api/relationships/v2/get", + "/api/PlayerReporting/v1/moderationBlockDetails", + "/chat/thread", + "/playersettings", + "/playersettings/v2", + "/playersettings/playersettings", + "/api/settings", + "/api/settings/v2", + "/api/settings/playersettings", + "/api/gameconfigs/v1/all", + "/api/versioncheck/v4" + }; + + var path = context.Request.Path.Value ?? string.Empty; + if (!diagnosticPaths.Contains(path)) + { + await next(); + return; + } + + var originalBody = context.Response.Body; + await using var capturedBody = new MemoryStream(); + context.Response.Body = capturedBody; + + try + { + await next(); + + capturedBody.Position = 0; + using var reader = new StreamReader( + capturedBody, + Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + leaveOpen: true); + + var responseBody = await reader.ReadToEndAsync(); + var diagnosticLine = + $"[PlayerDataDebug] {context.Request.Method} " + + $"{context.Request.Path}{context.Request.QueryString} -> " + + $"{context.Response.StatusCode} body={responseBody}"; + try + { + var diagnosticLogPath = Path.Join(DBConfig.PersistentLogs, "player-data-debug.log"); + await File.AppendAllTextAsync( + diagnosticLogPath, + $"[{DateTime.UtcNow:O}] {diagnosticLine}{Environment.NewLine}"); + } + catch + { + } + + capturedBody.Position = 0; + await capturedBody.CopyToAsync(originalBody); + } + finally + { + context.Response.Body = originalBody; + } +}); + + + +app.Use(async (context, next) => +{ + var leaderboardPath = context.Request.Path.Value ?? string.Empty; + if (leaderboardPath.Contains("leaderboard", StringComparison.OrdinalIgnoreCase) + || leaderboardPath.Contains("scoreboard", StringComparison.OrdinalIgnoreCase)) + { + context.Request.EnableBuffering(); + string requestBody; + using (var reader = new StreamReader( + context.Request.Body, + Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + leaveOpen: true)) + { + requestBody = await reader.ReadToEndAsync(); + } + context.Request.Body.Position = 0; + } + + await next(); +}); + + +app.MapControllers(); +foreach (var hubPath in RadicadeServer.API.Notifications.NotifyHubRoutes.Paths) + app.MapHub(hubPath); + +app.MapGet("/diag/build", () => Results.Json(new +{ + build = BuildInfo.Tag, + notifyIdFormat = BuildInfo.NotifyIdFormat, + assemblyBuiltUtc = BuildInfo.AssemblyBuiltUtc, + startedUtc = BuildInfo.StartedUtc, +})); + +app.MapGet("/diag/notify", async (long account, string? key, string? form) => +{ + if (key is null || !key.IsAdminKey()) return Results.Unauthorized(); + return Results.Text(await Notify.ReplayStoredCoachMessageAsync(account, form), "text/plain"); +}); + +app.MapGet("/diag/rewardpush", async (long account, string? key, string? id, IDbContextFactory dbFactory) => +{ + if (key is null || !key.IsAdminKey()) return Results.Unauthorized(); + + await using var db = await dbFactory.CreateDbContextAsync(); + return Results.Text(await RewardPushDiag.ReplayPendingRewardSelectionAsync(db, account, id), "text/plain"); +}); + +app.MapGet("/diag/rewards", async (long account, string? key, int? lines, IDbContextFactory dbFactory) => +{ + if (key is null || !key.IsAdminKey()) return Results.Unauthorized(); + + await using var db = await dbFactory.CreateDbContextAsync(); + return Results.Text(await RewardDiag.DescribeAsync(db, account, lines ?? 120), "text/plain"); +}); + +app.Lifetime.ApplicationStarted.Register(() => + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Radicade] started. env={app.Environment.EnvironmentName} url={DBConfig.ServerUrl} build={BuildInfo.Tag} assemblyBuilt={BuildInfo.AssemblyBuiltUtc:u}")); + +app.Lifetime.ApplicationStopping.Register(() => + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Radicade] stopping.")); + +app.Run(); + +static void EnsureLocalHttpsCertificate(string certificatePath, string password) +{ + if (File.Exists(certificatePath)) return; + + var certificateDirectory = Path.GetDirectoryName(certificatePath); + if (!string.IsNullOrWhiteSpace(certificateDirectory) && !Directory.Exists(certificateDirectory)) + Directory.CreateDirectory(certificateDirectory); + + using var rsa = CreateLocalHttpsKey(); + var request = new CertificateRequest( + "CN=localhost", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); + request.CertificateExtensions.Add(new X509KeyUsageExtension( + X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, + false)); + var enhancedKeyUsages = new OidCollection + { + new Oid("1.3.6.1.5.5.7.3.1") + }; + request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(enhancedKeyUsages, false)); + request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); + + var subjectAltNames = new SubjectAlternativeNameBuilder(); + subjectAltNames.AddDnsName("localhost"); + subjectAltNames.AddIpAddress(IPAddress.Loopback); + subjectAltNames.AddIpAddress(IPAddress.IPv6Loopback); + request.CertificateExtensions.Add(subjectAltNames.Build()); + + using var certificate = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddYears(5)); + File.WriteAllBytes(certificatePath, certificate.Export(X509ContentType.Pfx, password)); +} + +static X509Certificate2 LoadLocalHttpsCertificate(string certificatePath, string password) +{ + return X509CertificateLoader.LoadPkcs12FromFile( + certificatePath, + password, + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable); +} + +static string InstallLocalHttpsCertificate(string certificatePath, string password) +{ + using var certificate = LoadLocalHttpsCertificate(certificatePath, password); + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); + + var existingCertificates = store.Certificates.Find( + X509FindType.FindByThumbprint, + certificate.Thumbprint, + validOnly: false); + if (existingCertificates.Count == 0) + { + store.Add(certificate); + } + + return certificate.Thumbprint; +} + +static X509Certificate2 LoadLocalHttpsCertificateFromStore(string thumbprint) +{ + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + + var certificates = store.Certificates.Find( + X509FindType.FindByThumbprint, + thumbprint, + validOnly: false); + if (certificates.Count == 0) + throw new InvalidOperationException($"Local HTTPS certificate {thumbprint} was not found in CurrentUser\\My."); + + return certificates + .OfType() + .OrderByDescending(certificate => certificate.NotAfter) + .First(); +} + +static RSA CreateLocalHttpsKey() +{ + if (!OperatingSystem.IsWindows()) + return RSA.Create(2048); + + return new RSACryptoServiceProvider( + 2048, + new CspParameters(24, "Microsoft Enhanced RSA and AES Cryptographic Provider") + { + KeyContainerName = $"RadicadeLocalHttps-{Guid.NewGuid():N}", + KeyNumber = (int)KeyNumber.Exchange + }) + { + PersistKeyInCsp = false + }; +} + +static async Task EnsureRequiredGameConfigs(RecDBContext dbContext) +{ + await UpsertGameConfigValue(dbContext, "splitTestSoftOverrides", string.Empty); + await UpsertGameConfigValue(dbContext, "splitTestsAvailable", SplitTestConfigs.TestsAvailable); + await UpsertGameConfigValue(dbContext, "splitTestSegmentProbabilities", SplitTestConfigs.SegmentProbabilities); + await UpsertGameConfigValue(dbContext, "splitTestHardOverrides", SplitTestConfigs.HardOverrides); + var probabilitiesRow = await dbContext.GameConfig.FirstOrDefaultAsync(x => x.Key == "splitTestSegmentProbabilities"); + if (probabilitiesRow != null && probabilitiesRow.Value == "{}") + probabilitiesRow.Value = SplitTestConfigs.SegmentProbabilities; + await EnsureSplitTestHardOverrides(dbContext); + await UpsertGameConfigValue(dbContext, "Backtrace.versionRegex", ".*"); + await UpsertGameConfigValue(dbContext, "Backtrace.messageRegex", ".*"); + await UpsertGameConfigValue(dbContext, "Backtrace.stopTimeUTC", "2099-01-01 00:00"); + + await UpsertGameConfigValue(dbContext, "Growth.FriendFestPromosActive", "false"); + await UpsertGameConfigValue(dbContext, "Growth.FriendFestImageName", "FriendFest.jpg"); + await UpsertGameConfigValue(dbContext, "Growth.EnableProfilePhoneButton", "false"); + await RepairStoreLayoutConfigs(dbContext); + await RepairDebugLogConfigs(dbContext); + await UpsertGameConfigValue(dbContext, "Friends.PostGamePromptUnderFriendCount", "10"); + await UpsertGameConfigValue(dbContext, "Friends.SuggestFriendCodeOnFriendsScreenCount", "25"); + await UpsertGameConfigValue(dbContext, "MobileNotifications.UseFirebasePushNotifications", "true"); + await UpsertGameConfigValue(dbContext, "UGC.CV2.CooldownBeforeSubscriptionCheckSeconds", "240"); + await UnblockImportedRooms(dbContext); + await EnableBetaCreativeToolsConfigs(dbContext); + + await dbContext.SaveChangesAsync(); +} + +static async Task EnsureSplitTestHardOverrides(RecDBContext dbContext) +{ + var existing = await GameConfigSeeder.FindAsync(dbContext, "splitTestHardOverrides"); + if (existing == null) + { + await dbContext.GameConfig.AddAsync(new ConfigVal("splitTestHardOverrides", SplitTestHardOverrides)); + return; + } + + if (string.IsNullOrWhiteSpace(existing.Value)) + { + existing.Value = SplitTestHardOverrides; + return; + } + + var entries = existing.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var known = new HashSet(entries.Select(e => e.Split('=', 2)[0]), StringComparer.OrdinalIgnoreCase); + var additions = new List(); + foreach (var entry in SplitTestHardOverrides.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var name = entry.Split('=', 2)[0]; + if (known.Add(name)) + additions.Add(entry); + } + + if (additions.Count > 0) + { + existing.Value = (entries.Concat(additions).ToList()).Aggregate((a, b) => a + ";" + b); + } +} + +static async Task RepairStoreLayoutConfigs(RecDBContext dbContext) +{ + const string oldLayout = "banner=12;storecampaigns=11;tokenpack=10;allskus=9;hotitem=8;hotinvention=7;newitem=6;mysubscriptioninvention=5;rrplusitem=4;fivestaritem=3;skinsitem=2;hairdyeitem=1;"; + const string newLayout = "banner=9;hotitem=7;hotinvention=8;newitem=6;mysubscriptioninvention=5;rrplusitem=4;fivestaritem=3;skinsitem=2;hairdyeitem=1;"; + + var keys = new[] { "OldStoreLayout", "NewStoreLayout" }; + var configs = await dbContext.GameConfig.Where(x => keys.Contains(x.Key)).ToListAsync(); + + foreach (var config in configs) + { + var correct = config.Key == "OldStoreLayout" ? oldLayout : newLayout; + if (string.Equals(config.Value, correct, StringComparison.Ordinal)) continue; + + var value = config.Value ?? string.Empty; + if (string.IsNullOrWhiteSpace(value) || + string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "false", StringComparison.OrdinalIgnoreCase) || + !value.StartsWith("banner=", StringComparison.OrdinalIgnoreCase)) + { + config.Value = correct; + } + } + + await dbContext.SaveChangesAsync(); +} + +static async Task UnblockImportedRooms(RecDBContext dbContext) +{ + const string key = "RoomLoad.BlockedRoomNamesOnLowMemoryDevices"; + const string importedRoom = "Showdown"; + + var config = await dbContext.GameConfig.FirstOrDefaultAsync(x => x.Key == key); + if (config == null || string.IsNullOrWhiteSpace(config.Value) || + !config.Value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Contains(importedRoom, StringComparer.OrdinalIgnoreCase)) + return; + + config.Value = string.Join(",", config.Value + .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Where(name => !name.Equals(importedRoom, StringComparison.OrdinalIgnoreCase))); +} + +static async Task RepairDebugLogConfigs(RecDBContext dbContext) +{ + const string supportedFlags = "Platform|BootSequence"; + + var flags = await dbContext.GameConfig.FirstOrDefaultAsync(x => x.Key == "Debug.AdditionalTraceFlags"); + if (flags != null && + (flags.Value?.Contains("DissonanceLogs", StringComparison.OrdinalIgnoreCase) == true || + flags.Value?.Contains("Audio", StringComparison.OrdinalIgnoreCase) == true || + flags.Value?.Contains("TachyonClient", StringComparison.OrdinalIgnoreCase) == true)) + { + flags.Value = supportedFlags; + } + + var traceProbability = await dbContext.GameConfig.FirstOrDefaultAsync(x => x.Key == "Debug.TraceProbability"); + if (traceProbability != null && traceProbability.Value == "1") + traceProbability.Value = "0"; + + await dbContext.SaveChangesAsync(); +} + +static async Task RepairAvatarBootData(RecDBContext dbContext) +{ + const string repairedOutfitSelection = ""; + const string malformedUnlock = "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,,"; + const string repairedUnlock = "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,"; + string[] malformedOutfitSelections = + [ + "d0a9262f-5504-46a7-bb10-7507503db58e,,,,1", + "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,,,1", + "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,,1" + ]; + + var repairedAvatars = 0; + foreach (var avatar in await dbContext.Avatars.ToListAsync()) + { + var changed = false; + foreach (var malformedOutfitSelection in malformedOutfitSelections) + { + if (!string.IsNullOrWhiteSpace(avatar.Avatar) && + avatar.Avatar.Contains(malformedOutfitSelection, StringComparison.OrdinalIgnoreCase)) + { + avatar.Avatar = avatar.Avatar.Replace( + malformedOutfitSelection, + repairedOutfitSelection, + StringComparison.OrdinalIgnoreCase); + changed = true; + } + + if (!string.IsNullOrWhiteSpace(avatar.Outfits) && + avatar.Outfits.Contains(malformedOutfitSelection, StringComparison.OrdinalIgnoreCase)) + { + avatar.Outfits = avatar.Outfits.Replace( + malformedOutfitSelection, + repairedOutfitSelection, + StringComparison.OrdinalIgnoreCase); + changed = true; + } + } + + if (changed) repairedAvatars++; + } + + var repairedUnlockRows = 0; + foreach (var unlocks in await dbContext.Unlocks.ToListAsync()) + { + if (string.IsNullOrWhiteSpace(unlocks.AvatarItems) || + !unlocks.AvatarItems.Contains(malformedUnlock, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + unlocks.AvatarItems = unlocks.AvatarItems.Replace( + malformedUnlock, + repairedUnlock, + StringComparison.OrdinalIgnoreCase); + repairedUnlockRows++; + } + + if (repairedAvatars > 0 || repairedUnlockRows > 0) + { + await dbContext.SaveChangesAsync(); + } +} + +static Task UpsertGameConfigValue(RecDBContext dbContext, string key, string value) + => GameConfigSeeder.UpsertAsync(dbContext, key, value); + +static async Task EnsureReferenceGameConfigs(RecDBContext dbContext) +{ + var existingKeys = await dbContext.GameConfig + .Select(x => x.Key) + .ToHashSetAsync(StringComparer.OrdinalIgnoreCase); + + foreach (var tracked in dbContext.ChangeTracker.Entries()) + { + if (!string.IsNullOrWhiteSpace(tracked.Entity.Key)) + existingKeys.Add(tracked.Entity.Key); + } + + var added = 0; + + using var doc = JsonDocument.Parse(DBConfig.PlaceholderConfig); + foreach (var item in doc.RootElement.EnumerateArray()) + { + if (!item.TryGetProperty("Key", out var keyElement)) continue; + var key = keyElement.GetString(); + if (string.IsNullOrWhiteSpace(key) || !existingKeys.Add(key)) continue; + + var value = item.TryGetProperty("Value", out var valueElement) + ? valueElement.GetString() ?? string.Empty + : string.Empty; + + await dbContext.GameConfig.AddAsync(new ConfigVal(key, value)); + added++; + } + + if (added > 0) + { + await dbContext.SaveChangesAsync(); + } +} + +static async Task RepairSplitTestConfigs(RecDBContext dbContext) +{ + var desiredValues = new Dictionary + { + ["splitTestHardOverrides"] = ReferenceSplitTestValue("splitTestHardOverrides"), + ["splitTestsAvailable"] = ReferenceSplitTestValue("splitTestsAvailable"), + ["splitTestSegmentProbabilities"] = ReferenceSplitTestValue("splitTestSegmentProbabilities"), + }; + + var seeded = 0; + foreach (var (key, value) in desiredValues) + { + if (string.IsNullOrWhiteSpace(value)) continue; + + var config = await GameConfigSeeder.FindAsync(dbContext, key); + if (config == null) + { + await dbContext.GameConfig.AddAsync(new ConfigVal(key, value)); + seeded++; + continue; + } + + if (!string.IsNullOrWhiteSpace(config.Value)) continue; + + config.Value = value; + seeded++; + } + + if (seeded > 0) + { + await dbContext.SaveChangesAsync(); + } +} + +static async Task DisableCommunityBoardHowToBuildLink(RecDBContext dbContext) +{ + const string key = "CommunityBoard.HowToBuildLink"; + var config = await GameConfigSeeder.FindAsync(dbContext, key); + if (config == null || string.IsNullOrEmpty(config.Value)) return; + if (config.Value.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) < 0) return; + + config.Value = string.Empty; + await dbContext.SaveChangesAsync(); +} + +static async Task EnableBetaCreativeToolsConfigs(RecDBContext dbContext) +{ + var betaFlags = new Dictionary + { + ["UGC.Room2Enabled"] = "true", + ["UGC.AllowBetaInvertedTubeCreation"] = "true", + ["UGC.AllowNonBetaInvertedTubeCreation"] = "true", + ["UGC.EnableInventionsInBetaRooms"] = "true", + ["UGC.Room2PublishingEnabled"] = "true", + ["UGC.Room2InventionsEnabled"] = "true", + ["UGC.Room2InventionAllowBetaPublishing"] = "true", + ["UGC.Room2InventionPublishingEnabled"] = "true", + }; + + var updated = 0; + foreach (var (key, value) in betaFlags) + { + var existing = await GameConfigSeeder.FindAsync(dbContext, key); + if (existing == null) + { + await dbContext.GameConfig.AddAsync(new ConfigVal(key, value)); + updated++; + } + else if (!string.Equals(existing.Value, value, StringComparison.OrdinalIgnoreCase)) + { + existing.Value = value; + updated++; + } + } + + if (updated > 0) + { + await dbContext.SaveChangesAsync(); + } +} + +static string? ReferenceSplitTestValue(string key) +{ + try + { + var entries = JsonSerializer.Deserialize>(DBConfig.PlaceholderConfig); + return entries?.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.Ordinal))?.Value; + } + catch (Exception) + { + return null; + } +} + +static async Task RepairPlayerSettingsBootData(RecDBContext dbContext) +{ + const string neutralSplitTestAssignments = "1|{}"; + var repairedRows = 0; + + foreach (var settingsRow in await dbContext.PlayerSettings.ToListAsync()) + { + var settings = settingsRow.DeserializeSettings(); + var splitAssignments = settings.FirstOrDefault(x => + string.Equals(x.Key, "SplitTestAssignedSegments", StringComparison.OrdinalIgnoreCase)); + + if (splitAssignments == null) + { + settings.Add(new Setting + { + Key = "SplitTestAssignedSegments", + Value = neutralSplitTestAssignments + }); + repairedRows++; + } + else if (!string.Equals(splitAssignments.Value, neutralSplitTestAssignments, StringComparison.Ordinal)) + { + splitAssignments.Value = neutralSplitTestAssignments; + repairedRows++; + } + + settingsRow.Settings = settings.ToJson(); + } + + if (repairedRows > 0) + { + await dbContext.SaveChangesAsync(); + } +} + +static async Task EnsureRoomDeveloperOwnedColumn(RecDBContext dbContext) +{ + await dbContext.Database.ExecuteSqlRawAsync(@" +IF COL_LENGTH('dbo.Rooms', 'IsDeveloperOwned') IS NULL + ALTER TABLE [Rooms] ADD [IsDeveloperOwned] bit NOT NULL CONSTRAINT [DF_Rooms_IsDeveloperOwned] DEFAULT 0;"); +} + +static async Task EnsureRoomJoinGiftColumns(RecDBContext dbContext) +{ + await dbContext.Database.ExecuteSqlRawAsync(@" +IF COL_LENGTH('dbo.Rooms', 'JoinGiftAvatarItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftAvatarItem] nvarchar(max) NOT NULL DEFAULT N''; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftAvatarItemType') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftAvatarItemType] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftConsumableItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftConsumableItem] nvarchar(max) NOT NULL DEFAULT N''; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftContext') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftContext] int NOT NULL DEFAULT 2; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftCurrencyAmount') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftCurrencyAmount] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftCurrencyType') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftCurrencyType] int NOT NULL DEFAULT 1; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftEnabled') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEnabled] bit NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftEquipmentItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEquipmentItem] nvarchar(max) NOT NULL DEFAULT N''; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftEquipmentModificationGuid') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEquipmentModificationGuid] nvarchar(max) NOT NULL DEFAULT N''; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftFromPlayerId') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftFromPlayerId] bigint NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftMessage') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftMessage] nvarchar(max) NOT NULL DEFAULT N'Thanks for visiting!'; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftOncePerPlayer') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftOncePerPlayer] bit NOT NULL DEFAULT 1; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftRarity') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftRarity] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftTitle') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftTitle] nvarchar(max) NOT NULL DEFAULT N'Room Visit Gift'; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftVersion') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftVersion] int NOT NULL DEFAULT 1; +IF COL_LENGTH('dbo.Rooms', 'JoinGiftXp') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftXp] int NOT NULL DEFAULT 0; +"); + + // A room's join gift boxes, one row each. The Rooms row above still describes the first box, + // so a room saved before this table existed keeps handing out what it always did. + await dbContext.Database.ExecuteSqlRawAsync(@" +IF OBJECT_ID('dbo.RoomJoinGifts', 'U') IS NULL +BEGIN + CREATE TABLE [RoomJoinGifts] ( + [RoomJoinGiftId] bigint NOT NULL IDENTITY(1,1), + [RoomId] bigint NOT NULL, + [SortOrder] int NOT NULL DEFAULT 0, + [Version] int NOT NULL DEFAULT 1, + [Enabled] bit NOT NULL DEFAULT 1, + [OncePerPlayer] bit NOT NULL DEFAULT 1, + [Title] nvarchar(max) NOT NULL DEFAULT N'Room Visit Gift', + [Message] nvarchar(max) NOT NULL DEFAULT N'Thanks for visiting!', + [AvatarItem] nvarchar(max) NOT NULL DEFAULT N'', + [AvatarItemType] int NOT NULL DEFAULT 0, + [EquipmentItem] nvarchar(max) NOT NULL DEFAULT N'', + [EquipmentModificationGuid] nvarchar(max) NOT NULL DEFAULT N'', + [ConsumableItem] nvarchar(max) NOT NULL DEFAULT N'', + [CurrencyType] int NOT NULL DEFAULT 1, + [CurrencyAmount] int NOT NULL DEFAULT 0, + [Xp] int NOT NULL DEFAULT 0, + [Context] int NOT NULL DEFAULT 2, + [Rarity] int NOT NULL DEFAULT 0, + [FromPlayerId] bigint NOT NULL DEFAULT 0, + CONSTRAINT [PK_RoomJoinGifts] PRIMARY KEY ([RoomJoinGiftId]) + ); +END + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_RoomJoinGifts_RoomId' AND object_id = OBJECT_ID(N'dbo.RoomJoinGifts')) + CREATE INDEX [IX_RoomJoinGifts_RoomId] ON [RoomJoinGifts] ([RoomId]); +"); +} + +static async Task EnsurePlayerEventColumns(RecDBContext dbContext) +{ + await dbContext.Database.ExecuteSqlRawAsync(@" +IF OBJECT_ID('dbo.PlayerEvents', 'U') IS NULL +BEGIN + CREATE TABLE [PlayerEvents] ( + [PlayerEventId] bigint NOT NULL IDENTITY(1,1), + [Name] nvarchar(max) NOT NULL, + [Description] nvarchar(max) NOT NULL, + [ImageName] nvarchar(max) NOT NULL, + [StartTime] datetime2 NOT NULL, + [EndTime] datetime2 NOT NULL, + [CreatorPlayerId] bigint NOT NULL, + [AttendeeCount] int NOT NULL, + [RoomId] bigint NOT NULL, + [Accessibility] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [SubRoomId] bigint NULL, + [ModerationState] int NOT NULL DEFAULT 0, + [ClubId] bigint NULL, + [IsMultiInstance] bit NOT NULL DEFAULT 0, + [SupportMultiInstanceRoomChat] bit NOT NULL DEFAULT 0, + [DefaultBroadcastPermissions] int NOT NULL DEFAULT 0, + [CanRequestBroadcastPermissions] int NOT NULL DEFAULT 0, + [BroadcastingRoomInstanceId] bigint NULL, + [TagsJson] nvarchar(max) NOT NULL DEFAULT N'[]', + CONSTRAINT [PK_PlayerEvents] PRIMARY KEY ([PlayerEventId]) + ); +END + +IF OBJECT_ID('dbo.PlayerEventResponses', 'U') IS NULL +BEGIN + CREATE TABLE [PlayerEventResponses] ( + [PlayerEventResponseId] bigint NOT NULL IDENTITY(1,1), + [PlayerEventId] bigint NOT NULL, + [PlayerId] bigint NOT NULL, + [Type] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + CONSTRAINT [PK_PlayerEventResponses] PRIMARY KEY ([PlayerEventResponseId]) + ); +END + +IF COL_LENGTH('dbo.PlayerEvents', 'SubRoomId') IS NULL ALTER TABLE [PlayerEvents] ADD [SubRoomId] bigint NULL; +IF COL_LENGTH('dbo.PlayerEvents', 'ModerationState') IS NULL ALTER TABLE [PlayerEvents] ADD [ModerationState] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.PlayerEvents', 'ClubId') IS NULL ALTER TABLE [PlayerEvents] ADD [ClubId] bigint NULL; +IF COL_LENGTH('dbo.PlayerEvents', 'IsMultiInstance') IS NULL ALTER TABLE [PlayerEvents] ADD [IsMultiInstance] bit NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.PlayerEvents', 'SupportMultiInstanceRoomChat') IS NULL ALTER TABLE [PlayerEvents] ADD [SupportMultiInstanceRoomChat] bit NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.PlayerEvents', 'DefaultBroadcastPermissions') IS NULL ALTER TABLE [PlayerEvents] ADD [DefaultBroadcastPermissions] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.PlayerEvents', 'CanRequestBroadcastPermissions') IS NULL ALTER TABLE [PlayerEvents] ADD [CanRequestBroadcastPermissions] int NOT NULL DEFAULT 0; +IF COL_LENGTH('dbo.PlayerEvents', 'BroadcastingRoomInstanceId') IS NULL ALTER TABLE [PlayerEvents] ADD [BroadcastingRoomInstanceId] bigint NULL; +IF COL_LENGTH('dbo.PlayerEvents', 'TagsJson') IS NULL ALTER TABLE [PlayerEvents] ADD [TagsJson] nvarchar(max) NOT NULL DEFAULT N'[]'; + +IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20260805082102_FixModelChanges') + INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260805082102_FixModelChanges', '9.0.11'); +IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20260806004625_PlayerEventResponses') + INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260806004625_PlayerEventResponses', '9.0.11'); +IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20260814120000_AddPlayerEventModerationFields') + INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260814120000_AddPlayerEventModerationFields', '9.0.11'); +IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20260926120000_AddPlayerEventClubMetadata') + INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260926120000_AddPlayerEventClubMetadata', '9.0.11'); +IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20261008120000_AddPlayerEventBroadcastingRoomInstance') + INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20261008120000_AddPlayerEventBroadcastingRoomInstance', '9.0.11'); +"); +} + + +static async Task EnsureVoiceChatRestrictionTable(RecDBContext dbContext) +{ + await dbContext.Database.ExecuteSqlRawAsync(@" +IF OBJECT_ID('dbo.VoiceChatRestrictions', 'U') IS NULL +BEGIN + CREATE TABLE [VoiceChatRestrictions] ( + [Id] bigint NOT NULL IDENTITY(1,1), + [PlayerId] bigint NOT NULL, + [Reason] nvarchar(max) NOT NULL DEFAULT N'', + [Category] int NOT NULL DEFAULT 0, + [StartedAt] datetime2 NOT NULL, + [ExpiresAt] datetime2 NOT NULL, + [IssuedBy] nvarchar(max) NOT NULL DEFAULT N'Admin Panel', + [IssuedByUsername] nvarchar(max) NULL, + [ModeratorNote] nvarchar(max) NULL, + [ClearedAt] datetime2 NULL, + [ClearedBy] nvarchar(max) NULL, + CONSTRAINT [PK_VoiceChatRestrictions] PRIMARY KEY ([Id]) + ); + CREATE INDEX [IX_VoiceChatRestrictions_PlayerId] ON [VoiceChatRestrictions] ([PlayerId]); +END +"); +} + +static async Task EnsureValidSceneLocations(RecDBContext dbContext) +{ + var subRooms = await dbContext.SubRooms.ToListAsync(); + if (subRooms.Count == 0) return; + + var roomIds = subRooms.Select(s => s.RoomId).Distinct().ToList(); + var rooms = await dbContext.Rooms + .Where(r => roomIds.Contains(r.RoomId)) + .ToDictionaryAsync(r => r.RoomId); + + var byRoom = subRooms + .GroupBy(s => s.RoomId) + .ToDictionary(g => g.Key, g => g.ToList()); + + int repaired = 0; + foreach (var subRoom in subRooms) + { + if (SceneLocations.IsValid(subRoom.UnitySceneId)) continue; + + bool isDorm = rooms.TryGetValue(subRoom.RoomId, out var room) && room.IsDorm; + var siblings = byRoom.TryGetValue(subRoom.RoomId, out var list) + ? list.Select(s => s.UnitySceneId) + : Enumerable.Empty(); + + subRoom.UnitySceneId = SceneLocations.ResolveFallback(siblings, isDorm); + repaired++; + } + + if (repaired > 0) + { + await dbContext.SaveChangesAsync(); + } +} + +static async Task EnsureCatalogTables(RecDBContext dbContext) +{ + await dbContext.Database.ExecuteSqlRawAsync(@" +IF OBJECT_ID('dbo.ItemSets', 'U') IS NULL +BEGIN + CREATE TABLE [ItemSets] ( + [ItemSetId] bigint NOT NULL IDENTITY(1,1), + [FriendlyName] nvarchar(max) NOT NULL DEFAULT N'', + CONSTRAINT [PK_ItemSets] PRIMARY KEY ([ItemSetId]) + ); +END + +IF OBJECT_ID('dbo.Tags', 'U') IS NULL +BEGIN + CREATE TABLE [Tags] ( + [TagId] bigint NOT NULL IDENTITY(1,1), + [Tag] nvarchar(max) NOT NULL DEFAULT N'', + [Description] nvarchar(max) NOT NULL DEFAULT N'', + CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId]) + ); +END +"); +} + +internal sealed class CountingStream : Stream +{ + private readonly Stream _inner; + private long _bytes; + private readonly System.Collections.Generic.List _captured = new(); + public long BytesWritten => _bytes; + public byte[] Peek => _captured.ToArray(); + + public CountingStream(Stream inner) => _inner = inner; + + public override bool CanRead => _inner.CanRead; + public override bool CanSeek => _inner.CanSeek; + public override bool CanWrite => _inner.CanWrite; + public override long Length => _inner.Length; + public override long Position { get => _inner.Position; set => _inner.Position = value; } + public override void Flush() => _inner.Flush(); + public override Task FlushAsync(CancellationToken ct) => _inner.FlushAsync(ct); + public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count); + public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin); + public override void SetLength(long value) => _inner.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + _bytes += count; + if (_captured.Count < 8) + { + var take = Math.Min(count, 8 - _captured.Count); + for (var i = 0; i < take; i++) _captured.Add(buffer[offset + i]); + } + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + { + await _inner.WriteAsync(buffer, offset, count, ct); + _bytes += count; + if (_captured.Count < 8) + { + var take = Math.Min(count, 8 - _captured.Count); + for (var i = 0; i < take; i++) _captured.Add(buffer[offset + i]); + } + } +} diff --git a/Server/ProgressionEvents/Challenge.cs b/Server/ProgressionEvents/Challenge.cs new file mode 100644 index 0000000..95eff14 --- /dev/null +++ b/Server/ProgressionEvents/Challenge.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Progression.Challenges +{ + public class Challenge + { + public int Index { get; set; } + public int Group { get; set; } + public float Progress { get; set; } + public float VisualProgress { get; set; } + public bool IsCompleted { get; set; } + public bool HasClaimedReward { get; set; } + } +} diff --git a/Server/ProgressionEvents/ChallengeGroups.cs b/Server/ProgressionEvents/ChallengeGroups.cs new file mode 100644 index 0000000..d7e187e --- /dev/null +++ b/Server/ProgressionEvents/ChallengeGroups.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Progression.Challenges +{ + public class ChallengeGroups + { + public int Group { get; set; } + public bool IsCompleted { get; set; } + public DateTime ClearedAt { get; set; } + } +} diff --git a/Server/ProgressionEvents/ChallengeProgress.cs b/Server/ProgressionEvents/ChallengeProgress.cs new file mode 100644 index 0000000..3acf950 --- /dev/null +++ b/Server/ProgressionEvents/ChallengeProgress.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Progression.Challenges +{ + public class ChallengeProgress + { + public List ObjectiveGroups { get; set; } = new(); + public List Objectives { get; set; } = new(); + } +} diff --git a/Server/ProgressionEvents/ChallengeV2.cs b/Server/ProgressionEvents/ChallengeV2.cs new file mode 100644 index 0000000..85fef18 --- /dev/null +++ b/Server/ProgressionEvents/ChallengeV2.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; + +using Microsoft.AspNetCore.Authorization; +using System.Text.Json; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Progression.Challenges.V2; + +[Route("api/challenge/v2")] +[Route("econ/api/challenge/v2")] +[Authorize] +public class ChallengeV2(RecDBContext db) : Controller +{ + [HttpGet("getCurrent")] + public async Task GetCurrent() + { + var playerId = User.GetUserId(); + if (playerId == null) return Unauthorized(); + return Ok(await WeeklyChallenges.GetAsync(db, playerId.Value, DateTime.UtcNow)); + } + + [HttpPost("updateProgress")] + [RequestSizeLimit(32768)] + public async Task UpdateProgress() + { + var playerId = User.GetUserId(); + if (playerId == null) return Unauthorized(); + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + try + { + if (Request.HasFormContentType) + foreach (var field in await Request.ReadFormAsync()) fields[field.Key] = field.Value.ToString(); + else + { + using var json = await JsonDocument.ParseAsync(Request.Body); + if (json.RootElement.ValueKind != JsonValueKind.Object) return BadRequest(); + foreach (var field in json.RootElement.EnumerateObject()) fields[field.Name] = field.Value.ToString(); + } + } + catch (Exception ex) when (ex is JsonException or InvalidDataException) { return BadRequest(); } + if (!fields.TryGetValue("ChallengeMapId", out var mapRaw) || !int.TryParse(mapRaw, out var mapId) + || !fields.TryGetValue("ChallengeId", out var idRaw) || !int.TryParse(idRaw, out var challengeId) + || !fields.TryGetValue("Complete", out var completeRaw) || !bool.TryParse(completeRaw, out var complete) + || !fields.TryGetValue("Config", out var config)) return BadRequest(); + var error = await WeeklyChallenges.UpdateAsync(db, playerId.Value, mapId, challengeId, config, complete, DateTime.UtcNow); + if (error == "stale_challenge_map") return Conflict(new { error }); + return error == null ? Ok() : BadRequest(new { error }); + } +} diff --git a/Server/ProgressionEvents/ChecklistAPI.cs b/Server/ProgressionEvents/ChecklistAPI.cs new file mode 100644 index 0000000..cd55a09 --- /dev/null +++ b/Server/ProgressionEvents/ChecklistAPI.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Progression +{ + [Route("econ/api/checklist")] + [Route("api/checklist")] + public class ChecklistAPI : Controller + { + private static readonly object[] DefaultChecklist = + { + new { Order = 1, Objective = 32, Count = 1, CreditAmount = 25 }, + new { Order = 2, Objective = 2, Count = 1, CreditAmount = 25 }, + new { Order = 3, Objective = 30, Count = 1, CreditAmount = 25 }, + new { Order = 4, Objective = 6, Count = 1, CreditAmount = 25 }, + }; + + [HttpGet("v1/current"), Authorize] + [HttpGet("v2/current"), Authorize] + public IActionResult Current() + { + return Ok(DefaultChecklist); + } + + [HttpPost("v1/complete"), Authorize] + [HttpPost("v2/complete"), Authorize] + public IActionResult Complete() + { + return Ok(new Success(true, null, new + { + BalanceUpdates = Array.Empty(), + CurrencyBalances = Array.Empty(), + })); + } + } +} diff --git a/Server/ProgressionEvents/Enums/Objectives.cs b/Server/ProgressionEvents/Enums/Objectives.cs new file mode 100644 index 0000000..b21a16d --- /dev/null +++ b/Server/ProgressionEvents/Enums/Objectives.cs @@ -0,0 +1,102 @@ +namespace RadicadeServer.RecNet.Enums.Challenges +{ + public enum ObjectiveTypes + { + Default = -1, + FirstSessionOfDay = 1, + AddAFriend, + PartyUp, + AllOtherChallenges, + LevelUp, + CheerAPlayer, + PointedAtPlayer, + CheerARoom, + SubscribeToPlayer, + DailyObjective1, + DailyObjective2, + DailyObjective3, + AllDailyObjectives, + CompleteAnyDaily, + CompleteAnyWeekly, + OOBE_GoToLockerRoom = 20, + OOBE_GoToActivity, + OOBE_FinishActivity, + NUX_PunchcardObjective = 25, + NUX_AllPunchcardObjectives, + GoToRecCenter = 30, + FinishActivity, + VisitACustomRoom, + CreateACustomRoom, + ScoreBasketInRecCenter = 35, + UploadPhotoToRecNet, + UpdatePlayerBio, + SaveOutfitSlot, + PurchaseClothingItem, + PurchaseNonClothingItem, + CharadesGames = 100, + CharadesWinsPerformer, + CharadesWinsGuesser, + DiscGolfWins = 200, + DiscGolfGames, + DiscGolfHolesUnderPar, + DodgeballWins = 300, + DodgeballGames, + DodgeballHits, + PaddleballGames = 400, + PaddleballWins, + PaddleballScores, + PaintballAnyModeGames = 500, + PaintballAnyModeWins, + PaintballAnyModeHits, + PaintballCTFWins = 600, + PaintballCTFGames, + PaintballCTFHits, + PaintballFlagCaptures, + PaintballTeamBattleWins = 700, + PaintballTeamBattleGames, + PaintballTeamBattleHits, + PaintballFreeForAllWins = 710, + PaintballFreeForAllGames, + PaintballFreeForAllHits, + SoccerWins = 800, + SoccerGames, + SoccerGoals, + BowlingGames = 900, + BowlingWins, + BowlingStrike, + QuestGames = 1000, + QuestWins, + QuestPlayerRevives, + QuestEnemyKills, + QuestGames_Goblin1 = 1010, + QuestWins_Goblin1, + QuestPlayerRevives_Goblin1, + QuestEnemyKills_Goblin1, + QuestGames_Goblin2 = 1020, + QuestWins_Goblin2, + QuestPlayerRevives_Goblin2, + QuestEnemyKills_Goblin2, + QuestGames_Scifi1 = 1030, + QuestWins_Scifi1, + QuestPlayerRevives_Scifi1, + QuestEnemyKills_Scifi1, + QuestGames_Pirate1 = 1040, + QuestWins_Pirate1, + QuestPlayerRevives_Pirate1, + QuestEnemyKills_Pirate1, + QuestGames_Dracula1 = 1050, + QuestWins_Dracula1, + QuestPlayerRevives_Dracula1, + QuestEnemyKills_Dracula1, + ArenaGames = 2000, + ArenaWins, + ArenaPlayerRevives, + ArenaHeroTags, + ArenaBotTags, + RecRoyaleGames = 3000, + RecRoyaleWins, + RecRoyaleTags, + StuntRunnerGames = 4000, + StuntRunnerWins + } +} \ No newline at end of file diff --git a/Server/ProgressionEvents/GameRewardsAPI.cs b/Server/ProgressionEvents/GameRewardsAPI.cs new file mode 100644 index 0000000..1052c14 --- /dev/null +++ b/Server/ProgressionEvents/GameRewardsAPI.cs @@ -0,0 +1,603 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Storefront; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.GameRewards.V1 +{ + public class GameRewardRequestDTO + { + public RewardType RewardType { get; set; } + public string Message { get; set; } = string.Empty; + public GiftContext? GiftContext { get; set; } + } + + public class GameRewardSelectDTO + { + public long RewardSelectionId { get; set; } + public long GiftDropId { get; set; } + } + + [Route("api/gamerewards/v1")] + [Route("econ/api/gamerewards/v1")] + public class GameRewardsAPI(RecDBContext db) : Controller + { + [HttpGet("pending")] + public async Task> GetPendingGameRewards() + { + var userId = User.GetUserId(); + if (userId == null) return new List(); + + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId.Value); + var progression = data?.GetProgressionJson(); + var pending = progression?.PendingGameRewards; + if (pending == null) return new List(); + + var changed = false; + + var forgotten = pending.RemoveAll(r => r.Consumed && r.ConsumedAt is { } at && at < DateTime.UtcNow.AddDays(-1)); + changed |= forgotten > 0; + + var outstanding = pending.Where(r => !r.Consumed).ToList(); + foreach (var reward in outstanding) + changed |= ClampRewardIds(reward); + if (changed && data != null) + { + data.Progress = progression!.ToJson(); + await db.SaveChangesAsync(); + } + return outstanding; + } + + private static bool ClampRewardIds(GameReward reward) + { + var changed = false; + + void Clamp(Gift? gift) + { + if (gift == null || (gift.GiftDropId >= int.MinValue && gift.GiftDropId <= int.MaxValue)) return; + gift.GiftDropId = StorefrontShared.NewGiftDropId(); + changed = true; + } + + if (reward.RewardSelectionId > int.MaxValue || reward.RewardSelectionId < int.MinValue) + { + _ = reward.RewardSelectionId; + } + Clamp(reward.GiftDrop1); + Clamp(reward.GiftDrop2); + Clamp(reward.GiftDrop3); + return changed; + } + + [HttpPost("request"), Authorize] + public async Task> RequestReward() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var dto = await ReadRewardBodyAsync(); + if (dto == null) return BadRequest(); + + if (dto.RewardType == RewardType.WeeklyCompleteButHaveSkin) + return Ok(new OkResponse { Success = true }); + + Trace(userId.Value, $"claimed rewardType={dto.RewardType} ({(int)dto.RewardType}) giftContext={dto.GiftContext?.ToString() ?? "none"} body={_rawBody}"); + + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId.Value); + if (data == null) + { + data = new ProgressionData { PlayerId = userId.Value }; + db.Progression.Add(data); + } + var json = data.GetProgressionJson() ?? new ProgressionJson(); + json.PendingGameRewards ??= new List(); + + var ledger = RewardDay.For(json, DateTime.UtcNow); + + var completion = IsCompletion(dto.RewardType); + GameReward? reward = null; + + if (completion) + { + var rewardType = RewardDay.CompletionType(ledger, dto.RewardType); + + var paid = rewardType == RewardType.FirstActivityOfDay + ? RewardDay.TryTakeFirstActivity(ledger) + : RewardDay.TryTakeActivityBox(ledger); + + if (paid) + { + dto.Message = CompletionMessage(rewardType, dto.Message); + reward = await BuildRewardAsync(userId.Value, dto, rewardType); + } + } + else if (dto.RewardType == RewardType.DailyChallengesComplete) + { + if (RewardDay.TryTakeDailyChallengeBox(ledger)) + { + dto.Message = string.IsNullOrWhiteSpace(dto.Message) ? "Daily Challenge Reward" : dto.Message; + reward = await BuildDailyChallengeRewardAsync(userId.Value, dto); + } + } + else + { + reward = BuildReward(dto.RewardType, dto.Message, + BonusContext(dto.RewardType) ?? dto.GiftContext ?? GiftContext.GameRewards_Tokens); + } + + if (reward != null) + json.PendingGameRewards.Add(reward); + + data.Progress = json.ToJson(); + await db.SaveChangesAsync(); + + if (completion) + { + await ProgressionAwardService.AwardXpAsync(db, userId.Value, + ProgressionAwardService.XpPerGameCompleted, "game completed"); + } + + if (reward == null) return Ok(new OkResponse { Success = true }); + + if (RroRewardPools.IsQuestContext(reward.GiftContext)) + { + await DeliverQuestRewardAsync(userId.Value, reward); + data.Progress = json.ToJson(); + await db.SaveChangesAsync(); + return Ok(new OkResponse { Success = true }); + } + + await PushRewardSelectionAsync(userId.Value, reward); + + return Ok(new OkResponse { Success = true }); + } + + private async Task DeliverQuestRewardAsync(long playerId, GameReward reward) + { + var box = reward.GiftDrop1; + if (box == null) return; + if (box.GiftDropId == 0) box.GiftDropId = StorefrontShared.NewGiftDropId(); + + var already = await GameRewardRedemption.TakeRecentlyHandedOverAsync( + db, playerId, reward.GiftContext, TimeSpan.FromMinutes(10)); + + if (already == null) + await StorefrontShared.PersistGiftBoxAsync(db, playerId, box, notify: true); + reward.Consumed = true; + reward.ConsumedGiftDropId = already?.GiftDropId ?? box.GiftDropId; + reward.ConsumedAt = DateTime.UtcNow; + RewardTrace.Log(playerId, $"stage=DeliveredAsQuestBox context={RewardTrace.Context(reward.GiftContext)} " + + $"selectionId={reward.RewardSelectionId} alreadyHandedOver={already != null} box={RewardTrace.Box(box)} " + + "a quest's reward is the box its run generated, not a selection."); + } + + private static string CompletionMessage(RewardType rewardType, string? claimed) => rewardType switch + { + RewardType.FirstActivityOfDay => "First Game of the Day", + RewardType.PostQuestActivity => string.IsNullOrWhiteSpace(claimed) ? "Quest Complete" : claimed, + _ => "Game Reward" + }; + + private async Task BuildDailyChallengeRewardAsync(long playerId, GameRewardRequestDTO dto) + { + var owned = await LevelUpGiftService.CollectOwnedKeysAsync(db, playerId, GiftContext.Daily_Challenge_Complete); + var drops = RroRewardPools.BuildDailyChallengeSelection(owned: owned); + + if (drops.Count == 0) + { + return BuildReward(dto.RewardType, dto.Message, GiftContext.Daily_Challenge_Complete); + } + + foreach (var drop in drops) + drop.GiftDropId = StorefrontShared.NewGiftDropId(); + + return new GameReward + { + RewardSelectionId = StorefrontShared.NewGiftDropId(), + Message = string.IsNullOrWhiteSpace(dto.Message) ? "Daily Challenge Reward" : dto.Message, + GiftContext = GiftContext.Daily_Challenge_Complete, + RewardType = dto.RewardType, + GiftDrop1 = drops[0], + GiftDrop2 = drops.Count > 1 ? drops[1] : drops[0], + GiftDrop3 = drops.Count > 2 ? drops[2] : drops[0], + CreatedAt = DateTime.UtcNow + }; + } + + [HttpPost("select"), Authorize] + public async Task> SelectReward() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var dto = await ReadSelectBodyAsync(); + if (dto == null || dto.RewardSelectionId <= 0 || dto.GiftDropId <= 0) return BadRequest(); + + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId.Value); + if (data == null) return NotFound(); + + var json = data.GetProgressionJson(); + var reward = json?.PendingGameRewards?.FirstOrDefault(r => r.RewardSelectionId == dto.RewardSelectionId); + if (reward == null) return NotFound(); + + var drops = new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 } + .Where(g => g != null) + .ToList(); + + if (reward.Consumed) + { + return Ok(new OkResponse { Success = true }); + } + + var gift = drops.FirstOrDefault(g => g.GiftDropId == dto.GiftDropId); + + if (gift == null) + { + var available = string.Join(", ", drops.Select(d => d.GiftDropId)); + gift = drops.FirstOrDefault(); + if (gift == null) return NotFound(); + } + + await StorefrontShared.PersistGiftBoxAsync(db, userId.Value, gift, notify: true); + + reward.Consumed = true; + reward.ConsumedGiftDropId = gift.GiftDropId; + reward.ConsumedAt = DateTime.UtcNow; + data.Progress = json!.ToJson(); + await db.SaveChangesAsync(); + return Ok(new OkResponse { Success = true }); + } + + private async Task ReadRewardBodyAsync() + { + var fields = await ReadBodyFieldsAsync(); + if (fields.Count == 0) return null; + + var dto = new GameRewardRequestDTO(); + if (fields.TryGetValue("rewardType", out var rewardTypeRaw)) + { + if (int.TryParse(rewardTypeRaw, out var rewardTypeInt) && Enum.IsDefined(typeof(RewardType), rewardTypeInt)) + dto.RewardType = (RewardType)rewardTypeInt; + else if (Enum.TryParse(rewardTypeRaw, true, out var rewardTypeEnum)) + dto.RewardType = rewardTypeEnum; + } + if (fields.TryGetValue("Message", out var message)) + dto.Message = message; + dto.GiftContext = ReadClaimedContext(fields); + + return dto; + } + + private static GiftContext? ReadClaimedContext(IReadOnlyDictionary fields) + { + if (!fields.TryGetValue("giftContext", out var raw) || string.IsNullOrWhiteSpace(raw)) return null; + + if (int.TryParse(raw, out var numeric) && Enum.IsDefined(typeof(GiftContext), numeric)) + return (GiftContext)numeric; + + return Enum.TryParse(raw.Trim(), true, out var named) ? named : null; + } + + private async Task ReadSelectBodyAsync() + { + var fields = await ReadBodyFieldsAsync(); + if (fields.Count == 0) return null; + + var dto = new GameRewardSelectDTO(); + if (fields.TryGetValue("rewardSelectionId", out var selectionRaw) && long.TryParse(selectionRaw, out var selectionId)) + dto.RewardSelectionId = selectionId; + if (fields.TryGetValue("giftDropId", out var dropRaw) && long.TryParse(dropRaw, out var dropId)) + dto.GiftDropId = dropId; + + return dto; + } + + private async Task> ReadBodyFieldsAsync() + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + try + { + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var raw = (await reader.ReadToEndAsync()).Trim(); + Request.Body.Position = 0; + _rawBody = raw; + if (raw.Length == 0) return fields; + + if (raw.StartsWith('{')) + { + using var doc = System.Text.Json.JsonDocument.Parse(raw); + if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object) + { + foreach (var prop in doc.RootElement.EnumerateObject()) + { + fields[prop.Name] = prop.Value.ValueKind == System.Text.Json.JsonValueKind.String + ? prop.Value.GetString() ?? "" + : prop.Value.ToString(); + } + } + } + else + { + foreach (var pair in raw.Split('&')) + { + var kv = pair.Split('=', 2); + if (kv.Length < 2) continue; + fields[Uri.UnescapeDataString(kv[0])] = Uri.UnescapeDataString(kv[1]); + } + } + } + catch (Exception) + { + } + return fields; + } + + private async Task BuildRewardAsync(long playerId, GameRewardRequestDTO dto, RewardType rewardType) + { + var lastRoom = await MatchmakingUtils.GetLastVisitedRoomNameAsync(db, playerId); + var roomName = lastRoom; + var claimedContext = dto.GiftContext; + if (claimedContext.HasValue + && RroRewardPools.RoomForContext(claimedContext.Value) is { } claimedRoom + && !RroRewardPools.ContextsForRoom(roomName).Values.Contains(claimedContext.Value)) + { + roomName = claimedRoom; + } + + if (!RroRewardPools.IsRewardActivity(roomName) + && await MatchmakingUtils.GetLastRewardActivityRoomNameAsync(db, playerId, ActivityVisitWindow) is { } activityRoom) + { + Trace(playerId, $"lastRoom={lastRoom} is not an activity this server pays; {activityRoom} is the newest activity they were placed into, so that is the activity this completion is answered from."); + roomName = activityRoom; + } + + var paidByRoom = RroRewardPools.ContextsForRoom(roomName); + var paidList = string.Join(", ", paidByRoom.Select(x => x.Key + "=" + (int)x.Value)); + Trace(playerId, "rewardType=" + rewardType + + " claimedContext=" + dto.GiftContext + " lastRoom=" + lastRoom + + " activity=" + roomName + + " isRewardActivity=" + RroRewardPools.IsRewardActivity(roomName) + " roomPays=[" + paidList + "]"); + + if (!RroRewardPools.IsRewardActivity(roomName)) + { + Trace(playerId, "room=" + roomName + " is not an activity this server pays rewards for; paying the token package."); + return BuildReward(rewardType, dto.Message, dto.GiftContext ?? GiftContext.GameRewards_Tokens); + } + + var paid = RroRewardPools.ContextsForRoom(roomName); + var claimed = dto.GiftContext; + GiftContext context; + if (claimed.HasValue && paid.Values.Contains(claimed.Value)) + { + context = claimed.Value; + } + else if (RroRewardPools.TryFloorContext(roomName, out var floor)) + { + context = floor; + } + else + { + return BuildReward(rewardType, dto.Message, GiftContext.GameRewards_Tokens); + } + + if (RroRewardPools.PaysTickets(roomName)) + { + var games = LaserTagGamesToday.AddOrUpdate((playerId, DateOnly.FromDateTime(DateTime.UtcNow)), 1, (_, count) => count + 1); + var tickets = RroRewardPools.LaserTagTicketsPerGame + + (games <= RroRewardPools.LaserTagBonusGamesPerDay ? RroRewardPools.LaserTagFirstGamesBonus : 0); + return TicketReward(rewardType, dto.Message, GiftContext.Lasertag, tickets); + } + + var selection = RroRewardPools.OffersSelection(roomName); + var owned = await LevelUpGiftService.CollectOwnedKeysAsync(db, playerId, context); + + var fromOwnPool = !RroRewardPools.PaysNoItemReward(roomName) + && Random.Shared.NextDouble() <= RroRewardPools.DropChanceFor(context); + var drops = fromOwnPool + ? RroRewardPools.BuildDrops(context, selection, owned) + : new List(); + + var source = context; + + var reacquired = drops.Count == 0; + if (reacquired) + { + foreach (var weaker in RroRewardPools.LowerRanksOf(context)) + { + drops = RroRewardPools.BuildDrops(weaker, selection, owned); + if (drops.Count == 0) continue; + break; + } + } + + if (drops.Count == 0) + { + if (selection) + { + drops = RroRewardPools.BuildFallbackSelection(owned: owned, message: dto.Message); + } + else + { + drops = RroRewardPools.BuildFallbackDrops(selection: false, owned: owned); + } + } + + if (drops.Count == 0) + { + RewardTrace.Error(playerId, $"stage=Fallback activity={roomName} rank={RewardRankOf(context, roomName)} " + + $"context={RewardTrace.Context(context)} pool={DescribeSets(context)} " + + $"EligibleItems={RroRewardPools.Resolve(context).Count} UnownedItems=0 " + + "Reason=neither the activity's pool nor the reacquisition pool has anything left to give; " + + "paying the token package as the last resort."); + return BuildReward(rewardType, dto.Message, context); + } + + foreach (var drop in drops) + drop.GiftDropId = StorefrontShared.NewGiftDropId(); + + var reward = new GameReward + { + RewardSelectionId = StorefrontShared.NewGiftDropId(), + Message = string.IsNullOrWhiteSpace(dto.Message) ? "Game Reward" : dto.Message, + GiftContext = source, + RewardType = rewardType, + GiftDrop1 = drops[0], + GiftDrop2 = drops.Count > 1 ? drops[1] : drops[0], + GiftDrop3 = drops.Count > 2 ? drops[2] : drops[0], + CreatedAt = DateTime.UtcNow + }; + + var eligible = RroRewardPools.Resolve(context); + var unowned = eligible + .Where(item => !owned.Contains(LevelUpRewardPool.OwnershipKey(item.AvatarItemDesc))) + .ToList(); + var boxKind = IsTokenBox(drops[0]) ? "Tokens" : "AvatarItem"; + + RewardTrace.Log(playerId, $"stage=Quest activity={roomName} rank={RewardRankOf(context, roomName)} " + + $"context={RewardTrace.Context(context)} source={RewardTrace.Context(source)} rewardType={rewardType}"); + RewardTrace.Log(playerId, $"stage=RewardPool context={RewardTrace.Context(context)} " + + $"pool={DescribeSets(context)} EligibleItems={eligible.Count} {RewardTrace.Items(eligible)}"); + RewardTrace.Log(playerId, $"stage=UnownedItems context={RewardTrace.Context(context)} UnownedItems={unowned.Count} {RewardTrace.Items(unowned)}"); + RewardTrace.Log(playerId, $"stage=SelectedItem {RewardTrace.Boxes(drops)}"); + RewardTrace.Log(playerId, $"stage=BoxCreated selectionId={reward.RewardSelectionId} " + + $"boxType={boxKind} BoxRewardItem=\"{drops[0].AvatarItemDesc}\""); + RewardTrace.Log(playerId, $"stage=GrantReward giftDropId={drops[0].GiftDropId} " + + $"name=\"{drops[0].FriendlyName}\" item=\"{drops[0].AvatarItemDesc}\" " + + $"consumable=\"{drops[0].ConsumableItemDesc}\" currency={drops[0].CurrencyType} {drops[0].Currency}"); + RewardTrace.Log(playerId, $"stage=FinalReward rewardType={rewardType} context={RewardTrace.Context(source)} " + + $"boxType={boxKind} firstTime={!reacquired}"); + + return reward; + } + + private static readonly ConcurrentDictionary<(long PlayerId, DateOnly Day), int> LaserTagGamesToday = new(); + + private static GameReward TicketReward(RewardType type, string message, GiftContext context, int tickets) + { + var box = new Gift + { + GiftDropId = StorefrontShared.NewGiftDropId(), + FriendlyName = "Laser Tag Tickets", + Tooltip = string.IsNullOrWhiteSpace(message) ? "Tickets for playing!" : message, + CurrencyType = CurrencyType.LaserTagTickets, + Currency = tickets, + Context = context, + Rarity = Rarity.Common + }; + + return new GameReward + { + RewardSelectionId = StorefrontShared.NewGiftDropId(), + Message = string.IsNullOrWhiteSpace(message) ? "Laser Tag Tickets" : message, + GiftContext = context, + RewardType = type, + GiftDrop1 = box, + GiftDrop2 = box, + GiftDrop3 = box, + CreatedAt = DateTime.UtcNow + }; + } + + private static bool IsCompletion(RewardType type) => + type is RewardType.FirstActivityOfDay or RewardType.PostGameActivity or RewardType.PostQuestActivity; + + private static GiftContext? BonusContext(RewardType type) => type switch + { + RewardType.DailyChallengesComplete => GiftContext.All_Daily_Challenges_Complete, + RewardType.WeeklyCompleteButHaveSkin => GiftContext.All_Weekly_Challenge_Complete, + RewardType.Levelup => GiftContext.LevelUp, + _ => null + }; + + private static GameReward BuildReward(RewardType type, string message, GiftContext context) + { + int baseTokens = type switch + { + RewardType.DailyChallengesComplete => 150, + RewardType.WeeklyCompleteButHaveSkin => 300, + RewardType.Levelup => 100, + _ => RroRewardPools.CompletionTokenAmount + }; + + return new GameReward + { + RewardSelectionId = StorefrontShared.NewGiftDropId(), + Message = string.IsNullOrWhiteSpace(message) ? "Tokens" : message, + GiftContext = context, + RewardType = type, + GiftDrop1 = RroRewardPools.TokenGift(context, baseTokens, message), + GiftDrop2 = RroRewardPools.TokenGift(context, baseTokens * 2, message), + GiftDrop3 = RroRewardPools.TokenGift(context, baseTokens * 3, message), + CreatedAt = DateTime.UtcNow + }; + } + + private static async Task PushRewardSelectionAsync(long playerId, GameReward reward) + { + var number = Convert.ToInt64( + RadicadeServer.RecNet.Enums.Messages.NotificationType.GiftPackageRewardSelectionReceived, + System.Globalization.CultureInfo.InvariantCulture) + .ToString(System.Globalization.CultureInfo.InvariantCulture); + + foreach (var id in new[] { RewardSelectionNotificationName, number }) + { + try + { + await RadicadeServer.API.Notifications.Notify.SendNotify(playerId, new RadicadeServer.RecNet.Notifications.Notification + { + Id = id, + Msg = reward + }); + } + catch (Exception) + { + } + } + } + + private static readonly TimeSpan ActivityVisitWindow = TimeSpan.FromHours(6); + + private string? _rawBody; + + private static void Trace(long playerId, string line) => RewardTrace.Log(playerId, line); + + private static string RewardRankOf(GiftContext context, string? roomName) + { + foreach (var (rank, paid) in RroRewardPools.ContextsForRoom(roomName)) + { + if (paid != context) continue; + return string.IsNullOrEmpty(rank) ? "unranked" : rank; + } + + return RroRewardPools.IsDeprecated(context) ? "deprecated" : "not paid by this room"; + } + + private static string DescribeSets(GiftContext context) + { + var sets = RroRewardPools.SetsFor(context); + if (sets.Count == 0) return ""; + + return string.Join(", ", sets.Select(set => + string.IsNullOrEmpty(set.Colour) ? set.Family : $"{set.Family} {set.Colour}")); + } + + private static bool IsTokenBox(Gift box) => + string.IsNullOrWhiteSpace(box.AvatarItemDesc) + && string.IsNullOrWhiteSpace(box.ConsumableItemDesc) + && box.CurrencyType != CurrencyType.Invalid + && box.Currency != 0; + + private const string RewardSelectionNotificationName = "RewardSelectionReceived"; + } +} diff --git a/Server/ProgressionEvents/LevelProgressionMap.cs b/Server/ProgressionEvents/LevelProgressionMap.cs new file mode 100644 index 0000000..e16784a --- /dev/null +++ b/Server/ProgressionEvents/LevelProgressionMap.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Leveling +{ + public class LevelProgressionMap + { + public int Level { get; set; } + public int RequiredXp { get; set; } + public long? GiftDropId { get; set; } + } +} diff --git a/Server/ProgressionEvents/LevelRewardTable.cs b/Server/ProgressionEvents/LevelRewardTable.cs new file mode 100644 index 0000000..f127590 --- /dev/null +++ b/Server/ProgressionEvents/LevelRewardTable.cs @@ -0,0 +1,49 @@ +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.Utils +{ + public static class LevelRewardTable + { + public enum RewardKind + { + None, + Consumable, + Clothing + } + + public const int LastItemLevel = LevelUpGiftService.MaxLevel; + + private static readonly HashSet ConsumableLevels = [1, 3, 5, 7, 9]; + + private static readonly HashSet ThreeStarLevels = + [22, 24, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 37, 38, 39]; + + private static readonly HashSet FourStarLevels = + [31, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]; + + public static bool IsItemLevel(int level) => level >= 1 && level <= LastItemLevel; + + public static RewardKind KindForLevel(int level) + { + if (level < 1 || level > LastItemLevel) return RewardKind.None; + return ConsumableLevels.Contains(level) ? RewardKind.Consumable : RewardKind.Clothing; + } + + public static Rarity RarityForLevel(int level) + { + if (level == 50) return Rarity.Legendary; + if (FourStarLevels.Contains(level)) return Rarity.Epic; + if (ThreeStarLevels.Contains(level)) return Rarity.Rare; + return Rarity.Uncommon; + } + + public static string StarLabel(Rarity rarity) => rarity switch + { + Rarity.Legendary => "5-star", + Rarity.Epic => "4-star", + Rarity.Rare => "3-star", + Rarity.Uncommon => "2-star", + _ => "1-star" + }; + } +} diff --git a/Server/ProgressionEvents/LevelUpGiftService.cs b/Server/ProgressionEvents/LevelUpGiftService.cs new file mode 100644 index 0000000..7cecf73 --- /dev/null +++ b/Server/ProgressionEvents/LevelUpGiftService.cs @@ -0,0 +1,290 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Store; + +namespace RadicadeServer.Utils +{ + public static class LevelUpGiftService + { + public static List ParseMap() + { + var map = FirstTimeUtils.LevelProgressionMap.FromJson>() ?? new List(); + return map.OrderBy(x => x.Level).ToList(); + } + + public const int MaxLevel = 50; + + public static int ComputeLevel(List map, int xp) + { + int cumulative = 0; + int level = 1; + foreach (var entry in map.OrderBy(x => x.Level)) + { + if (entry.Level <= 0) continue; + cumulative += entry.RequiredXp; + if (xp < cumulative) break; + level = entry.Level; + } + return level > MaxLevel ? MaxLevel : level; + } + + public static int ThresholdXpForLevel(List map, int level) + { + int threshold = 0; + foreach (var entry in map.OrderBy(x => x.Level)) + { + if (entry.Level <= 0 || entry.Level > level) continue; + threshold += entry.RequiredXp; + } + return threshold; + } + + public static int RequiredXpForLevel(List map, int level) => + ThresholdXpForLevel(map, level) - ThresholdXpForLevel(map, level - 1); + + public static int DisplayLevel(int level) => level > MaxLevel ? MaxLevel : level; + + public static int XpIntoCurrentLevel(List map, int level, int totalXp) + { + if (level >= MaxLevel) return RequiredXpForLevel(map, MaxLevel); + + int reached = ComputeLevel(map, totalXp); + int from = level < reached ? level : reached; + + int into = totalXp - ThresholdXpForLevel(map, from); + return into < 0 ? 0 : into; + } + + public static int XpIntoCurrentLevel(ProgressionData data) => + XpIntoCurrentLevel(ParseMap(), data.Level, data.XP); + + public const bool LevelUpGiftDropsAreSeeded = false; + + + private static List? _avatarItems; + private static List? _consumables; + + internal static List ClothingCatalogue(string? json = null) + { + if (!string.IsNullOrWhiteSpace(json)) return ParseClothingCatalogue(json); + return _avatarItems ??= ParseClothingCatalogue(DBConfig.DefaultAvatarCatalog); + } + + private static List ParseClothingCatalogue(string json) => + (json.FromJson>() ?? new List()) + .Where(x => x != null && !string.IsNullOrWhiteSpace(x.AvatarItemDesc) && !x.IsBaseAvatarItem) + .OrderBy(x => x.AvatarItemId) + .ThenBy(x => x.AvatarItemDesc, StringComparer.Ordinal) + .ToList(); + + private static List ConsumableCatalogue(string? json = null) + { + if (!string.IsNullOrWhiteSpace(json)) return ParseConsumableCatalogue(json); + return _consumables ??= ParseConsumableCatalogue(DBConfig.DefaultConsumableItems); + } + + private static List ParseConsumableCatalogue(string json) => + (json.FromJson>() ?? new List()) + .Where(x => x != null && !string.IsNullOrWhiteSpace(x.ConsumableItemDesc)) + .OrderBy(x => x.ConsumableItemDesc, StringComparer.Ordinal) + .ToList(); + + public static Gift? BuildItemReward( + int level, + string? avatarCatalogJson = null, + string? consumableCatalogJson = null, + ISet? owned = null, + Random? random = null, + LevelUpRewardPool.Pool? pool = null) + { + if (!LevelRewardTable.IsItemLevel(level)) return null; + + var ownedKeys = owned ?? new HashSet(StringComparer.OrdinalIgnoreCase); + var rng = random ?? Random.Shared; + + return LevelRewardTable.KindForLevel(level) == LevelRewardTable.RewardKind.Consumable + ? BuildConsumableReward(level, consumableCatalogJson, ownedKeys, rng, pool) + : BuildClothingReward(level, avatarCatalogJson, consumableCatalogJson, ownedKeys, rng, pool); + } + + private static Gift? BuildClothingReward( + int level, + string? avatarCatalogJson, + string? consumableCatalogJson, + ISet owned, + Random random, + LevelUpRewardPool.Pool? pool) + { + var rarity = LevelRewardTable.RarityForLevel(level); + var pick = LevelUpRewardPool.PickAvatarItem(ClothingCatalogue(avatarCatalogJson), rarity, owned, random, pool); + if (pick == null) + { + return BuildConsumableReward(level, consumableCatalogJson, owned, random, pool); + } + + var label = LevelRewardTable.StarLabel(rarity); + return new Gift + { + FriendlyName = $"Level {level} Reward - {label} {pick.FriendlyName}", + Tooltip = $"Congrats on getting to Level {level}!", + AvatarItemDesc = pick.AvatarItemDesc, + AvatarItemType = AvatarItemType.Outfit, + Rarity = rarity, + Currency = 0, + Xp = 0 + }; + } + + private static Gift? BuildConsumableReward( + int level, + string? consumableCatalogJson, + ISet owned, + Random random, + LevelUpRewardPool.Pool? pool) + { + var pick = LevelUpRewardPool.PickConsumable(ConsumableCatalogue(consumableCatalogJson), owned, random, pool); + if (pick == null) + { + return null; + } + + var token = ConsumableCatalog.ToClientDesc(pick.ConsumableItemDesc); + if (string.IsNullOrWhiteSpace(token)) return null; + + return new Gift + { + FriendlyName = $"Level {level} Reward - {pick.FriendlyName}", + Tooltip = $"Congrats on getting to Level {level}!", + ConsumableItemDesc = token, + Rarity = pick.Rarity, + Currency = 0, + Xp = 0 + }; + } + + public static Gift? BuildConsumableGift( + string? consumableCatalogJson, ISet owned, Random random, LevelUpRewardPool.Pool? pool = null) + { + var pick = LevelUpRewardPool.PickConsumable(ConsumableCatalogue(consumableCatalogJson), owned, random, pool); + if (pick == null) return null; + + var token = ConsumableCatalog.ToClientDesc(pick.ConsumableItemDesc); + if (string.IsNullOrWhiteSpace(token)) return null; + + return new Gift + { + FriendlyName = pick.FriendlyName, + Tooltip = pick.FriendlyName, + ConsumableItemDesc = token, + Rarity = pick.Rarity, + Currency = 0, + Xp = 0 + }; + } + + public static async Task> CollectOwnedKeysAsync( + RecDBContext db, long playerId, GiftContext? pendingContext = GiftContext.LevelUp) + { + var owned = new HashSet(StringComparer.OrdinalIgnoreCase); + + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (unlocks != null) + { + foreach (var item in unlocks.GetAvatarItems()) + if (!string.IsNullOrWhiteSpace(item?.AvatarItemDesc)) + owned.Add(LevelUpRewardPool.OwnershipKey(item.AvatarItemDesc)); + + foreach (var item in unlocks.GetConsumables()) + if (!string.IsNullOrWhiteSpace(item?.ConsumableItemDesc)) + owned.Add(LevelUpRewardPool.OwnershipKey(item.ConsumableItemDesc)); + } + + var gifts = await db.Gifts.FirstOrDefaultAsync(x => x.AccountId == playerId); + foreach (var pending in StorefrontShared.ParseGiftList(gifts?.Data)) + { + if (pending == null) continue; + if (pendingContext.HasValue && pending.Context != pendingContext.Value) continue; + Remember(owned, pending); + } + + return owned; + } + + private static void Remember(HashSet owned, Gift gift) + { + if (!string.IsNullOrWhiteSpace(gift.AvatarItemDesc)) + owned.Add(LevelUpRewardPool.OwnershipKey(gift.AvatarItemDesc)); + if (!string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)) + owned.Add(LevelUpRewardPool.OwnershipKey(gift.ConsumableItemDesc)); + } + + public static async Task> DeliverLevelUpGiftsAsync(RecDBContext db, long playerId, bool recomputeFromXp, int? fromLevel = null, bool notify = true) + { + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + if (progression == null) return new List(); + + var map = ParseMap(); + int oldLevel = fromLevel ?? progression.Level; + int target = progression.Level; + if (recomputeFromXp) + { + int computed = ComputeLevel(map, progression.XP); + if (computed > target) target = computed; + } + if (target <= oldLevel) return new List(); + + var json = progression.GetProgressionJson() ?? new ProgressionJson(); + var delivered = json.DeliveredLevelUpGiftLevels ?? new List(); + + var owned = await CollectOwnedKeysAsync(db, playerId); + var random = Random.Shared; + + var toPersist = new List(); + foreach (var entry in map.OrderBy(e => e.Level)) + { + if (entry.Level <= oldLevel) continue; + if (entry.Level > target) break; + if (delivered.Contains(entry.Level)) continue; + + var gift = LevelRewardTable.IsItemLevel(entry.Level) + ? BuildItemReward(entry.Level, owned: owned, random: random) + : null; + if (gift == null) continue; + + Remember(owned, gift); + + gift.GiftDropId = StorefrontShared.NewGiftDropId(); + gift.Context = GiftContext.LevelUp; + gift.FromPlayerId = 0; + + toPersist.Add(gift); + delivered.Add(entry.Level); + } + + if (toPersist.Count == 0) return new List(); + + json.DeliveredLevelUpGiftLevels = delivered; + progression.Progress = json.ToJson(); + progression.Level = target; + + foreach (var gift in toPersist) + { + await StorefrontShared.PersistGiftBoxAsync(db, playerId, gift, notify: false); + } + await db.SaveChangesAsync(); + if (notify) + { + foreach (var gift in toPersist) + await StorefrontShared.SendGiftPackageNotificationAsync(playerId, gift); + } + return toPersist; + } + } +} diff --git a/Server/ProgressionEvents/LevelUpRewardPool.cs b/Server/ProgressionEvents/LevelUpRewardPool.cs new file mode 100644 index 0000000..5d8004d --- /dev/null +++ b/Server/ProgressionEvents/LevelUpRewardPool.cs @@ -0,0 +1,166 @@ +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)]; + } + } +} diff --git a/Server/ProgressionEvents/MyProgress.cs b/Server/ProgressionEvents/MyProgress.cs new file mode 100644 index 0000000..0ec6ace --- /dev/null +++ b/Server/ProgressionEvents/MyProgress.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Progression.Objectives; + +public class MyProgress +{ + public List Objectives { get; set; } = new(); + + public List ObjectiveGroups { get; set; } = new(); +} \ No newline at end of file diff --git a/Server/ProgressionEvents/Objective.cs b/Server/ProgressionEvents/Objective.cs new file mode 100644 index 0000000..1c56d21 --- /dev/null +++ b/Server/ProgressionEvents/Objective.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums.Challenges; + +namespace RadicadeServer.RecNet.Progression.Objectives +{ + public class Objective + { + public ObjectiveTypes type { get; set; } = ObjectiveTypes.ArenaWins; + public int score { get; set; } = 3; + public int xp { get; set; } = 10; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? descriptionOverride { get; set; } = null; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? tooltipOverride { get; set; } = null; + } +} diff --git a/Server/ProgressionEvents/ObjectiveGroupProgress.cs b/Server/ProgressionEvents/ObjectiveGroupProgress.cs new file mode 100644 index 0000000..4ce5d81 --- /dev/null +++ b/Server/ProgressionEvents/ObjectiveGroupProgress.cs @@ -0,0 +1,17 @@ +namespace RadicadeServer.RecNet.Progression.Objectives; + +public class ObjectiveGroupProgress +{ + public int Group { get; set; } = 0; + + public bool IsCompleted { get; set; } = false; + + public DateTime ClearedAt { get; set; } = DateTime.Now; + + public bool RequiresCompleteOnServer { get; set; } = false; + + public static ObjectiveGroupProgress Empty(int group) + { + return new ObjectiveGroupProgress { Group = group }; + } +} \ No newline at end of file diff --git a/Server/ProgressionEvents/ObjectiveGroupRequest.cs b/Server/ProgressionEvents/ObjectiveGroupRequest.cs new file mode 100644 index 0000000..89a8766 --- /dev/null +++ b/Server/ProgressionEvents/ObjectiveGroupRequest.cs @@ -0,0 +1,6 @@ +namespace RadicadeServer.RecNet.Progression.Objectives; + +public class ObjectiveGroupRequest +{ + public int Group { get; set; } +} \ No newline at end of file diff --git a/Server/ProgressionEvents/ObjectiveProgress.cs b/Server/ProgressionEvents/ObjectiveProgress.cs new file mode 100644 index 0000000..c131d40 --- /dev/null +++ b/Server/ProgressionEvents/ObjectiveProgress.cs @@ -0,0 +1,17 @@ +namespace RadicadeServer.RecNet.Progression.Objectives; + +public class ObjectiveProgress +{ + public int Index { get; set; } = 0; + + public int Group { get; set; } = 0; + + public float Progress { get; set; } = 0; + + public float VisualProgress { get; set; } = 0; + + public bool IsCompleted { get; set; } = false; + public bool IsRewarded { get; set; } = false; + + public bool HasClaimedReward { get; set; } = false; +} \ No newline at end of file diff --git a/Server/ProgressionEvents/ObjectivesV1.cs b/Server/ProgressionEvents/ObjectivesV1.cs new file mode 100644 index 0000000..9a91ad6 --- /dev/null +++ b/Server/ProgressionEvents/ObjectivesV1.cs @@ -0,0 +1,148 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Progression.Objectives; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.Progression.Objectives.V1 +{ + [Route("api/objectives/v1")] + [Route("econ/api/objectives/v1")] + public class ObjectivesV1 : Controller + { + RecDBContext db; + public ObjectivesV1(RecDBContext context) + { + this.db = context; + } + + [HttpGet("myprogress"), Authorize] + public async Task> GetMyProgress() + { + var userId = User.GetUserId(); + + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId); + if (data == null) return StatusCode(500); + + var deserialized = data.GetObjectiveProgress(); + if (deserialized == null) return StatusCode(500); + + return Ok(deserialized); + } + + [HttpPost("cleargroup"), Authorize] + public async Task> ClearObjectiveGroup([FromBody] ObjectiveGroupRequest? dto) + { + if (dto == null) return BadRequest(); + + var userId = User.GetUserId(); + + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId); + if (data == null) return StatusCode(500); + + ProgressionJson? progression = data.Progress.FromJson(); + if (progression == null) return StatusCode(500); + + progression.ObjectiveProgress.ObjectiveGroups.RemoveAll(x => x.Group == dto.Group); + progression.ObjectiveProgress.Objectives.RemoveAll(x => x.Group == dto.Group); + + data.Progress = progression.ToJson(); + + await db.SaveChangesAsync(); + + return Ok(ObjectiveGroupProgress.Empty(dto.Group)); + } + + [HttpPost("completegroup"), Authorize] + public async Task> CompleteObjectiveGroup([FromBody] ObjectiveGroupRequest? dto) + { + if (dto == null) return BadRequest(); + + var userId = User.GetUserId(); + + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId); + if (data == null) return StatusCode(500); + + ProgressionJson? progression = data.Progress.FromJson(); + if (progression == null) return StatusCode(500); + + var groupIndex = progression.ObjectiveProgress.ObjectiveGroups.FindIndex(x => x.Group == dto.Group); + if (groupIndex >= 0) + { + progression.ObjectiveProgress.ObjectiveGroups[groupIndex].IsCompleted = true; + progression.ObjectiveProgress.ObjectiveGroups[groupIndex].RequiresCompleteOnServer = false; + } + + var groupJson = progression.ObjectiveProgress.ObjectiveGroups[groupIndex].ToString(); + + data.Progress = progression.ToJson(); + + await db.SaveChangesAsync(); + + return Ok(groupJson); + } + + [HttpPost("updateobjective"), Authorize] + public async Task> UpdateObjective([FromBody] ObjectiveProgress? progress) + { + MainConfig? config = await db.Config.FirstOrDefaultAsync(); + if (config == null) return StatusCode(500); + + if (progress == null) return BadRequest(); + progress.IsRewarded = true; + progress.VisualProgress = progress.Progress; + + + + + + try + { + var objectives = config.DailyObjectives.FromJson(); + if (objectives != null + && progress.Group >= 0 && progress.Group < objectives.Length + && progress.Index >= 0 && progress.Index < objectives[progress.Group].Length) + { + var objective = objectives[progress.Group][progress.Index]; + if (objective == null) return BadRequest(); + + if (!float.IsFinite(progress.Progress) || progress.Progress < 0) + progress.Progress = 0; + if (progress.Progress > objective.score) + progress.Progress = objective.score; + + progress.IsCompleted = progress.Progress >= objective.score; + progress.VisualProgress = progress.Progress; + } + } catch + { + progress.IsCompleted = false; + } + + var userId = User.GetUserId(); + + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId); + if (data == null) return StatusCode(500); + + ProgressionJson? progression = data.Progress.FromJson(); + if (progression == null) return StatusCode(500); + + var existingIndex = progression.ObjectiveProgress.Objectives.FindIndex(x => x.Index == progress.Index); + if (existingIndex >= 0) + progression.ObjectiveProgress.Objectives.RemoveAt(existingIndex); + + progression.ObjectiveProgress.Objectives.Add(progress); + + data.Progress = progression.ToJson(); + + await db.SaveChangesAsync(); + + return Ok(Success.Ok); + } + } +} diff --git a/Server/ProgressionEvents/PlayerProgressionLevelUpdateNotification.cs b/Server/ProgressionEvents/PlayerProgressionLevelUpdateNotification.cs new file mode 100644 index 0000000..0f47d73 --- /dev/null +++ b/Server/ProgressionEvents/PlayerProgressionLevelUpdateNotification.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; +using RadicadeServer.Utils; + +namespace RadicadeServer.RecNet.Progression +{ + public class PlayerProgressionLevelUpdateNotification + { + public PlayerProgressionLevelUpdateNotification() { } + + public PlayerProgressionLevelUpdateNotification(ProgressionData data) + { + Id = data.PlayerId; + PlayerId = data.PlayerId; + Level = LevelUpGiftService.DisplayLevel(data.Level); + XP = LevelUpGiftService.XpIntoCurrentLevel(data); + } + + public long Id { get; set; } + public long PlayerId { get; set; } + public int Level { get; set; } + public int XP { get; set; } + + [JsonPropertyName("id")] public long IdLower { get { return Id; } } + [JsonPropertyName("playerId")] public long PlayerIdLower { get { return PlayerId; } } + [JsonPropertyName("level")] public int LevelLower { get { return Level; } } + [JsonPropertyName("xp")] public int XpLower { get { return XP; } } + } +} diff --git a/Server/ProgressionEvents/ProgressionAwardService.cs b/Server/ProgressionEvents/ProgressionAwardService.cs new file mode 100644 index 0000000..102c4fe --- /dev/null +++ b/Server/ProgressionEvents/ProgressionAwardService.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Progression; + +namespace RadicadeServer.Utils +{ + public static class ProgressionAwardService + { + public const int XpPerGiftBoxOpened = 1; + + public const int XpPerGameCompleted = 2; + + public static async Task AwardXpAsync(RecDBContext db, long playerId, int xp, string reason) + { + if (playerId <= 0 || xp <= 0) return 0; + + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + if (progression == null) + { + progression = new ProgressionData { PlayerId = playerId }; + await db.Progression.AddAsync(progression); + } + + int granted = (int)Math.Min(xp, int.MaxValue); + if (granted <= 0) + { + await db.SaveChangesAsync(); + return 0; + } + + int oldLevel = progression.Level; + progression.XP = (int)Math.Min(progression.XP + granted, int.MaxValue); + await db.SaveChangesAsync(); + + int computedLevel = LevelUpGiftService.ComputeLevel(LevelUpGiftService.ParseMap(), progression.XP); + if (computedLevel > progression.Level) progression.Level = computedLevel; + + await LevelUpGiftService.DeliverLevelUpGiftsAsync(db, playerId, + recomputeFromXp: true, fromLevel: oldLevel, notify: true); + await db.SaveChangesAsync(); + + await RoomProgressionService.SendProgressionLevelUpdateAsync(db, playerId, progression); + + return granted; + } + } +} diff --git a/Server/ProgressionEvents/ProgressionData.cs b/Server/ProgressionEvents/ProgressionData.cs new file mode 100644 index 0000000..595b882 --- /dev/null +++ b/Server/ProgressionEvents/ProgressionData.cs @@ -0,0 +1,106 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Progression.Challenges; +using RadicadeServer.RecNet.Progression.Objectives; +using RadicadeServer.RecNet.Storefront; + +namespace RadicadeServer.RecNet.Progression +{ + public class ProgressionJson + { + public MyProgress ObjectiveProgress { get; set; } = new(); + public List ChallengeProgress { get; set; } = new(); + + public DateTime? LastXpEarnedAt { get; set; } + public int XpEarnedToday { get; set; } + public DateTime? XpEarnedTodayDate { get; set; } + + public DateTime? XpDailyCapReachedAt { get; set; } + + public long? RoomXpRoomId { get; set; } + public int RoomXpEarned { get; set; } + public DateTime? RoomXpLastGrantAt { get; set; } + + public List DeliveredLevelUpGiftLevels { get; set; } = new(); + + public List PendingGameRewards { get; set; } = new(); + + public RadicadeServer.Utils.RewardDayLedger? RewardDay { get; set; } + } + + public class ProgressionData + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long PlayerId { get; set; } + + public int XP { get; set; } = 0; + public int Level { get; set; } = 1; + + public string Progress { get; set; } = new ProgressionJson().ToJson(); + + + public ChallengeProgress? GetChallengeProgress() + { + return Progress.FromJson()?.ChallengeProgress.LastOrDefault(); + } + + public void SetChallengeProgress(ChallengeProgress progress) + { + var deserialized = Progress.FromJson(); + if (deserialized == null) return; + + deserialized.ChallengeProgress.Add(progress); + Progress = deserialized.ToJson(); + } + + public List? GetChallengeProgresses() + { + return Progress.FromJson()?.ChallengeProgress; + } + + + public MyProgress? GetObjectiveProgress() + { + return Progress.FromJson()?.ObjectiveProgress; + } + + public void AddObjectivesProgress(ObjectiveProgress progress) + { + var deserialized = Progress.FromJson(); + if (deserialized == null) return; + + var existingProgress = deserialized.ObjectiveProgress.Objectives.Find(x => x.Group == progress.Group && x.Index == progress.Index); + + if (existingProgress == null) + deserialized.ObjectiveProgress.Objectives.Add(progress); + else + existingProgress = progress; + + Progress = deserialized.ToJson(); + } + + public ProgressionJson? GetProgressionJson() + { + return Progress.FromJson(); + } + + public void RecordXpGrant(int amount) + { + if (amount <= 0) return; + + var json = GetProgressionJson() ?? new ProgressionJson(); + var today = DateTime.UtcNow.Date; + if (json.XpEarnedTodayDate != today) + { + json.XpEarnedTodayDate = today; + json.XpEarnedToday = 0; + } + + json.XpEarnedToday += amount; + json.LastXpEarnedAt = DateTime.UtcNow; + Progress = json.ToJson(); + } + } +} \ No newline at end of file diff --git a/Server/ProgressionEvents/RewardDay.cs b/Server/ProgressionEvents/RewardDay.cs new file mode 100644 index 0000000..eb60c3c --- /dev/null +++ b/Server/ProgressionEvents/RewardDay.cs @@ -0,0 +1,67 @@ +using System.Globalization; +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.Utils +{ + public sealed class RewardDayLedger + { + public string Day { get; set; } = string.Empty; + + public bool FirstActivityPaid { get; set; } + + public bool DailyChallengePaid { get; set; } + + public int ActivityBoxes { get; set; } + } + + public static class RewardDay + { + public const int ActivityBoxesPerDay = 12; + + public const int BoxesPerDay = 1 + 1 + ActivityBoxesPerDay; + + public static string DayKey(DateTime nowUtc) => + nowUtc.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + + public static RewardDayLedger For(RecNet.Progression.ProgressionJson json, DateTime nowUtc) + { + var day = DayKey(nowUtc); + var ledger = json.RewardDay; + if (ledger == null || !string.Equals(ledger.Day, day, StringComparison.Ordinal)) + { + ledger = new RewardDayLedger { Day = day }; + json.RewardDay = ledger; + } + return ledger; + } + + public static RewardType CompletionType(RewardDayLedger ledger, RewardType claimed) + { + if (claimed == RewardType.PostQuestActivity) return RewardType.PostQuestActivity; + return ledger.FirstActivityPaid ? RewardType.PostGameActivity : RewardType.FirstActivityOfDay; + } + + public static bool TryTakeFirstActivity(RewardDayLedger ledger) + { + if (ledger.FirstActivityPaid) return false; + ledger.FirstActivityPaid = true; + return true; + } + + public static bool TryTakeActivityBox(RewardDayLedger ledger) + { + if (ledger.ActivityBoxes >= ActivityBoxesPerDay) return false; + ledger.ActivityBoxes++; + return true; + } + + public static bool TryTakeDailyChallengeBox(RewardDayLedger ledger) + { + if (ledger.DailyChallengePaid) return false; + ledger.DailyChallengePaid = true; + return true; + } + + public static RewardDayLedger NewDay(DateTime nowUtc) => new() { Day = DayKey(nowUtc) }; + } +} diff --git a/Server/ProgressionEvents/RewardTrace.cs b/Server/ProgressionEvents/RewardTrace.cs new file mode 100644 index 0000000..beb201d --- /dev/null +++ b/Server/ProgressionEvents/RewardTrace.cs @@ -0,0 +1,75 @@ +using RadicadeServer.DB; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.Utils +{ + public static class RewardTrace + { + public const string Prefix = "[QuestReward]"; + + public const string ErrorPrefix = "[ERROR][QuestReward]"; + + private const string FileName = "game-rewards.log"; + + public static void Log(long playerId, string line) => Write(Prefix, playerId, line); + + public static void Error(long playerId, string line) => Write(ErrorPrefix, playerId, line); + + public static void LogAndError(long playerId, string line) + { + Log(playerId, line); + Error(playerId, line); + } + + private static void Write(string tag, long playerId, string line) + { + var text = $"{tag} player={playerId} {line}"; + + try + { + System.IO.File.AppendAllText( + System.IO.Path.Join(DBConfig.PersistentLogs, FileName), + $"[{DateTime.UtcNow:O}] {text}{Environment.NewLine}"); + } + catch + { + } + } + + public static string Context(GiftContext context) => $"{context} ({(int)context})"; + + public static string Box(Gift? box) + { + if (box == null) return ""; + + var item = string.IsNullOrWhiteSpace(box.AvatarItemDesc) ? "-" : box.AvatarItemDesc; + if (item.Length > 120) item = item[..120] + "..."; + + var kind = !string.IsNullOrWhiteSpace(box.AvatarItemDesc) ? "AvatarItem" + : !string.IsNullOrWhiteSpace(box.ConsumableItemDesc) ? "Consumable" + : box.CurrencyType != CurrencyType.Invalid && box.Currency != 0 ? "Tokens" + : "Empty"; + + return $"{{id={box.GiftDropId} type={kind} name=\"{box.FriendlyName}\" item=\"{item}\" " + + $"consumable=\"{box.ConsumableItemDesc}\" currency={box.CurrencyType} {box.Currency} " + + $"context={Context(box.Context)}}}"; + } + + public static string Boxes(IEnumerable boxes) + { + var list = boxes.ToList(); + return list.Count == 0 ? "[]" : "[" + string.Join(", ", list.Select(Box)) + "]"; + } + + public static string Items(IEnumerable items, int limit = 12) + { + var list = items.ToList(); + if (list.Count == 0) return "[]"; + + var names = list.Take(limit).Select(x => $"\"{x.FriendlyName}\""); + var more = list.Count > limit ? $", ... +{list.Count - limit} more" : string.Empty; + return $"[{string.Join(", ", names)}{more}]"; + } + } +} diff --git a/Server/ProgressionEvents/SubscriptionSeasonsAPI.cs b/Server/ProgressionEvents/SubscriptionSeasonsAPI.cs new file mode 100644 index 0000000..efe146f --- /dev/null +++ b/Server/ProgressionEvents/SubscriptionSeasonsAPI.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.API.Progression.SubscriptionSeasons.V1 +{ + [Route("api/subscriptionseasons/v1")] + [Route("econ/api/subscriptionseasons/v1")] + public class SubscriptionSeasonsAPI : Controller + { + [HttpGet("seasons/current")] + public IActionResult GetCurrentSeason() => NotFound(); + } +} \ No newline at end of file diff --git a/Server/ProgressionEvents/WeeklyChallengeState.cs b/Server/ProgressionEvents/WeeklyChallengeState.cs new file mode 100644 index 0000000..1aaa9d8 --- /dev/null +++ b/Server/ProgressionEvents/WeeklyChallengeState.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.RecNet.Avatar; + +namespace RadicadeServer.RecNet.Progression.Challenges; + +[PrimaryKey(nameof(PlayerId), nameof(ChallengeMapId))] +public class WeeklyChallengeState +{ + public long PlayerId { get; set; } + public int ChallengeMapId { get; set; } + public string MapJson { get; set; } = ""; + public long? RewardGiftDropId { get; set; } +} + +public class WeeklyChallengeMap +{ + public bool Enabled { get; set; } = true; + public string Revision { get; set; } = ""; + public int FallbackTokens { get; set; } = 300; + public int ChallengeMapId { get; set; } + public string ChallengeThemeString { get; set; } = "Weekly Challenges"; + public List Challenges { get; set; } = []; + public Gift Gift { get; set; } = new(); + public string FallbackGiftName { get; set; } = "300 Tokens"; + public DateTime StartAt { get; set; } + public DateTime EndAt { get; set; } + public DateTime ServerTime { get; set; } + public bool CompletedRequired { get; set; } +} + +public class WeeklyChallenge +{ + public int ChallengeId { get; set; } + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public string Tooltip { get; set; } = ""; + public string Config { get; set; } = ""; + public bool Complete { get; set; } +} diff --git a/Server/ProgressionEvents/WeeklyChallenges.cs b/Server/ProgressionEvents/WeeklyChallenges.cs new file mode 100644 index 0000000..eb0f549 --- /dev/null +++ b/Server/ProgressionEvents/WeeklyChallenges.cs @@ -0,0 +1,210 @@ +using System.Data; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Progression.Challenges; + +namespace RadicadeServer.Utils; + +public static class WeeklyChallenges +{ + public const int Required = 3; + private static readonly DateTime Epoch = new(2026, 10, 7, 0, 0, 0, DateTimeKind.Utc); + private static readonly SemaphoreSlim[] Gates = Enumerable.Range(0, 128).Select(_ => new SemaphoreSlim(1)).ToArray(); + + + + public static WeeklyChallengeMap CreateMap(DateTime now) + { + var week = (int)Math.Floor((now.ToUniversalTime() - Epoch).TotalDays / 7); + var start = Epoch.AddDays(week * 7); + return new WeeklyChallengeMap + { + ChallengeMapId = 100000 + week, + StartAt = start, EndAt = start.AddDays(7), ServerTime = now, + Enabled = false + }; + } + + public static WeeklyChallengeMap MergeProgress(WeeklyChallengeMap definition, WeeklyChallengeState? state) + { + var map = JsonSerializer.Deserialize(JsonSerializer.Serialize(definition))!; + if (state == null) return map; + var previous = JsonSerializer.Deserialize(state.MapJson)!; + foreach (var goal in map.Challenges) + { + var saved = previous.Challenges.FirstOrDefault(x => x.ChallengeId == goal.ChallengeId); + if (saved != null && TryMergeConfig(goal.Config, saved.Config, saved.Complete, out var config)) + { + goal.Config = config; + goal.Complete = saved.Complete; + } + } + map.CompletedRequired = state.RewardGiftDropId.HasValue || map.Challenges.Count(x => x.Complete) >= Required; + return map; + } + + + + public static bool TryMergeConfig(string original, string submitted, bool complete, out string merged) + { + merged = original; + if (submitted.Length > 16384) return false; + try + { + var expected = JsonNode.Parse(original)!.AsObject(); + var actual = JsonNode.Parse(submitted, documentOptions: new JsonDocumentOptions { MaxDepth = 16 })!.AsObject(); + if (!ValidateNode(expected, actual) || (actual["c"]?.GetValue() ?? false) != complete) return false; + merged = actual.ToJsonString(); + return true; + } + catch (Exception ex) when (ex is JsonException or InvalidOperationException or FormatException or NullReferenceException) + { return false; } + } + + private static bool ValidateNode(JsonObject expected, JsonObject actual) + { + foreach (var p in actual) + { + if (p.Key == "c") { _ = p.Value!.GetValue(); continue; } + if (p.Key == "ipc") { _ = p.Value!.GetValue(); continue; } + if (p.Key is "in" or "ex") + { + if (p.Value?.GetValue() != true) return false; + continue; + } + if (p.Key == "wc" && p.Value is JsonArray { Count: 0 }) continue; + if (!expected.ContainsKey(p.Key)) return false; + } + foreach (var p in expected) + { + if (p.Key is "c" or "ipc" or "in" or "ex" or "wc") continue; + if (p.Key == "rc") + { + if (p.Value is not JsonArray left || actual[p.Key] is not JsonArray right || left.Count != right.Count) return false; + for (var i = 0; i < left.Count; i++) + if (!ValidateNode(left[i]!.AsObject(), right[i]!.AsObject())) return false; + } + else if (!JsonNode.DeepEquals(p.Value, actual[p.Key])) return false; + } + return true; + } + + public static async Task GetAsync(RecDBContext db, long playerId, DateTime now) + { + var current = await GetDefinitionAsync(db, now); + var state = await db.WeeklyChallenges.AsNoTracking().SingleOrDefaultAsync(x => x.PlayerId == playerId && x.ChallengeMapId == current.ChallengeMapId); + var map = MergeProgress(current, state); + if (!map.Enabled) { map.Challenges.Clear(); map.Gift = new(); } + map.ServerTime = now; + return map; + } + + public static string ScheduleKey(int mapId) => $"WeeklyChallengeMap:{mapId}"; + + public static async Task GetDefinitionAsync(RecDBContext db, DateTime now) + { + var defaults = CreateMap(now); + var key = ScheduleKey(defaults.ChallengeMapId); + var scheduled = await db.ServerConfig.AsNoTracking().SingleOrDefaultAsync(x => x.Key == key); + var map = scheduled == null ? defaults : JsonSerializer.Deserialize(scheduled.Value)!; + map.ServerTime = now; + return map; + } + + public static bool IsSupportedDefinition(string config) + { + try + { + var node = JsonNode.Parse(config)!.AsObject(); + if (node["ct"]?.GetValue() != 0 || node["rc"] is not JsonArray { Count: 1 } rc) return false; + var predicate = rc[0]!.AsObject(); + if (predicate["ct"]?.GetValue() != 6 || predicate["vs"] is not JsonArray { Count: 1 } values) return false; + var eventId = values[0]!.GetValue(); + if (eventId is not (2 or 4 or 5 or 8 or 10)) return false; + var canonical = JsonSerializer.Serialize(new { ct = 0, rc = new[] { new { ct = 6, vs = new[] { eventId } } } }); + return TryMergeConfig(canonical, config, false, out _) + && !(predicate["c"]?.GetValue() ?? false); + } + catch (Exception ex) when (ex is JsonException or InvalidOperationException or FormatException or NullReferenceException) + { return false; } + } + + public static async Task UpdateAsync(RecDBContext db, long playerId, int mapId, int challengeId, + string config, bool complete, DateTime now) + { + var current = await GetDefinitionAsync(db, now); + if (mapId != current.ChallengeMapId) return "stale_challenge_map"; + var gate = Gates[(int)((ulong)playerId % (uint)Gates.Length)]; + await gate.WaitAsync(); + Gift? notification = null; + try + { + var error = await db.Database.CreateExecutionStrategy().ExecuteAsync(async () => + { + db.ChangeTracker.Clear(); + notification = null; + await using var tx = db.Database.IsSqlServer() + ? await db.Database.BeginTransactionAsync(IsolationLevel.Serializable) : null; + + var scheduleKey = ScheduleKey(mapId); + var scheduleQuery = db.Database.IsSqlServer() + ? db.ServerConfig.FromSqlInterpolated($"SELECT * FROM [ServerConfig] WITH (UPDLOCK, HOLDLOCK) WHERE [Key] = {scheduleKey}") + : db.ServerConfig.Where(x => x.Key == scheduleKey); + var schedule = await scheduleQuery.AsNoTracking().SingleOrDefaultAsync(); + if (schedule == null) return "weekly_not_published"; + current = JsonSerializer.Deserialize(schedule.Value)!; + if (!current.Enabled) return "weekly_not_published"; + var query = db.Database.IsSqlServer() + ? db.WeeklyChallenges.FromSqlInterpolated($"SELECT * FROM [WeeklyChallenges] WITH (UPDLOCK, HOLDLOCK) WHERE [PlayerId] = {playerId} AND [ChallengeMapId] = {mapId}") + : db.WeeklyChallenges.Where(x => x.PlayerId == playerId && x.ChallengeMapId == mapId); + var state = await query.SingleOrDefaultAsync(); + var map = MergeProgress(current, state); + var challenge = map.Challenges.SingleOrDefault(x => x.ChallengeId == challengeId); + if (challenge == null) return "unknown_challenge"; + if (!TryMergeConfig(challenge.Config, config, complete, out var merged)) return "invalid_challenge_config"; + if (!challenge.Complete) + { + challenge.Config = merged; + challenge.Complete = complete; + } + if (state == null) + { + state = new WeeklyChallengeState { PlayerId = playerId, ChallengeMapId = mapId }; + db.WeeklyChallenges.Add(state); + } + map.CompletedRequired = state.RewardGiftDropId.HasValue || map.Challenges.Count(x => x.Complete) >= Required; + state.MapJson = JsonSerializer.Serialize(map); + if (map.CompletedRequired && !state.RewardGiftDropId.HasValue) + { + var gift = map.Gift.Clone(); + var unlocks = await db.Unlocks.SingleOrDefaultAsync(x => x.AccountId == playerId); + var pending = await db.Gifts.SingleOrDefaultAsync(x => x.AccountId == playerId); + var pendingGifts = StorefrontShared.ParseGiftList(pending?.Data); + var owned = !string.IsNullOrWhiteSpace(gift.AvatarItemDesc) + ? unlocks?.GetAvatarItems().Any(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc) == LevelUpRewardPool.OwnershipKey(gift.AvatarItemDesc)) == true + || pendingGifts.Any(x => !string.IsNullOrWhiteSpace(x.AvatarItemDesc) && LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc) == LevelUpRewardPool.OwnershipKey(gift.AvatarItemDesc)) + : unlocks?.GetEquipment()?.Any(x => x.PrefabName == gift.EquipmentPrefabName && x.ModificationGuid == gift.EquipmentModificationGuid) == true + || pendingGifts.Any(x => x.EquipmentPrefabName == gift.EquipmentPrefabName && x.EquipmentModificationGuid == gift.EquipmentModificationGuid); + if (owned) gift = RroRewardPools.TokenGift(GiftContext.All_Weekly_Challenge_Complete, map.FallbackTokens, "Weekly Challenge Complete!"); + gift.GiftDropId = StorefrontShared.NewGiftDropId(); + state.RewardGiftDropId = gift.GiftDropId; + await StorefrontShared.PersistGiftBoxAsync(db, playerId, gift, notify: false); + notification = gift; + } + else await db.SaveChangesAsync(); + if (tx != null) await tx.CommitAsync(); + return (string?)null; + }); + if (error == null && notification != null) + await StorefrontShared.SendGiftPackageNotificationAsync(playerId, notification); + return error; + } + finally { gate.Release(); } + } +} diff --git a/Server/Promotions/InfluencerPartnerProgramAPI.cs b/Server/Promotions/InfluencerPartnerProgramAPI.cs new file mode 100644 index 0000000..71ef4c0 --- /dev/null +++ b/Server/Promotions/InfluencerPartnerProgramAPI.cs @@ -0,0 +1,169 @@ +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.API.InfluencerPartnerProgram +{ + [Route("api/influencerpartnerprogram")] + [Route("econ/api/influencerpartnerprogram")] + public class InfluencerPartnerProgramAPI : Controller + { + [HttpGet("influencers")] + [HttpGet("v1/influencers")] + public IActionResult GetInfluencers([FromQuery] int? take) + { + return Ok(new + { + ContinuationToken = (string?)null, + InfluencerIds = DBConfig.TestInfluencerAccountIds.ToArray(), + }); + } + + [HttpGet("myinfluencer")] + public IActionResult GetMyInfluencer() + { + var accountId = User.GetUserId() ?? 0; + return Ok(DBConfig.TestInfluencerAccountIds.Contains(accountId) ? accountId : 0L); + } + + [HttpGet("influencer")] + public IActionResult GetInfluencer([FromQuery] long accountId) => + DBConfig.TestInfluencerAccountIds.Contains(accountId) + ? Ok(accountId) + : NotFound(); + + [HttpGet("isinfluencer")] + public IActionResult IsInfluencer([FromQuery] long accountId) => + Ok(DBConfig.TestInfluencerAccountIds.Contains(accountId)); + + [HttpGet("code/{accountId:long}")] + [HttpGet("v1/code/{accountId:long}")] + public IActionResult GetInfluencerCode(long accountId) + { + if (!DBConfig.TestInfluencerAccountIds.Contains(accountId)) + return NotFound(); + return Ok(new { AccountId = accountId, Code = "COACH" }); + } + + [HttpPost("redeem")] + [HttpPost("v1/redeem")] + [HttpPost("useCode")] + public async Task RedeemCode() + { + var fields = await ReadRequestFieldsAsync(); + var request = new InfluencerCodeRequest + { + Code = FirstField(fields, "Code", "code", "InfluencerCode", "influencerCode") + }; + + if (!string.Equals(request.Code?.Trim(), "COACH", StringComparison.OrdinalIgnoreCase)) + return BadRequest(new { Success = false, Message = "Invalid code." }); + return Ok(new { Success = true, Code = "COACH", InfluencerAccountId = 1L }); + } + + [HttpGet("validate")] + public IActionResult ValidateCode([FromQuery] string? code) => + string.Equals(code?.Trim(), "COACH", StringComparison.OrdinalIgnoreCase) + ? Ok(new { Success = true, Code = "COACH", InfluencerAccountId = 1L }) + : BadRequest(new { Success = false, Message = "Invalid code." }); + + [HttpPost("support")] + [HttpPost("v1/support")] + public IActionResult SupportCoach() => Ok(new + { + Success = true, + Supported = true, + InfluencerAccountId = 1L, + Code = "COACH" + }); + + [HttpPost("remove")] + public async Task RemoveInfluencer() + { + var fields = await ReadRequestFieldsAsync(); + var rawAccountId = FirstField(fields, "influencerAccountId", "InfluencerAccountId", "accountId", "AccountId"); + if (!int.TryParse(rawAccountId, out var influencerAccountId)) + return BadRequest(Success.Error("Missing request body.")); + + var request = new InfluencerRemoveRequest { influencerAccountId = influencerAccountId }; + return Ok(Success.Ok); + } + + private async Task> ReadRequestFieldsAsync() + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var kv in Request.Query) + fields[kv.Key] = kv.Value.ToString(); + + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var kv in form) + fields[kv.Key] = kv.Value.ToString(); + return fields; + } + + if (Request.ContentLength is not > 0) return fields; + + try + { + Request.EnableBuffering(); + Request.Body.Position = 0; + string body; + using (var reader = new StreamReader(Request.Body, leaveOpen: true)) + body = await reader.ReadToEndAsync(); + Request.Body.Position = 0; + + var trimmed = body.TrimStart(); + if (trimmed.StartsWith("{", StringComparison.Ordinal)) + { + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + foreach (var prop in doc.RootElement.EnumerateObject()) + fields[prop.Name] = prop.Value.ValueKind == JsonValueKind.String + ? prop.Value.GetString() ?? "" + : prop.Value.ToString(); + } + else if (trimmed.Contains('=')) + { + foreach (var pair in body.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var eq = pair.IndexOf('='); + if (eq < 0) + { + fields[Uri.UnescapeDataString(pair)] = ""; + continue; + } + + fields[Uri.UnescapeDataString(pair.Substring(0, eq))] = Uri.UnescapeDataString(pair.Substring(eq + 1)); + } + } + } + catch { } + + return fields; + } + + private static string? FirstField(Dictionary fields, params string[] names) + { + foreach (var name in names) + if (fields.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)) + return value; + + return null; + } + } + + public sealed class InfluencerCodeRequest + { + public string? Code { get; set; } + } + + public sealed class InfluencerRemoveRequest + { + public int influencerAccountId { get; set; } + } +} diff --git a/Server/Promotions/Referral.cs b/Server/Promotions/Referral.cs new file mode 100644 index 0000000..ed8d3f3 --- /dev/null +++ b/Server/Promotions/Referral.cs @@ -0,0 +1,10 @@ +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Store.Referrals +{ + public class Referral + { + public ReferralStatus Status { get; set; } + public ReferralReward ReferralRewardTier { get; set; } = null!; + } +} diff --git a/Server/Promotions/ReferralProgress.cs b/Server/Promotions/ReferralProgress.cs new file mode 100644 index 0000000..41e5a05 --- /dev/null +++ b/Server/Promotions/ReferralProgress.cs @@ -0,0 +1,13 @@ +using RadicadeServer.RecNet.Store.Referrals; + +namespace RadicadeServer.RecNet.Store +{ + public class ReferralProgress + { + public int ReferralsVerifiedCount { get; set; } + public List PlayerReferralRewards { get; set; } = new(); + public List ReferralRewardTiers { get; set; } = new(); + public List AvailableReferralRewards { get; set; } = new(); + public bool HasReferralRewardAvailable { get; set; } = false; + } +} diff --git a/Server/Promotions/ReferralReward.cs b/Server/Promotions/ReferralReward.cs new file mode 100644 index 0000000..3dbf82e --- /dev/null +++ b/Server/Promotions/ReferralReward.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Store.Referrals +{ + public class ReferralReward + { + public long ReferralRewardTierId { get; set; } + public int RequiredReferralCount { get; set; } + public int RewardSelectionCount { get; set; } + } +} diff --git a/Server/Promotions/ReferralsAPI.cs b/Server/Promotions/ReferralsAPI.cs new file mode 100644 index 0000000..4b8eb4e --- /dev/null +++ b/Server/Promotions/ReferralsAPI.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.RecNet.Store; +using RadicadeServer.RecNet.Store.Referrals; + +namespace RadicadeServer.API.IncentivizedReferrals +{ + [Route("api/incentivizedreferrals")] + [Route("econ/api/incentivizedreferrals")] + public class ReferralsAPI : Controller + { + [HttpGet("progress")] + [HttpGet("v1/progress")] + public ReferralProgress GetReferrealProgress() + { + return new ReferralProgress(); + } + + [HttpGet("referrals")] + [HttpGet("v1/referrals")] + public List GetReferrals() + { + return new List(); + } + } +} diff --git a/Server/Properties/launchSettings.json b/Server/Properties/launchSettings.json new file mode 100644 index 0000000..0012475 --- /dev/null +++ b/Server/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:3030" + }, + "https": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:3030" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} diff --git a/Server/Radicade/Responses/AuthResponses.cs b/Server/Radicade/Responses/AuthResponses.cs new file mode 100644 index 0000000..a33822b --- /dev/null +++ b/Server/Radicade/Responses/AuthResponses.cs @@ -0,0 +1,10 @@ +using RadicadeServer.RecNet.Responses; + +namespace RadicadeServer.Radicade.Responses; + +public static class AuthResponses +{ + public static readonly Success MissingPresence = new Success(false, "p-radicade1", null); + public static readonly Success MissingProfile = new Success(false, "p-radicade2", null); + public static readonly Success MissingPlayer = new Success(false, "p-radicade3", null); +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/2022-avatar.json b/Server/RadicadeServer-Config/2022-avatar.json new file mode 100644 index 0000000..6989aab --- /dev/null +++ b/Server/RadicadeServer-Config/2022-avatar.json @@ -0,0 +1,44364 @@ +[ + { + "AvatarItemDesc": "b33dbeee-5bdd-443d-aa6a-761248054e08,", + "FriendlyName": "Team Jersey", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b33dbeee-5bdd-443d-aa6a-761248054e08", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1213992309 + }, + { + "AvatarItemDesc": "6d48c545-22bb-46c1-a29d-0a38af387143,", + "FriendlyName": "Team Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d48c545-22bb-46c1-a29d-0a38af387143", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1037333475 + }, + { + "AvatarItemDesc": "24b8b757-4168-4b3b-ba11-46f29d19044d,", + "FriendlyName": "Glowing Team Jersey", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24b8b757-4168-4b3b-ba11-46f29d19044d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1816296005 + }, + { + "AvatarItemDesc": "e41c32ac-61b7-4e59-9d0e-b231ebdc2a68,", + "FriendlyName": "Glowing Team Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e41c32ac-61b7-4e59-9d0e-b231ebdc2a68", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1140800281 + }, + { + "AvatarItemDesc": "d84993dd-4e15-4481-9450-f88a34cdbc82,FnhttSsCoUCSAW8j-stC5A", + "FriendlyName": "Default Placeholder Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d84993dd-4e15-4481-9450-f88a34cdbc82", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1234016191 + }, + { + "AvatarItemId": 1185405589, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "c5010738-41fa-4eca-aeac-e24adaa29789,", + "FriendlyName": "Helmet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5010738-41fa-4eca-aeac-e24adaa29789", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.489778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1842916691, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "018b39ed-cbef-4fff-862f-f39373b32e2e,", + "FriendlyName": "Curly Helmet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "018b39ed-cbef-4fff-862f-f39373b32e2e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.489778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 986, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "47u2if3fx2rt8p4wj90qo988g.png", + "AvatarItemDesc": "1d27b674-f9e2-4ffc-9d8c-a58a1be06457,", + "FriendlyName": "Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1d27b674-f9e2-4ffc-9d8c-a58a1be06457", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:49.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 556, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9qr2ce4r54kz6w69sxrp0cm74.png", + "AvatarItemDesc": "d84c0ff9-8fbe-4ed8-abf3-7996e81888ab,", + "FriendlyName": "Large Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d84c0ff9-8fbe-4ed8-abf3-7996e81888ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-10-26T01:09:06.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 584, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ctj8umc3ax8se6s476eqty6l0.png", + "AvatarItemDesc": "e5b83dfc-b2e1-4dcb-a4ab-9d3a4c8a34ae,", + "FriendlyName": "Long Wavy Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e5b83dfc-b2e1-4dcb-a4ab-9d3a4c8a34ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:12.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 585, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "85pkqd12j64n3n0av6xlffsxm.png", + "AvatarItemDesc": "7dd6f7b0-7ba0-429f-a04f-e32d3a79ee61,", + "FriendlyName": "Short Wavy Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7dd6f7b0-7ba0-429f-a04f-e32d3a79ee61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T02:21:32.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 587, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eds0e7isjgdyo1d98d51ibdiw.png", + "AvatarItemDesc": "eb9611c6-bb50-41a2-93e9-7f959815a846,", + "FriendlyName": "Dreads Long Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb9611c6-bb50-41a2-93e9-7f959815a846", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T01:07:52.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 998, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8k7za5lr83ybmyz1z2kx03poc.png", + "AvatarItemDesc": "1fd69ef8-0b74-4962-af5a-67f0bf0358f2,", + "FriendlyName": "Ponytail Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fd69ef8-0b74-4962-af5a-67f0bf0358f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:18.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1002, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cy0bdcxljjgonlcc039y6o326.png", + "AvatarItemDesc": "a12f724f-4a73-4ab8-aad4-6bfc662b4dd6,", + "FriendlyName": "Undercut Long Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a12f724f-4a73-4ab8-aad4-6bfc662b4dd6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:24.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 993, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5h63afsihyt4jqw4p15rg5bnh.png", + "AvatarItemDesc": "0753d7a4-8247-4fca-a6fc-359c26086140,", + "FriendlyName": "Fonzie Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0753d7a4-8247-4fca-a6fc-359c26086140", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:12.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1001, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "95n2l3ib6p6i60mz8mpfd18uj.png", + "AvatarItemDesc": "77d3c585-4928-4471-a425-89036efe7299,", + "FriendlyName": "Spiky Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77d3c585-4928-4471-a425-89036efe7299", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:22.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 996, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "epvz80ugc50j724bv8du1ivqo.png", + "AvatarItemDesc": "92302d9d-c527-418c-ac5d-1fa869727505,", + "FriendlyName": "Part Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "92302d9d-c527-418c-ac5d-1fa869727505", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:16.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1003, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4wmgnksu1wj9yabh6v9uxjmtp.png", + "AvatarItemDesc": "f9dd08f8-16d3-4c39-af4f-89f7bb6e80d3,", + "FriendlyName": "Undercut Short Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f9dd08f8-16d3-4c39-af4f-89f7bb6e80d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:26.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 990, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3mpvrp2oqpvbugfnbb57ooj1m.png", + "AvatarItemDesc": "b148cb1e-df81-442f-aea6-ab1727aad00e,", + "FriendlyName": "Chunky Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b148cb1e-df81-442f-aea6-ab1727aad00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:04.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 988, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ddt2ggc02cukv16x74awz2f5s.png", + "AvatarItemDesc": "e36bcd98-7e85-43fa-89f8-57e4ec33823a,", + "FriendlyName": "Bob with Bangs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e36bcd98-7e85-43fa-89f8-57e4ec33823a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:58.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 987, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aj4ljso81bddjrnou7k4r9jvc.png", + "AvatarItemDesc": "880a3cc0-7407-4b61-b759-f9dd890fe9e5,", + "FriendlyName": "Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "880a3cc0-7407-4b61-b759-f9dd890fe9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:55.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 994, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dmg8ywtfbzy57cutb6j4h3vjh.png", + "AvatarItemDesc": "21599b51-c50f-43d8-ac5f-62c30cd02ca5,", + "FriendlyName": "Lori Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21599b51-c50f-43d8-ac5f-62c30cd02ca5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:13.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 991, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "60usmea9f77xjypmnvoptbc9g.png", + "AvatarItemDesc": "193a3bf9-abc0-4d78-8d63-92046908b1c5,", + "FriendlyName": "Emo Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "193a3bf9-abc0-4d78-8d63-92046908b1c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:06.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 997, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9gvfh0l74sodnh53dk03ioc7k.png", + "AvatarItemDesc": "79b90274-6eec-4664-acfb-4a123334661e,", + "FriendlyName": "Pig Tails Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "79b90274-6eec-4664-acfb-4a123334661e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:17.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 989, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5yfm6hc4pnazd6475re6lhwdc.png", + "AvatarItemDesc": "da4e7b34-2095-4a9e-801e-4f409039e0dd,", + "FriendlyName": "Buzz Cut Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da4e7b34-2095-4a9e-801e-4f409039e0dd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:01.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 995, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2k57qatxawouqn71m6owgx9bc.png", + "AvatarItemDesc": "9d9fadb6-97eb-480e-a224-4e0179082071,", + "FriendlyName": "Meatball Buns Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d9fadb6-97eb-480e-a224-4e0179082071", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:15.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1000, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5en879epjtfbbwep4k3qz00wa.png", + "AvatarItemDesc": "d8280c0c-d803-4513-be10-a0ba96d8821e,", + "FriendlyName": "Flowhawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8280c0c-d803-4513-be10-a0ba96d8821e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:21.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 992, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2a7algukgsrdsvd69r2r6nqlw.png", + "AvatarItemDesc": "e286863c-2967-4d00-b837-b49487b9484a,", + "FriendlyName": "Fauxhawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e286863c-2967-4d00-b837-b49487b9484a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:10.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 999, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0v8t308zwge73zqhs45g27w4f.png", + "AvatarItemDesc": "2cb4f372-3372-4583-8b57-c4e3988e3c28,", + "FriendlyName": "Punky Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2cb4f372-3372-4583-8b57-c4e3988e3c28", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:19.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1004, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "814p75pufultq4i0yd07s46mi.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,", + "FriendlyName": "Cat-Eye Glasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:28.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1005, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "56tk3ruae6anx7arnsnyk6k03.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,", + "FriendlyName": "Glasses (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:29.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1006, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "f0wdmxgtyo7xkofqt6gy6s0hn.png", + "AvatarItemDesc": "8d10cc78-6b00-45f3-affb-205e9cc5b03f,", + "FriendlyName": "Beard (Close)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d10cc78-6b00-45f3-affb-205e9cc5b03f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:30.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1007, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "amvkakyjtnfjh0sip52ftw6uk.png", + "AvatarItemDesc": "cc96f8a5-bc5b-4f89-83b7-ecd53905ada7,", + "FriendlyName": "Beard (Thick)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc96f8a5-bc5b-4f89-83b7-ecd53905ada7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:32.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1008, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dfc0t1w1r9z7p33f1562b1u2u.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,", + "FriendlyName": "Business Tie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:33.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1017, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3rr58syq4bpp9v0ot7isaecb3.png", + "AvatarItemDesc": "4d507dfa-4a99-4ac0-8537-229e9dc0eb4a,", + "FriendlyName": "Rec Room Tank Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d507dfa-4a99-4ac0-8537-229e9dc0eb4a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:44.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1019, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2theh2wmoj85n9yoigkwvalhv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,", + "FriendlyName": "Rec Room Shirt (Crew Neck, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:48.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1749, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "67l6cwkqcs2tczds20bgj5rxy.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45", + "FriendlyName": "Rec Room T-Shirt (Crew Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-30T01:35:18.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1009, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aouoxffd4nu47wnb2ngptwmhv.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,", + "FriendlyName": "Collared Shirt (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:34.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1020, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8h9x3f8d3cbsn1mixmjnnxakw.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,", + "FriendlyName": "T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:49.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1018, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9b4c3le7lmjt85hxgjr9xtaz9.png", + "AvatarItemDesc": "8aa79563-ace1-4ba7-ad0c-f3210a78142f,", + "FriendlyName": "Rec Room Shirt (V-Neck, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8aa79563-ace1-4ba7-ad0c-f3210a78142f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:46.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 675, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4n6q6y6u8lbqej4m7zacg6pgv.png", + "AvatarItemDesc": "8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c", + "FriendlyName": "Rec Room T-Shirt (V-Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8aa79563-ace1-4ba7-ad0c-f3210a78142f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:40.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1021, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4hlua7fbqw8nlu7f4buxxbjgy.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,", + "FriendlyName": "Tennis Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:51.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 681, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aqlcnxazbwcmfgx7cp4mpgqpy.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,c5deba2a-6e35-4b13-8e94-8ba5457f39df,b75ef67d-00c3-4ac1-9b72-212032460294", + "FriendlyName": "Tennis Skirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:48.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 682, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bjhjw79pvyn169f35rgbnyhqa.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,758752bd-db2f-43d2-b580-55b3e1efffd5,b75ef67d-00c3-4ac1-9b72-212032460294", + "FriendlyName": "Tennis Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:50.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1022, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c45s37acw47y54ey191vc8g64.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,", + "FriendlyName": "Zipper Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:52.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1023, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9a4gj5yigfksglnec4np95a2h.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,", + "FriendlyName": "Wristbands (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:53.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1024, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b0nybni7980f1n0fvwjlcpwba.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,", + "FriendlyName": "Wrist Tape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:54.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 674, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0bb1u3you3uny9smblxzcmwyf.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,55901f12-d5b5-4fa8-b4c8-e479689ee39d,f600037d-c9c0-43fa-b45b-02f456f9dd5f", + "FriendlyName": "Collared Shirt (Denim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:38.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 672, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "077ospe8q8kka3lqwcvd2df6w.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,bf82f2f6-9af8-431e-a296-0890dea48ba7,d015cae7-a905-49e4-8823-6dec069689a6", + "FriendlyName": "Collared Shirt (Argyle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:36.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "130079y56zsb2j2jv5bahhp77.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,EfdMcnfHt0mr0PQ_maaYOg,DRJcNhkqvkKFEaZpOguR6w", + "FriendlyName": "Collared Shirt (Flowers, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:43.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 676, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "apumxi9psa3dgu88d82sv7a82.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,6d703981-2734-4c45-8983-cdd5f328902f,a0271cd0-e172-4d3f-aa2f-9806f21a82d2", + "FriendlyName": "Tank Top (Camo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:41.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 679, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cyeqj6jrqz7lap5blce53jyxc.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,9c03f381-7357-4d0f-8cda-8737d4c43d25", + "FriendlyName": "Tank Top (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:45.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 680, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b5581rifqvl56c14u3pjcb613.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:47.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 678, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5pbiviztmh19hus4bp2sifgsl.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tank Top (Dots)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:44.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 677, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4iwbk5q2yccf8huy5ut62aqjm.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,48abd952-214f-48b2-a8f1-1146f6f69aa2,b78008e8-abbd-4ece-be34-9a911f721fcc", + "FriendlyName": "Tank Top (Zebra)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:42.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 669, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3eon8zbs1np0ijovp5ri8gb2h.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:29.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 670, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "drn8bzb22wxe4zkanykjdw7il.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:32.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 664, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6xdunx482y5cr10gt2joszy8u.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:22.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 663, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9x7n4sd9ecxc01cpbo417a6h9.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:16.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 667, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8a4fwzyn75woyrsc9rz1hucpr.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Orange, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:26.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 666, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d1xnxod7dtl3g6htemy60mxxj.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Pink, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:25.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 665, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bt1et6gecchq8vwnxdydsijvs.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:23.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 668, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5j9o29zmheavd8i64j65gc7d5.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,6dd95046-acf8-42fe-ab78-80a334096a9d,56a92c8d-af53-413e-929e-4a9a3cfad780", + "FriendlyName": "Headband (Red, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:27.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 686, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6pf2ftyxdwcm1n73wbvsvqgww.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:56.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 684, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7etdcocmjntbessmyb4q1yrql.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:53.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 685, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dkn9mf26l54mq2qdaq2wxjmmb.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:55.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 687, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8ymxu6vs24zw45qrtx7r3wiir.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:57.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 683, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cbxl5wtnmplfz5vth23g97xve.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:52.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 390, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5n8kq96onh4dhg7u9k46jmucw.png", + "AvatarItemDesc": "fcfcaf63-deb4-45f7-b711-c051c9ea45cb,", + "FriendlyName": "Top Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fcfcaf63-deb4-45f7-b711-c051c9ea45cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:28.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 143, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ap1up0rb8i4a1km9mn9e44dsm.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,", + "FriendlyName": "Blazer (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 147, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00wlb607co30g3d5s8a51rat5.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,6f2e74bf-1e95-463d-97db-d5d1a53b2c28,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Black, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 146, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "crmfr0dn581ysgtbeh5d3m2mr.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,9374bf66-2ee5-493b-8439-efce4b201904,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Grey, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 145, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cugl1bt43o2xnok3ehawcralp.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,272fe8eb-5061-4729-a7a8-414ff667a82f,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Grey, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 148, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0z5rg3veja6t3bw5vpad3759e.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,0ffad843-d6c9-425a-8686-7217009c867e,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Green, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 152, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "28ly2m56wth4ldtr1r5anpadx.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,e0397982-c2c2-4733-9a40-46e18675b5af,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 150, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dw4gnw7y1oclj75acv9nvncv6.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,", + "FriendlyName": "Button Top (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 151, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e1qpklmk4sy92lxlwf1zus9lh.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,49f5864f-9d40-497c-88c8-e87f64d41d74,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 153, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "08g89jon46625gx6apyzgaq7p.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,c5deba2a-6e35-4b13-8e94-8ba5457f39df,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 194, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9lqg5xqiktt0cxifvy1kkwpa3.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,f750de46-3758-4f7d-9709-0a84b1027009,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 195, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3nhman1tbwhikxyha0tj215xw.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,d66aa400-aa5a-4539-a25d-5f8ce94dc281,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 196, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7nlns9i6osonn2bq8867ceizn.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 193, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "02aazblyiy14oivk4h218q67i.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,", + "FriendlyName": "Collared Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 417, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3w6yoi9bpa2pl8w7wab8zwodb.png", + "AvatarItemDesc": "241506f6-bf88-4b46-b5fe-513a225421f4,", + "FriendlyName": "Half Up Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "241506f6-bf88-4b46-b5fe-513a225421f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:30:04.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 459, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "08e7y3dct2h7xht6m1vfda9yg.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,d6edbc00-3c1d-4f49-8412-3ef8c7c5f4c2,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:24.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 255, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8aayhgqe518c46fqbtd1x55jk.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,6d703981-2734-4c45-8983-cdd5f328902f,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 254, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6ih1uyez4f5apbi0u4n5szfns.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,830be2fa-60a5-48cc-931f-34b670eae4bd,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 253, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b119rnoqjo60vhf5wcwc9my42.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,bbfa08e3-8e6b-4e0f-b264-1b398d7cd44a,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 289, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0r354na62acon4jev863fj1vh.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 294, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1eoaif6mmgo9dolltq50ob9o6.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 287, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "16b0f84wdbvxcp649og13dk8v.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 230, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b1hdewklda06oi0oveqpmi9wc.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 229, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5k1gsvgmixc21tqvaomwh0yft.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 227, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "csvjo09nx4adboqztoamuuq7p.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 228, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7wrlcy8lbaezttb5m6olhzmu2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 461, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "58gu9l73pm4ez45p70yrkyplt.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:26.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 460, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3akcjd4a37lw3qz88ajis1kcz.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:25.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 15, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5kh0jn19pdtrpbc2g180026s5.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 10, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4nxr6u9ei794sh6gu0ucu4ddi.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 7, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0sr1gkccjdo3hx6ruc2ss61p8.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 6, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9tssl8yucapudl7wbupmjwwfw.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,", + "FriendlyName": "Headband (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 16, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5uw2gb9nq7yeqmoplvw1pcsqf.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 582, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7a6q8lttqfwygyoo0xob5drc6.png", + "AvatarItemDesc": "24a240f4-1574-420b-b898-a7e91f170759,", + "FriendlyName": "Back Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24a240f4-1574-420b-b898-a7e91f170759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:09.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 583, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b8pcnjd03lqqw0jzj8ueug6d4.png", + "AvatarItemDesc": "c45ed7b8-99bd-4a4b-a9ff-e16edf5d7a18,", + "FriendlyName": "High Pony Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c45ed7b8-99bd-4a4b-a9ff-e16edf5d7a18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:11.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 29, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "35b472lvh2ejmoj4ppvh8nqvy.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 983, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6u0dzdrw11sr5n7ix18tfuhu1.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:26.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 31, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aghnwd2jnaxd0vtt7oq9ks3pp.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 25, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d7lvri8z5oviwaw0fgktu2pqv.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,", + "FriendlyName": "Baseball Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 30, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9si5yzip1v33nbt5clp7t208l.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 172, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d004gewj67bopyl1n223awz19.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,", + "FriendlyName": "Equestrian Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 173, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9sz04th4t4o0izh811x1j6i3j.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 174, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0ssz8kkzkquzlbsny70teq07u.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,4828b50c-95b6-466a-bb25-514891d78202,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 175, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1ww790yppnpxni1c0om3j7p3e.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,d6823e01-69f0-4f85-b94a-74894356a2cf,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 81, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4q0530imylzalquyt6zrs4br4.png", + "AvatarItemDesc": "09177621-9ecd-4f6a-b6a5-64490139141d,", + "FriendlyName": "Flat Top Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "09177621-9ecd-4f6a-b6a5-64490139141d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 83, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "63iadlt4uzd2bynj3varsfesy.png", + "AvatarItemDesc": "95ab7a7c-c35d-4da5-9955-0921064470b6,", + "FriendlyName": "Gekko Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "95ab7a7c-c35d-4da5-9955-0921064470b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 11, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cwxdnevuziov7r3meknz7vmkr.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 12, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c4pq7xaobf8onr988cn3t6lzb.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 13, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "338vomh9pt5m5bf07wih64yjo.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 14, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6onf6k881dne4cqk3bid15ryf.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 20, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1sdg06t7cb2h44qy4zmiv7uz8.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Red, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 149, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "22i8ypgdvikndkalc1fn6g0be.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,", + "FriendlyName": "Slim Blazer (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 469, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0no2vbmhmulieokvn9ikhxy23.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,cd5d7285-202d-42d0-b93f-04245875793e,0f36bb97-c61b-4281-929f-ff1d0d11be86", + "FriendlyName": "Slim Blazer (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:37.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 468, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "27ztuh73mvhqa7kbriw9y9jjt.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,0f36bb97-c61b-4281-929f-ff1d0d11be86", + "FriendlyName": "Slim Blazer (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:36.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 91, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "16t3t321doi4ngo5nknn2kxkt.png", + "AvatarItemDesc": "8b9f1413-e786-4a30-946c-9292f207875a,", + "FriendlyName": "Pulp Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b9f1413-e786-4a30-946c-9292f207875a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 124, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "79i106lkaeghior2bmvlgc8kb.png", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,", + "FriendlyName": "Rec Room Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 95, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7taocf1uwgbysywpo7p7fmcsm.png", + "AvatarItemDesc": "1a71064b-794f-40fa-9109-8ad36602b6e1,", + "FriendlyName": "Shagg Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a71064b-794f-40fa-9109-8ad36602b6e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 200, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "enwc99fhlpla2ltqpv7ug4r1z.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,761a3193-60f0-4190-80c7-285b8192e794,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 201, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "de87szlv72hk7tfhds4c7qowj.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,a819f49b-6c7a-49d3-9e6a-d9d79ef5019f,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 199, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dvdbsnev23t30jm3v75yrn25a.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,64850553-cdfe-455a-ac00-dafbe63d613e,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 198, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dqkxn82hm6chx9ysdnrcfvj60.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,", + "FriendlyName": "Stoll Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 297, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9x6rq5lgtswz7z6pcm6ccptiy.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wrist Tape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 296, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6w33yvf6g7oo4h3fayjgbs5gj.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wrist Tape (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 288, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "beff1xr2vo6gnh2vgobzx0hrw.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 292, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "el05p2p32cqdh78wlg2aq7vhl.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 131, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "74jrfu3nny1joqt8ee5vlxpok.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,", + "FriendlyName": "Long Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1014, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cpcymryww9ol7mfcdpyp63o47.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,0iSsaY-HgkmLaRHCn5vEdw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:41.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1012, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "60nyc11l4xym89g52ynudx3bm.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,jGj28vhq8EGwP2RuM074aQ,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:38.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1011, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0f3qdah3r9o4qv7cqk39g3l7s.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,kmj5zOjcwku_WWKroCeiVQ,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:37.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1015, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4pvbqpmw9ghysmvyr445pupr1.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,FAviMCQ_EE2Mpt6QPo5OEw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:42.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1013, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "13r4rms2vr6r67kp1gm17g388.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,MFrcSQ1DYUm8imvy4ypgvw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:40.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 144, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7si3m7no7ulqg9he671mhowgx.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,05ac07e1-67f0-486c-abf5-a62866475abb,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Black, Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 620, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "am6f16pg9zk7wcjm1ks7meivl.png", + "AvatarItemDesc": "0088603e-ec3b-4478-8694-e6fb1989b3f2,", + "FriendlyName": "Angled Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0088603e-ec3b-4478-8694-e6fb1989b3f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-22T19:07:25.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 621, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "91fafagogc6ouzeyn500okchs.png", + "AvatarItemDesc": "ffea7a65-613f-4835-921e-6dd15f357b7e,", + "FriendlyName": "Long Bangs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffea7a65-613f-4835-921e-6dd15f357b7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-22T19:07:26.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 112, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "acy5d2ixrgg8d4qlb09csvhm4.png", + "AvatarItemDesc": "45f5e714-8a5f-4385-a97f-675066167011,", + "FriendlyName": "Seventies Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45f5e714-8a5f-4385-a97f-675066167011", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 361, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1eb87x8cqfkan1tpf4fnxe6e5.png", + "AvatarItemDesc": "9bf5d259-7774-4cbe-a90f-7f188cc0dce7,", + "FriendlyName": "Thick Goatee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf5d259-7774-4cbe-a90f-7f188cc0dce7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:01.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 726, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3pvimigcs9imx8k1sd5juxlf5.png", + "AvatarItemDesc": "a6cbfe76-534a-4655-a8a8-3fed13d001c7,", + "FriendlyName": "Bald Top Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6cbfe76-534a-4655-a8a8-3fed13d001c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T01:29:49.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 727, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6eylc335jw4t1874pjzcwpudy.png", + "AvatarItemDesc": "CTcrvbo3OEepIV4oW8bx4w,", + "FriendlyName": "Receding Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd2b3709-37ba-4738-a921-5e285bc6f1e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T19:25:02.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 728, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bvkq9o8mojgbtpcokr90wuxiv.png", + "AvatarItemDesc": "-twtjyBdQ02EAdOfBGTiEw,", + "FriendlyName": "Van Dyke Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8f2ddcfa-5d20-4d43-8401-d39f0464e213", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T22:56:15.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1199, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6xn7zsaqaimo5w5vryw7cyaxt.png", + "AvatarItemDesc": "45eaab67-19c2-4601-8f80-3565a4dceba4,", + "FriendlyName": "Pompadour Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45eaab67-19c2-4601-8f80-3565a4dceba4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:36.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1200, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dpd9st8mzzq7fb7gyrsby9mnh.png", + "AvatarItemDesc": "c855dcc3-96cb-470d-b159-d37a025a47d1,", + "FriendlyName": "Dutch Braid Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c855dcc3-96cb-470d-b159-d37a025a47d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:41.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1477, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3ybnk0r2sa9x5nfulb4n5hfua.png", + "AvatarItemDesc": "d7730a9e-78a1-4356-bc09-6b066615850b,", + "FriendlyName": "Afro Updo Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7730a9e-78a1-4356-bc09-6b066615850b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:09.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1478, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "256bejo1cjwehqhza4ysng9ho.png", + "AvatarItemDesc": "8c35c804-e8d5-49d2-8d5a-ea19fb70bfa6,", + "FriendlyName": "Pencil Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c35c804-e8d5-49d2-8d5a-ea19fb70bfa6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:13.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1480, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eezddupufyjfaxdd5lstfz5gn.png", + "AvatarItemDesc": "5beeb4c4-f276-4eae-87aa-9302e45b05b7,", + "FriendlyName": "Cornrows Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5beeb4c4-f276-4eae-87aa-9302e45b05b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:21.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1457, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dhqnvyh6gae90iy240h1b07t5.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,sxUE0iOSZEmezm54T7xI3Q,tlpa7195x0CkmSjpR1RArQ", + "FriendlyName": "Rec Room Hoodie - Pride (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:18.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1459, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0c78877wqm87gczt3yy70z08e.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,", + "FriendlyName": "Winged Hat - Pride (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:42.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1458, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9lq1a36dbi2qvm0v5g0dci5ne.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,D_Xmo0rOzkS-kgq1CYXt3g,tnCJp2eDI0SwjVfJMhk3LQ", + "FriendlyName": "Rec Room Hoodie - Pride (Trans Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:24.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1460, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7ulpsllq59oaufnjyuw78wn2q.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,knXPidb-Rkayfc3kSHfZeQ,1yMyo6oTjU-VAygoeWaohQ", + "FriendlyName": "Winged Hat - Pride (Trans Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:45.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1621, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2h1h9p8vlyjw7l757o4oin73b.png", + "AvatarItemDesc": "88b6ddeb-a455-460d-91d9-a4569ef6903c,", + "FriendlyName": "Square Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88b6ddeb-a455-460d-91d9-a4569ef6903c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:15.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1620, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3xh3iq78mo5epeepsiol6ghz1.png", + "AvatarItemDesc": "0abb6b08-20ce-444f-879e-0d1344df096c,", + "FriendlyName": "Round Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0abb6b08-20ce-444f-879e-0d1344df096c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:12.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1619, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1jnggaqfe4t82pv8okgvje9e7.png", + "AvatarItemDesc": "9b5bde11-7408-4798-9fcb-c7ec175444df,", + "FriendlyName": "Hoop Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5bde11-7408-4798-9fcb-c7ec175444df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:09.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1742, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4wuzoct4aayihilzg800scbtb.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,", + "FriendlyName": "Leather Necklace (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:33.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1745, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ctkuncvuk0nur9iwwe48eo2ed.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,gsHBUqA1gE2XNTPA3VvBLQ,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:41.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1744, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8umo8jp69be1v9log6wxdhkct.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,JUQn0_PLIkytCrkAg0FflQ,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:38.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1740, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "byk3riscj527wx5q6aq2u1h1y.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,", + "FriendlyName": "Basic Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:20.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1738, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3bofvoeb5yy5y86dljqn9nqyw.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,dBR1uNgk206eVoWntVzVAg,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:14.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1737, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3g3ewvd49267e67sqmpy9df0v.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,zPGCmxM_b0yTt7tqMaomcw,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:10.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1772, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2jsxx5bos2r2jual185q0pgcu.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,", + "FriendlyName": "Pocket Tee (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:07.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1779, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9d8ocr6t26ofb3frswgln8ghp.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,El0s9F6cfkywcKOkqwbJtQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:31.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1775, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9r42wuf4n5jgtidnopmbsho11.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,N0fBxjrCekO-HUqmc3MufQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:24.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1774, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "97wov16x2whj90451paeq5wr9.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,J9cK9BKtHUinG-DwAoPi3g,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Dreen)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:22.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1773, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aj0epqqoal6o3lrbfmlvdjvrw.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,-VF3K4miGE28LxMgqVehRQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:21.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1778, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1hb9mgxu2zrau3k0gdj4psvsv.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,pQ_3mZI7yEaxMw9n_Fr4Sg,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:30.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1777, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "avz1q0x4dp2ujj3k9yzfvcufz.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,TpFWTdEW2kqtpvc0sPv-dw,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:28.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1780, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2q0u4aonxjqnyx4hulnj3xvcj.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,vxZVeH2b_E6XlqyqCW0hBA,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:33.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1776, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0u6tuseoo4aelelolic3o693u.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,oNoEl-nDqk-HLsfz1538oA,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:26.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1781, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9yt8aczo24n68e4zolsg3k0cs.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,Aex0Zh1RQ0u9oTDFF1cFIg,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:35.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1757, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8z6u9864bufzxe9myyoisnl28.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,DIeasJUIj0KNetAzPzuIrg,o3Q2gADhh06dgaipkkdMsA", + "FriendlyName": "Rec Room Hoodie - Pride (Bi Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:15.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1756, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aa9c59vzcixu27kaoazqzpbsd.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,G6Udm6kMdE28vmRY5KPanA,Fw79pqDPP0C8H07AnTY-Vg", + "FriendlyName": "Rec Room Hoodie - Pride (Lesbian Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:11.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1771, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7px3ud7g3dce8g0qtfmbzy9ar.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,3a1oW3_MY0e4lDN2V1X4ig,za8Mz14Ww0KtC6wavUPXdg", + "FriendlyName": "Rec Room Hoodie - Pride (Nonbinary Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:59.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1791, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "arzqgzmg36st1ri623d76xhhx.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,sf962XJ0tkis0svkMkZCXQ,4bI7QRvdnEansPkwx8Di9A", + "FriendlyName": "Rec Room Hoodie - Pride (Pan Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-08T01:17:55.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1770, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5xxg31ft29j91d3n4nxdkw270.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,tm7Cnd3LzkqWHA1JMrgoeA,nbLr3c3lOkiWhNg9IYbt6Q", + "FriendlyName": "Rec Room Hoodie - Pride (Ace Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:50.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1970, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3jkufkhi2w8i2n3lsvlvf65c1.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,t2fOeVWloUOKDlIaf4Sxnw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:21.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1971, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2izsye42u9sx691sx6b1bpdil.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,3JrW3FFUvEe6HZ9VjOVjMw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:24.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1972, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "955r71z2z3dwqtuuo4h7wtm00.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,OT5APcpbpEiMNNah8n4Txw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:26.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1973, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bm3lgqlrymyw0u7z8sipii563.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,NjCLn5A5h0-V8jB72etCNQ,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:29.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1974, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bbonh2ov1akhkcgu49ws63rse.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,jeEWPFvzaEGmXaUrxTFvwg,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:35.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1984, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "79dqcbiedmmieho50ebbbddj9.png", + "AvatarItemDesc": "edc7888c-dc42-48ab-8815-884d227760af,", + "FriendlyName": "Short Locs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edc7888c-dc42-48ab-8815-884d227760af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:28.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1978, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2qqensahxq2o8r4avbpt9zoo7.png", + "AvatarItemDesc": "6e48363d-cc62-40a8-87f9-a69fae90c002,", + "FriendlyName": "Box Braid Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6e48363d-cc62-40a8-87f9-a69fae90c002", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:09.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1979, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d45c0mzfbqha85oe0312hkatw.png", + "AvatarItemDesc": "0d1ae26e-aaa4-4914-ad70-c9ab8cfabc61,", + "FriendlyName": "Box Braids with Cuff Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d1ae26e-aaa4-4914-ad70-c9ab8cfabc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:10.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1980, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6vl2aov7qwvmkpf2wux0rctq6.png", + "AvatarItemDesc": "04e348b7-107f-47b4-9331-ae3b9fa184ca,", + "FriendlyName": "Braided Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "04e348b7-107f-47b4-9331-ae3b9fa184ca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:18.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1986, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bathop4rdgdsysv3jg6wi2uua.png", + "AvatarItemDesc": "ec474314-fd79-441d-852b-d8999274fe57,", + "FriendlyName": "Wavy Middle Part Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec474314-fd79-441d-852b-d8999274fe57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:43.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1982, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "abs1vh2rd21swanjh7vd6hi3h.png", + "AvatarItemDesc": "36b2ccf4-4f42-4dc4-aeaf-f3d10b2a6d30,", + "FriendlyName": "Etched Fade Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "36b2ccf4-4f42-4dc4-aeaf-f3d10b2a6d30", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:23.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1983, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "252m5s6mamg4ajchjb98l8me6.png", + "AvatarItemDesc": "37f7b94a-92ba-4f5e-b94c-de00acfa4722,", + "FriendlyName": "Etched Flattop Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37f7b94a-92ba-4f5e-b94c-de00acfa4722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:26.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1977, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1qr2ehkeevot7hvkonb7ygtv6.png", + "AvatarItemDesc": "4eaaad39-aa44-4791-8b67-2eafa8305850,", + "FriendlyName": "Afro Puffs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4eaaad39-aa44-4791-8b67-2eafa8305850", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:05.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1985, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dzytj60lnm9n8bstqbvcrikss.png", + "AvatarItemDesc": "9bf75b23-84e9-436b-bbb8-4180ccd86b94,", + "FriendlyName": "Wolf Cut Hair)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf75b23-84e9-436b-bbb8-4180ccd86b94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:40.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1981, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e9zj12umhuny6tnh1cdk7tu47.png", + "AvatarItemDesc": "2822aa50-4344-434d-b46f-fd9443f865b9,", + "FriendlyName": "Curly Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2822aa50-4344-434d-b46f-fd9443f865b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:20.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1987, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2fujgcfkoqu654pjz73tner1m.png", + "AvatarItemDesc": "9c20fdd5-acf1-426c-b85a-2220ceee9472,", + "FriendlyName": "Short Curly Twists Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c20fdd5-acf1-426c-b85a-2220ceee9472", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:47.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2059, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mNgon98gpkamzdOKrEwBhQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,PIwziHk1BUuYkAf6kuK6wA", + "FriendlyName": "Rec Room Hoodie - BHM (Black History Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:52.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2192, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3qt9ioRkZ0ClPXKVL2YMvw.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,PRLBrEEZhUumm3VnYVJP6Q", + "FriendlyName": "Rec Room Hoodie - Pride (Aromantic Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:26.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2193, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "pJQjmPlV80Kjht3dBWJjEw.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Xt3Ndm8mC0qsLClPIbgwUw", + "FriendlyName": "Rec Room Hoodie - Pride (Genderfluid Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:29.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2194, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "PO7jrliur0CVizm0SH3TbA.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,MfF0wizDjUa7RUE-AVJpvA", + "FriendlyName": "Rec Room Hoodie - Pride (Intersex Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:33.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2232, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7Z-IZ-6LvUypmMCX8IwHSQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,LovycnPHoUiP_N9w7BzsnA", + "FriendlyName": "Rec Room Hoodie (Juneteenth)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:08.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2337, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "Exs987LYAUOtBgHMuw7Rqw.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,", + "FriendlyName": "Head Scarf (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:08.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2338, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5L_MOzszIUOYUpjCKDoaog.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,B3jdXiOJs0WO2ytm3-hXPg", + "FriendlyName": "Head Scarf (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:11.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2339, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bymnCdtwu0SDAsat4NNB4g.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,VudVdOAjZUibHmtFeNkvSw", + "FriendlyName": "Head Scarf (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:13.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2340, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "lEfY-3T1Lk6G8rEL0ZkSOg.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,5z3jZhYvq0CGmdJ9IcUUsg", + "FriendlyName": "Head Scarf (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:15.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2341, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "J-3USpg66EG6TP6A0OF22g.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,_T4zEki_wk-UIPdhSaBr_Q", + "FriendlyName": "Head Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:17.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2359, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "O9Rxhi7Xp0uMsUE3NL64uQ.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,", + "FriendlyName": "Head Wrap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:55.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2360, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yEzjZ8Usm0289gIRWJ6TqA.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,nPenrNSTE0mDK5cWhxtEug", + "FriendlyName": "Head Wrap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:57.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2361, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mcJSwkCUbUSoHOpUPPSxcw.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,a0A1q_e1q06qquiu2KaZhA", + "FriendlyName": "Head Wrap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:59.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2362, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "TAGSVr0Y6kmZcsvKeNOdxw.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,rwESUxIrQEKA2hdQpcaPaA", + "FriendlyName": "Head Wrap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:01.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2363, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "vlAp24nn10uyws8TDY5Rug.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,M_BhFve7SkC7IJoymLiCLg", + "FriendlyName": "Head Wrap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:04.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2364, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ZBnmV3ahdUa6yCVkJ16fGQ.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,0zldWzi4H0OQwsWPt6gl-w", + "FriendlyName": "Head Wrap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:06.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2342, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "OfSv7-HEQEudAjfpEbowJA.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,EgEoJzG7A0aJ92qfInbX7g", + "FriendlyName": "Head Scarf (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:20.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2343, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "L9nYH9XNvkGX3ZD8c3UfLw.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,OlfQDuz1NUOr88AL2n0jXg", + "FriendlyName": "Head Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:23.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2344, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dBD0grhWnUavKcZsyyx1FQ.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,NETGR8iysUyxcZM_oGmrwQ", + "FriendlyName": "Head Scarf (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:25.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2328, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "EcBeEDeJN0a4mHWBsi4Npg.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,", + "FriendlyName": "Brimless Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:49.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2329, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Eu1IKmX_QkyptaNvKNG26w.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,Rh-RaEHap0e858QeTtCqJg", + "FriendlyName": "Brimless Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:51.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2330, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rUQ5YQ1ymk6xtBaPHA9obw.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,hgLq0WyUAUSl2sboUrpTKQ", + "FriendlyName": "Brimless Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:53.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2331, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "BqkhCnPdjkqPD8KI_tvDBg.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,M5uFQQpoQE-5CKMJnDv_LA", + "FriendlyName": "Brimless Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:55.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2332, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mWIwHLWbnE2ZVzfBK3Y4CQ.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,JzLPlVNG20G9mxG_4PF8Fg", + "FriendlyName": "Brimless Cap (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:57.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2333, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "jfoJFkvJhEaInI7ytPqonA.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,VuMDnpnK50mzgUMwJ4F6Jw", + "FriendlyName": "Brimless Cap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:59.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2334, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3PQQmjlqZE2pSQnjsADLfw.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,", + "FriendlyName": "Headdress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:00.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2335, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hgq25v2vbk6EY3TPtz8O8A.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,gLCis5TFxEyRvvZ0PZq_Jw", + "FriendlyName": "Headdress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:02.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2336, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ad1rqlw7oUiEHiwbXRJHuQ.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,1WBsT99QM0anps6ZoWE59Q", + "FriendlyName": "Headdress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:05.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2352, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "taLvR9OtMkuwOHCj9sa8Yw.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,", + "FriendlyName": "Head Scarf Twist (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:39.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2353, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "aTq6lg1RF0KsrotD8g8PVA.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,jcG-vMduWEq14vo2sKYdWw", + "FriendlyName": "Head Scarf Twist (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:42.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2354, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "vkUv2dsvVEqj7J_oudnCnQ.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,s-QTVT5eTECKHL9FnHFCAQ", + "FriendlyName": "Head Scarf Twist (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:44.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2355, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "n71xleEsHEKKy_YJgkOBbQ.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,irw2uj4c2Eil0ndPCTqodQ", + "FriendlyName": "Head Scarf Twist (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:46.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2356, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9EfM-NBnuUud7oDeXW_t_w.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,UnHAtXkd2kCdamzw6B3Mvg", + "FriendlyName": "Head Scarf Twist (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:48.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2357, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8mn7hHezlUePzyx-HN48rg.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,sO8e2epLrUud243Xi9odXg", + "FriendlyName": "Head Scarf Twist (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:50.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2358, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "DSuLZADyxUa5EzpsYky2cg.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,pPZ3sDw3B06mKiYsAiAt9w", + "FriendlyName": "Head Scarf Twist (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:53.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2345, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "j8rw1jLEy0qFDD5N24UOaw.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,", + "FriendlyName": "Head Scarf Halo (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:27.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2346, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e0Y4CS-9IU2km0B3jwPCtg.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,3cPbhpDX-EqDQy6eN1E5Ug", + "FriendlyName": "Head Scarf Halo (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:29.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2347, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "VH7wAcdrW0aDxhWBl3qs3Q.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,iCVglqDJrkKB36za9UiUxg", + "FriendlyName": "Head Scarf Halo (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:31.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2348, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9ftPEgJCtkmrO_fAsxmXxw.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,gEuE-X67b0-q5C0Tv11_nA", + "FriendlyName": "Head Scarf Halo (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:32.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2349, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "UD9QOouEvE2Msgsa8r4Qng.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,gk_mQ-jsakmxhZYYNlRl_w", + "FriendlyName": "Head Scarf Halo (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:34.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2350, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "kHKrIJ-sKU-U7fZBCFu4rg.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,0p_eXn2gXUKrshSEmrQx6Q", + "FriendlyName": "Head Scarf Halo (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:36.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2351, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rcFCLdci0EKcU2Y-zY7EuQ.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,fAmEEm6ziUO2ZXTscQWtSg", + "FriendlyName": "Head Scarf Halo (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:38.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2732, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "7Seg-2HnmEuiyQanSBov5A.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,NI8a4j9fl0egSrLbrMTn7Q", + "FriendlyName": "Rabbit Ears (Lunar New Year)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:24:58.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2824, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "iegw0q7VnkCblb2xmJZiRQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,kBaeiw5WG0OHOIsric74Ag", + "FriendlyName": "Rec Room Hoodie - WHM (Women's History Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:58.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262140308, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "k4xHpFAnkEm0feaYPcjMzQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,EkA09Hj2m0u3c5STynn3Bw", + "FriendlyName": "Rec Room Hoodie - ANAM (Autism & Neurodiversity Awareness Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.490779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2875, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4lvbe8qhhnk5qhf9gs7iglqbj.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,", + "FriendlyName": "Hearing Aids (BTE - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:42.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2877, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8v81gej0s8m49iztzzyb4eicc.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,xn8Or1kV-EGhpOqnYj6_0A", + "FriendlyName": "Hearing Aids (BTE - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:46.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2876, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b84s2wgn3n5fz4wcz7jmslcez.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,1n-ekJos-EeViiz_zG4jEg", + "FriendlyName": "Hearing Aids (BTE - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:44.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2878, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4hydnfkwj5yum1oq7pdpajh6t.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,", + "FriendlyName": "Hearing Aids (BTE Earmold - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:48.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2880, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9ejntda7ab26w71takrqy1wuc.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,aEsXeTI_SEOclbm82n17tg", + "FriendlyName": "Hearing Aids (BTE Earmold - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:52.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2879, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5kr6spnm1wjdvlki1808o40y6.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,pZDvYABTN06KdJ5Ck60XLA", + "FriendlyName": "Hearing Aids (BTE Earmold - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:50.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2881, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6g30dvwaavnpkdqlzdb6czfja.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,", + "FriendlyName": "Hearing Aids (Cochlear Implant - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:54.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2883, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bmepd5uq48h51oc7xxq9kepf2.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,7ZRNVpxhWk-j8R3oj0oVXg", + "FriendlyName": "Hearing Aids (Cochlear Implant - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:58.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2882, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3vu86qw9p3ikkjm0hx774naxc.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,8WJI7RAuaEy4KvQiQhoKBw", + "FriendlyName": "Hearing Aids (Cochlear Implant - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:56.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "6d55261b-5434-49d4-8057-f4c748f1a238,", + "FriendlyName": "Full Body Clothes Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d55261b-5434-49d4-8057-f4c748f1a238", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1968384766 + }, + { + "AvatarItemDesc": "6f47500a-92d2-42d4-8dfa-42a815c43c02,", + "FriendlyName": "Full Body Clothes Sneakers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f47500a-92d2-42d4-8dfa-42a815c43c02", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1551964329 + }, + { + "AvatarItemDesc": "ff89985c-db80-45f6-9c49-9b7478c38ae0,", + "FriendlyName": "Full Body Clothes Long Pants", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff89985c-db80-45f6-9c49-9b7478c38ae0", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1647504812 + }, + { + "AvatarItemDesc": "8c3885a1-b1b5-407e-98e3-4d909724b6c6,", + "FriendlyName": "Full Body Lumberjack Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c3885a1-b1b5-407e-98e3-4d909724b6c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1416203662 + }, + { + "AvatarItemId": 1275, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eauoml21rxj3iqwoccpn04y5p.png", + "AvatarItemDesc": "2c3773a5-a0b4-41c5-a4df-6572dff516c9,", + "FriendlyName": "2020 Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c3773a5-a0b4-41c5-a4df-6572dff516c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:18.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 967, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0mvz1timijk659hshhzec8jtt.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,", + "FriendlyName": "Swing Dress (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:13:02.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1255, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "holidays", + "ThumbnailImage": "3rls67cl0dxudh9kj7rqgqzlp.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,PEGqjoxP6E2yRx_A-HphrQ,I_OF1c0YWESMwxJr9sXYOA", + "FriendlyName": "Swing Dress (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:48.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1353, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6zl0lixajd47uo5t8r26lce00.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,K17dIz_TRUq5D50NBfLjyw,3drRcfyKkku0qGwCy16m9Q", + "FriendlyName": "Swing Dress (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:35.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 757, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1ofk56iwklqug6u3ihb27rfex.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,", + "FriendlyName": "Swing Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:22.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 807, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aje5q2u76zjjzgeehfysg6dik.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,BLgkgLeS-EyazgPQxGcavg,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-10T19:02:35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 790, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "clsb49us68oqy8d5uyq5q31r0.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,ZvPidQ57rUS50Qz89ERmLw,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Halloween)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:31.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 797, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7wagu7eu1akvlpi9036qjkgkx.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,eb3SmJTIh0628LrZUkI4KQ,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 789, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3rko5wknqf4g6gbl016rfci55.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,Yr0mF1EWzEaIyRMOWFiY6w,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:27.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 788, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6sddopu5d9s5sghb2jjycwa4u.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,QzjakcO8tUaPeiI5KBLKtg,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2777, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MGp8JbVCX0OTgJwakQwccA.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,NpvUosaJTECohTIAR38egg", + "FriendlyName": "Ladybug Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:36.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TzqM7z7igEq2-tMG1UDu0Q.png", + "AvatarItemDesc": "4b51679e-96e4-4a11-96fe-1d2754d12481,", + "FriendlyName": "Clock Chain (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b51679e-96e4-4a11-96fe-1d2754d12481", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:03.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BktGoS3OckmmBQXz83eY4g.png", + "AvatarItemDesc": "383d4bb7-1ffc-4b40-9b7e-cfeaf9ac5fdd,", + "FriendlyName": "Thick Shades (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "383d4bb7-1ffc-4b40-9b7e-cfeaf9ac5fdd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:05.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2768, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "q3OFxpl3vkyZ2Y6zGTfobg.png", + "AvatarItemDesc": "3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686,", + "FriendlyName": "Retro Layered Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:07.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 755, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8p01g7rfkflo0g5se24b17jny.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,", + "FriendlyName": "Shutter Shades (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:19.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 913, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1md3c8azai7phcdzdwuu83inx.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,ZpcM5H2Q1UCt0hh82nSu9w,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:47.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bo52eq6h1gjw33zb6kj234swy.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,7B7vpnMTfkGLcMdHCZ_kWA,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:46.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ajtgswd058zwm4k13wu9gtw62.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,nO-YacKU1kmLjTFAAqNeYQ,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:45.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c9ilqoetseg0h6s3twpchmkfp.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,76CA_KCr806y1FA35m-YTg,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:43.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1811, + "AvatarItemType": 0, + "Tooltip": "For earning $1000", + "Rarity": 50, + "ThumbnailImage": "3bgeqvxk1ut8bz0ipoy471x1b.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,u_H8YUV4MEGNGh5h9ckm_w,a3OlD7iI_EqurYq_RIQCvw", + "FriendlyName": "Shutter Shades (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:55.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2074, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DePlu75lck-gLWofC8uKmg.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,1DQi3Wt8ukK2gWvl0zJnkw", + "FriendlyName": "Shutter Shades (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:01:38.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1159, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "daw2bnn661kjxraiwzbj4o13s.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,", + "FriendlyName": "Stunt Singlet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:50.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1158, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0q2tmuvx7l3fxuhgiq8vaftru.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,6t2rCb6WWEW6pJbcjsAFCw,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:46.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1157, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4u9h5rvuhapi02hegh14s9zet.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,zu0Jk24WTkan6XWSwGU1Uw,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:43.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1156, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9jul59tnxdwa6w6bjib681az8.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,i1XbjBqTu0CILIuJClWimA,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:40.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1155, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tpxkoq49mfgt3w5y2vsjnqdm.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,3frH6KxZ9kGUYkjeCDpylQ,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:36.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1154, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "92q8bqs03d33p72ljcvn79lpe.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,Lf4FjROSAEqXedFxpXFQfg,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:32.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 758, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6qo0q0lqcx2crqugl2xmhnz4d.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,", + "FriendlyName": "Windbreaker (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:23.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 896, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5rgodebtv8essp0sxgveqp7fd.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,grutFwJD_0Cb3t6fddNg6w,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:25.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 895, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "37ag45u7zn3cqqya03wz5nmgw.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,EbVDqEJQOk6j0UvX3t-8rQ,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:23.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 894, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0kifl6w66mqa2wbhliszoe52l.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,C3ll7uPfUkeZ1QLX80_BpQ,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:22.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 893, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "au0r6ubbl0fyr6xi7sqch0b59.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,JS7aa4QSUUOpUYPqRZEb5g,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:20.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2065, + "AvatarItemType": 0, + "Tooltip": "Breakdance inspired colorway", + "Rarity": 10, + "ThumbnailImage": "5aqX6fFXKkulI-FwAVm9Kw.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,rwojzpr0E0ylG6_Vv_MVaQ", + "FriendlyName": "Windbreaker (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:18.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 761, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8v6i3ja1btg3mkpog657iixa1.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,", + "FriendlyName": "Windbreaker Cuffs (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:26.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 862, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0xnunm0kmgc2va3zevhooe9ya.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,HncURj5yi0ykk2cVyCZ4Sw,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:15.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 861, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9qf35u5xai7bpf8k7d518b9gz.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,42ZNGHXMvU29yo9s9tVpOQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:13.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 860, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8mq00l9l162o9unwk33egvmsd.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,VDo682NvSUa8xYn8zTX-AQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:02.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 859, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7ndz1tjworge5u01d8cvf815f.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,FvEjo9let0e_Sr-kWq5AvQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:00.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2071, + "AvatarItemType": 0, + "Tooltip": "Breakdance inspired colorway", + "Rarity": 10, + "ThumbnailImage": "HQaEvdJ2yEKBpIlsj1ChiA.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,6c-AhDM6K0iHbNMf4BNiUg", + "FriendlyName": "Windbreaker Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:31.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2217, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "sVLupklIaUKmgYNatMdcSg.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,", + "FriendlyName": "Acoustic Guitar (Wood)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:02.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2243, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "ElDAaYT06kqpFifv_brl_Q.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,O38XfZ3gh0-ofqu36y3mAg", + "FriendlyName": "Acoustic Guitar (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:54.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2242, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "vpkdcMOxAUylrfe2PrxZfg.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,BPp9SkUpsE2PZlStZrIDIg", + "FriendlyName": "Acoustic Guitar (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:51.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2385, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0n2uuqp3f7gj3z8o0ynlzl3et.png", + "AvatarItemDesc": "b07a3964-7a53-4467-aa85-ec19cc27a57b,", + "FriendlyName": "Alien Head Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b07a3964-7a53-4467-aa85-ec19cc27a57b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:26.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5ed0a62a-19b5-42c0-a255-a2e83f063d04,", + "FriendlyName": "Alien Hunter Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5ed0a62a-19b5-42c0-a255-a2e83f063d04", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1733459499 + }, + { + "AvatarItemDesc": "b841c65f-ed5e-4d2a-803f-48939b909100,", + "FriendlyName": "Alien Hunter Glasses Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b841c65f-ed5e-4d2a-803f-48939b909100", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1626620437 + }, + { + "AvatarItemDesc": "8972c6f8-6e11-49dd-92dc-7b12c417fd40,", + "FriendlyName": "Alien Hunter Tie Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8972c6f8-6e11-49dd-92dc-7b12c417fd40", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1145186950 + }, + { + "AvatarItemId": 363, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6i5j2nqmud9i7j69mikjo4wjl.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,", + "FriendlyName": "Angler Shirt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:03.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 403, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7q4pokzc6588ccgqdjft4wov4.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,cac27853-e2df-4d77-b3fd-af3bc0813f01,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:28.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 404, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8gpdgnc9rmf5fzvsp4sio5zla.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:29.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 405, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4qj5sy46am7e176dh6xnr2ejv.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,97938f4c-52b8-4c85-8575-856654666316,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:30.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2520, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "Joz7a1NRQES_i5VXRjxiKg.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,ugpS7o-ZxkCv7HAvhewZgw", + "FriendlyName": "Angler Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:31.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1804724359 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,mWw1qw6jAEuZ9ygGYsHNew", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1020115116 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,AmKqwhgKAUSHt9axdxe80g", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1611305998 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,-eEKJlUIbkqHtsPJeHz19Q", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1014853776 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,LA7Kx2pBIEu07CnM-XRLtw", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1426268107 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,MocUXmOH70-h3RxBJNhYYA", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1429871593 + }, + { + "AvatarItemId": 357, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2taoaui5i9giqj24levmdbst6.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,", + "FriendlyName": "Angler Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:54.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 396, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2i2e0tjhgvflcauuva6tqucvx.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,3f6c8bd2-2c1d-4c8c-a9de-16600fee9645,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:17:57.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 397, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6e94bkakcfscxp997xunr45se.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,d7f0ffd3-ccf9-465e-8afe-4d7efb78dd2c,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:20.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 398, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a791dc940zynp12ezpj4sdavg.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,225209dc-3afc-40cf-858e-89f7afa00c0d,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:22.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2515, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "lRJW1xa-LU2CmI1Uvlzzsw.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,G7p-E7TJHkSojZtj_5pDXw", + "FriendlyName": "Angler Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:19.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1658, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2i9990m2ddrxscxf3csgnpkm5.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,", + "FriendlyName": "Animal Sweater (Ferret)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:38.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1659, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4say0eirgh8p9gunfvf23skx6.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,azbt5pgvrUS4LQwhndFeJg,cHZ4lDnSoUypIXCiRRowzg", + "FriendlyName": "Animal Sweater (Axolotl)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:46.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1795, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "diqbk46zibrfi814wmivw7o97.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,6dzz5ZcGtE6bEjQHxWmo0A,2cSC290cPkCOrW5vtlVUxw", + "FriendlyName": "Animal Sweater (Penguin)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:19.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1796, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1viyefngw9h64p6bq76mhiaym.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,yXVeN9SzQUKE_6u_2KPzgw,sWgRczvpUU6ZHeoMrMD_yw", + "FriendlyName": "Animal Sweater (Toucan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:21.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2402, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "92nW2XOGOE2VbuJ1_fV_2A.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,qzvnj7D1tUuf01HvpiIvqw", + "FriendlyName": "Sweater (Cow Face)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:43.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2748, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wI9h7dAeEUOvsa1ogCNjiw.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,oIVUmOrjjUysnN4XoWvJSA", + "FriendlyName": "Friendship Sweater (Olivia Otter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:14.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2749, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Q5OR2v7ST0mft6WFh5KyAg.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,0wRS4Rs4ykqjJE_5Fk-Flg", + "FriendlyName": "Friendship Sweater (Oliver Otter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:18.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1937, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "emruqyjzh6ox3jrozmu8mooyp.png", + "AvatarItemDesc": "4c032271-a0db-47b0-bfc6-4c330c2c4b21,", + "FriendlyName": "Alpaca Lights Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c032271-a0db-47b0-bfc6-4c330c2c4b21", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:36:34.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 960, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "68fe918b1l9gkogciqeizqf0o.png", + "AvatarItemDesc": "AbYdU7O_9U-Lwg7zFVZzXQ,", + "FriendlyName": "Reindeer Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "531db601-bfb3-4ff5-8bc2-0ef31556735d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T03:04:20.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2085, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X0LbhAoDYk-wQI2tMehxJQ.png", + "AvatarItemDesc": "aaed6385-0ba5-45f3-b0d2-6b0a9ab79628,", + "FriendlyName": "Disguise Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aaed6385-0ba5-45f3-b0d2-6b0a9ab79628", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:03.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 33, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d0ct7ztnb49pz8t2575vjicnw.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,", + "FriendlyName": "Archer Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 34, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "665whx3sukltm50bvhdjb35kb.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9b35ae52-febe-486a-8557-ec7190a9756e,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 35, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "85o1rs3o13bqcy2gu8qjvzgij.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 36, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6neueh6asjwdnysb7fxmpi9ep.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,6e152cc7-e93a-4408-8092-236db420b680,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 37, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ilinnf748ep3x9ag9hht0gds.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9ad37173-e640-4b9e-a474-716d482b73d2,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1123, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6jvwgyrx37n8e3913svxb1s0i.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,lgqLGi01o02ZFC9Xs5Ofdg,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:11.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1301, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wxsli15xh05ark51tui5k2b0.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,61VZkFjB402brzWm4UsftA,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:06.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 216, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vybukcx6j4f40n6enwd6d9mh.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,", + "FriendlyName": "Archer Tunic (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 217, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "azaisqtidm1kgo72k6ahsn245.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9b35ae52-febe-486a-8557-ec7190a9756e,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 218, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5ifgufbqvr5jnazfx4cruhixk.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,6e152cc7-e93a-4408-8092-236db420b680,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 219, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "05hq8v4ju1q79njz45lwlti5v.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dkhkp10elyre43sbqzangzn0e.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9ad37173-e640-4b9e-a474-716d482b73d2,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1115, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "71zptzc6ptipah0zjqi4oj5ks.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,lgqLGi01o02ZFC9Xs5Ofdg,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:23.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1311, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c2wrex5zlcm4q7bek6fuavelp.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,61VZkFjB402brzWm4UsftA,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:02.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bstnvjdujs0tgbj3uwztvija9.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,", + "FriendlyName": "Archer Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 262, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "btgunfy4izmpoj8xglwadngpl.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,47da86b9-4e6e-423c-b8c4-ea6bf8472509,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 263, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "883f69uc5v73zvor8ltld7zdx.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,d5987fee-e3cc-4336-853a-5df57f73df05,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 264, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5a2i66dhqnlvimed4a3fjhz2j.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,0c433086-9501-48c4-8d0e-4474eb420c61,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 265, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78f5tat5pkmywligx33rcvv4k.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,2be2f040-68e4-44dd-9099-29f11110f3a4,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1102, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dclo54kfdapgc9n3yba702l93.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,1VGZ1QCvcU6rsKVkUDBNjQ,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:36.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1317, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1v4tdvaof4avz08bp7oapq1nq.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,8hUBfmq28EixYjv92Xcrgg,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:42.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 125, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "32boa62st14xrnw6alwpdqer3.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,", + "FriendlyName": "Archer Quiver (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 126, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1d7kcdxyqd6vbcvooyqhqiqsv.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,d1eb3573-672c-4bb4-9ec5-7c81e6a38d3d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 127, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "65ob47rf57lzttas5qiw96qkt.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,e548124c-d7f3-45de-aaa0-f66130eca576,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 128, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dakgj68r389mpgtl45ly2nsvo.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,7221132e-053a-4c58-b719-ed3c8558907d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "46p3j6coqok4330sha7pm7whe.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,74b009f4-e128-4c59-8a6a-4c0ba6119bde,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1116, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "88qgugcpk7v2ra2fi4pz1ehjy.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,29EZ6r2SsUakWjzuevxeoA,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:32.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1307, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a87ioalwyvn2xvaj4bhy5mu0n.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,imDGL6dhrka-KliYlfpdgw,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:42.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "xIvzfECzA02itIo1Gx1FnQ.png", + "AvatarItemDesc": "c65a94e6-7475-4030-8fcd-9ce25852ff7c,", + "FriendlyName": "Arrow Through Head Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c65a94e6-7475-4030-8fcd-9ce25852ff7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:44.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 364, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "49e7ejka5p8qmdd0wplyjic3s.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,", + "FriendlyName": "Artist Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:04.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 406, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eiioy82z0xbvbvebsm9vamslv.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,8c94e45c-6605-4b4f-9ba4-2c6c6e557228,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "FriendlyName": "Artist Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:32.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 407, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7e9wiybe217zugyt61b0i6ten.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,17b16982-871c-476a-860f-f6953c89ee8c,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "FriendlyName": "Artist Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:33.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 358, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "buiszvrl96xas7c9f7b7i84ia.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,", + "FriendlyName": "Artist Beret (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:56.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 428, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c8eo8kzis4cfksqzadwx60q7a.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,14b43bf9-5ba0-484e-ae5d-55cfeb09a70d,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "FriendlyName": "Artist Beret (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:55.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 429, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "er774snrrar0qxts7cl1d2zb0.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,5c8f36c5-07da-49f4-af02-d23207a43216,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "FriendlyName": "Artist Beret (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:56.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a95a1b36-7849-4791-89be-c1a56868bfc5,", + "FriendlyName": "Artsy Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a95a1b36-7849-4791-89be-c1a56868bfc5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1538583493 + }, + { + "AvatarItemId": 1849, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "emuh8dfzl0l4dji5yae6lwwde.png", + "AvatarItemDesc": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,", + "FriendlyName": "Ascot", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:14.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,ffUVovxlWUCgOxJyUJscIA", + "FriendlyName": "Ascot Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1091914702 + }, + { + "AvatarItemId": 134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "60g0w2ayv9lwn8fokpqhccd0m.png", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,", + "FriendlyName": "Astronaut Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4pm9zzbb13116s8257iggug13.png", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,9GhnX4Pj4E-wxbPlD8GB-g,YSrGwXybGEqaqHNCS64qOA", + "FriendlyName": "Astronaut Suit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:30.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1938, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6z6iwsstjeii7vhx905bsn4tj.png", + "AvatarItemDesc": "ae7c9d1f-d371-4dfe-a6df-d74662606aae,", + "FriendlyName": "Stranded Astronaut Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae7c9d1f-d371-4dfe-a6df-d74662606aae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:36:40.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ae7c9d1f-d371-4dfe-a6df-d74662606aae,i9rUo62Tgk-UaM9BsYRGgQ", + "FriendlyName": "Astronaut Stranded Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae7c9d1f-d371-4dfe-a6df-d74662606aae", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1162904659 + }, + { + "AvatarItemId": 266, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h8xjhjeecs5u49k0pqh1txaz.png", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,", + "FriendlyName": "Astronaut Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 801, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ayc9cl76c3xmxyeui2n4vtju.png", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,y_gTbnuYS0GKJmnCs2retw,hjk9OO8CW0KNGR7A6_p84A", + "FriendlyName": "Astronaut Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:43.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,LqBO6GM7F0mGswRqJqCbtg", + "FriendlyName": "Astronaut Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1310840186 + }, + { + "AvatarItemId": 38, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3xwp4xtloierefgfubh97k778.png", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,", + "FriendlyName": "Astronaut Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f218e2de-46fd-45b9-9e0e-670b6bb905e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ovfqg649aj942uo3a5rqkbni.png", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,JFalLDUvm0aMm3wh1UVfVg,iQjkS1xYP0S63ltvKpkz5g", + "FriendlyName": "Astronaut Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f218e2de-46fd-45b9-9e0e-670b6bb905e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:47.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "66bfec71-882b-4981-9d02-9c03d8d79789,", + "FriendlyName": "Astronaut Helmet Hat Sglitch", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66bfec71-882b-4981-9d02-9c03d8d79789", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1672693583 + }, + { + "AvatarItemId": 1933, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0w9bwz7zburoq47xylvojlem9.png", + "AvatarItemDesc": "eacebd1a-070e-4733-a6b6-65811ef7891a,", + "FriendlyName": "Stranded Astronaut Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eacebd1a-070e-4733-a6b6-65811ef7891a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:34:19.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2063, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "hItB5Kz2jkyqLdN93apHFQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,", + "FriendlyName": "Women's Athleisure Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:13.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2769, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wKJw4wkhlE2Io6Asemls-Q.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,QF05cIuqhEeY8oNnO9CocA", + "FriendlyName": "Floral Athleisure Crop Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:10.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2770, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Lt9m7wE1U0-91--MzopOoQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,FbedbSonT0izB7Q6cLXCzw", + "FriendlyName": "Orange Floral Athleisure Crop Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:17.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2771, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XNVa9yB1X0SSTUTytHozCQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,SoQerU2dOUu9BdRBAsEllA", + "FriendlyName": "Floral Athleisure Crop Top (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:21.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "8ae154ba-8e69-48da-a314-507b419d4338,", + "FriendlyName": "Atlantis Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ae154ba-8e69-48da-a314-507b419d4338", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1123636091 + }, + { + "AvatarItemDesc": "27fa9f11-5368-4f6c-a114-18501478a9fa,", + "FriendlyName": "Atlantis Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27fa9f11-5368-4f6c-a114-18501478a9fa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1068640728 + }, + { + "AvatarItemDesc": "546ca714-8355-4455-be44-d7987974f8af,", + "FriendlyName": "Atlantis Trident Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "546ca714-8355-4455-be44-d7987974f8af", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1015465370 + }, + { + "AvatarItemDesc": "ee835538-058e-43d9-8268-b7d079563799,", + "FriendlyName": "Atlantis Warrior Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee835538-058e-43d9-8268-b7d079563799", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1924464343 + }, + { + "AvatarItemDesc": "f10f2d46-ec03-452b-9b94-0e476ccbb9c6,", + "FriendlyName": "Atlantis Warrior Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f10f2d46-ec03-452b-9b94-0e476ccbb9c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1699140506 + }, + { + "AvatarItemDesc": "a112b8b5-ea0e-4d23-a7eb-2daa4683a87c,", + "FriendlyName": "Atlantis Warrior Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a112b8b5-ea0e-4d23-a7eb-2daa4683a87c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1473012152 + }, + { + "AvatarItemDesc": "05809f10-98b7-4228-8828-e72cb37fe70f,", + "FriendlyName": "Autumn Colors Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05809f10-98b7-4228-8828-e72cb37fe70f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1575197462 + }, + { + "AvatarItemDesc": "2036fbec-e562-43a7-bcf1-c12f0307131a,", + "FriendlyName": "Autumn Colors Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2036fbec-e562-43a7-bcf1-c12f0307131a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1242079628 + }, + { + "AvatarItemDesc": "9d630b9b-7f48-4f34-880c-f52bad628b25,", + "FriendlyName": "Autumn Colors Cuff Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d630b9b-7f48-4f34-880c-f52bad628b25", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1240388911 + }, + { + "AvatarItemId": 379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzqcpgtdl5etxzj1p61vv6r5r.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,", + "FriendlyName": "Aviator Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:17.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 399, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cq2ejdon4vbkzqptgu6lhugwi.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,a8de80d3-286a-4fdc-bed2-75d52f8c0964,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:23.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 400, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ewo7xaztrkgxbdwc8nnspdmtl.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,c6cf096c-f0cb-4500-80a0-bc2916b5a426,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:24.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1606, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vm41bj3a6vlqwhygscxqlcy8.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,FjnZ-D1hcESLOWL4Ukv7kw,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:36.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 383, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bqf55zk90gccgpaf57wtsf14z.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,", + "FriendlyName": "Aviator Coat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:23.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 408, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "66bl5lmlw2ayp67bzz7mlitzm.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,da185fbb-ad2b-4f30-87a2-491f24b78ebb,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:34.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 409, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c32bq37y8bertjy6ivb388xbe.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,bea795cc-aabd-4229-84dd-49ab4f91a445,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:35.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1611, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2tmvikftvfs2sv1ll3oaqfq0w.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,2APfqVnnQUaDYKi-fQg8jg,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:20.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 384, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dik18b9lkprego4zu9vx7mv75.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,", + "FriendlyName": "Aviator Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:24.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 410, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cfko6vz2smqr4u7q3b5jh5jvh.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,97bbb384-b37e-4ea1-902c-88fbd6854ab5,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:36.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 411, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c6s4wxr4cgcoqmqlgwz47lrnh.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7fb8496f-0e6c-4e48-b8fa-5709ebbf4820,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:37.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1613, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "68cuiknni4ah1844qwlndids2.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7nRz9Jmnn066_PzngkhfFA,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:35.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 381, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xipn9pseh8wykz3t6hpadi3l.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,", + "FriendlyName": "Aviator Goggles (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:21.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 401, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dzmu6jtu8g9jr5k1y002d2m6v.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,7b82c3c5-fd1f-45ec-b83f-e32668aa8dce,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:25.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 402, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "edzgkcu9p46xum15ttgjb0r3p.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,23428ca9-be75-4146-b400-df01d1dadac9,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:27.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1629, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bx1ejsvvshgpo945a7kst7w1x.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,qJGX62O_HUiZWN61NAc1Lg,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:54.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 135, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6sqmpmy9umgo11ba977kv392b.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,", + "FriendlyName": "Babydoll Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 136, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9qamxgi6spdwdg7gwy4xnedzt.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,d6823e01-69f0-4f85-b94a-74894356a2cf,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 137, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c24u162frxoa2ztfi76luw9ji.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c5deba2a-6e35-4b13-8e94-8ba5457f39df,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 138, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9zwdqgsqcsxt5ced3hkhejxc2.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,e13cf33d-7588-4e2e-955e-f0a29d6380b7,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 514, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6ag9z8xgaq11l42y9relrmux1.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c9589974-c261-485e-b571-cb2f34fb178f,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-20T20:52:13.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 660, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7f7hajiw2luckub1h51u4co33.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,da6d0ced-3d43-4878-a868-925f2ed6fd5b,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:26.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2893, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HsPbJKuTm0WZI6Of5AnXQA.png", + "AvatarItemDesc": "c2d22328-b41a-4e53-9b1e-814e84b86d15,", + "FriendlyName": "Back Kite", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2d22328-b41a-4e53-9b1e-814e84b86d15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-24T17:08:22.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1dbc517c-432b-4c85-8c5e-dfcf22b269cb,", + "FriendlyName": "Back Scythe Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1dbc517c-432b-4c85-8c5e-dfcf22b269cb", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1359892989 + }, + { + "AvatarItemId": 1762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58mihxk3jk5j3t6ag197iz7x8.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,", + "FriendlyName": "Backsword (Leathered Iron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-12T21:10:22.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1886, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8him9qwzellzbybe456axlevp.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,GfC01_o8QkGTbnGNjv_DSw,7mLb5jMJMUug7Nzwxv6D4A", + "FriendlyName": "Backsword (Wolf Steel)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:16.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2114, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "u0lK1AO41UOMSqQHLjVEEw.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,rFCKF2ku1UWj-TF-VClPRg", + "FriendlyName": "Backsword (Cricket)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:23.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2441, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "45ZUSM5vSES1q6qkk-rkFQ.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,zld80uf3y0uu__Yejms-3Q", + "FriendlyName": "Backsword (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:15.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1570227863 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,c2e62c10-3120-4991-9aa7-3f3e2cd29054", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1413041026 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,b56bf738-4c41-4169-bee5-b3e36db6d6a9", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1874084400 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,96585250-512f-4559-9526-1ef77356a72e", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1481339357 + }, + { + "AvatarItemId": 1455, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d93bbdei1z4uj950nvf3c8ai6.png", + "AvatarItemDesc": "49800740-33d6-4280-aefb-3497597c6366,", + "FriendlyName": "Folded Bandana (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49800740-33d6-4280-aefb-3497597c6366", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:22.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2075, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "QBEmq47zQ0ipqcJfw_re5Q.png", + "AvatarItemDesc": "49800740-33d6-4280-aefb-3497597c6366,0oMo4q5cLEi9LGpkOFIlkA", + "FriendlyName": "Riveter Bandana", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49800740-33d6-4280-aefb-3497597c6366", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:10.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2365, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "mrQZPItuWEiqGyXjajov1Q.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,", + "FriendlyName": "Banjo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:05.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2366, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "6Y0DhxPd1UaxX3rZPdXFYQ.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,7ykIpgU8Tkmmq_zzpjTVrg", + "FriendlyName": "Banjo (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:08.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2367, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "I6hIZL0Kp0CyPWnLkxSSMg.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,iqYc9qtbZEig8jl_ZqKe6w", + "FriendlyName": "Banjo (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:10.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2258, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SBxpBk0gcUG34NYQ8jtNvw.png", + "AvatarItemDesc": "764b4c4e-1009-4772-b01f-7b326dd77e8a,", + "FriendlyName": "Banker Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "764b4c4e-1009-4772-b01f-7b326dd77e8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:00.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2772, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BRPCDGHJ1USSO-gAt69-HQ.png", + "AvatarItemDesc": "764b4c4e-1009-4772-b01f-7b326dd77e8a,93vga8NJQEe5nWdqrroAaw", + "FriendlyName": "Banker Vest (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "764b4c4e-1009-4772-b01f-7b326dd77e8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:24.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2259, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "n1YJ_xf3VE6EzTlwUlW_9g.png", + "AvatarItemDesc": "62e480de-27e9-4719-adac-360698c7a2c1,", + "FriendlyName": "Banker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62e480de-27e9-4719-adac-360698c7a2c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:04.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2815, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "oea3WEkmiU6otRXRPGNm3w.png", + "AvatarItemDesc": "879c43e8-c877-4c8e-a5c9-100123e7a102,", + "FriendlyName": "Banker Hat (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "879c43e8-c877-4c8e-a5c9-100123e7a102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:35.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2260, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9Wd8fqP_uUygfK_VJAt3UA.png", + "AvatarItemDesc": "7d6cf363-76ba-45c8-91f1-cf4434c31b36,", + "FriendlyName": "Banker Monocle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7d6cf363-76ba-45c8-91f1-cf4434c31b36", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:05.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "amQEmFoB-EK_ExgRPUaVoQ.png", + "AvatarItemDesc": "1a0319e4-24e7-40b3-9976-70bff2c7ff23,", + "FriendlyName": "Banker Neck Tie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a0319e4-24e7-40b3-9976-70bff2c7ff23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:07.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1677, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0j5eiu15ludyj18o9vlghi84f.png", + "AvatarItemDesc": "07023692-97e1-4a65-a881-64ca6b4ae08e,", + "FriendlyName": "Barbarian Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07023692-97e1-4a65-a881-64ca6b4ae08e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:28.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1676, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7m1uy36svx5mpua6eq7senkzm.png", + "AvatarItemDesc": "e48955ae-50e0-4f55-93a1-611150142331,", + "FriendlyName": "Barbarian Tunic", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e48955ae-50e0-4f55-93a1-611150142331", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:24.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chch6uszf5qnxlwl6jtw9bftb.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,", + "FriendlyName": "Barbershop Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:28:57.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 412, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d67bfbfq6h2c493xoldetiunp.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,631c9015-2a00-47ea-bb35-465a2ddbca5d,83158223-e268-4036-8a6a-218a8430eb2c", + "FriendlyName": "Barbershop Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:19.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 413, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4d6mxhehvygwsuycquilcwmtt.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,7f9cb939-36a4-40da-8337-025e1d7bf8e5,83158223-e268-4036-8a6a-218a8430eb2c", + "FriendlyName": "Barbershop Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:20.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 387, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "224pkbmy07eojqcchbf4djy4z.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,", + "FriendlyName": "Barbershop Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:28:59.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 414, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "93dtf3uavctg3si13zdcnelxe.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,a5925738-a451-4f25-b622-ccbfa12bd88c,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "FriendlyName": "Barbershop Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:22.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 415, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cw4kg51jcq1ugso9kcmngkt04.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,c64ac7a9-7fd4-4587-8626-0413a190b706,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "FriendlyName": "Barbershop Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:23.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 388, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "24zf9tyok63h84td17n6zo3j5.png", + "AvatarItemDesc": "ce81210f-8eb8-4cd2-95d5-046d4da229c8,", + "FriendlyName": "Barbershop Wrist (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce81210f-8eb8-4cd2-95d5-046d4da229c8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:29:00.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 416, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "06z3ng7dlwz8wcsxzadsfgslx.png", + "AvatarItemDesc": "ce81210f-8eb8-4cd2-95d5-046d4da229c8,bb01b51e-a95f-441e-8c52-abc3aeed7145,f7c7d05f-5454-445a-af38-25f110bd204a", + "FriendlyName": "Barbershop Wrist (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce81210f-8eb8-4cd2-95d5-046d4da229c8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:24.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5687a39e-e5d7-40e4-9b32-190754fc6292,", + "FriendlyName": "Barbie Western Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5687a39e-e5d7-40e4-9b32-190754fc6292", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1670250213 + }, + { + "AvatarItemDesc": "5687a39e-e5d7-40e4-9b32-190754fc6292,km94i3lHJUWfjKgHrV-mHQ", + "FriendlyName": "Barbie Western Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5687a39e-e5d7-40e4-9b32-190754fc6292", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1077124656 + }, + { + "AvatarItemDesc": "7b7574f3-0b0a-4954-a644-6473ddb86424,", + "FriendlyName": "Barbie Western Ken Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b7574f3-0b0a-4954-a644-6473ddb86424", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1076331846 + }, + { + "AvatarItemDesc": "d85c010f-891e-4ce2-a9b8-ed68874da228,", + "FriendlyName": "Barbie Western Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d85c010f-891e-4ce2-a9b8-ed68874da228", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1008995980 + }, + { + "AvatarItemDesc": "0952eb17-9779-490a-98fd-af734a698c76,", + "FriendlyName": "Barbie Western Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0952eb17-9779-490a-98fd-af734a698c76", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1951232994 + }, + { + "AvatarItemDesc": "0952eb17-9779-490a-98fd-af734a698c76,uQcf_scBfU-XAii9HExRfQ", + "FriendlyName": "Barbie Western Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0952eb17-9779-490a-98fd-af734a698c76", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1279100943 + }, + { + "AvatarItemId": 1887, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25kjk74xrqchihponvtrfcese.png", + "AvatarItemDesc": "f23e86eb-8b59-4b32-a72a-5d382568e399,", + "FriendlyName": "Bard's Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f23e86eb-8b59-4b32-a72a-5d382568e399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:20.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2503, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VHA9zLkhZ0uAaJkYQaimwQ.png", + "AvatarItemDesc": "f23e86eb-8b59-4b32-a72a-5d382568e399,wuCrGxBRQUa5BLwx1nASPw", + "FriendlyName": "Bard's Vest (Solstice Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f23e86eb-8b59-4b32-a72a-5d382568e399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:50.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1888, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dub0ri6t7szkccf54a8e3p9ef.png", + "AvatarItemDesc": "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26,", + "FriendlyName": "Bard's Cuffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:24.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1885, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7ixk1u407fxtupt3m3sjujxlx.png", + "AvatarItemDesc": "b17591ba-3c2d-4309-ab01-b6743424e07e,", + "FriendlyName": "Bard's Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b17591ba-3c2d-4309-ab01-b6743424e07e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:05:57.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2794, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "l1uILA2f4EKvZEBtGvSETQ.png", + "AvatarItemDesc": "b17591ba-3c2d-4309-ab01-b6743424e07e,l9cF1s3B8EC6BGlQ4Tx1cg", + "FriendlyName": "Bard's Hat (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b17591ba-3c2d-4309-ab01-b6743424e07e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:13.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2268, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aSaV2nvIFkOp_qOuz0uFBA.png", + "AvatarItemDesc": "8f137b1f-4125-4657-85c4-bcdb74d3b0c2,", + "FriendlyName": "Barrel Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8f137b1f-4125-4657-85c4-bcdb74d3b0c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:08.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 26, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4rmm71n7z0wmlx9lj3u30z4u0.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (White))", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1217, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "593u0j7z80kmuoamluu1rn0dg.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "FriendlyName": "Graphic Cap (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:03.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1630, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3lp8gu18idyrp1ify9n7g7h8m.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "FriendlyName": "Graphic Cap (Buckaneer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:59.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1216, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "d45mukwdi8abvszf0t0hbk5hr.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,nZor-clxlkm8EO9Gco0Otg,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Red, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:59.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1633, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9x0xbf4dhn0sn8fttdfpdd2k2.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,M8IYDK6aTUyn5mRhr51GtA,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Orange, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:18.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1632, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ajy5peougbbudsm27960iaohq.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,yxWEn-oaYE2YvvnrTVV5WQ,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Turquoise, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:16.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1631, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dospnezxsaqyxcdej4ac9k08b.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,XLYLo5qgW0eociGDgQ16PA,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Yellow, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:14.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1707, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "59nj2htuon9c6gj41ptaat7jk.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,Vk1n941ZgkKrhrL9mh18oA,lxmw7EbzsUuoy0roRy5McQ", + "FriendlyName": "Graphic Cap (Minitron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:17.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2g7h0rh45zr980awo5cvp4si2.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,mdSSJyipq0KvjDNQ2NyUaw,LQheTa-EvUaAonSyoWNmXg", + "FriendlyName": "Graphic Cap (Holographic)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T23:41:16.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2003, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8jcewsfim13hla1ex6nlwucmp.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,nzFUAOCAFUCd9S-x4EKRwQ,Qx7nrZH-K06CgIoJYfFCCQ", + "FriendlyName": "Baseball Cap (Heart)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:53.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2172, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YHvgN-qla0-yF9DyWIkm1g.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,xPs8BvGgdUC1i_luWAfSFA", + "FriendlyName": "NBA 75th Anniversary Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:21.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2773, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-lxSQ88W0UOAP-8tne9rSw.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,cfiCxhJZ20u07X1x93tNZQ", + "FriendlyName": "Baseball Hat (QuinnXCII)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:27.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1186165494, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BJUjvFUapUqyNI-sHp5Plw.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,VB_2uEBs70-9MnLn3__upw", + "FriendlyName": "Skater Hat (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.492778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1712, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0jjswupors8a1sy83fp1oeipi.png", + "AvatarItemDesc": "ee9a7c8a-7383-4a09-b44c-474b66f9008a,", + "FriendlyName": "Robo Logo Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee9a7c8a-7383-4a09-b44c-474b66f9008a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:08.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1713, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "26f4i1hdx3pb8xotdo0vkhqr9.png", + "AvatarItemDesc": "ee9a7c8a-7383-4a09-b44c-474b66f9008a,W-xMu8n2CUuHSXfHvEL4yw,-apO3Y7om0GKhC4V_hVYdQ", + "FriendlyName": "Robo Logo Cap (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee9a7c8a-7383-4a09-b44c-474b66f9008a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1218, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9gmjkk497nr0pq75gtfcilfjl.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,", + "FriendlyName": "Graphic Cap (High Fashion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:10.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1219, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bfdnrz82ejhtasrdzh7azcyxx.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "FriendlyName": "Graphic Cap (Sticker, Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:15.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1721, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cllwjm7phajd4j2pjnk1lq87l.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "FriendlyName": "Graphic Cap (Buckaneer, Collector's Edition)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:31.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3cnf1qorsglce2vtvvj1nf4cz.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,cpTKHXjj0kulivZCKVQqiA,Mrf17aidXEeCF-fFURUUcQ", + "FriendlyName": "Graphic Cap (Hyper Ramen)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:20.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2221, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IDVmIRptBkSiYWCmC3zeVA.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,-C1ui-T280GEfW6-vPQ-ig", + "FriendlyName": "Flat Brim Hat (Basketball)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-24T19:42:09.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 39, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e9shr5go0vsw8gb8dght6710s.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,", + "FriendlyName": "Baseball Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 984, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6at7h1u119kqoj0j8z06fflxl.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,FVdDQFOMQ06teQm2ANDPng", + "FriendlyName": "Baseball Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:44.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 985, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ampb4pvmmtdhc5913do72skwc.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,FVdDQFOMQ06teQm2ANDPng", + "FriendlyName": "Baseball Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:46.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 139, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "703se3u45kueznyorg28dx2d4.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,", + "FriendlyName": "Baseball Jersey (White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 492, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b124kxp8adqvgs4bryq7tcrra.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,e5de2e45-207c-46af-9a5e-eb01a0c621fe,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (White, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:15.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 493, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "coknzs3avuwcbyl6z2drrxtd1.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,de0a5a3a-8aff-4799-8269-9efa3aeb383a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:16.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 478, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4ur6eolr90837mh7229goao2z.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-10T16:50:09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 477, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5iy6yfd9qvpfzdo6symnk32xf.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-10T16:50:08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1736, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e9d1k2tttj2ohpp10c8fp9uu1.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,LIgagNf4gk68EYQdlDruxg,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:07.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1739, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a6xlnnqmochthrsk0xmuemrpf.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,6fmUy748hU2dgwgaijSkUA,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:17.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1741, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "48ag7b3c5m2vy68rclwrb8km0.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,buuNFd8sK0eap78TL1gMNQ,-UFHTNvJs0CGDStb0PFkbQ", + "FriendlyName": "Basic Belt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:23.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1743, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "05r4o0sr1cfy4hmhwsw9aaygk.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,SDbpnjVYmEGQneK5fhZ0Cw,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:36.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1746, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1z2bq777lr7jukwio4h4ahhcr.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,uwoWPY4YjEmZSQcNCrx1Gg,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:42.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1747, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b5f3a10evhggitywma8wlxt69.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,COZbkLDc20mdUlWSkqMX-w,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:44.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 140, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dyz7t3e9p724lfy2lqohcvl0m.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,", + "FriendlyName": "Basketball Jersey (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 650, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2z5f34xrfn5i5rko8oh08suh0.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,59a62fa3-9e15-463e-9bd5-3aecdf604c1b,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:35.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 651, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5csh82iknt6zuuty86qfpx32k.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,4effd67c-a52f-4b41-a5b0-04287f91a7bc,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:37.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1728, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4nvt3kwtn2k367794r49ve87e.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,ojTIShRZHEOV1qQRfhwvTA,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:41.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1727, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "24n1sqxebe37pk9ina0smo1nn.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,se0-2ylLD0u6sqCVPkeP6A,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:35.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1726, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "86hv8nip4c5kphcs19ulpi7xv.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,RU-B71GUF0WPduoSOI1XMg,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:25.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1909, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "2nbi9m6b47r4wal4q24bnxfms.png", + "AvatarItemDesc": "0736e71b-a32b-4485-bedf-6fde8fe3b09f,", + "FriendlyName": "Bat Hair Bow", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0736e71b-a32b-4485-bedf-6fde8fe3b09f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:03:59.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1908, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "1snahzhx4gvw2hmv9vx8ves5r.png", + "AvatarItemDesc": "01d0fc12-3ebe-42e1-8caa-5513a40436c1,", + "FriendlyName": "Bat Bowtie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01d0fc12-3ebe-42e1-8caa-5513a40436c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:03:50.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1927, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7s85boxheb6528qy06bkgcocn.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,", + "FriendlyName": "Bat Costume Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:56:23.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2521, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "PImwtIHMIkmPg7xHGM7PXQ.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,GPX92X5n3UWMbh3C13oB5Q", + "FriendlyName": "Bat Costume Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:35.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2522, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6wBcnTssRk-eCxqSkfa3Mw.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,dE_ZWKPV90utoyWtRalh8w", + "FriendlyName": "Green Bat Costume Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:38.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2523, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "ERAg0XXt4EicefSqiFHYcQ.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,jIAMeDkA80GVHYTi0d9LkQ", + "FriendlyName": "White Bat Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:41.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1926, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "cheqz81i6ydobk59q4ybrjfks.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,", + "FriendlyName": "Bat Costume Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:55:55.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2516, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "lQoHL1kpXEKcBNWXhquQXw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,mnYzlq3Rw0-Q89gmc5f9XA", + "FriendlyName": "Bat Costume Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:21.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2517, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "kWsDoa_x1UmDw33BY2rMCw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,85pvpo4QqUSrdsbofGkoWg", + "FriendlyName": "Green Bat Costume Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:23.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2518, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "TP2iURkjWE2-TVyQJGWkVw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,f6XXbe_mhEuZJ9in5vDRGw", + "FriendlyName": "Bat Costume Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:26.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1928, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "5ox8ama5khsmyb0okqnvem5p2.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,", + "FriendlyName": "Bat Costume Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:56:39.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2528, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "R-4c5-nUAkebQ9WzWNLZgQ.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,oO_Ij0Vsvki4Fa6qonrf7A", + "FriendlyName": "Bat Costume Wings (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:59.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2529, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "-KAlFceGmEy5iT39k9-pNQ.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,IgocvfGIn0u_Jq4GP2_fJg", + "FriendlyName": "Green Bat Costume Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:00.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2530, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "DdcTk6htq0eP97IwF_w0mA.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,Q2BNGA-idUyHDPu7m-ZSnA", + "FriendlyName": "White Bat Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:02.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1819, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f4jxrcx7v17e4u9uskgyxjvje.png", + "AvatarItemDesc": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,", + "FriendlyName": "Sarong Swimsuit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:16.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2238, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "CbY0IoFre0CFmDX2p82E4g.png", + "AvatarItemDesc": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,PNnfMibAUkqetPThlhtBgw", + "FriendlyName": "Sarong Swimsuit (Cactus)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:43.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9cqrtp4vgci8mnuccke5s2itk.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,", + "FriendlyName": "Towel Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:29.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2016, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HTVSEzDsKUSnkvdGvplfvg.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,Hae8Pfe8mEWeF8mL9hC9dw,Vat8nL4ZBEK9mLABJiXS6A", + "FriendlyName": "The Amazings Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:40.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2015, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "S0ChF4sn40ewzPHkvLcNGA.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,kE9vr2bKoUSFm7AkcXSmew,5o-249dUdUOhW28lrJY82A", + "FriendlyName": "Valor Society Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:37.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2014, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2IWuPA8HGU6KrdC4ohARXQ.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,GAzH2pThjECghCmCwKJPrQ,4T-xj-xtSEiv1PDYv30URg", + "FriendlyName": "The Defenders Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:36.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2013, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "avkdszecpez6tb8kq3563bp59.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,pEWioft3gkOMWS4tZFping,gOw6NOX-QUegt4mxjtfpfA", + "FriendlyName": "Masterminds Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:34.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2181, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pyxx1jg0F0qzsZMmUcX81w.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,SN8HDRADZ0GvrFgEHKMhWQ", + "FriendlyName": "Towel Cape (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:39.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,Im55dSJ4mkmdmHPtX9tYpg", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1727884092 + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,adETWZLIA0O0feRzUjxKQw", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1454267065 + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,z7uu8CimMkCjsCGTDPCZLw", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1018984627 + }, + { + "AvatarItemId": 1400, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2jzqx1rojlzf9q9pado9u0t1o.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,", + "FriendlyName": "Brimmed Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:48.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1401, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "110fo1snzkucd8yr0l83dt96u.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,-XSM6ZcSjE-vrWL7jyF4fQ,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:50.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1402, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5tpxbss93q52nayhe1d5fpao8.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,RL6HkufaJ0yubTpCGVI9eQ,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:51.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1403, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7k8ttzh9wehwhm80g5p4gopa0.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,9tTuREkoQkW0ycqOjWXsiA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:52.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1404, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cijefzd028p1ktkkbndlk0iio.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,MVkY4sU5BkGC4QhweRiyIA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:54.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1405, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9bijtfphf1w416loccnpc2v4m.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,ux9DQVSRyU-_5GA-t197OA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:55.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1406, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bl1xc0woq0y8j4zowmiyc2wbz.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,t2bByt5oUEG90PMMePCP6A,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:57.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1407, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "340d5n3lde4t69b8l5kc2jupr.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,m7KXVnULz0iHvKfCFzFoyw,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:58.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1408, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7bp85mlgk50a43bna39drnbwj.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,gT0WA7Y1H0my5uMlteIMMg,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:59.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2093, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "CCiQ0TXblUORW6PKW2gHkg.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,Hd9xNn-UA0uxwv1axFVqGQ", + "FriendlyName": "Brimmed Beanie (Pastel Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:22.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2094, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "nJX4hvcYAEaOsI07CK_m0w.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,C8b4lTZ-TEOlCVv7Gs219g", + "FriendlyName": "Beanie with Brim Holiday Market", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:28.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2095, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8i3Shpwj70mbaFZ0_YazXg.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,3Xlg70ev6UqdcIAmJ9hmlA", + "FriendlyName": "Brimmed Beanie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:30.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2774, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ODGgLfHg60m9YanIDHhtGA.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,QCOMyeCUK0Gudx4ljBmAdw", + "FriendlyName": "Beanie (Musical Notes)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:29.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1389, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3kk89nprwfzgkc47ddr46nppo.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,", + "FriendlyName": "Beanie (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:43:56.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1390, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0si8omfozqsiv0jjns5uz7x3v.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,58YffnGSUU6umcI1i0RvZw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:43:59.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1391, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6go7wf3t618gcga8rjoywcwo1.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,c24f1DNZjEmqkt-FibKUjQ,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:02.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1392, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "clgnc91prro3ozkp7tnx5ld66.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,SeVmHtrMS0y7gKCYkHcrOA,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:04.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1393, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "59xbft3ixffciz9wybly1ufd4.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,uDcJZClcFkiNdkEEh9IIyA,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:06.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1394, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2rnnyghpth3r3etbu364c02mc.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,Iezz6gCFfkKdVcmPXIhunw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:07.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1395, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e4rjntdbfbgxnzzds63z8k3i7.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,fLWfBSEO6U6aHrDm3nKFEQ,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:09.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1396, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2ejkmb3z1znhlpdst7q3jztb2.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,F5vJzMfNB0Ob6Abjadho7Q,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:10.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1397, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7gi71ssytha4oltxzdg3vekb0.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,vbOUaJsGHEaSfQCivNcMWg,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:12.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1398, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5p5se3p4opi1nd646csh6pvsd.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,wbFPXIg5gUK3QK6YycZexg,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:14.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1399, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6w4cwzijj4bww69aju5fi9pzj.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,_fBrHCkTKUe82QgxCyI-Kw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:15.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2219, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zsqqOW8x1EqW9fjjiRFwTw.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,CJKB3OO2CkeEVsAK0jeeDA", + "FriendlyName": "Beanie (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:06.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2438, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7kcx60xKr0C5aWqnubk3Og.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,k-WZIhNJAkSpIWLxC2D40Q", + "FriendlyName": "Pumpkin Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:06.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2439, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "j73o6WaQmEubCEKCMVcsLQ.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,VE_cHMHKBE2Wuugoqj4rZg", + "FriendlyName": "Pumpkin Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:10.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 369, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1jj51yv6mj3i8duvbctn67g5j.png", + "AvatarItemDesc": "7553d851-1986-4325-870d-e425dfb7d007,", + "FriendlyName": "Farmer Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7553d851-1986-4325-870d-e425dfb7d007", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:09.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1281, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e8i3fa3rlphdsfofxs7sewwio.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,", + "FriendlyName": "Bear Hug Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:23.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1556, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6rqv598elyp668kqusa9g2dxe.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,OnZujqqU_UqwxmznvBBMfg,_zrnGn1ErEWR8WqrZmKZsA", + "FriendlyName": "Bear Hug Hat (Holiday Panda)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:31.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1557, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ujfxsx62pzx2i549y1zix2uy.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,XKoxnTXYGkSUu52nVS9dnA,TFOAk7kwh0uUgtsyDNxMuQ", + "FriendlyName": "Bear Hug Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:33.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2102, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CneUY-gB-U2whOHJXkONBw.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,9nPWmpM4mUmEpHfthqYeng", + "FriendlyName": "Honeybear Hug Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:13.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1187, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ubi3qjigszacxmdn65q4d2nj.png", + "AvatarItemDesc": "2786fc22-3d6c-440d-afcc-15890c061577,", + "FriendlyName": "Bee Antenna", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2786fc22-3d6c-440d-afcc-15890c061577", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:52.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2775, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TgcLGc1A7UK1C6_N3Au8WA.png", + "AvatarItemDesc": "2786fc22-3d6c-440d-afcc-15890c061577,dXz9B9OH2UuLNV35u8KB-A", + "FriendlyName": "Ladybug Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2786fc22-3d6c-440d-afcc-15890c061577", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:31.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1186, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3w6s9ko5eqhqffltdu2xx937j.png", + "AvatarItemDesc": "bb8602d4-f91f-4e35-9c88-1cdf3a04ff0a,", + "FriendlyName": "Bee Body", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb8602d4-f91f-4e35-9c88-1cdf3a04ff0a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:48.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1188, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ex0h92b7vqpk3ao6jhdf8acdf.png", + "AvatarItemDesc": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,", + "FriendlyName": "Bee Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:58.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2776, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NvZjNy5k-k6-IOXr5Mm2TA.png", + "AvatarItemDesc": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,USdUyjIzY0q5af3AeTFCOQ", + "FriendlyName": "Ladybug Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:33.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2816, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GTnJ7Ox8b0SRwSSDw8LESQ.png", + "AvatarItemDesc": "df29d81e-851b-40b8-95ae-8461f702b1e0,", + "FriendlyName": "Beehive Beanie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df29d81e-851b-40b8-95ae-8461f702b1e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:37.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d0s9wj7bjb9h01c8w7xwma53o.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,", + "FriendlyName": "Beekeeper Veil (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:48.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 696, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zg7qszxejhn7ysl2ofksqed8.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,1919d319-6cce-4500-8766-15fed6a13fa8,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:45.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 926, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2zodiht9cu9f4628u37rle1f0.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,lBe3yxuE40eBmTrnEHBqQQ,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:00.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 925, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "28ohmms47updexq8mqh9ey2ih.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,oZ8k0Qv3SkG-DkY0_dP7UA,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:59.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "67ho8qppd1uvf14zlw9l7bdmg.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,", + "FriendlyName": "Beekeeper Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:50.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2k4lcmck9k4oohw7zr54f80ke.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,1919d319-6cce-4500-8766-15fed6a13fa8,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:50.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4if4tnkvzu35yc8tyran086ks.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,lBe3yxuE40eBmTrnEHBqQQ,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:19.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 891, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bbjryyglvkoz6f7rpa627np9b.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,oZ8k0Qv3SkG-DkY0_dP7UA,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:17.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 710, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "36kccfcoypwfay2raguim9kb9.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,", + "FriendlyName": "Beekeeper Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:52.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 703, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dqln9eqsu4l7dk5wpe8ad2i8c.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,1919d319-6cce-4500-8766-15fed6a13fa8,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:56.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 927, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "82jnl4qpioioo5e84i408z96v.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,lBe3yxuE40eBmTrnEHBqQQ,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:03.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 928, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "egoh9oarmlgl1e2wh5az6t7vu.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,oZ8k0Qv3SkG-DkY0_dP7UA,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:04.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 40, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8zfbgqf2y36bwmt9v0iq15tuj.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,", + "FriendlyName": "Beret (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1601, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cpgewpuv8enuu5r81mj51rdpb.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,7_n11PF4-0KqTZ9az23hqg,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:21.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1602, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5sln9xpcozzr4gxw3l8dxk5cs.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,TKbjEeTWfUyA1u4laoULMg,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:23.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1603, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "61ormf3tp9m7rfze5jen89ywn.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,JShcoWWPz0297HeEUJbDCQ,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:27.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1604, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "16amrlrlhliiat2ld7vswaurs.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,_psc7WCgOk2JzRQwuFAqNw,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:30.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1605, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4b809ml4l1gchsyq9ftgm08bj.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,WeP_tPjUNUuGojXrVBOAIA,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:32.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2519, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r4KMHNh_x0-32HKgY1U3sQ.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,8xqL--ghKUunlS0r4ugCgg", + "FriendlyName": "Mushroom Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:29.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 41, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6bwvu5xxyxigc6heyrh7sw2bi.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,", + "FriendlyName": "Bike Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 483, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2mdqjp0pan01n0jy9wmab9gka.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,83522c12-9549-4d74-8c87-bae210bcd2bf,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:02.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 484, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bfdtco5996aiaxyo9h38yw0b7.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:03.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 850, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d8wwf7fmiptl4zd7nj52mqemx.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,XNq4Cns4lEGydtktdsXlmQ,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-05T20:03:02.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 267, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "28jeraj9pdlhoxmjt0mhkqvj7.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,", + "FriendlyName": "Bike Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 929, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "30u7o3e8jtfekidt2dw5uia5v.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:05.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 930, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "53455700060fde41p5z5thud2.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,XNq4Cns4lEGydtktdsXlmQ,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:06.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 931, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5tnpmoxpb4gujzn1apm2ywi3z.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,83522c12-9549-4d74-8c87-bae210bcd2bf,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:07.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2024, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X4W5cZITW06S0a_Bere_PQ.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,qspNifi92keI-XssPB5qDA,RVraJOPsOUqZjd8bEiPduQ", + "FriendlyName": "Bike Gloves (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:11.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 141, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "do070rgyuzlvn3h08jelsszce.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,", + "FriendlyName": "Bike Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 494, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1yuq0u2hdapelj37471lkep1i.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,c5884778-a87f-4612-9717-86fbb65d440f,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:17.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 495, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4guodx87w200pz0ev441o50w0.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,83522c12-9549-4d74-8c87-bae210bcd2bf,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:18.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 799, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "13zz3iuzmt1r8kfhefj6kqj9h.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,XNq4Cns4lEGydtktdsXlmQ,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:38.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 751, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3m8jywcsatm5rq2um5dbimatv.png", + "AvatarItemDesc": "n52um7A-b0utGxixd1ESvA,", + "FriendlyName": "Biker Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bae9d9f-3eb0-4b6f-ad1b-18b1775112bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:22.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 749, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dyz4zg15sdjewdf5zjojjk36p.png", + "AvatarItemDesc": "_qXaNbm4yEeyalo372QbvA,", + "FriendlyName": "Biker Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35daa5fe-b8b9-47c8-b26a-5a37ef641bbc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:19.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 746, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1shya0fxztg1hrivkr6kz5t5s.png", + "AvatarItemDesc": "VQnoLstCyEGDPy1c38-GvA,", + "FriendlyName": "Biker Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ee80955-42cb-41c8-833f-2d5cdfcf86bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:07.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dfffe31f-9f82-4622-a920-ea5f5bd26042,", + "FriendlyName": "Binoculars Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfffe31f-9f82-4622-a920-ea5f5bd26042", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1059720796 + }, + { + "AvatarItemId": 1682, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9gv2f7uy1uo763i2urf7qdtys.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,", + "FriendlyName": "Birthstone Earrings (June Pearl)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:24.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1683, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "awvrj98971hzwbeeg8i9zxo3l.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,mM7I9kDTx0ut243UkWKXvw,YKaJjmMHo02FWwem8nw1yw", + "FriendlyName": "Birthstone Earrings (March Aquamarine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:25.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1684, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ekdcofkpc4pqcwpk0rwxxdu21.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,46f7Ewe99E6tF2qFaMtQ0g,8IK1U23XlUOV-2ibVznkDQ", + "FriendlyName": "Birthstone Earrings (April Diamond)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:27.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1685, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4xaaxp8lgcjr1lrwq0jnqoxbt.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,R0f3hHGY9UqUEW4aR1fssA,KSqfmVTiGUKOKijwGDSwgQ", + "FriendlyName": "Birthstone Earrings (May Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:28.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1686, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2qnbcqo4a9vldg7eer5ofwli9.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,adwzLAQEykK8D5hZGQ6yiA,0Nca5nBUpUquFhExd5dxcQ", + "FriendlyName": "Birthstone Earrings (July Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:33.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1687, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3ap2lnn1cayd34hkw01ody958.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,ZsWc8wRafEGl3B7cr6UkzA,ByIfwCtDwU-p9rEfqpjfGg", + "FriendlyName": "Birthstone Earrings (August Peridot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:38.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1688, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4z8ff179kc1znimkl5hhclu3c.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,kQD41xK4dU6dI_6pRkP0qA,aSeusgYlPE6vOBu9Ua_dWA", + "FriendlyName": "Birthstone Earrings (September Sapphire)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:40.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1689, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "62x43h2lnvgu2pa5ubjuz4yuk.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,ro4sD9txVE29_eT2jK-wEg,0YF5bACMsk2sN4tixbJbKw", + "FriendlyName": "Birthstone Earrings (October Opal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:45.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1690, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "17kngr9n3ywkzhzibzp9gckn0.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,-YsxhNXLAk2qbeXMqwAqng,aNOhXS4nuUeqs0CVw6K7Hg", + "FriendlyName": "Birthstone Earrings (November Citrine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:51.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1691, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1o20o1igl0kxcykasd3bf7drr.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,imOiYzABkUm9C5EVzvjXgg,0eZVIsv7wUyZ0ohLmF1jGQ", + "FriendlyName": "Birthstone Earrings (December Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:19.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1692, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b4372447x75hrt7zmp36xexi3.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,FgLAEMh1e0GyHO8J8_xj5Q,iM-04HpG6k2N8n-P3XS22w", + "FriendlyName": "Birthstone Earrings (January Garnet)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:22.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1693, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "51ybcms7jhdsyx03odmfrhned.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,C9CZUwEZeUiuOue4CK4luA,gUxl7WNXlUqFYHZ9eGq1cg", + "FriendlyName": "Birthstone Earrings (February Amethyst)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:26.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 74, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "a1hubxyh2x781ngkvrnl6q4r2.png", + "AvatarItemDesc": "b861e5f3-fc6d-43b3-9861-c1b45cb493a8,", + "FriendlyName": "Bishop Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b861e5f3-fc6d-43b3-9861-c1b45cb493a8", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 221, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "c7r9nusyu8a3jy7ydrv4shwx2.png", + "AvatarItemDesc": "6930ce13-4be4-4ab9-9817-667bd261ffc3,", + "FriendlyName": "Bishop Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6930ce13-4be4-4ab9-9817-667bd261ffc3", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 268, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "d5ga9td4yt4xjgrj7q9m03toz.png", + "AvatarItemDesc": "abc25091-ed5f-4c72-9364-fffeef1bc239,", + "FriendlyName": "STOP", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "abc25091-ed5f-4c72-9364-fffeef1bc239", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RWXXh3Ixg0KSe82fQIeKiw.png", + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,", + "FriendlyName": "Bladed Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:39.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2552, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "te1b6n09g0WjqI3yH5n3mg.png", + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,fWZekjhIxUWhxgVGTWrVsg", + "FriendlyName": "Midnight Gala Bladed Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:38.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,a7477071-b04f-4b9e-9fcd-bd44991e5f0c", + "FriendlyName": "Bladed Wings Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1516632647 + }, + { + "AvatarItemId": 2447, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uF47LVvccUucyESgIuygpw.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,", + "FriendlyName": "Blaster Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:09.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2471, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "oLjvlMCC2kuFc3_lVEfvEA.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,BizZfhG8ikOOEvAxN6w48A", + "FriendlyName": "Blaster Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:07.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2450, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6JCWD1J9U-qP46fezaoXQ.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,KfjLWNUbpEClo_qafEfUEw", + "FriendlyName": "Blaster Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:23.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2448, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KX8r_kEiAU2h5vaiH70O2g.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,zJKd2ZtIsUGCdHP1M0215Q", + "FriendlyName": "Blaster Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:16.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2451, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "P9xsKr4uU0mPWfnYZEGItQ.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,Adyl5MkwQEKK_MT5AluYkA", + "FriendlyName": "Blaster Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:27.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2449, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "JYKGsy8zbE-uVRh5F0aELw.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,xtgcJ8klHUyAYRntKhwCzw", + "FriendlyName": "Blaster Suit Hat) Blaster Suit Hat Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:21:20.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2452, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZQvc0FZMVUKVsARripsYXA.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,", + "FriendlyName": "Blaster Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:31.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2454, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HE3yVvkZXE294hudT6aXLQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,bgWM6wBe60q2mIHN5dPh4Q", + "FriendlyName": "Blaster Suit (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:39.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2425, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QVu4sV34w0mJ62ZfIhLifQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,cqYUoDpfkEi8Qp2DrpQytA", + "FriendlyName": "Blaster Suit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:21.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2453, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CY_yVMmPd0SsiIdZ5xLZfw.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,lRmNfk8vuEe4Z5ToWvWcTg", + "FriendlyName": "Blaster Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:34.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2455, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wa-FDCaLx0er4dEZFzygbw.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eQy0qwre-Ui7_sCCNuWCSw", + "FriendlyName": "Blaster Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:42.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2472, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yD1_cAEdh0a6Uf6vIWKVyQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eU2ePFNmbUCPoQUp1WCbzQ", + "FriendlyName": "Blaster Suit Shirt) Blaster Suit Shirt Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:23:11.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2456, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "zmUtvtALhEyonXMMbmhMLQ.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,", + "FriendlyName": "Blaster Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:46.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2458, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OlimAxzdbEWXp8C2t0UdRw.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,csLWYuunH0K9Ij1q1FoBsg", + "FriendlyName": "Blaster Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:51.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2460, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "C3a5a-EHuUeFBNVj8c4TcA.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,Zf0z5Ghkwk-QJJq4u7Fjww", + "FriendlyName": "Blaster Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:56.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2457, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a8n5oQkGBU6RKd7IECdwjA.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,sP8lTqgdk0WyuwQczT_ijg", + "FriendlyName": "Blaster Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:49.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2461, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0aPPOEDh1EqRvNey2G6Buw.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,dudShaXMYkaIZdZfyleoXw", + "FriendlyName": "Blaster Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:58.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2459, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "swrVud60vU-52WTaynUCqQ.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,3EOUuzxLSkSopjIohgQ-zQ", + "FriendlyName": "Blaster Suit Wrist) Blaster Suit Wrist Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:21:53.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1254, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "holidays", + "ThumbnailImage": "d57lxs90awzdwpbei5f4j6nz8.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,_ycDQ6RcyEGAntVlSDpPeA,6UBZuZQBG0eZtNmty2IGdA", + "FriendlyName": "Blazer (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:42.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1352, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "10hg6w8e7ui5q5yod1bb5dhab.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,7WrSbK4Ml0K2z-ig5HIUCw,9skD_xxLq0mpfP13f9n8hQ", + "FriendlyName": "Blazer (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:21.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1918, + "AvatarItemType": 0, + "Tooltip": "Available until 10/25", + "Rarity": 50, + "ThumbnailImage": "dwz0pxz74xy4zmr653hor487a.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,uCA4tnv9Mk2mX-NnFlj2Mg,NvlKJFz4IkKDliVOJKJYhA", + "FriendlyName": "Hallow's Blazer (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:20:50.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1929, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dswjk6ox1i8gqtdq0td0c43qy.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,YlgQOY1VJUuGzYxoTYAqGg,jhQ23U8Pmk2K2wfBdrEWNA", + "FriendlyName": "Snowflake Blazer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:59:25.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1338, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5g3kf0itlpuocmwiedgwzxbrn.png", + "AvatarItemDesc": "20b424a9-861a-4d39-a583-22d7b7348c14,", + "FriendlyName": "Bog Monster Hood", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20b424a9-861a-4d39-a583-22d7b7348c14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:59.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1654, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9uu4lgs1wp05xxg31w3ixcj1t.png", + "AvatarItemDesc": "20b424a9-861a-4d39-a583-22d7b7348c14,afnIqin4lkKKqWrvBvVagA,bCuW1YadE06p6v1dBHsx4w", + "FriendlyName": "Bog Monster Hood (Movie Magic Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20b424a9-861a-4d39-a583-22d7b7348c14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:50.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1336, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cdcrail4xl68zz5i57rfmgvcm.png", + "AvatarItemDesc": "dedd2e98-2721-4de6-92f2-9d399f34e8fe,", + "FriendlyName": "Bog Monster Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dedd2e98-2721-4de6-92f2-9d399f34e8fe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:46.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1653, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7jabhuhr3ufptld365gkfrn58.png", + "AvatarItemDesc": "dedd2e98-2721-4de6-92f2-9d399f34e8fe,2jj5lxx6Gka5U6j0cWjN5w,HQnd9m0Wi0upaXRNsE5HPQ", + "FriendlyName": "Bog Monster Suit (Movie Magic Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dedd2e98-2721-4de6-92f2-9d399f34e8fe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:45.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1337, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "15ql8y6860hwqib0t4k041550.png", + "AvatarItemDesc": "4e42e02e-33fd-4ff9-b74e-9bde26253c35,", + "FriendlyName": "Bog Monster Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4e42e02e-33fd-4ff9-b74e-9bde26253c35", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:52.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2462, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "QQLvWQYc7Uuml1bMBoD0UQ.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,", + "FriendlyName": "Book of Elseware Vol I* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:00.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2463, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "UV1LMH8or0KUAZAidj0x-Q.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,bpzKQ-B4_0KpMPf4ixTumw", + "FriendlyName": "Book of Elseware Vol III (Cursed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:03.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2466, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "vHLg9EajA02BxJcYdCE1Iw.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,14Gft1zCQECXPxPmiUc5VQ", + "FriendlyName": "Cosmic Cultist Book", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:11.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2464, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "kXsMoNdwF0e5eg7JqgZnMA.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,7ziZgKKqXkuyo9v77chIlA", + "FriendlyName": "Cipher of Elseware (Uncursed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:05.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2465, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "DFFMheRvREahgpIp_KezuA.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,hKkdXrgEQ0SStVnZsXu0uQ", + "FriendlyName": "Book of Elseware Vol II* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:08.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2734, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "BwziYgoOMkSILcJf4wBllg.png", + "AvatarItemDesc": "3b30adc7-a1ad-46ff-96ff-08e656396c23,", + "FriendlyName": "Boombox*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b30adc7-a1ad-46ff-96ff-08e656396c23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:35.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1549, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0rwlmd8pei5yizua0rge6be77.png", + "AvatarItemDesc": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,", + "FriendlyName": "Bounty Hunter Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:00.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1932, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7hb7tjten2kt2wn72jc6b3djj.png", + "AvatarItemDesc": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,szDKGCS30UCwXJJcVocFDQ,3v_zUVDTUku_iEf-olDk1w", + "FriendlyName": "Bounty Hunter Belt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-01T21:38:20.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1550, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wi7442sqsy6n251dpxoai2q1.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,", + "FriendlyName": "Bounty Hunter Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:03.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1976, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e82j0afggi0d70h9zv4jq8z5q.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,Q3ctsvvDDEy_S1TDiGdf6w,DobTmYhC7EigHmvYnCTm3w", + "FriendlyName": "Bounty Hunter Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-29T17:07:39.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ayapiP7cAE2QSEcl72wYQw.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,2N0meQR4GEGu9Yy61C0Lwg", + "FriendlyName": "Bounty Hunter Cape (Western)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:06.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1552, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1kkukibngh4tv7hnld1z6zmzk.png", + "AvatarItemDesc": "2ddd2c2b-596c-4022-a488-655b0db1c265,", + "FriendlyName": "Bounty Hunter Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ddd2c2b-596c-4022-a488-655b0db1c265", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:09.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1548, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3j8vqf9bf8mtlc1esp7pjg52y.png", + "AvatarItemDesc": "2fadddc1-babd-4cb3-8f5a-0e7c889c3785,", + "FriendlyName": "Bounty Hunter Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2fadddc1-babd-4cb3-8f5a-0e7c889c3785", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:39:57.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1551, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9sotdnybus40og7ig3za9imke.png", + "AvatarItemDesc": "9ffa6e06-5200-4e53-ac13-f8491aecc864,", + "FriendlyName": "Bounty Hunter Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ffa6e06-5200-4e53-ac13-f8491aecc864", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:07.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 42, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73yfl2xb8qwuws91gzirri5dd.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,", + "FriendlyName": "Bow (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 629, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3suvedjn7frrhme4rxyn56qtt.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,c9589974-c261-485e-b571-cb2f34fb178f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Bow (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:50.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 630, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8hxvd0blvnfweb21vtd3vyy7o.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:51.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 631, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2d53cleqqzz1la8h0m130s58q.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Bow (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:52.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1192, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ftlnff1zjo948sndiuh1x636.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,", + "FriendlyName": "Bowler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:16.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2206, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Z8BRM2D_akKVUKgLncneKA.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,YBpBFNAFGEunM5GX5lFxHg", + "FriendlyName": "Bowler Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:19.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2208, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "wztK_Auwy0SJicMfhG1M-w.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,VyqdOGaUnEa8WzccUIn48w", + "FriendlyName": "Bowler Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:24.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2207, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "w97a0JIhSkGDeHj7x9Tx8Q.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,Dvwz4YRu_ES_rl94y6FNBA", + "FriendlyName": "Bowler Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:22.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 812, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9mi7d2raqzh1s4hk8ya5hwblu.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,", + "FriendlyName": "Bowling Shirt (Turkey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:17.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 811, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5qcfg0698s80wf9u8vhxu0ykx.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,-k9_-2onrEG407or8t1RFw,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Hambone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:13.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 890, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uwainjpb5gco3zqlgd0ofvkm.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,GDvaUU10-EKh3pKUQ9tnag,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Runway)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:16.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 889, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73mnfpvyaf58qnzfmjq9gdmfp.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,kLyM-6otSUy2ql5SEmnF9Q,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Spared)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:15.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ewgjupwpbyyvetn7svdbg3pka.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,8xjU4GQ4OkaqGyad-ypwtw,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:20.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 888, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ib04jli4dwo8vm3pm3qmk46c.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,032pE8s9Ik-2CImFVDrSqg,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:13.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 887, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a8ua2rvt9gw4r4necvamkx7sz.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,M4765UHzsUeNTwH-Pcxp1g,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:12.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1161, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ddse4tp99yydkypotqc8d4hpg.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,FC8ZFBPt4EyTe7IYbv2Tpg,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:03:06.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 814, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2sj414tpbb69wiylcanfba7e7.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,ghM-uFq5-0Ci38AOHa9zZA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Deadwood)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:22.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 886, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e46hwhpywl6eqefwj0lqe1mah.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,Gtq8yRlt1UeFW8U-sc7FiA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Brooklyn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:11.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 885, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0t1wg2mro2gwc3d4flp6re5pk.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,XrSL-v2MbkK0md-FMOweOA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Sleeper)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:09.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 884, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "apdshw5awntmir8e59mptvuwj.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,KqjQ1xRpokGLKp9GVN1EFA,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Striker)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:07.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 883, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3lw7exr10ocq54wlzytnktw7h.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yrQCLXiqRUSwnuQRYX2Rsw,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:06.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 882, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f20xxqkrb5z55ft9ia6vtt5u4.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yZR5aX3gQ0WpRUT6cfK1cA,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Split)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:04.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 825, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2rglb359vpaaizusjkmzxzqiu.png", + "AvatarItemDesc": "GAWURQSqrEW4G5slrPMgZA,", + "FriendlyName": "Dude Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45940518-aa04-45ac-b81b-9b25acf32064", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:20.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 113, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0ua96avoonx4qcwxsrbdl8lqb.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,", + "FriendlyName": "Bowtie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 442, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1ipmq038lb20nmgpf0k7p3lj8.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:06.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 443, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ec965jtympxjh0juzh6uea66i.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:07.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 444, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ey39vqepkupw80nkatijbvxm6.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e69372eb-2fd8-4e17-a448-ee0be61a2c7a,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:08.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 648, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "7lwepk9pha13ajtddc8yjp6i5.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Bowtie (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:32.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1064, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "88tf66ieaq5s0d6ezi59mgoni.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e4IDo-fuH0C9YByzcX5k-A,qCIoeLQw-Uq-SKIetIgIpA", + "FriendlyName": "Soda Clerk Bow Tie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:35.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1250, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "holidays", + "ThumbnailImage": "6ypl01cto10al3qpkqm95fhe3.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,W4a144so90eRzCojUnDowA,rcuK1wZcXkCe0_LGX7E-Tw", + "FriendlyName": "Bow Tie (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:24.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2789, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DIaJORsIX0GaMSsklR3SxQ.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,wF-WrDaY_kmm1AzA20RsZw", + "FriendlyName": "Bow Tie (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:00.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,Q9TGwLgrgk2t8ba8WlpM1w", + "FriendlyName": "Bowtie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1726152636 + }, + { + "AvatarItemId": 1268, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bdv0ee6pbzi0i8z8f5n5zfc2n.png", + "AvatarItemDesc": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,", + "FriendlyName": "Bow Tie (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:47.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1615, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9oo75aubpgwd257zfkh94c7y8.png", + "AvatarItemDesc": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,Wm30oV5BtEuFQZFk3dPK_w,0R3Mh96tS0631EZYs03dmw", + "FriendlyName": "Bow Tie (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:50.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1354, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8z118afr4npaz1lqtevl4cq9f.png", + "AvatarItemDesc": "86af3a97-18d8-4dda-a2f2-cfd3b4224254,", + "FriendlyName": "Bow Tie (Green Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86af3a97-18d8-4dda-a2f2-cfd3b4224254", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:42.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1266, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8nu7ynhikwf10a7dnyr1bqmi7.png", + "AvatarItemDesc": "988e4069-4341-428b-bbf5-023309891385,", + "FriendlyName": "Bow Tie (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "988e4069-4341-428b-bbf5-023309891385", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:39.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0mfqwinkpsyr6llxo7yocofz3.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,", + "FriendlyName": "Boxing Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 437, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dwa5wuhnhdrvml7z2kour4vi.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "FriendlyName": "Boxing Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:05.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 881, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a9oypi8hweb517wrupg36vi1w.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,rzUd1IMTKUmW0HeMPSIOBQ,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "FriendlyName": "Boxing Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:03.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a39t7yom90yxeiwitw6n8iohp.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,", + "FriendlyName": "Boxing Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 438, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3tg5nlsq7otflej53cfartbv3.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "FriendlyName": "Boxing Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:07.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 932, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0to9e0lty6offdt6cnxeq1qpk.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,rzUd1IMTKUmW0HeMPSIOBQ,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "FriendlyName": "Boxing Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:08.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 43, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2c8gnq9h5a6jrn81ru4497q6i.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,", + "FriendlyName": "Boxing Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 430, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cwp28pmovv17mrz6o4w62ejpa.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "FriendlyName": "Boxing Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:57.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 918, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0oaynqpwbmosrcxdikxyfnnm7.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,rzUd1IMTKUmW0HeMPSIOBQ,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "FriendlyName": "Boxing Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:52.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2829, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "l6JiqgrjoU6Pe9nqG_A2Yw.png", + "AvatarItemDesc": "b9987d0c-0cbf-41dc-9a9c-8fdb5165800e,", + "FriendlyName": "Braided Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9987d0c-0cbf-41dc-9a9c-8fdb5165800e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-28T20:16:50.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 823, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9fef5u01k6k4fcki20zg26pey.png", + "AvatarItemDesc": "sL7iHCnjyEuQKzrTvciEzQ,", + "FriendlyName": "Bride of Frank Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ce2beb0-e329-4bc8-902b-3ad3bdc884cd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:52:39.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 824, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "239w9hcx69t1gd8xuj8wathwz.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,", + "FriendlyName": "Bride of Frank Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:17.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1528, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6jn69aku0kec2denwy9axd25m.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,YTR8qybyVkOhNh4iqtU_UQ,H8LXGRJj8UOuH5dil8L9qw", + "FriendlyName": "Crimson Bride Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:15.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6sLaNK31602XAlV-PLUz2Q.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,zNBLaOhCAEKWvgLK4P_X7w", + "FriendlyName": "Mardi Gras Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:10.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e7zfauqbveb04wiazlcnt3nna.png", + "AvatarItemDesc": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,", + "FriendlyName": "Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T20:59:28.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,97f3e262-8bac-4238-9d85-3872f293f204", + "FriendlyName": "Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1497412622 + }, + { + "AvatarItemId": 2290, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "AlSw_umA9E-_tWoN6PsFsg.png", + "AvatarItemDesc": "aeda4e5d-9cfc-44ec-b325-ab8b2d906a8f,", + "FriendlyName": "Bullwhip Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aeda4e5d-9cfc-44ec-b325-ab8b2d906a8f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:35.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 622, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1m40flxsxhcbfatcc8mhbk1kx.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,", + "FriendlyName": "Bunny Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-27T21:42:46.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1950, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4o8g6zqfewfnr504cvcbano60.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,wZC46s7Id0CUICN_XOK-Rg,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:30.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1951, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a0g5v9gzs5d8fpdvoeh8rvqbm.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,yrR3ogjJ50q6ypAnCF-ctQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:34.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1952, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b45xlo71b6cuovl9no7k2cacs.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,ss931x_Hy0-y1au-fT3zTQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:41.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1953, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "95hz7svaey618hpsf9c4or7xo.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,UQfLaKHzo0WqlRul_O61xQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:44.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 454, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e0ix17ip3n8ghjd6zp8tpdsz1.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:19.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 455, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "47orvw4rgb2dcq2uysth7k5kz.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:20.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 456, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9govgvnlf9gabvjvh5rckwqt2.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:21.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 457, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1eq0w8wx46ey92gsw096z68mj.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:22.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 458, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "863juzzox17unyjhagvnbslfh.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:23.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 649, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dftbch1qkdqq457hq33rhhz9h.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,0809a5cf-a0f3-4614-9f98-71518524518a,8212812b-e7e7-4f6d-aa27-d1112af45473", + "FriendlyName": "Business Tie (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:33.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 968, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7x53ntzd6jkkh7xk2h9lwypz0.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,31jelNWFyk6FlWt4HyPAlw,9zGwmle_yk25V-KnC_PJEg", + "FriendlyName": "Business Tie (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:13:07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1251, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "holidays", + "ThumbnailImage": "22ppyftjkt057kgxnmn84jhax.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,W4a144so90eRzCojUnDowA,pHw_MghIYUazSFve6L237w", + "FriendlyName": "Business Tie (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:28.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1902, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "f3b04h0f6kp8xvajy1fhvj8se.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,3svqbYmGt0uyVWnx7vJNmw,iLo1YhFY-06HNWgM6gkB7g", + "FriendlyName": "Business Tie (Neighborly)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:30:59.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1362, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7l8qp4zd4ankofzt9s3j5k8c6.png", + "AvatarItemDesc": "fc96ea5d-b49b-479b-a963-92f1eab44b2c,", + "FriendlyName": "Butterfly Wings (Cool Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc96ea5d-b49b-479b-a963-92f1eab44b2c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:10.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2105, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ypuue6OM0Si-gMkXnm_LA.png", + "AvatarItemDesc": "fc96ea5d-b49b-479b-a963-92f1eab44b2c,opDwtb3OdESsA2Zpfx7luQ", + "FriendlyName": "Moth Wings (Pepper)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc96ea5d-b49b-479b-a963-92f1eab44b2c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:17.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1717, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "345iaeu3y6o2ptwgo6fuggdrw.png", + "AvatarItemDesc": "ec5f5089-7154-425b-8be2-743954ea2d69,", + "FriendlyName": "Butterfly Wings (Crystal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5f5089-7154-425b-8be2-743954ea2d69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:55.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1364, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "99uel8r3j0b7wyrporms60tok.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,", + "FriendlyName": "Butterfly Wings (Monarch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:36.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1591, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1kwmi22q1osg0qzg3f3truukf.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,PTqpWqqU9k--lNlRrmlVbA,yViVKOLHc0qLiPw9Oy1Obg", + "FriendlyName": "Butterfly Wings (Candy Agrias)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:47.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1593, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4rdnvl3uo6e2xmx16jac16cg2.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,nODTglESMEC9tBY1k-LrsA,Do__oD53x0-dNtVeHsr4yA", + "FriendlyName": "Butterfly Wings (Midnight Gossamer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:15:33.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3kupnkadfcqst9wcpz79lvelg.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,1qHXl55Qn0qSb1gAhr8Wjg,GlB_scM-5kuc4A5TmGe6Yw", + "FriendlyName": "Butterfly Wings (Frontier Bluebottle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:15:25.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,10f0c6ce-319d-4231-ae88-446d33eead54", + "FriendlyName": "Butterfly Wings Monarch Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1092022807 + }, + { + "AvatarItemId": 1363, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4mbd5lxxwtmjvp5enggelazu8.png", + "AvatarItemDesc": "27c2eb7f-9fac-4c42-90c7-44e7a33d5303,", + "FriendlyName": "Butterfly Wings (The Blue One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27c2eb7f-9fac-4c42-90c7-44e7a33d5303", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:31.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "620934cd-aa73-42ec-97cd-65dd9eed1226,", + "FriendlyName": "Camera Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "620934cd-aa73-42ec-97cd-65dd9eed1226", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1929411673 + }, + { + "AvatarItemId": 2055, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "oROtFcbPVUKgu5jyR4cmxQ.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,", + "FriendlyName": "Oversized Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:43.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "rf7qGqus3keA_hz_CqguUw.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,bIQKJloWV0SEnBEtoxP_Wg", + "FriendlyName": "Candy Corn Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:28.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2387, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4wbSOraJ9UqV8L-NG1fvjA.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,kKpiR8YM8UG7xcaIgS9aXA", + "FriendlyName": "Oversized Cardigan (Forbidden)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:32.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2586, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2-BK_tv4akuy0tNJWv2PRA.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,W1LdNEXmX0m72Wr7EjJ_gQ", + "FriendlyName": "Oversized Cardigan (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:43:52.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1802535203, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pkxpJOu830etcGZdkiTlFw.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,oQspLDUDxEaiudqzXY7gbg", + "FriendlyName": "Oversized Cardigan (Painted Egg)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1320636581 + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,7d025ccf-f3b9-4601-9dba-9f25b135d11e", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1033755188 + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,2d035c24-8437-4403-a024-4c3bfd0d80a9", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1774245390 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1665457670 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,e5172c0a-3099-4e61-a181-f6b114b0a608", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1632878111 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,bab8193c-f67b-4022-834a-597cbf5dc7dc", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1186975696 + }, + { + "AvatarItemDesc": "5929bc1e-7ead-4818-a9ef-8e6d3bc885b3,", + "FriendlyName": "Casual Necklaces Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5929bc1e-7ead-4818-a9ef-8e6d3bc885b3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1738196188 + }, + { + "AvatarItemId": 2371, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "ZImIysD_H0S8AXDFmb2-vw.png", + "AvatarItemDesc": "1fab9cd8-257e-4825-8b7d-bd8bd281d362,", + "FriendlyName": "Science Shovel (Invasion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fab9cd8-257e-4825-8b7d-bd8bd281d362", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:26.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2700, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "n4mGv9jTj0GUDOgF6nFXVA.png", + "AvatarItemDesc": "1fab9cd8-257e-4825-8b7d-bd8bd281d362,q9hshPgwT0ijshnbKEBKWw", + "FriendlyName": "Science Shovel (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fab9cd8-257e-4825-8b7d-bd8bd281d362", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:31.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2086, + "AvatarItemType": 0, + "Tooltip": "This item may come in handy at concerts, afterparties, or in dark, forbidden lairs...", + "Rarity": 50, + "ThumbnailImage": "TU5LjIU2P0WNnRai4I1qHQ.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,", + "FriendlyName": "Cat Ear Headphones (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:05.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2098, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QRxUhTvrak6Pv3s0nde-8w.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,etbo7k9dq0OyjIrNS4sdkQ", + "FriendlyName": "Cat Ear Headphones (Neon Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:02.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2097, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ov9bwV4XmEuoyMkR3zIVgg.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,7Ww7mumWXkiyj0R3u_-hrA", + "FriendlyName": "Cat Ear Headphones (Glitch Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:02.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1207385977, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Cr0h8X1hoUuexTHM501mLA.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,8br3f77_CEiE2Oetiof1Pw", + "FriendlyName": "Cat Ear Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,8606473d-8d58-4e93-8123-3d6cd1d47ad4", + "FriendlyName": "Cat Ear Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1214875831 + }, + { + "AvatarItemId": 44, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a78j3mr78eeqcg6enr7t2usio.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,", + "FriendlyName": "Cat Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 712, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ee3wf3szcjj03dklm3r2yu3qx.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,37f8378b-b98c-477a-b8c9-9340772800b5,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:11.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 713, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e4o0sq9fto4a26lfver3ezwyq.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,713781e5-ce47-41c6-a24f-94d42a565e30,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:12.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 714, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cs7qiwunx61p8789gg5w4alhp.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:14.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 715, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9k2zja3ouictarzennr87c6lx.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,c1e430c5-fd8d-4032-9e3f-8d87cc8784da,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:15.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1750, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "18xiejb6mr7ln2d5ix9xk4i7f.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,", + "FriendlyName": "Alley Cat Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:28.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2174, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "319gh5kstwntozibq3ylve24n.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,dr6MrV8O30G0i-c7peOoKQ", + "FriendlyName": "Alley Cat Ears (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:25.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2173, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "432bfm0lzuskgxgwj4id7tebe.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,UcK6aipko0iE4THJUO1TvA", + "FriendlyName": "Alley Cat Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:23.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2175, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "10khixt04h6x1ktssnlbwt88h.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,9yNuA59JO0avaKVA6YxQyA", + "FriendlyName": "Alley Cat Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:27.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1716, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7c9vakaf9hozc3kfkfgvgows1.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,", + "FriendlyName": "Cat Hat (Calico)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:20.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1715, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "825i9txrb63ysexwqx6avbebh.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,V1M2-FXZSEG782d6D0AhUg", + "FriendlyName": "Cat Hat (Burmese)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1714, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4c5dywgjhkaf3fk8o08x91iz8.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,wNde5lsob06eQxEH2t-zUw,5KeLrnvaKEWnEiap1xzmjw", + "FriendlyName": "Cat Hat (Tabby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:14.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1942, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5xbgkkfn1legwnjtvtt5uvco3.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,T-JZqXUd3kC1P8L5Pu4ZmA,ao-eeUnb8ECcW2IFGuk76w", + "FriendlyName": "Tiger Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:27:29.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2479, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GsDtJeTdtUqMwfH0lH8bfg.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,NaXiRmxgkkGK1U7IQ4yn8w", + "FriendlyName": "Grinning Cat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:24.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2480, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HbIV9ocBIE6QOVO0wQaSrA.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,VeZQlPI72E6e57IQqAOq6w", + "FriendlyName": "Grinning Cat Hat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:27.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2467, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ufrfw4K9xkuYsOmmzZdOVg.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,", + "FriendlyName": "Chainsaw", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:13.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2470, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U51KTR3VHEuTJ810dMgdVA.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,qWS_znZq2km_MUGvv5QDHg", + "FriendlyName": "Red Chainsaw", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:22.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2469, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01j_eNlSvEmV2Czfp-GEwA.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,jCfhcP5ftk6_ruDnFjIhJA", + "FriendlyName": "Chainsaw (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:18.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2426, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DB_j3Vtx0U-xhO9yu4xlIw.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,5p1Hwa8IE0KIuucb7hV16w", + "FriendlyName": "Chainsaw (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:27.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2468, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T6Ig5zJQmkWoduzZdbHTKw.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,ZqWSyRnr9ECIvoU5yFyqXQ", + "FriendlyName": "Chainsaw (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:16.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1852235451, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "6uJcUEMxpEG8ljDFGcByMg.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,odtEahcf10isGbVQCyc2bg", + "FriendlyName": "Science Chainsaw (Restoration)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1982845644, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "a03e9576-acfc-48d1-82e4-4d320d151a50,", + "FriendlyName": "Chameleon Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a03e9576-acfc-48d1-82e4-4d320d151a50", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 142, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3josy4gpalnfb4bgbv15krqjb.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,", + "FriendlyName": "Cheerful Dress (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 431, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bwdc3w9brd906m9tlr3iwesz3.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:59.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 432, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1hw2ja5gln6hu18squbodnpaa.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,faedf215-289b-40f7-90cc-f5b98517d133,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:00.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 433, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aseuozf59hxlehklnyniqbre9.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,1773d5d8-4714-4721-b30c-97e25d6f2a5a,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:01.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1598, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8z3e22ju1mrd78t30pzbm5z5l.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,05K7dmjigkCByO2ufw9jtw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:05.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1599, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "56blq2erf400cehs8x4zcnwrf.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,yOw1WqEN0EWqR9t53EsQpw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:08.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1600, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cs5izn6ddok4ol512yitiw3cs.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,qoVZuGa5NkOmyfikHjDhXw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:12.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1366, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eum5bolzoy8k5w31k9j0rtzcj.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,yDZFr27Zq0-JH-EsEsxG2g,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:44.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1367, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eppwkjhvfbuczv31sgbty26v0.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,PLRhiOhhb0KhatFH7f3pwA,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:50.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1368, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8q1o0y04vs1ve8bpxz6iqkwui.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,RwgnKTfMEkWrAp_OMGy__Q,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:56.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1137, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e5wnr991h4k0p24z7l5fzcchr.png", + "AvatarItemDesc": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,", + "FriendlyName": "Chef Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1573, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6da55yu9ap4a2qw81ndsgake8.png", + "AvatarItemDesc": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,PR5dh2jf2kqGLO0lXgA69A,-lYK7LhYyEy6wqL1tIE9cg", + "FriendlyName": "Chef Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:48.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1141, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5bz83i3j02o47w4zlz433lomz.png", + "AvatarItemDesc": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,", + "FriendlyName": "Chef Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:35.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1574, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9lxxqrqcchmxcnzz3uf7de4kv.png", + "AvatarItemDesc": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,PR5dh2jf2kqGLO0lXgA69A,VDn1hO6ahky0GDJt5dRmyg", + "FriendlyName": "Chef Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:55.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ogxBM3YvhUu3E19PxYAYwQ.png", + "AvatarItemDesc": "9f2cf07e-f98f-48bc-9c74-784ba4b94527,", + "FriendlyName": "Chess Knight Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9f2cf07e-f98f-48bc-9c74-784ba4b94527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:15:33.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ZkEpc01eUGaW4hzohQxlw.png", + "AvatarItemDesc": "c2362f96-0a73-451b-9a85-46410eefd2ce,", + "FriendlyName": "Chess Knight Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2362f96-0a73-451b-9a85-46410eefd2ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:15:35.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1751, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "az5ok7syap6nxgjegr2yu68bu.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,", + "FriendlyName": "Chicken Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:41.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2500, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_inbDi877kyEOl5VH0d_AQ.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,wl3nqp4FsU2wqEkYFqv3_Q", + "FriendlyName": "Chicken Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:44.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2499, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PO9FzkR9AkerfmOa9FPB6w.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,4v1-TIIgnU-sg5ROppnEPQ", + "FriendlyName": "Chicken Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:42.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2501, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Gh8G1oJ4Zkmt6qsBqaj_XA.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,AvIuVibXj0u7UzWFh90_LA", + "FriendlyName": "Pink Chicken Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:46.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2502, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aRh-bOy1702NIh5iAUUksg.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,fF9qwnew3Eao2gF0xcOqrw", + "FriendlyName": "Chicken Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:48.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1299568996, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PPwy0yri-EypHFSutVm7-A.png", + "AvatarItemDesc": "0058a7c6-04ca-4931-b2ff-03c46f99079f,", + "FriendlyName": "Eggshell Chick Beanie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0058a7c6-04ca-4931-b2ff-03c46f99079f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2870, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DI6H0HT2tUqy7jNzWn9WDg.png", + "AvatarItemDesc": "6a51d8e1-8844-4999-ad8b-06f4d414571e,", + "FriendlyName": "Eggshell Chick Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a51d8e1-8844-4999-ad8b-06f4d414571e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:30.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2871, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "iF4tjDkdYEe3NUEZrY7znA.png", + "AvatarItemDesc": "9eb95934-90f3-4f48-a6b4-d87d75d9bdc4,", + "FriendlyName": "Eggshell Chick Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb95934-90f3-4f48-a6b4-d87d75d9bdc4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:34.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1965, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5gq339wje3p8y6ppfyku4gwyt.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,", + "FriendlyName": "Chunky Scarf (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:14.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1966, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "015zn2g9spl2iit7pplji0g62.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,RvVVu7gWUEmofuJiZqevlg,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:19.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1967, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a3r79cjrswbxqhnvo4sy9xfav.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,g4_xqu9o9US9lIiBsn2BKQ,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Olive)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:22.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1968, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6cyfecuvuiz6xxc0mom9o8a12.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,09zFwIT2AEaQsijLJR2aSw,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:25.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1969, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2wll5s9pcyyfdicg3i1sgsbf4.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,mtIyC_me2EuHSur1ghsKDA,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:27.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1891, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6qgeth2im91d0g6kce2zm8k7h.png", + "AvatarItemDesc": "b1ca1dc4-6867-48b6-a332-4efff06a5db1,", + "FriendlyName": "Cleric Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ca1dc4-6867-48b6-a332-4efff06a5db1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:10.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2yzkfucefna1jy3vuypzb7b3y.png", + "AvatarItemDesc": "b73ae23b-c911-4b09-a803-40999bdef6b8,", + "FriendlyName": "Cleric Headpiece", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b73ae23b-c911-4b09-a803-40999bdef6b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:15.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1893, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ugmcn0xr6htk34ah2hhep52p.png", + "AvatarItemDesc": "1bd891b1-6ec0-4227-9ed8-73c59af4f819,", + "FriendlyName": "Cleric Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1bd891b1-6ec0-4227-9ed8-73c59af4f819", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:25.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 45, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2q5g72y38c6wxtl9ob1g6osjo.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,", + "FriendlyName": "Cloche (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1634, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3ft3mh0blr3stuvbs8p0kyll2.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,w9I-I0GWRE-3RgOUMMlHjg,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:43.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1635, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8dwxff103s58nebb8y2kbn194.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,bmi_S166JEGWhf_7woVL0A,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:45.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1636, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b8f53f91dy30khotxt5f1ktnl.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,dSEiNUAZnUmErO5Vfv51zg,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:54.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1637, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0sm192b6zl94b0y64i6sh5wpn.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,jTG-jnKn0k-Etg_-4Gb9gw,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:01.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2473, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zNy5rjlaD06F8Uq9ifQuGg.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,", + "FriendlyName": "Clockwork Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:15.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2474, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0LdcFFwAQkSh1FSiaDwp6Q.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,0EjPIiuYpEGIWup3pKeC8g", + "FriendlyName": "Clockwork Shoulder Bag (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:17.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2476, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vd7QP1vfykeRmhB2_5D88g.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,dkJNh15DnEuO1BatzpI_uw", + "FriendlyName": "Clockwork Shoulder Bag (Rose/Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:24.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2427, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XOVkcyCEQUWRJU73kORscw.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,XMAOwHGXVECoeKoWb_-luw", + "FriendlyName": "Clockwork Shoulder Bag (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:32.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2475, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "A6d5UocVhE-gR4dNG396UA.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,GAWBaCDBzUqdyTGA-HPczQ", + "FriendlyName": "Brown Clockwork Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:22.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2025, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ES_b9-XkfEGzWShw1v1EMg.png", + "AvatarItemDesc": "06adb976-ca7f-4b8b-8b72-2e03c8bc10b3,", + "FriendlyName": "Clover Head Bopper", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06adb976-ca7f-4b8b-8b72-2e03c8bc10b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:18.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "aeadad3b-b83a-442c-829a-3eaccd9726fd,", + "FriendlyName": "Clown Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aeadad3b-b83a-442c-829a-3eaccd9726fd", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1199232272 + }, + { + "AvatarItemDesc": "6dcc5a53-621e-4f8c-97ed-e7fe42fc86c2,", + "FriendlyName": "Clown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dcc5a53-621e-4f8c-97ed-e7fe42fc86c2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1491263048 + }, + { + "AvatarItemId": 729, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bjg8u844kgyz1lqasebmtopqf.png", + "AvatarItemDesc": "TcgHS_0aPkehrBg7ytMOMQ,", + "FriendlyName": "Clubmaster Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b07c84d-1afd-473e-a1ac-183bcad30e31", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-11T22:30:20.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1748, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6rf1m7pd44oq6kn8h6yf9xeqj.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,", + "FriendlyName": "Overcoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:43:55.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2213, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fZkRsxu0GEq0HJicKi0Gxg.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,_3hxdSy9wU6OUPZJA9kGmA", + "FriendlyName": "Brown Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:32.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1096982540, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FSw9DUpPzECdOB8T9OOx4g.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,B5t5RbzKdEObKDlXLcRp7Q", + "FriendlyName": "Pastel Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1199022146, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QqL58lbLCkSvxjPln1rEsQ.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,OaOC278jEE-owjtDP9ZLow", + "FriendlyName": "Pastel Yellow Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "G1k3m0JjFkWpKsbWb3D4eQ.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,J36t8RwGPUqtLBsF-_UtEw", + "FriendlyName": "Pastel Purple Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1010, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4t0t0evfwd04sjvdwb35fvj5n.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,ojvIFCTpJkeegestF065wA,10fYSIBytk-yUjveDa407w", + "FriendlyName": "Collared Shirt (Plaid)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:36.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 158, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7k5gf8gh1bklveyos3l1ydl68.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,071d1bf3-86dc-4a31-b54a-e6579fab8649,d015cae7-a905-49e4-8823-6dec069689a6", + "FriendlyName": "Collared Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 166, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "b3stfwojthp7css2c8xqi5n5y.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,48abd952-214f-48b2-a8f1-1146f6f69aa2,c2a91a10-2f5b-4ab0-a79f-51408cf4b047", + "FriendlyName": "Referee Shirt (White Collar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1042, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "29urcstxxxjo9a30k66pvi43j.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,48abd952-214f-48b2-a8f1-1146f6f69aa2,RNN0ejjlm0yqGDcEuUkLjw", + "FriendlyName": "Referee Shirt (Black Collar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-03-11T21:45:58.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2026, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cjm3isfczhcb0g2sceven3lwz.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,n4SdiVms4kaqdjXdbjRBbw,YEMLBlOWsEO4tELHseZ7LA", + "FriendlyName": "Referee Shirt (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:22.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 656, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "57y04gj7sxv3ffgs9mbnww3wk.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,", + "FriendlyName": "Conductor Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:17.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1553, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "augnnz7p6ycztoyhc5w3sitrn.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,jwXdjDbEmkOFyKZHT_RYOw,YJGsYfwkik2lp7qWqXyXTw", + "FriendlyName": "Conductor Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:48.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1562, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f11gnls9uil36r3thydrn8hk1.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,LdKaVAveokmp3qRwVBxnFA,YJGsYfwkik2lp7qWqXyXTw", + "FriendlyName": "Conductor Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:34.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2263, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6-G1yAklOU-Vz5BRJzhaAw.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,e-oZt-PbKEOfawVammBQTA", + "FriendlyName": "Conductor Hat (Striped)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:12.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 657, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1xwbdqjl5uws8zmxy544nzuk6.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,", + "FriendlyName": "Conductor Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:19.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1554, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6hvy6jdqfz4iasmq7xqwcfpgr.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,Chiz8maLoEGDpZOJZEtdQg,cQUquXMk5EGt0VUCuPKBgg", + "FriendlyName": "Conductor Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:56.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1561, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "16dvdrgwodptixm96hemhraul.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,2axtF2iOGkez1kfRRGUbYw,cQUquXMk5EGt0VUCuPKBgg", + "FriendlyName": "Conductor Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:21.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1373, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ptrsoi2msj3q9zp0nv3opjho.png", + "AvatarItemDesc": "5154be09-bb8a-4917-95a0-51c91b6a4e09,", + "FriendlyName": "Hatter Suit (Wonderland Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5154be09-bb8a-4917-95a0-51c91b6a4e09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:11:11.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 658, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3jwqqbdhd5j68gzti0f3ov9d8.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,", + "FriendlyName": "Conductor Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:21.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1555, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "btsztyyman7ydhbo8ch7havr5.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,5Dbc8qCaY0OXEuwykM4naA,awWjleyNP0akWNYJoehCLA", + "FriendlyName": "Conductor Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:02.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1560, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1rj6j38s3tlgfuz9dlk7rlcbs.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,HBt3ToOkUUereoPKwyvzuw,awWjleyNP0akWNYJoehCLA", + "FriendlyName": "Conductor Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:18.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "asrlf9nb0mfzjx6vbj87jqc2z.png", + "AvatarItemDesc": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,", + "FriendlyName": "Construction Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1914259171, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "2F2USYfWgkewvbwK6oa1WA.png", + "AvatarItemDesc": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,uym5X1GZgUSzVb8U-pwlIw", + "FriendlyName": "Science Hard Hat (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 223, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7qj8vsrrhy3lfmq74f8h4kp4t.png", + "AvatarItemDesc": "e9913b06-58cb-4b90-bd08-981a3f19f352,", + "FriendlyName": "Construction Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e9913b06-58cb-4b90-bd08-981a3f19f352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1651838298, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "pfFTo8CNSEWIT3J-S7fQHg.png", + "AvatarItemDesc": "e9913b06-58cb-4b90-bd08-981a3f19f352,QXtBrjnRckKJGiRtsy4AVQ", + "FriendlyName": "Science Hi-Viz Vest (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e9913b06-58cb-4b90-bd08-981a3f19f352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 270, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4w02bnkzhi1ylgnq3yy60xyag.png", + "AvatarItemDesc": "3bb50a88-09ae-48c7-a42c-df5879680eff,", + "FriendlyName": "Construction Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bb50a88-09ae-48c7-a42c-df5879680eff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 109, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5xt4rvf95k2jm6bpmmwurfs07.png", + "AvatarItemDesc": "3e139dd9-4757-43a8-8546-8da70dc17f0a,", + "FriendlyName": "Ranger Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e139dd9-4757-43a8-8546-8da70dc17f0a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1921, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b8r15kxxhpkjnd8grlinjcsqd.png", + "AvatarItemDesc": "f2db0ec1-e536-4218-8385-dc6913941880,", + "FriendlyName": "Cosmic Hero Gauntlet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2db0ec1-e536-4218-8385-dc6913941880", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:06.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1923, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5m4wvktjw7xe4tdyw0cs3p4wk.png", + "AvatarItemDesc": "3739a9e1-6dfa-45d2-af54-cc83a58d436a,", + "FriendlyName": "Cosmic Hero Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3739a9e1-6dfa-45d2-af54-cc83a58d436a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:15.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2743, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6daz2-CA9keE0A25Vugkvw.png", + "AvatarItemDesc": "3739a9e1-6dfa-45d2-af54-cc83a58d436a,GpYghupLTUK9cuiL-0uHYg", + "FriendlyName": "Cosmic Hero Vest (Rhythm and Rooms Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3739a9e1-6dfa-45d2-af54-cc83a58d436a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:55.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2005, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ews3jw7aorj2ii9jvvnd7rr81.png", + "AvatarItemDesc": "19984948-b6b9-4d3b-8562-d0624930df6e,", + "FriendlyName": "Alloy Armor (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19984948-b6b9-4d3b-8562-d0624930df6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:12.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2099, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "o9dqbqFcv0SWG6AF4UzpWA.png", + "AvatarItemDesc": "19984948-b6b9-4d3b-8562-d0624930df6e,Owi_JROR80-apNHw-4KNcg", + "FriendlyName": "Alloy Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19984948-b6b9-4d3b-8562-d0624930df6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:05.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202152364, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "11462b96-dd62-4057-a187-49d8322c009a,", + "FriendlyName": "Costume Tank Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11462b96-dd62-4057-a187-49d8322c009a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 832, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "clebbebesxjzf8wnllqxmejvg.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,", + "FriendlyName": "Aristocrat Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:47.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 841, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "80vyoqh3t9fmtu13lcn8r3h87.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,i5RKLo-RbEqisnGII1wpaQ,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:46.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 842, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "40s4vjnqn68azxmiparci9gtm.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,IzkFARlv6EenmrfcNkcOuw,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:47.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 853, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3ky2r4usf4c9jpd0da9kxr9az.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,LyVf9lnzdku1F4iPBGqk-g,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:58.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 834, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "axv14g3ocxvf01h82ybehl3rg.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,", + "FriendlyName": "Aristocrat Cuff (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:49.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 845, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9zr3klyo948zva87e3ynyoij8.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,i5RKLo-RbEqisnGII1wpaQ,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:51.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 846, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01xxmh8nnuu5xc4oom2f1r1qj.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,jwEB6LyEaEekqtPNRc6TVA,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:52.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 854, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "due9stf9zpfkmdoblb2wlfcuk.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,Wj7I2YiVoE6zp8J_HYs8tw,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:59.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 46, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4jywgtwnb8sydqwmy9qd6aszg.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,", + "FriendlyName": "Cowboy Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 47, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0sgisiuk6yb2x3qtliioxly31.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,624c85e5-095a-48b6-85fb-c9bbade19a8d,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 48, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d9rwabvsu3fo05lmoaqnv6udq.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,192ba73d-6990-42da-8fe0-914c2a7181eb,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 49, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ioaonefjjjvtyvufi87rptrp.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 917, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5p88yvfhaz0xqx9u944krzm90.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,teVKVbVEsEyqOSuRdqHQzA,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:51.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 916, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "55ecravwda31t7zdi490k0t6u.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,K5vPqD3BrEuODRDzFdC6Hw,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:50.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2229, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hFGaKfD6BkyXWpL9Z29Htw.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,_6LmNp0uX0Ky4jp2j62U2w", + "FriendlyName": "Cowboy Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:54.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2230, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2nwp59be1lvbayv88iqb1rr4k.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,cNarx7yecE2gk-7mYaoHwA", + "FriendlyName": "Cowboy Hat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:00.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2308, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YisZ-46Na02k8dv191Spew.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,AEqCasEqOUCgetoTwuMTug", + "FriendlyName": "Science Hat (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:59.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2701, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0b-BFgiGEEer1cPxNaI-YQ.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,zoUmQIcOrU6jgBu_0urrGA", + "FriendlyName": "Science Hat (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:32.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 168, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7p83fzhqxxbxo8vh3t9otc9e1.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,", + "FriendlyName": "Cowboy Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 169, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ev3mq9lpdefdivq6ayo76ak58.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,192ba73d-6990-42da-8fe0-914c2a7181eb,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2l9uggmozvea8gtpv9ytjfglp.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "digjcfruws97b6pe7orkqjunq.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,624c85e5-095a-48b6-85fb-c9bbade19a8d,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 898, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dg66aywux3nkmv6q4rwlf4uwy.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,teVKVbVEsEyqOSuRdqHQzA,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:27.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 897, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7bsroab0vzswtlgqzzd5mijnk.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,K5vPqD3BrEuODRDzFdC6Hw,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:26.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2225, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RxR8AFtkNky4E7NUQNbCCQ.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,ODOnI3iYoEKrlQfFabbvoQ", + "FriendlyName": "Cowboy Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:11.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2226, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kxjJxDcwh0u3MPgS6XdCzA.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,BW5oLDA36EK9nLzTm7xViQ", + "FriendlyName": "Cowboy Vest (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:25.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 271, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "aq9fxwenux4k6wi50q7r0yqm9.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,", + "FriendlyName": "Cowboy Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 272, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "408o1nfsbf7uwx8ji3868l7j4.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,17e44296-1cd9-4f8e-9aa7-3837dc69279a,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 273, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "74jyglxoijr47llzzcuyhnhe6.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,c4ebfd1a-fcda-4b98-a151-a941b1801867,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 274, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2abrokbv6i5wmyx0dj57x43zo.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,c0c33a35-494c-4a1a-aa53-d27d9b9ef5a4,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 933, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5lr5c7ooc5e1x5c8f29zbk048.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,wat8f6fs80KptZbW39e8tA,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:09.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 934, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b8w1fmsevjv9eaqf2yjooy8ta.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,wkqKPZmnoU-8TXSWzi2-Ow,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:10.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2234, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "fGnMN9gdtUWyx2MEQZbOrQ.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,tOlpTL8F1Ui_akCjHq5vRg", + "FriendlyName": "Cowboy Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:26.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2235, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "qy6K5v706kGJUmNb4oGH0A.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,rfIzQjxeRU60VdRQXG9Ccw", + "FriendlyName": "Cowboy Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:36.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 957, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6ck4di8cgqlprwg40uowlar1a.png", + "AvatarItemDesc": "vpP2ZvnL0Uy3n-I_Jv3eeQ,", + "FriendlyName": "Cowboy Hat (Hairy - Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66f693be-cbf9-4cd1-b79f-e23f26fdde79", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:09.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 115, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "21rygk1c1r8ccciu1jlzdsan4.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,", + "FriendlyName": "Bandana (Flowers, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 445, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "72m3n2l0f0v5ryummqatwer5t.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:10.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 446, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8pn1j2wbx9ivldclxa1ubgufx.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:11.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 447, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1prcefvfnwi7yewdgbbeapboi.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:12.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1575, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7756o1qqkod0e7anndh2zr867.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Cowboy Scarf Mask (Plaid/Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:45.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1576, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "37j4pw9ip3kccqd36l4be3uw6.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Black Polka Dot Cowboy Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:49.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1577, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5en0adt83zfaj5j1bemmxhldj.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:54.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 448, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9xy3gwxnxiy9pqpf90r0hq3kc.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:13.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 449, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5653hakuvc2f7zkqwh2cln0lx.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:14.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 450, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dh0x7lyldgb793zodhe1sscg4.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:15.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1578, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "exyzdp83a10yb68pl68bc1t70.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:25.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1579, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e7ru80lk5fp2s35uvhj2a8m45.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:28.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1580, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "18r0xqjozrmq3c6iyz17zdqhv.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:30.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 451, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4as6ol0a5z9gfyofshfg9ed39.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:16.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 452, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "59fpaqmmt45bv0ojtuamhjir0.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:17.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 453, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2mmw0lzovs5bx1dwz8tid3cze.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:18.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1581, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "a0g2oy7wel38y6w7abixqi1zi.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:33.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1582, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bv2g4hsg5h2czu7ouxhuzozj9.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Black Plaid Cowboy Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:35.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1583, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "99zusz6icmt0hmbe2pk2wulet.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:37.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2224, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "MUhkmM6xxkKXejZd4iDh6A.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6o6sNWyPoUCcKJBHW0KC9w", + "FriendlyName": "Bandana (Flowers, Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:59.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2223, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "U-3blhOO3kWIzeZFNKXgTA.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,X1OFXyDyXES3CgMpIyV3nQ", + "FriendlyName": "Bandana (Flowers, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:46.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 110, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e01eh3p4tl9606u818mhsjsx1.png", + "AvatarItemDesc": "cca7ab3d-5609-4700-92fd-3280fcea9b77,", + "FriendlyName": "Cowboy Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cca7ab3d-5609-4700-92fd-3280fcea9b77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2227, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gpHXKk3jR061QowF7qKdGw.png", + "AvatarItemDesc": "9d2ea3c2-c199-40ed-a6c3-fab5f8dca7d6,", + "FriendlyName": "Cow Ears Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d2ea3c2-c199-40ed-a6c3-fab5f8dca7d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:40.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2254, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d837uciWekyImW4Ny0wliQ.png", + "AvatarItemDesc": "2a61c844-81c6-4f82-9c75-66cf308ed4e1,", + "FriendlyName": "Cowgirl Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2a61c844-81c6-4f82-9c75-66cf308ed4e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:38.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2255, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "USrRub60Gk6zTcni3MGCjQ.png", + "AvatarItemDesc": "ed695ac2-ff70-4ebe-b1db-18d71a4efdd1,", + "FriendlyName": "Cowgirl Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed695ac2-ff70-4ebe-b1db-18d71a4efdd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:46.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2256, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "sH2WnqiB8E6lk4hZ9s70RA.png", + "AvatarItemDesc": "67c0f944-2f70-46d2-8588-2f67028f8bed,", + "FriendlyName": "Cowgirl Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67c0f944-2f70-46d2-8588-2f67028f8bed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:49.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2587, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "J2d0ZFyToECidRm1EHVhLA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,", + "FriendlyName": "Cozy Winter Coat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:43:57.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2591, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PhvdPQnEikmC4BSDxeHfeA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,B3t4hjeUeEyT17nfbZgxAQ", + "FriendlyName": "Red Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:09.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2588, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tWrlF7BJxU-nPUjBYNlJxw.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,IEnB86oIL0KDa-zzcxhjwg", + "FriendlyName": "Black Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:00.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "m7LntT7DgUy0q0ALmr_4sA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,ZzJxs2oz6UuXCX5rGvLHFQ", + "FriendlyName": "White Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:12.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2590, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "t0ntXj7xqUqXyp7YAxwnGg.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,xLcqKXKH-0Gw1lrSSQe-Ag", + "FriendlyName": "Cozy Winter Coat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:06.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2589, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "doAF6sFqkEWtgY2AjjfQKg.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,nloukujf9kSwK1T2HiwJwg", + "FriendlyName": "Cozy Winter Coat (Pastel Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:03.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2593, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5_Is3y0bw0-_CDD5izOqfQ.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,Grr0Jm6r9kuiZKvnLPz9tQ", + "FriendlyName": "Yellow Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:18.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1472, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6l2rw2xqkm2h6er2jcsouoohm.png", + "AvatarItemDesc": "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f,", + "FriendlyName": "Crab Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:29.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2477, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "spUKGpKq3keVBvBjMRkRIQ.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,", + "FriendlyName": "Grinning Cat Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:15.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2478, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4yiLN6Z-r0msFhufhO36Mg.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,McX_jAOowUmjXNPEHbdS7Q", + "FriendlyName": "Grinning Cat Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:20.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2594, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-iEX4DOt8Ems8CPrv9vFPg.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,4YH_0kN1ZUSVEQmCBiTt7Q", + "FriendlyName": "Penguin Onesie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:21.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,kTyKeivEskaxQNLhfPWo1A", + "FriendlyName": "Crazy Cat Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1791659539 + }, + { + "AvatarItemId": 1625, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vvcu1ehg70m9a5l4pccyi05l.png", + "AvatarItemDesc": "6423f23e-dde3-4428-96f5-6091027c92cb,", + "FriendlyName": "Martial Arts Gi (Cunning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6423f23e-dde3-4428-96f5-6091027c92cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:38.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 360, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dn6xgp8b6mjoilolmwxfn8tom.png", + "AvatarItemDesc": "6b337a35-95d9-4b3a-9891-d9fbb06262aa,", + "FriendlyName": "Curly Mustache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b337a35-95d9-4b3a-9891-d9fbb06262aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:00.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2300, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "jhTnA-cti0mo7tgXVYQI3Q.png", + "AvatarItemDesc": "cf360f73-b349-44e7-b5c1-f175a0f5e70d,", + "FriendlyName": "Belt of Elseware*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf360f73-b349-44e7-b5c1-f175a0f5e70d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:41.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2562, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "bIvb2VlYREWBdsNT4kX_2Q.png", + "AvatarItemDesc": "cf360f73-b349-44e7-b5c1-f175a0f5e70d,zQgy7h3CO0a02zJfhA9wXA", + "FriendlyName": "Belt of Elseware (Shadowy Illusion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf360f73-b349-44e7-b5c1-f175a0f5e70d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:55.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2301, + "AvatarItemType": 0, + "Tooltip": "A mysterious and powerful object from an unknown place. No one knows what it does yet but it seems to respond when worn... (Catalyst)", + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "7YFPA4lmf0iQcexgqXhxaA.png", + "AvatarItemDesc": "f6eee46c-084b-473e-954b-a0c8aa99a02d,", + "FriendlyName": "Amulet of Elseware* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6eee46c-084b-473e-954b-a0c8aa99a02d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:43.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2563, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "egJ0Guweb06skCFq1ExhQw.png", + "AvatarItemDesc": "f6eee46c-084b-473e-954b-a0c8aa99a02d,qbEYPChG5EmnFDUfzwAakg", + "FriendlyName": "Amulet of Elseware (Cleansed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6eee46c-084b-473e-954b-a0c8aa99a02d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:57.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2302, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "hyRSoiE5ZUGnAC91bFgIIg.png", + "AvatarItemDesc": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d,", + "FriendlyName": "Bindings of Elseware*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:44.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2564, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "BA2wuggUAkyxEev61GWUYg.png", + "AvatarItemDesc": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d,cfAfhzaNvkG7wGx0gkaRUg", + "FriendlyName": "Bindings of Elseware (Shadowy Illusion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:58.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 659, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2qs4c3hd1jm20l964lznj7hj8.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,", + "FriendlyName": "Deep Sea Diver Helmet (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:21.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 924, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "970zmxcbriwvube7waktj8lib.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,Su7kiW24d0KapNns96JoFQ,a3Er_BCApUiPtBaGkSH9LA", + "FriendlyName": "Deep Sea Diver Helmet (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:58.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 923, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d47yw12jvdnmgznyxtsb774ss.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,CahBzgN3D0y46AQsvGC14A,a3Er_BCApUiPtBaGkSH9LA", + "FriendlyName": "Deep Sea Diver Helmet (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:57.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2j7gd6uoyus6lyn8g1yvji91e.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,", + "FriendlyName": "Deep Sea Diver Suit (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:28.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 880, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "clcdiuwdbo9i9gcuvogdg36me.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,Su7kiW24d0KapNns96JoFQ,EiIow5mS80qpoS10r1ZdvQ", + "FriendlyName": "Deep Sea Diver Suit (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:00.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 879, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ejlghhpy20pr22d1be9zamyak.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,CahBzgN3D0y46AQsvGC14A,EiIow5mS80qpoS10r1ZdvQ", + "FriendlyName": "Deep Sea Diver Suit (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:56.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 691, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9rtsv91ax2euh05felzvtd1w7.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,", + "FriendlyName": "Deep Sea Diver Tank (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:15.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 905, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c2brbedhjhfswczcszbb6vdn5.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,Su7kiW24d0KapNns96JoFQ,ZpyoWbO6GkGglXlMuYUARw", + "FriendlyName": "Deep Sea Diver Tank (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:37.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 904, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8qnxcx64rh4jdr41v292zqnvl.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,CahBzgN3D0y46AQsvGC14A,ZpyoWbO6GkGglXlMuYUARw", + "FriendlyName": "Deep Sea Diver Tank (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:35.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 662, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8yxna4s3e3firsdmmowshealh.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,", + "FriendlyName": "Deep Sea Diver Gloves (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:29.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 935, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4isq4kmfnatlmnrbk9ylp4ur2.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,Su7kiW24d0KapNns96JoFQ,0p5N9U5qokKdQhW07yYLGQ", + "FriendlyName": "Deep Sea Diver Gloves (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:11.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 936, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ael0gchmqiiy4xijypkebw5gy.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,CahBzgN3D0y46AQsvGC14A,0p5N9U5qokKdQhW07yYLGQ", + "FriendlyName": "Deep Sea Diver Gloves (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:12.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1527, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "a2skv7f3dcpvd1rrpejoce5q8.png", + "AvatarItemDesc": "1a742ff9-c4f7-45d8-8e24-8f24aa021db2,", + "FriendlyName": "Demon Horns", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a742ff9-c4f7-45d8-8e24-8f24aa021db2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:51.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1525, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "badoqabz6dkrchkzj2vkx5fxy.png", + "AvatarItemDesc": "b84f5e9e-11fc-424d-ad39-86901edb9edc,", + "FriendlyName": "Demon Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b84f5e9e-11fc-424d-ad39-86901edb9edc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:43.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1526, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6fwxedm1vpp6m7vdyj1mjush3.png", + "AvatarItemDesc": "26f5e849-14b3-4cb3-ab92-673c94b37c51,", + "FriendlyName": "Demon Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26f5e849-14b3-4cb3-ab92-673c94b37c51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:48.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2179, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZrqDYptat0GjjSq9O6O2TQ.png", + "AvatarItemDesc": "26f5e849-14b3-4cb3-ab92-673c94b37c51,CwObTyhghkWonhm5nTnAaw", + "FriendlyName": "Flying Machine Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26f5e849-14b3-4cb3-ab92-673c94b37c51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:35.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1376, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1vas1a6all7s8fowlh0nithtn.png", + "AvatarItemDesc": "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae,", + "FriendlyName": "Dino Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:37.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1259, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2za37ica0cvevp732s8h5llq0.png", + "AvatarItemDesc": "a5b747b8-c1a5-4bce-999f-e13dbe77bde6,", + "FriendlyName": "Disco Ball Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a5b747b8-c1a5-4bce-999f-e13dbe77bde6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:13.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1956, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b70wx32unnc05ssstrdjeqdl9.png", + "AvatarItemDesc": "4095a06c-ccdb-41a0-ac95-5a97372c9a02,", + "FriendlyName": "Disco Ball Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4095a06c-ccdb-41a0-ac95-5a97372c9a02", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:57:55.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1957, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4qarl3kkz504i1mf21lhb6dhg.png", + "AvatarItemDesc": "84992fc3-4801-492b-8a6c-ffa09ae5b593,", + "FriendlyName": "Disco Ball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84992fc3-4801-492b-8a6c-ffa09ae5b593", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:57:59.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "adbd2eaa-0dd0-4955-be4c-0d5a66d64ed2,", + "FriendlyName": "DJ Headphones Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adbd2eaa-0dd0-4955-be4c-0d5a66d64ed2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1329276406 + }, + { + "AvatarItemId": 2, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "egsz5bap153w61sfwpsx9tgjw.png", + "AvatarItemDesc": "5ac25e5a-8c8e-445c-82a4-7272baec1eb5,", + "FriendlyName": "Doctor's Head Mirror", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5ac25e5a-8c8e-445c-82a4-7272baec1eb5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 224, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2chr298lunemuuurc2ka18725.png", + "AvatarItemDesc": "27e742bf-3081-4274-ba70-301f0edeb4f1,", + "FriendlyName": "Doctor's Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27e742bf-3081-4274-ba70-301f0edeb4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 123, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8lo669z9xca0upv4z2ocdcde1.png", + "AvatarItemDesc": "587ca2bb-dd42-42bf-833f-4e2a97349866,", + "FriendlyName": "Doctor's Stethoscope", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587ca2bb-dd42-42bf-833f-4e2a97349866", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 365, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0czsyf22bw9er1mbnytxi0ytj.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,", + "FriendlyName": "Dodgeball Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:05.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 439, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5dzmypownxu9mz6fttdzw0cjp.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,4a85c181-92fe-47a5-925e-05f0ccb25006,944d934d-5e21-42d7-8196-fff9a81ee469", + "FriendlyName": "Dodgeball Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:09.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 440, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "08j0sgwi31ma53zy69e9h1cvs.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,0ba92db2-690a-44ce-92f4-130e9503b6c1,944d934d-5e21-42d7-8196-fff9a81ee469", + "FriendlyName": "Dodgeball Shirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:10.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 851, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8nnw6vhiwxlys6lvncv55q2xd.png", + "AvatarItemDesc": "h_To9WQYdEusYQxlbMmMmQ,", + "FriendlyName": "Dracula Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8f487-1864-4b74-ac61-0c656cc98c99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:53.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 857, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2l8ozzohqd9a0k2deuxxucq9c.png", + "AvatarItemDesc": "uubY_Cn9A0-Ww4lsIRCF6w,", + "FriendlyName": "Dracula Medal", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fcd8e6ba-fd29-4f03-96c3-896c211085eb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-14T01:26:24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2388, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "k1-iNuM_jUadcrVNAvy83Q.png", + "AvatarItemDesc": "05ab6e79-1198-49f2-945b-528523e306bc,", + "FriendlyName": "Wicked Sorceress Amulet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ab6e79-1198-49f2-945b-528523e306bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:35.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2389, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pR-mOlAIOEiskisbb6ZQUw.png", + "AvatarItemDesc": "05ab6e79-1198-49f2-945b-528523e306bc,aUsPNgV2c0u15VhGBAIyvg", + "FriendlyName": "Dragon Necklace (Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ab6e79-1198-49f2-945b-528523e306bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:37.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1678, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7i6yh1qwt3e3zrrb77zwz3wta.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,", + "FriendlyName": "Dragon Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:49.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2201, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B0mOl7jCckGx4VBglg52ww.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,cbk6h_-1Y0exRpGyrQMDMA", + "FriendlyName": "Dragon Wings (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:59.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2200, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gN3tkjocB0iH14KDkjDACg.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,E_pm-PPj_U-nsfMti-AlCA", + "FriendlyName": "Dragon Wings (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:57.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 3, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a3yzsuscvgxw37qx4iplxyqvr.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,", + "FriendlyName": "Drum Major Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 418, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "81kgcsyk5xpfnseux5kk365d6.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,5a614f97-ef7f-4b70-afff-9ed837a86407,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:20.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 419, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "06rlbrw6875digtc4y8c0l3i8.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,b444ed0a-eab5-4bd6-af34-d080de74a149,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:21.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 420, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4visb1djbytbo881ltks1hzo.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,8053b5e5-0f67-4329-a45d-1ee9fc830820,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:22.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 225, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2hyiw1ost44xvt2vreduatn4w.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,", + "FriendlyName": "Drum Major Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 422, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d1ickhvbj03efs8x35tvbakq2.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,fe5288bc-0e02-4ea3-b17d-dee0e576979d,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:25.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 423, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "93qrv8cwm8k71rmnk1o22mzpq.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,82de1ca3-67d6-454d-84fb-0eeece71fcbc,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:26.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 424, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5la3vym26ex06q0rm9ykg3w1f.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,54d3c589-30ba-44c1-a77c-8f9e16eb0b20,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:27.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 275, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0rq8z5vxkj1jdvbjqezvbckyx.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,", + "FriendlyName": "Drum Major Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 425, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "817bm8rsomfrxcjwad8033u20.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,fe5288bc-0e02-4ea3-b17d-dee0e576979d,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:28.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 426, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3kkimmvnludjevzzo56uhvuk6.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,b444ed0a-eab5-4bd6-af34-d080de74a149,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:29.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 427, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73tjv2ussx3904ivxkhezp2yr.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,8053b5e5-0f67-4329-a45d-1ee9fc830820,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:30.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2735, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "J_F4me5Ad06VJ52Xr7npMw.png", + "AvatarItemDesc": "162a3442-e6d0-4895-be09-40d78af16dc8,", + "FriendlyName": "Drumstick Hand", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "162a3442-e6d0-4895-be09-40d78af16dc8", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:40.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1439, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7fx9lc7nedtemokvl10u71bvo.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,", + "FriendlyName": "Dryad Dress (Summer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:09.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1440, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ae92fjqai6ajms6tff2mfv81j.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,AHL9ydUbqEqMw1tluIvyQA,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Fall)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:11.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1441, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b660cr3jdsc9mlg5dejtru1o.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,_SPKrYIcnUWhTiSrTFLwoQ,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:12.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1442, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7amwyi3juehdo73arc4mepxm5.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,n2pZHS8A3kSgyFUCI_-8TA,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Spring)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:14.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zg42szCjeUGZC18WPpdf6Q.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,BYbKbXAgUk6vE7H7w4jSMA", + "FriendlyName": "Dryad Dress (Cherry Blossom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:41.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1443, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bzjuuf0r1ocwg6am5t2mtrvxg.png", + "AvatarItemDesc": "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb,", + "FriendlyName": "Flowing Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:19.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1436, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "65ofog1r81v83c1zkimz5dc7n.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,", + "FriendlyName": "Fae Horns (Fall)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:49.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1437, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aom69vkzej29cjh1jcqgxddom.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,M16XTvLPKUWRpiAspgsatQ,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Summer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:57.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1438, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8s3arpmce6wx552qttsj9og6n.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,rXhNZtcZCU21dwEHOCgWOA,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:59.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1752, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bq7qytcrpeczoqxkplbewz1uf.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,OxO9ivoKB0e-fNIEk33Erw,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Spring)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:49.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1506, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bvjmdwvfu70toj5uhdjc9bmyn.png", + "AvatarItemDesc": "2cf6ceee-4c45-4fb4-b9df-cb8437742e85,", + "FriendlyName": "Doubloon Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2cf6ceee-4c45-4fb4-b9df-cb8437742e85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:56.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2717, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4jhq0ihf9i0h357l82r1f4ugm.png", + "AvatarItemDesc": "f8514fc6-93f3-4063-bafc-378ddf454836,", + "FriendlyName": "Edgy Earrings (Dangle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f8514fc6-93f3-4063-bafc-378ddf454836", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:54.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2718, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9ter0n7sprtghgk2o4clgjugr.png", + "AvatarItemDesc": "f641d4f4-e197-4cf4-b477-89069797967d,", + "FriendlyName": "Edgy Earrings (Gauge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f641d4f4-e197-4cf4-b477-89069797967d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:56.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "02dj9jh2t6iwcatksesgdp9v5.png", + "AvatarItemDesc": "2af73868-b619-43f7-9b7a-37a8fe2502bf,", + "FriendlyName": "Edgy Earrings (Spiked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2af73868-b619-43f7-9b7a-37a8fe2502bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:58.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2681, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "GVvKZUlMlU-oAczCFnnkPA.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,", + "FriendlyName": "Electric Guitar (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:48.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2683, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "K9ApHkO1I0WYcDiKg_7bMw.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,zBfCq7T69kqZPTz3P8KKhg", + "FriendlyName": "Electric Guitar (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:54.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2682, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "uZtpsMTIe0WfPlipjC6sfQ.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,liZWUNZih0iakAa7UJkk9A", + "FriendlyName": "Electric Guitar (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 356, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8o02lqyi0dx2nwz9l24ei0511.png", + "AvatarItemDesc": "34654303-9760-447f-8614-1c26506db994,", + "FriendlyName": "Electrician Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34654303-9760-447f-8614-1c26506db994", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:51.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 351, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7mhhpensvbg501x8mtatplhsh.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,", + "FriendlyName": "Electrician Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:31.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 352, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73egrs7lt4bztq4mkimehdlwx.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,b995b419-1897-4c1d-98be-83d551f7ba71,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:34.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 353, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a4ha61zluoi17g7smiinsv5pp.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,67fc9269-8dd8-4d6f-b594-c77e716f2482,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:35.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 354, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ef0ejvn9b4x628qk2dfjie7xo.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,b539777f-48b2-44a2-9d63-17ac703e4d57,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:37.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 355, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "40dmxzroivdmghm7ola3zu9u1.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,5fb2e235-8c67-4335-b432-edb10db2b139,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:40.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1622, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "7m2mi3d8ga6cj0snk6kbhszx8.png", + "AvatarItemDesc": "686ac516-630f-4989-9c99-0b8d055a00f9,", + "FriendlyName": "Holiday Elf Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "686ac516-630f-4989-9c99-0b8d055a00f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:20.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1623, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "av5quwapam4mgd2prq9f64nkf.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,", + "FriendlyName": "Holiday Elf Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:23.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2526, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "tRXo9yhf00enptfGUMyqHQ.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,Hd5IIMd7VUqndR_og0zxPg", + "FriendlyName": "Elf Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:52.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2524, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "QWw5Yklbyk2cF85WzqFl-g.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,8k3q_1d_-kSBzNLCl1HQhw", + "FriendlyName": "Elf Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:45.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2527, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "lNu1DktQikamJcxDfb6XpQ.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,YwossyXZNEm3OPWwXsyTAg", + "FriendlyName": "Elf Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:55.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2525, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "A9c3qAoffkKGpt3NGDQj7Q.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,i3pwJQoNDkO_68pQ_3Y0tw", + "FriendlyName": "Elf Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:48.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1624, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "6m4lkkcj4tnqk0xc8aqctsa6v.png", + "AvatarItemDesc": "61969200-2790-41e0-bccb-d9feff8c7b7d,", + "FriendlyName": "Holiday Elf Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "61969200-2790-41e0-bccb-d9feff8c7b7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:30.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "2e50d8d2-b7fc-44e9-a882-a58bd3b6f099,", + "FriendlyName": "Elseware Cloak Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e50d8d2-b7fc-44e9-a882-a58bd3b6f099", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1829511188 + }, + { + "AvatarItemDesc": "a73e1a56-2710-4448-a594-ecc80571e999,", + "FriendlyName": "Elseware Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a73e1a56-2710-4448-a594-ecc80571e999", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1208807230 + }, + { + "AvatarItemDesc": "c7ea70bd-7681-4b90-b4e3-10a100e4652e,", + "FriendlyName": "Elseware Earrings Ear", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7ea70bd-7681-4b90-b4e3-10a100e4652e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1175771749 + }, + { + "AvatarItemId": 1792, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "26o0a59b3xk9bngu5q6rjwddq.png", + "AvatarItemDesc": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,", + "FriendlyName": "Elven Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:04:59.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2186, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E6CVDPt2mUWUQ6WM1GzI2g.png", + "AvatarItemDesc": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,z1djgGqy502ha9X_uB3enA", + "FriendlyName": "Elven Crown (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:10.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1797, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ss3ohgl1cgeoknp1gcpackva.png", + "AvatarItemDesc": "485bbda2-37e8-45a7-90db-7c35518dcdd0,", + "FriendlyName": "Elven Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "485bbda2-37e8-45a7-90db-7c35518dcdd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:26.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2191, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sR5683LqRUiQsI8U2LXZVA.png", + "AvatarItemDesc": "485bbda2-37e8-45a7-90db-7c35518dcdd0,M5QwJJRRBUy42HLyp6ghtg", + "FriendlyName": "Elven Armor (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "485bbda2-37e8-45a7-90db-7c35518dcdd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:23.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1793, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9pskxjn1i5vvqw267s8jfq4cn.png", + "AvatarItemDesc": "a37c3161-1476-44b0-a9d1-d6eb854db2d9,", + "FriendlyName": "Elven Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a37c3161-1476-44b0-a9d1-d6eb854db2d9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:05.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2189, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DRy_t5nuCUqcj18J33q3Hg.png", + "AvatarItemDesc": "a37c3161-1476-44b0-a9d1-d6eb854db2d9,EmlwkB77pUiiDEmc2pMVaA", + "FriendlyName": "Elven Cape (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a37c3161-1476-44b0-a9d1-d6eb854db2d9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:18.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1799, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "275lkom5he8sdivhtrbsz9ioj.png", + "AvatarItemDesc": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,", + "FriendlyName": "Elven Bracer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:33.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2196, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "42hTRTPSNkmQRqAiceQwVA.png", + "AvatarItemDesc": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,MzNNQ6T1HEqbqcLXZIulLw", + "FriendlyName": "Elven Bracer (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:38.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2493, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vskg3C6Z10uF_tdfqjarlA.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,", + "FriendlyName": "Beam Sword (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:12.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2494, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OodKnoLtC0KlwWUbUQQJjQ.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,u0bLCSxcA0yDHC29XnLZOg", + "FriendlyName": "Beam Sword (Science Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:14.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2496, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7NNMQvKd_02w8L9dd1zvxA.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,6WJKUeFNn0amYNvKMxijcw", + "FriendlyName": "Beam Sword (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:20.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2497, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ObiBqarizkuy3DPPXfx4dw.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,VPvezA1A8UKt2qNoNRW9dg", + "FriendlyName": "Beam Sword (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:38.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2498, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RuBjPNvqok6wRw6kkXH2GQ.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,OIfcKSEil0uHgT5baEFuOw", + "FriendlyName": "Beam Sword (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:40.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2495, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OCi7E50f4UqQMDGTuaWxCg.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,hNFdYhoJYk24MqU2GhQXlw", + "FriendlyName": "Beam Sword (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:16.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 50, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6gyrqvu42014zf0m05yueqgws.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,", + "FriendlyName": "Equestrian Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 632, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0dobf9ialtu2o3i6ia3w9whbc.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,55901f12-d5b5-4fa8-b4c8-e479689ee39d,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:53.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 633, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ebxhd6rhrlk3qem5ly6uri8od.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,4828b50c-95b6-466a-bb25-514891d78202,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:55.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 634, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7w9g6a5fxuoeblfcf3ahgvbs7.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,d6823e01-69f0-4f85-b94a-74894356a2cf,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:56.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1409, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "avhll17max3g5tbutzgkr4j3d.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,", + "FriendlyName": "Night Coat (Zzz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:01.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1410, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "10ks6xzccejyj65uikvyl23xf.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,uDJ5ZqU1f0CiOAH8WY22Yw,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Dozy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:03.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1411, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4g0dvwsd3dz5sthxkhgq6scu0.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,j66We9SYr0SNQ2ufZqIqhA,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Sandman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:05.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1412, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6lrk2hle92tc35cdc3fs9nqhp.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,_jkPDSXFUkSU_yOfutQ7BA,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Zonked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:06.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2066, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "m9a7PytQ6kK8OBPmwOadSw.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,MLB_YJtNtUi2Qw9D4a5i2w", + "FriendlyName": "Starry Night Robe", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:21.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1584, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bbiqjubciywrt9n52sthcrv57.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,", + "FriendlyName": "Fancy Cape (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:06:59.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1585, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "appu9oibb8u3du39aint01ugz.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,crl8VlZ59kGxG01noGEWmA,EckOEU0mJkGIVDvYoKBN9g", + "FriendlyName": "Fancy Cape (Gilded)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:07:02.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1925, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8eli5j86ufvn0nzgy2emp66xo.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,WqX8pAB7LkyPKEzIUCtnNg,8Y2F-cVStkSn9YB47VCoKA", + "FriendlyName": "Fancy Cape (Holiday)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:47.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2027, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-FiTuUGn-kGGXgQjYaAC7Q.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,l2BbgbHCe0S_SDUbfmpSeQ,XHJ0lW4zYkGcL5UyI1HfEg", + "FriendlyName": "Fancy Cape (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:28.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2161, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GadkXWWyHEOkWhpEtq2mrw.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,bTAQ-CkyRUOZoXqXuap2Ig", + "FriendlyName": "Fancy Cape (Basketball)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:28.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 973, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2so63kdd99rxf93sphtoxpre7.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,", + "FriendlyName": "Belt Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:48.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1450, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1n94yhtw4pxw2vp1k9nf43brr.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,whs9olNz9Uani7Gou31mvw,y6aASPUwDkas0W5Yl27ydQ", + "FriendlyName": "Belt Bag (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:29:03.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2028, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7iYxky7EC0WBmVm1yt7YDQ.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,ncERd5bELk23jSRxA7NV9g,N-cAmfOgv0eJZMTo18zJHQ", + "FriendlyName": "Belt Bag (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:32.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2282, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "w2agGc4e5E2zgiHvAsFu7Q.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,eaRDMCA4fE6KJHi639QFvQ", + "FriendlyName": "Rec Con Belt Bag (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:27.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2281, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "UnP3rBcwpEmBR6ED8vVH0A.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,zIIQkllHFEyjEvAGYC0cPQ", + "FriendlyName": "Rec Con Belt Bag (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:22.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1420, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0x4ghshpxazgbb0zpcyu0g22v.png", + "AvatarItemDesc": "4ee2133d-c419-4fbc-9117-746d756a250d,", + "FriendlyName": "Belt Bag (Cosmic)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ee2133d-c419-4fbc-9117-746d756a250d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:26.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 367, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dd3ww7dcaihkg0ags2cxuiyuz.png", + "AvatarItemDesc": "cea5c141-32d8-4dae-80d9-6fb74834a79c,", + "FriendlyName": "Farmer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cea5c141-32d8-4dae-80d9-6fb74834a79c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:05.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 371, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9iudjbzlizfmrgxtwdwwsj6hp.png", + "AvatarItemDesc": "967187b8-df20-4f4f-b48f-10b9ea75cedf,", + "FriendlyName": "Farmer Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "967187b8-df20-4f4f-b48f-10b9ea75cedf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:11.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2023, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kv11sGN1q0uGTiTIyHPeTA.png", + "AvatarItemDesc": "b6cee099-5840-401a-9d74-0b80ff71ee69,", + "FriendlyName": "Feather Boa (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6cee099-5840-401a-9d74-0b80ff71ee69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-02T18:10:57.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rwu9Nb6pTU6Pjd3orGYWOQ.png", + "AvatarItemDesc": "b6cee099-5840-401a-9d74-0b80ff71ee69,an_Z5HMFjkaHyHoZZV4AFA", + "FriendlyName": "Feather Boa (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6cee099-5840-401a-9d74-0b80ff71ee69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1547, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cl2yzoi5qm9yxxli0m8dx34ey.png", + "AvatarItemDesc": "16707919-756d-45ae-95fc-cfb6715b8ae6,", + "FriendlyName": "Winged Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16707919-756d-45ae-95fc-cfb6715b8ae6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:28:04.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1765, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ceggd7cookytj7l34cfoxzeuw.png", + "AvatarItemDesc": "7df5697f-2f71-47d1-bc27-cfa43792ffd7,", + "FriendlyName": "Felted Shawl", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7df5697f-2f71-47d1-bc27-cfa43792ffd7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:36.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_jqJN6iJzUuEi-MlHpRpyg.png", + "AvatarItemDesc": "7df5697f-2f71-47d1-bc27-cfa43792ffd7,GCethPzUyUKYxWdfLykbSg", + "FriendlyName": "Felted Shawl (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7df5697f-2f71-47d1-bc27-cfa43792ffd7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:08.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 975, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dj7x2xk5ttme8r1jpcmeebmjj.png", + "AvatarItemDesc": "BJ2XrGvl40iIl28IbTfbKg,", + "FriendlyName": "Cheerful Ponytail", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ac979d04-e56b-48e3-8897-6f086d37db2a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T02:00:33.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 176, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "41wwwt83ors7iub44o0ftgtzs.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,", + "FriendlyName": "Cheerleader Skirt (Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1626, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4o1fhoua48d5fb5rbrs2ijloi.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,eWf2c62cqEOkRukLzhl_wQ,KFq8d1VsO0KDqBWUj4qRFg", + "FriendlyName": "Cheerleader Skirt (Victory)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:07.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1627, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "edpmi0dav6ft7rwfn86f1km9l.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,a0oe68pikUykAgrRo-axkQ,KFq8d1VsO0KDqBWUj4qRFg", + "FriendlyName": "Cheerleader Skirt (Rally)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:12.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2422, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Hhv0MwY_0EitaME8aQhP_A.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,cFQ1Z7_wp0meS8wdC1hemQ", + "FriendlyName": "Space Suit Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:46.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 132, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5rmads8tgd0mb71na01q6g4l4.png", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,", + "FriendlyName": "Rec Room Sash (Small)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1359256033, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,IodiruapIEqjGym-YcXzXA,LjQ1UKN2xkqJkok40Dxaew", + "FriendlyName": "Female Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.502777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1597463395, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,KUAMuM41hk-YLZoqTiKncA,LjQ1UKN2xkqJkok40Dxaew", + "FriendlyName": "Green Cheer Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.502777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2514, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "F2FsXvPi5UqyVCDKuPzs0Q.png", + "AvatarItemDesc": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b,", + "FriendlyName": "Festive Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:12.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2542, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "fNauUoIJ10uT7PJQCp9Agg.png", + "AvatarItemDesc": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b,1LtCxaCDjUmg62fYCjNUSQ", + "FriendlyName": "Festive Antlers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:58:27.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 179, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "39n1fu8uzgykuoibgylda8en0.png", + "AvatarItemDesc": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,", + "FriendlyName": "Field Hockey Uniform (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "akd6dknn22mbqs5rpgcphqekv.png", + "AvatarItemDesc": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,7b0d6661-e856-4ec1-ab90-d89c28a70826,1fede91a-434e-49a5-882e-e6db107112a8", + "FriendlyName": "Field Hockey Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:24.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 347, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cz08dx8wptcrolonzvozwn4w1.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,", + "FriendlyName": "Fighter Pilot Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:03.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 338, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ef7205mj0de2hjfkm2shgsjo3.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,cea53c7f-24a9-4e3b-80b9-fbf95738f973,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:20.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 339, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5f87fvdl1iouxqppbtsu50ic0.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:21.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 340, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f54hknoj615yks00svpyjvttz.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,8c74ee27-3d56-401b-8a8c-7868a80770e8,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:24.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 349, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1wcxmgwhypyd2evqqigqmes19.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,", + "FriendlyName": "Fighter Pilot Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:07.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 341, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1d9tx3eid2kmtnq3wejxoceub.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,cea53c7f-24a9-4e3b-80b9-fbf95738f973,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:25.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 342, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bfv2kddhqvn7dw1f648zhjf63.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:27.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 343, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8ko7qcft9b1b9aubbimm8cjfb.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,8c74ee27-3d56-401b-8a8c-7868a80770e8,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:29.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 350, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0tlurbm4ftp2t94h35qrfzqtm.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,", + "FriendlyName": "Fighter Pilot Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:08.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 344, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7q0ythybtrbsgg7c2njbi9c68.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,18b52db5-0261-46a0-9214-ff510d455ed9,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:30.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 345, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6328cpp2tizf50t3nsq5yrbz9.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,55b4e661-43e1-4708-82d5-2dd2bebd4f45,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:31.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 346, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7l7k238jg65h9nok7guj535ix.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,8c74ee27-3d56-401b-8a8c-7868a80770e8,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:35.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 955, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "99bp2zxrcuohv18hmmh26iuao.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,", + "FriendlyName": "Figure Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:15:20.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2163, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ls7NQ0Vos0SREhMD3UGSJg.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,dZyVey0_oU-3u6FLbIdGLA", + "FriendlyName": "Figure Skater Collar", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:34.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2164, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kO-X2uWxJEGueWZ1XNqQfA.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,rP0LMoFjqUCU1LWA6_wOnw", + "FriendlyName": "Figure Skater Shirt (Burgundy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:36.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 956, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "arq4ixmsd54yjuazkfe5xglft.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,", + "FriendlyName": "Figure Skater Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:15:24.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2166, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "_J7s68Cf2kKhS_I47a_7Cg.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,ngF6e9k3wkme3HdodiB03w", + "FriendlyName": "Figure Skater Dress) Figure Skater Dress ng F6e9k3 Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-04-13T01:18:42.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2165, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c_ldiPCEC0uVSLKrER_-tQ.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,FpvZjltPtU6AKTBphz3ZMA", + "FriendlyName": "Figure Skater Skirt (Burgundy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:39.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,z7bASCi-JEKuLiOmFx3EFQ", + "FriendlyName": "Figure Skater Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1092505797 + }, + { + "AvatarItemId": 51, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "agf9y9srlnxo896ku56vbgb2e.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,", + "FriendlyName": "Firefighter Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 635, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4jk6yhqfbgqan7m5g4uz672n.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,891d902e-3257-4a8c-9001-858046007997,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:58.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 636, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8mvts9qqf1v3kmd2l4mgbqjkx.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,4c26d1e7-2382-4478-b9c4-a068ed01a607,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:59.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2006, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bnhtf2lj2d5khggvf485zqnxb.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,uahcQNh3IEiooPFq-SQi5w,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:26.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 180, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "573itausg418c5prdz6bv3dpo.png", + "AvatarItemDesc": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,", + "FriendlyName": "Firefighter Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2007, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8gwu9vgybc4jmwudmwkep73uk.png", + "AvatarItemDesc": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,0x7a3hV5XUyzyf8cUuzVtQ,vYAcZMymO0m6bCNQIsI8rQ", + "FriendlyName": "Firefighter Jacket (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:31.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "7ed81aea-0e77-4b38-a87c-729048c6fe41,", + "FriendlyName": "Fireworks Antennae Headband Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ed81aea-0e77-4b38-a87c-729048c6fe41", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1920491330 + }, + { + "AvatarItemDesc": "b734172c-e405-4fc7-88c3-c859309bb4ac,", + "FriendlyName": "Flamingo Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b734172c-e405-4fc7-88c3-c859309bb4ac", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1246829493 + }, + { + "AvatarItemId": 588, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a1vs7a8qu2iy5kaxke59mq5sa.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,", + "FriendlyName": "Flapper Dress (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T18:57:31.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 641, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cu5pah7uuv614frsr65tvynut.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,565c17e0-496d-45bd-a2c3-564e118c06cc,a240d44f-963d-4d35-b22f-980938c7788d", + "FriendlyName": "Flapper Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:19.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 642, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a1844ejer8zz68n1qyajbci5m.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,a240d44f-963d-4d35-b22f-980938c7788d", + "FriendlyName": "Flapper Dress (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:22.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 589, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "acmwuunndj6ex6zr3885jbewo.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,", + "FriendlyName": "Flapper Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T18:57:33.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 643, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5b73p030rhcezdz56gzz5ke13.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,30c61e63-5e09-4a6f-8915-1e1fe4d55cf0,0e42108e-3b08-49b4-86c0-7984719e8380", + "FriendlyName": "Flapper Gloves (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:23.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 644, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3v8m5i8t4wkf4669wvp55t2ex.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,2cf2b644-3fa6-449c-9fe8-d0ab917f3106,0e42108e-3b08-49b4-86c0-7984719e8380", + "FriendlyName": "Flapper Gloves (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:25.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 586, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2q0dg92w8rd2uzk57ib0y1cj0.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,", + "FriendlyName": "Flapper Headband (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T01:07:49.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 624, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e7tt4jrd0jwhdkeastwunm8bb.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,565c17e0-496d-45bd-a2c3-564e118c06cc,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "FriendlyName": "Flapper Headband (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:41.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 625, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uukseprpubic1ed6c0zapb86.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "FriendlyName": "Flapper Headband (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:43.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 52, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6s2gbnnr5jsigj562mzity2yp.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,", + "FriendlyName": "Flat Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 637, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7iezcsxl2wujed3zpx0sad4pp.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:06.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 638, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e688283ak00jg806f3cw5gc0k.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,0ba92db2-690a-44ce-92f4-130e9503b6c1,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:09.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1638, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "24i3534uix45v9rp0lwimi7sf.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9aYT6E9sd0OSavlmkhtkGw,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:20.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1639, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5w6vsff1wv2psbj5naflpxebd.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,H-ypJNyVSka9as7rDFRZ_A,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:22.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1640, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0xi16q5h0z36akxhihlw7prh3.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,-7uPJmO-wUu3cUhqo0caPg,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:24.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "f567f792-e5bf-41d9-a8a9-e3af7aaddaf7,", + "FriendlyName": "Flipped Up Shades Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f567f792-e5bf-41d9-a8a9-e3af7aaddaf7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1877445964 + }, + { + "AvatarItemId": 2218, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TgzWE1dBGkqvgHpdtFU2Uw.png", + "AvatarItemDesc": "c7c85554-e118-4433-8d6b-b61a2971e045,", + "FriendlyName": "Wrist Floaties", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7c85554-e118-4433-8d6b-b61a2971e045", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:04.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c7c85554-e118-4433-8d6b-b61a2971e045,MS0Mglf3qEmzKzbCa6uOAg", + "FriendlyName": "Floaties Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7c85554-e118-4433-8d6b-b61a2971e045", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1483238024 + }, + { + "AvatarItemId": 22, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "20q0kpiy9pbsmzz8qgeyhz0kt.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,", + "FriendlyName": "Flower Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 479, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "548zksu11edcwj6jg6aoag1s7.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,c5884778-a87f-4612-9717-86fbb65d440f,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "FriendlyName": "Flower Headband (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:34:47.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 480, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c1g1jmqtrkrhxhm8c2puubehx.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,827a1538-51bb-4fef-99c1-7f1bebd9866c,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "FriendlyName": "Flower Headband (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:34:50.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1875, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bmw4n7qs969ehy3whhp8d7x2q.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,rZOgFhv59UKssAH7k-UDyQ,eVl7mLA1VkmnR81YLF-ZNg", + "FriendlyName": "Flower Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:39.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 181, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5bmu6mglbinvcbqu05awxhls9.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,", + "FriendlyName": "Flower Swimsuit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 434, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3oz7p1t6fe7pqfgq0hoilmiey.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,cf515c87-3c0a-4443-9f63-5ea1bdbaa61a,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:02.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 435, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3bd0st7c55qfn1i4j4cuevmu6.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,27680277-aec1-41be-a476-9f51cf775580,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:03.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 436, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7kp3fr2rwvgorhc9uaqpahhmb.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,5a96ac4d-03c3-4214-9dcb-c2044ff28f2e,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:04.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 53, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b9vsrdat9b9pm7myqavsy8mbw.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,", + "FriendlyName": "Football Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 54, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6sd4dcoqxuhvhrf3casbx3080.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,d8d490aa-7088-4163-9305-1441367f490c,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 55, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ekahaz7vcxq5ejy0zaxrfx28a.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,61ff7a02-50ff-49ea-b95e-c31b95a30c8e,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1662, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d2vzz38oww35dcnjvsxmoj4yd.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,X24EvVokeUqyDNdqevV2EA,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:25:59.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1504587764, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3jI4Pafh9k2B-YTxx1gQOg.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,UQKS4u4SO0umKOHncLqKpw,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Black Yellow Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1720424941, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "T0KyAJMATUqP3TLksvoBxA.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,Q3bhpoWbVkyUBTKUn5A5bg,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Blue Green Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1663, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7t42ep2l7eezztyekwigomtjj.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,UfhOpLemQ0ypYh7CIC3W1Q,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:07.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1155274878, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "mYOYiE057EOkRmW2vXuTdw.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,_MS-R9DB5kinLBszWle8hg,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Black Gold Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 182, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ewgz9ts3guc8ukhtofeng0haj.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,", + "FriendlyName": "Football Shoulder Pads (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 183, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e8nojpjvm8xk8hxnuivjumxe8.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,32dde6dc-e3dc-43ee-9b7c-987bc6778c17,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 184, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5wkaquhpl7lseclkpi4mgjdkp.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,88c3ed05-ce41-4a9d-a4a7-8a428d240b3f,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1664, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d34fyy9i80gyrjrhwyh2coy4s.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,vMqhT19x302aYEo6E9HhOA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:23.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1192138115, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6FJtINu9eEeqW-_S2Cd5Rw.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,G7B-u9qn5EGqSNskBCVsAA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Black Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1242284106, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Y_q7sebeeUuwLnOb_qMjtw.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,jV1TGp1dHUWCJzVMbJskyQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Blue Green Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1665, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2oj4qcrvtpo7yivbcpx3f2voa.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,kCqthBeVvUKZA9b76kxuVQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:34.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016992135, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cJAAR0vVREamuQT8FfpdHQ.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,dzCc_QmTYUGQ67r4yy3XpA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Black Gold Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1260, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1wfcf6b158y11zvf8rsj18asi.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,", + "FriendlyName": "Formal Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:17.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1hodrkppvhvmxs18btxdu979c.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,rcdwgNvO30Cvx2ycE1GfHw,JmFzNL5v_0q1p0bgoOlBpQ", + "FriendlyName": "Formal Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:21.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1924, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dhy0nd82sozehq400v5il9pjj.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,ktnYOVEhtEmuxmjM1TjIOQ,EqNosRGf5U-5P1nKIRV3fw", + "FriendlyName": "Hallow's Dress (Patchwork)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:26.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2553, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sWwqVZYnhEuU3MEH8_419g.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,oM7qz730lke-off_cQw-cA", + "FriendlyName": "Midnight Gala Fancy Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:41.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2819, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "abYMjMnf3UyRf8ys43UA1A.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,HzTJsuYdLkC_NY3ClszRpQ", + "FriendlyName": "Formal Dress (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:45.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d2403tow1ivxsp7dq769lkahh.png", + "AvatarItemDesc": "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14,", + "FriendlyName": "Formal Dress (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:25.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1264, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3o9n9ip2a7y91q3v2kwe6jpnj.png", + "AvatarItemDesc": "73a119ab-d2c8-4da9-98b2-29edbb38e2c2,", + "FriendlyName": "Formal Dress (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73a119ab-d2c8-4da9-98b2-29edbb38e2c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:32.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1263, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8op0aqlcnxdsbinjfpuqdslgn.png", + "AvatarItemDesc": "73c0e2ba-cba1-482a-89d5-1508997aceeb,", + "FriendlyName": "Formal Dress (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73c0e2ba-cba1-482a-89d5-1508997aceeb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:28.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "80f68c67-2df6-4b58-bf2a-dfd76653a781,", + "FriendlyName": "Fortune Teller Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "80f68c67-2df6-4b58-bf2a-dfd76653a781", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1199731224 + }, + { + "AvatarItemDesc": "d536cf31-5649-4d09-94b0-995c4d6f89df,", + "FriendlyName": "Fortune Teller Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d536cf31-5649-4d09-94b0-995c4d6f89df", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1077066165 + }, + { + "AvatarItemDesc": "a98251fe-3eb9-41cb-9982-6ce44e454d85,", + "FriendlyName": "Fortune Teller Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a98251fe-3eb9-41cb-9982-6ce44e454d85", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1777474640 + }, + { + "AvatarItemId": 1454, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dmas6u15o98831skwq4yiijbp.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,", + "FriendlyName": "Fox Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:17.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2178, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2Q7b4md8JE2YQ8VUXFaioQ.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,A-xoYdSUF0aEr_XZ9Me-3Q", + "FriendlyName": "Fox Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:32.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2176, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "oTmagc6WVkCO9huhLC83Lw.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,db-JiDmRhUKX69weDLiHsQ", + "FriendlyName": "Fox Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:28.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2177, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "puLgwYOR7UaDsU2V90_S0A.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,UZY81cnsV0meMp2lg3_UaA", + "FriendlyName": "Fox Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:30.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 808, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "9w8alcxm82aptgu5w3qz5fmq0.png", + "AvatarItemDesc": "OTnfT-4NQU6Gd_iVPDx8eg,", + "FriendlyName": "Frankenstein Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fdf3939-0dee-4e41-8677-f8953c3c7c7a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:33.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 809, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "3yzcedaeybzym9rpwv7nr1pkg.png", + "AvatarItemDesc": "ZsggwOJ8XUOGfaJltM418g,", + "FriendlyName": "Frankenstein Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c020c866-7ce2-435d-867d-a265b4ce35f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:35.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 810, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "8rb3glamh1kq36xnt8gupn6np.png", + "AvatarItemDesc": "tkbHKZ9wu0W00VGeUbIaaw,", + "FriendlyName": "Frankenstein Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "29c746b6-709f-45bb-b4d1-519e51b21a6b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:37.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2428, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YAStiIqV1kOk0QSNHUcb4w.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,", + "FriendlyName": "Frantic Rabbit Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:34.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2431, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mjpFdsz4lkijguGsysSzgQ.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,J8z15LYBak6jHu3ZaaKAXw", + "FriendlyName": "Frantic Rabbit Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:44.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2432, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dTQ_d68ca0Ct3Gh71yHo4Q.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,QSXXWZrS6UaoAUn3u7hXFw", + "FriendlyName": "Yellow Frantic Rabbit Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:49.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2429, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PKZOkAkm-EK4joFEnKiyWg.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,AcLApWMhAU6tK10O7KoOog", + "FriendlyName": "Frantic Rabbit Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:37.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2430, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SsUBkL6SS0OyRRh-yZ886g.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,dhJm0moskU6urIwG1TXs_A", + "FriendlyName": "Frantic Rabbit Vest (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:41.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2433, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "JmX7fBYzcUyHQStd9MQjHQ.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,", + "FriendlyName": "Frantic Rabbit Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:52.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2436, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XOxmltIN6kSzCdXiXACw6w.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,MVhTW78G_E6z2mY0Y5ZnAA", + "FriendlyName": "Frantic Rabbit Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:00.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2437, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NAR-63A3vUSNmcNgu2Jhyw.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,Adiw7dQqyk6pS0q3T0lcSg", + "FriendlyName": "Frantic Rabbit Wrist) Frantic Rabbit Wrist Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:20:03.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2434, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "Ny4bGecU1kqDWzGnr1B1Uw.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,_4mk8E4PUEGf_3SPuFUy2g", + "FriendlyName": "Frantic Rabbit Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:55.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2435, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "VqjRqu51m0mkMRhHinsJ9w.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,aO5Q4BPpuU-bsNavGii40A", + "FriendlyName": "Frantic Rabbit Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:57.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2228, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b4ixc77euj1q8qnhp0oronjic.png", + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,", + "FriendlyName": "Frog Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:47.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2690, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6s01rm1gm99uf83uib3fvl2fe.png", + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,f-RfHoCWik6QgNKDhCs7Vw", + "FriendlyName": "Frog Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:13.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,f8100fb5-92fc-4520-9755-2deaa5430ddb", + "FriendlyName": "Froggy Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1783947886 + }, + { + "AvatarItemDesc": "2b1f0884-c887-4ec9-9984-426405c50f93,", + "FriendlyName": "Froggy Floatie Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b1f0884-c887-4ec9-9984-426405c50f93", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1878911014 + }, + { + "AvatarItemId": 1286, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8xe1257ru45z0b0867roht3eu.png", + "AvatarItemDesc": "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2,", + "FriendlyName": "Froggy Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:57.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2675, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "bU_6Nph8n0KifAktXPcZww.png", + "AvatarItemDesc": "9117bd15-674e-4f22-9ff8-e7365fa43907,", + "FriendlyName": "Acoustic Guitar", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9117bd15-674e-4f22-9ff8-e7365fa43907", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:12.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2676, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "RhxHB33yikq68wkkfgieQA.png", + "AvatarItemDesc": "9117bd15-674e-4f22-9ff8-e7365fa43907,UdQxfRULDEG6BvJV7HWO_g", + "FriendlyName": "Acoustic Guitar (Dark Oak)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9117bd15-674e-4f22-9ff8-e7365fa43907", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:18.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2691, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "63a52r43lcwxjmtic35ksma5j.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,", + "FriendlyName": "Keytar (Teal/Pink)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:16.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2694, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "exgelbk5b8wyv6c26lb3m0bpe.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,GCwZMIr720iH3TszOb6SQA", + "FriendlyName": "Keytar (Black/Red)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:21.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2692, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "9xylg3wbs72hs1ygy34nllz47.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,LmMg-y8w8kiXdre4vUJrZA", + "FriendlyName": "Keytar (Silver)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:18.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2693, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "28bnag6bw350qkhpaeyn6qxbi.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,ze-eTUX6WUqE-n5nIcH0aA", + "FriendlyName": "Keytar (Lime)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:20.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "309f3ab1-d2a4-4d63-b248-93d9795a8a23,", + "FriendlyName": "Fruit Salad Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "309f3ab1-d2a4-4d63-b248-93d9795a8a23", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1227548947 + }, + { + "AvatarItemDesc": "58d3f945-510f-4f92-87e3-a57e29611e4f,", + "FriendlyName": "Fruit Salad Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58d3f945-510f-4f92-87e3-a57e29611e4f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1528201612 + }, + { + "AvatarItemDesc": "c9badd8f-3f81-4c05-8a31-24abd8839872,", + "FriendlyName": "Fruit Salad Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c9badd8f-3f81-4c05-8a31-24abd8839872", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1617129328 + }, + { + "AvatarItemDesc": "788baa71-6ffb-42f5-8dc8-d0f117b04a8b,", + "FriendlyName": "Full Body Beard Close", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "788baa71-6ffb-42f5-8dc8-d0f117b04a8b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1033187763 + }, + { + "AvatarItemDesc": "83ff0105-37a7-4c71-902c-20f473223d1e,", + "FriendlyName": "Full Body Clothes Boots", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83ff0105-37a7-4c71-902c-20f473223d1e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1780430972 + }, + { + "AvatarItemDesc": "ccae1771-434a-48b7-9b4e-1dcf7b70e0da,", + "FriendlyName": "Full Body Clothes Shorts", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ccae1771-434a-48b7-9b4e-1dcf7b70e0da", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1326708353 + }, + { + "AvatarItemDesc": "a3648455-51cb-4ed4-85c5-9a11f96ad3e7,", + "FriendlyName": "Full Body Hair Emo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3648455-51cb-4ed4-85c5-9a11f96ad3e7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1690082234 + }, + { + "AvatarItemDesc": "c5760078-15bc-4ea6-acb9-0ea295a1894b,", + "FriendlyName": "Full Body Hair Flat Top Etched", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5760078-15bc-4ea6-acb9-0ea295a1894b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1377927688 + }, + { + "AvatarItemDesc": "fb64a2ef-ae83-48a9-b021-0b3f0de2d7a5,", + "FriendlyName": "Full Body Hair Fonzie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb64a2ef-ae83-48a9-b021-0b3f0de2d7a5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1598195373 + }, + { + "AvatarItemDesc": "0d58f3a0-7165-41a4-8d77-f88f393d0003,", + "FriendlyName": "Full Body Hair Wavy Middle Part2", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d58f3a0-7165-41a4-8d77-f88f393d0003", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1183373744 + }, + { + "AvatarItemId": 185, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3tc8n5oesvirh7gtgk7ky4kbe.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,", + "FriendlyName": "Fur Coat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 470, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7yjxqxjcdtwsuu11rr9ijfijs.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "FriendlyName": "Fur Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:38.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 471, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ekhbvef0wyu4wu32a5dvhlqqq.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,643b5f2d-0666-4869-b699-03742de34d16,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "FriendlyName": "Fur Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:39.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 555, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bw125vxk818kcf1j36i5htzc4.png", + "AvatarItemDesc": "e042433a-40a2-4758-8074-3b9d407835ec,", + "FriendlyName": "Romper (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e042433a-40a2-4758-8074-3b9d407835ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-10-26T00:08:07.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2790, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zR88MAKiPkimNE_ZJTgJ5w.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,", + "FriendlyName": "Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:03.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2791, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ih-SEYO0UGR8D_71mKT7A.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,1sxxUbsQMki0sTwxEQIbtw", + "FriendlyName": "Blue Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:06.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2792, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1Ud6HT3jLUC49RkE6B4aIA.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,CEnWMDmcMEyjX5mRdhtXXg", + "FriendlyName": "Gardener Gloves (Floral Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:08.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2793, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "vyZN5MKfwEqqAG1yJQQsbw.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,ySbPHkP8WEee2V_P8lt_RA", + "FriendlyName": "Pink Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:11.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2820, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uc9Y0cmhYUi-BL1OyWYdxQ.png", + "AvatarItemDesc": "2e31fd6c-dffa-4571-8e53-fc28fd0c98e0,", + "FriendlyName": "Gardener Apron", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e31fd6c-dffa-4571-8e53-fc28fd0c98e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:47.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2821, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BC8BhDnhAUWqYwj2dvuT2g.png", + "AvatarItemDesc": "11cec676-0ec3-4778-abe4-a9d399900b54,", + "FriendlyName": "Gardener Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11cec676-0ec3-4778-abe4-a9d399900b54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:50.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2822, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Jr88nPGi9EOu05WlVylKgg.png", + "AvatarItemDesc": "ad6e7c98-8056-4e12-92ad-bfa19a00f4af,", + "FriendlyName": "Gardener Shoulder Basket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad6e7c98-8056-4e12-92ad-bfa19a00f4af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:52.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1452, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7z1qr4njpz9qbv9yoq0metngg.png", + "AvatarItemDesc": "cf1a6f8f-1649-423b-b513-b8d40d242b1f,", + "FriendlyName": "Gathered Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1a6f8f-1649-423b-b513-b8d40d242b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:06.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2083, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "sWYoOZIiY0KBXKZYOQ38zw.png", + "AvatarItemDesc": "cf1a6f8f-1649-423b-b513-b8d40d242b1f,8lwfuCRnU0291wqwgTwgRQ", + "FriendlyName": "Riveter Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1a6f8f-1649-423b-b513-b8d40d242b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:26.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2372, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HpB5RkqKYkWc9i_l8Ul66Q.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,", + "FriendlyName": "Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:28.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1343124314, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c6waJLUmskK9zsdjD-IYZw.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,tp0jNW7f0Uubr9nTMWuhXw", + "FriendlyName": "Gentlelady Hat (Pastel Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1842821000, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "H2YS04F1YEKwQ4HAk1GV9A.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,clKD22mme0y8apzUhHmI_A", + "FriendlyName": "Pastel Purple Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1605291822, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NKtxE0Ix9Emzu3RF2XLuxg.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,9liduuR6IE-6C4YN_FKcZg", + "FriendlyName": "Pastel Green Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2373, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wUMF4BFbWkqp6MofjZVxOw.png", + "AvatarItemDesc": "39dde11e-7aa1-4bd2-8532-62665e8d8dd3,", + "FriendlyName": "Gentlelady Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39dde11e-7aa1-4bd2-8532-62665e8d8dd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:30.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2374, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67o7qPb1O0q7OC60gJfjew.png", + "AvatarItemDesc": "f7764314-c02c-4b46-9c56-8c60351d9b14,", + "FriendlyName": "Gentlelady Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f7764314-c02c-4b46-9c56-8c60351d9b14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:33.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1845, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4wnr9uzel6d0lekcez3cw73vw.png", + "AvatarItemDesc": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,", + "FriendlyName": "Gladiator Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:42.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2058, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjmkJTDtvkWsVfz6feF-eQ.png", + "AvatarItemDesc": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,RjX1VTHHoEiNBfXkkniDgw", + "FriendlyName": "Gladiator Torso (Folklore Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:50.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1846, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "azdd85r2w7xl4v8s9ahpnhll3.png", + "AvatarItemDesc": "1633766e-fa77-48f3-8c6c-a233e56bf320,", + "FriendlyName": "Gladiator Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1633766e-fa77-48f3-8c6c-a233e56bf320", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:54.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1847, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ds517zev16c5r1kkrizriku7.png", + "AvatarItemDesc": "594a69fb-9f76-4f83-b2aa-94f0e72e996c,", + "FriendlyName": "Gladiator Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "594a69fb-9f76-4f83-b2aa-94f0e72e996c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:59.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2056, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "JohKrR2o7kqMPVnZzCfTyA.png", + "AvatarItemDesc": "594a69fb-9f76-4f83-b2aa-94f0e72e996c,GL6frGGo2E25VLcw61otXA", + "FriendlyName": "Gladiator Helmet (Folklore Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "594a69fb-9f76-4f83-b2aa-94f0e72e996c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:46.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1880, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6s6sp9bunolpihm8u9g9a538k.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,BqNgpjP7a0qmm3I3DNbKLA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:54.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1879, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9hfdcf1titio5gbw8o07wtk2x.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,5H0I1anlkkuUYIM28_ZOlA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:52.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1878, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ax893cneyhf2wbm98vxenyxxl.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,9tZq7Xvfj0WhhLBAuAacxA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:49.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2092, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "tD99sEOpYUyYZ-MFVKK3lQ.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,dRQI80Qn4E-kzhPJoW66MA", + "FriendlyName": "Acetate Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:19.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2087, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "SYNkbSO4106ID65XGWzI2A.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,FuMUQFSnnU6awB-vPaeuZw", + "FriendlyName": "Acetate Glasses (Bubble Gum)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:06.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2089, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1lmEHPGbEUmTeG7d_lJ6Uw.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,OBcu3bf1NEio_7t9smqGag", + "FriendlyName": "Acetate Glasses (Amber)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:08.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2090, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "h-G0dl2uBEKQJ4YBI0Sw4w.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,F1IV-zZ22kKtXkr2pHp_YQ", + "FriendlyName": "Acetate Glasses (Seafoam)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:12.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2091, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bV0WFKjHdUGPdgeJ9k_BZA.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,Sad0HQX_Y0eYTx1qqgxKwQ", + "FriendlyName": "Acetate Glasses (Sunrise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:18.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2088, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "yt7jg1pEiUuJZMJuLmP55A.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,-OgDdlvL9EyzeiA50YY5kw", + "FriendlyName": "Acetate Glasses (Charcoal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:07.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "0b38899f-6429-49aa-b626-2696359dc1cf,", + "FriendlyName": "Glitch Wings Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b38899f-6429-49aa-b626-2696359dc1cf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1805065453 + }, + { + "AvatarItemId": 2831, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GenNVM5HVEKt_DZK0VQsQw.png", + "AvatarItemDesc": "f90773c6-d0f0-417a-9af5-183d1e5a657f,", + "FriendlyName": "Glowstick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f90773c6-d0f0-417a-9af5-183d1e5a657f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:37.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2832, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "veozF1-boEi1gm3EPXGN2Q.png", + "AvatarItemDesc": "c51780fd-9eb5-4bc0-9da0-34ab22d800df,", + "FriendlyName": "Glowstick Bracelet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51780fd-9eb5-4bc0-9da0-34ab22d800df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:39.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1916, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "084qi740gtrdi1jyi3yjt5q6r.png", + "AvatarItemDesc": "b772a5a9-dd79-43e4-8d95-f965d81bed8d,", + "FriendlyName": "Kevin the Goblin Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b772a5a9-dd79-43e4-8d95-f965d81bed8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:19:16.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 116, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "04xgdtlhrslfn0o41so5fnjoi.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,", + "FriendlyName": "Winner's Medal (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8xn26bulod52740rbedbiwok2.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,ERnUPsyT6kuEnIRZF8zPwg,wzt-5C_LjEmIQWUVPcYWhA", + "FriendlyName": "Winner's Medal (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:40.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 906, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4rm8hpw76k9han4s5k58jc9gc.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,iaDL2im4lkCXUo0NWM-F3g,wzt-5C_LjEmIQWUVPcYWhA", + "FriendlyName": "Winner's Medal (Bronze)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:38.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 503, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3dzbcs7n1malcbemyi60hae9g.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,", + "FriendlyName": "Golfer Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:30.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 504, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bwzoz4dtuwlngxk9f0tu4ubak.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,48c18418-01dd-40ff-9dfe-c4cf5f4e1245,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "FriendlyName": "Golfer Hat (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:31.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 505, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1iqnljjvv128u9d8mlok69amn.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,6584dada-499e-4d7c-958d-466955b20640,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "FriendlyName": "Golfer Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:32.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2309, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "V_xCN9YtNEqAF6pAvOkE9w.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,2Mgx6S9f8k2H1AAYbkzjQg", + "FriendlyName": "Golfer Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:01.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 506, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67p6qo2ys9s1k0dg867wt3hlo.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,", + "FriendlyName": "Golfer Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:33.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 507, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "arl9g7pg2z44m7uqebdjk9ool.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,4c845dbb-26c3-48a1-b562-e28a7416b154,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "FriendlyName": "Golfer Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:34.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 508, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "01ylknc03jnajh84tyy03knwg.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,96e2b29e-e553-40f1-9eb6-8a9f7e64da39,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "FriendlyName": "Golfer Shirt (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:34.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 510, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eqstzzls6wlgjfdkwtcpj53gp.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,", + "FriendlyName": "Golfer Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:36.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 511, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6etin2gx8z6mi85m8niz96so2.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,72a010c0-bddd-4e5d-bca6-9821d82de9a6,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "FriendlyName": "Golfer Gloves (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:37.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 512, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bi4tmrffhhhfm5xmt03sfyvnu.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,32a5e42d-2b60-40b1-a145-ed96fc91af8e,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "FriendlyName": "Golfer Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:38.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1085, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "02907rdlfgdak15kydocegpp7.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,", + "FriendlyName": "Graduation Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:19:58.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1163, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ll333kyucn9aqhff96o0txse.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,FaWMLkJJgEKFIO493fzGxg,yzL0M_N5Q0eAa8IxACHPAQ", + "FriendlyName": "Graduation Cap (Maker Pen Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:05:53.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1803, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2lx5rfp2pcfj2z8z1wr7f1uuu.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,nCPVMrWXGkeeIN-969xzuQ,yzL0M_N5Q0eAa8IxACHPAQ", + "FriendlyName": "Graduation Cap (Video Creation Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:53.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1114, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bm925en2c6cq90rczyqx8jtek.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,", + "FriendlyName": "Graduation Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:20.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1162, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "44739pgupptu8ki1u1i0efr2i.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,iZy7RmfAz0603oMO-jD-WA,ica0LZyM_kOox8gxW7CaMw", + "FriendlyName": "Graduation Gown (Maker Pen Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:05:43.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1810, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "91oh3z6dug9f7buh2555h4ggn.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,4D4ge1PYokaj_g96k4oasA,ica0LZyM_kOox8gxW7CaMw", + "FriendlyName": "Graduation Gown (Video Creation Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:36.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1065, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "98c5cktigvhw45kiu5db444wj.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,", + "FriendlyName": "Grillmaster Apron (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:36.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1066, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4z1alsikbw52tg9t7o4b0tmf8.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,eV3Nv6SWS0yl3f-fgjPb-w,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (Denim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:45.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1067, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f08vx9xbl5n8d0cdwaqunjhs5.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,Lxx3q6JRnUW_cPG_kGG4ag,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:48.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1768, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4ulmpmgcp45eu79inqrlhz2d.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,vek37vRROUKnSJKVP0CH6w,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (Charcoal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:22.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1076, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5gw83joytwxpoarjjrzas61ip.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,", + "FriendlyName": "Grillmaster Mitt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:09.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1077, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "10xu47yb16kjm62v7fasn937i.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,90qQe0AR3kOou0MsuoTmPA,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:11.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1078, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uhjnjl997sivm0uih15zk8ug.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,hZKPr4sg10SRj9DLuEgOrw,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:12.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1782, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4oo9dvqgiu2yehkvk6dey3jdc.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,x3Q0vVeG_E6juqGM1o0SOQ,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:38.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 961, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2i4t0ybjtnnq1pyn2a2zmo32b.png", + "AvatarItemDesc": "m5Plrduzr0yj_9mTn1x3Vw,", + "FriendlyName": "1988 Jacket (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ade5939b-b3db-4caf-a3ff-d9939f5c7757", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T22:07:36.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2736, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PEHKs2wQe0SFdV5ekTdSkA.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,", + "FriendlyName": "Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:42.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2741, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "h6jjmTpeBkGLdb3Sbt_-0Q.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,dtuR9xLXVUGzSi2G1DMh7A", + "FriendlyName": "Red Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:50.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2737, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GjQ7GjOgmUSLmyl9DH0hjA.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,0FlA_T8GsUe9PdiZamlP_g", + "FriendlyName": "Guitar Pick Necklace (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:43.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2740, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-IEuy3vB4kK_xxTTWTsQXw.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,U8ZZiMEeZ0-qJfcpqBP72A", + "FriendlyName": "Guitar Pick Necklace (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:48.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2739, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rVsH83vYHkO80KaIwEVyyw.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,Gct9Vt-L4UiTjAl61hsebQ", + "FriendlyName": "Guitar Pick Necklace (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:47.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2738, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EfUdI1XtV0eRy8PsZBG2dg.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,2r_gYLhQrkS0bjwaxLecfw", + "FriendlyName": "Grey Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:45.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2185, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NboCAaiehkigdPqWgZxRoA.png", + "AvatarItemDesc": "2d07c0d0-b277-46dd-8980-382bdaa4749e,", + "FriendlyName": "Angel Halo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2d07c0d0-b277-46dd-8980-382bdaa4749e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:07.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2554, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "z5qNLmnhd0aHhDdIxO-wsg.png", + "AvatarItemDesc": "2d07c0d0-b277-46dd-8980-382bdaa4749e,f6O5cw0vc0ineEptRysocg", + "FriendlyName": "Science Halo (Invasion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2d07c0d0-b277-46dd-8980-382bdaa4749e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:44.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2742, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "i6sQmmZpgEWvZNkWaYpaWg.png", + "AvatarItemDesc": "fe16e054-9cae-4a97-a0c0-256b51cdb0e5,", + "FriendlyName": "Microphone Hand", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe16e054-9cae-4a97-a0c0-256b51cdb0e5", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:52.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1147, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2y48o7b7lqi2zpho283lmy51n.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,", + "FriendlyName": "Runner Harness (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:00.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1146, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6xgus9q1g8stsgzfpsinr3li4.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,sX0ZZDcCUkyFSJISfvq0vg,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:56.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1145, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5fi6lrfurv47a5jv8vcz3fmxv.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,TnEDe1QoMUCUdqPL_7CxTA,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:51.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1144, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "axtrgokadupvoy6xhgle28790.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,AC3zfm38JEyBZWuXDh4Ukg,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:47.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1143, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ke3ipxnfopjshcyjl3kc7mhu.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,RfZNDPUok0WyQEa3Ko08ow,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:43.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1142, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c7vhxic6odjal3d4elovf6li9.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,dyEaPloE8kazWC7Zblv8uQ,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:38.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,E7zTfb8zbUKuld0rxNt7fg", + "FriendlyName": "Harness Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1003112581 + }, + { + "AvatarItemId": 1164, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "435egcfj9uj17vp4n7ey9u4rv.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,", + "FriendlyName": "Runner Wrist Guard (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:29.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1165, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bwo3vsz07ltq6bsv2c8bszrgs.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,sX0ZZDcCUkyFSJISfvq0vg,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:32.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1166, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "aiygi6wtiasforjki3lnkg1ab.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,TnEDe1QoMUCUdqPL_7CxTA,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Lavender)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:34.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1167, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4poast86ikgyo8gx5xw0v6tl8.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,AC3zfm38JEyBZWuXDh4Ukg,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:35.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1168, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3qn2bl4qw76u5zafxwim8hslp.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,RfZNDPUok0WyQEa3Ko08ow,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:37.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1169, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9ny1060mulzukvbnj8r8jsk77.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,dyEaPloE8kazWC7Zblv8uQ,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:38.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,MvC6bRHxmkaXvWS6Q0cIFg", + "FriendlyName": "Harness Gloves Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1884541935 + }, + { + "AvatarItemId": 1153, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ac4yc0p8960f0pcub6ys93vk1.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,", + "FriendlyName": "Runner Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:28.913Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1152, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "diwahrdx0ag6e8a70yal6uos9.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Euf1h1qsZ02evDsyoHYbPQ,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:19.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1151, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8i6nmv636c2mf1lxl17ox1d4i.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,lsyYlxY9o0a3TbUrHkxeyQ,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:14.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1150, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6urhwi4bkj3jxkd4pq9lxzapp.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,JVZHZm6nGEqE2zEEPoHbwA,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:10.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1149, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6m4gs8ysz42vgc0rn06moyk71.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,zlqLrGEa8E2WW9ey_szREA,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:06.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1148, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a22f3ny3x4czh17wi8o7cxnp9.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,L2iFQcL170OajqfW7t6sVw,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:03.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Iz6cWpY2B0uOQ9BnY3fSbA", + "FriendlyName": "Harness Helmet Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1073107630 + }, + { + "AvatarItemDesc": "8705d28f-49f4-48d9-9946-13f18aabfe4b,", + "FriendlyName": "Hammer Head Shark Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8705d28f-49f4-48d9-9946-13f18aabfe4b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1285875244 + }, + { + "AvatarItemId": 1088, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6t7adsd6rb8v7w110axmy2fpk.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,", + "FriendlyName": "Big Head Bow (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:16.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1089, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e6lw7lptzoy6kedqecx8jk0ob.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,8iZO162KNUiMC8l_z3OC_A,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:21.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1090, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzilh25e7rcim8kgspfpj95ba.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,f_c50saStkaGJuhLjJ9lfA,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:25.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1091, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "18wsz87rejkgdxqq1wc65icjt.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,FUP4iRnoc0ikNmjrFD06lg,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:28.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1092, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d40z3ipsfgbe9hux480cvd5a2.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,GmS0wKwzVEqdf3NoTMHG5A,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Teal, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:32.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1093, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3bok5be6yuo76i8jwnvzr1kf2.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,ZNKGkou_gU-1cxBFy7pDKw,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Red, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1094, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bo0hnyxq2ym7xxw3uicmaydgj.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,-Iqmonry4EKoqAULCKzRPA,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (White, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:38.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1294, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9y2nffs1iso5dhjr38gstlhpq.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,D-SngK7hhkOpoZ2aMWv9EA,mkDVc0Mz4kmuLZ7powBHpQ", + "FriendlyName": "Big Head Bow (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:35.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1355, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "824gaa1rig6e7wnf2tax1j1j9.png", + "AvatarItemDesc": "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3,", + "FriendlyName": "Big Head Bow (Green Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:45.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 563, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ckopqewba1tguffkue6s78rc3.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Headband (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-13T20:56:57.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2076, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "JGqSITx8dk6mMtS4gTrMaQ.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,TeSudgm-NEy3DXe3z_VRqg", + "FriendlyName": "Headband (Retro)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:11.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2077, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dl4fJip8EEukov2aRWgT7A.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,RnrXuIRvMkeW5w1lcNiuaQ", + "FriendlyName": "Soccer Print Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:14.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1095, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3cc5bj2gxpsblgaruo3t9egwm.png", + "AvatarItemDesc": "J6IQt4G1zUeN8v9Wn50BUw,", + "FriendlyName": "Tied Headband (YouTuber, SoulFox)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b710a227-b581-47cd-8df2-ff569f9d0153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:43.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dn3d4707ue1k2osfrlcvavlzo.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,", + "FriendlyName": "Notes Headphones (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:43.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1045, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6gdfkv6dgyb7aup991rfx4ns8.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,viL4CPch2ECwG-RNaWcyZw,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:47.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1046, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ftwtrq2ong4idyrsil0vdmek.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,8ANbOhNnO0yLQOCIBsDwfg,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:48.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1047, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cph7oedpx7hgjd38qq4qtgoal.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,a5nUvJZgTEGBUvdHx0Y6Ng,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:49.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1048, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "60mqpyf61x722pn9if4oa70qi.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,JWBK5v78Y0uRVEXlD-Qk6Q,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:50.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1502, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ecbfwjrpe9e79d5vrdubmh2c4.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,thG3Dc1PFEqAvT9xCVshuw,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:44.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1049, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6k8vpwyhep8o2yv8j2grqd8h5.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,hf0gkFVS8ESvceRsC3LAXQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Psy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:51.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1050, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bhaum9si19ovo5u6f0ik7fcyv.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,4scG_UBlzE-5DWRhSrtm6g,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Chillout)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:52.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1051, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f012aaez937gwxfcp6amwbh1a.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,OqsbZ8Xls0i8uA6ha7H3qQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Trance)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:54.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1052, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0qoenhowtvhtxia42kmo22j5v.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,nX7krD8e2km_ZlOLWcoxdQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Lounge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:55.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1053, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "75w0v1yh3q3o0dfoelib0un32.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,ns5tIRishUOp-nYqLedcwA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Techno)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:56.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1501, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1czj45wblivuiu4m5plehpt8f.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,k_ZYy0SjtU2TB7IFdq2vDA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Synth)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:40.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1054, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "48dwhvntvbcgfw5d6yly1wr9y.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,I1J-xY4YNU2dB_od4P_7Dg,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Heat)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:57.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1055, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2jwdp3bzhm7xcqru8enpkveu7.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,2sf-FU4WckGBDWUkVz4z1g,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Sky)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:58.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1056, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7yuwkss6may3hvmhg1sepbsvw.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,3PfP661Fg0CruzRxis9EiA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Dreams)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:59.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1057, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "19ts2iqffk8o10duwieerytd8.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,d9mwowRelE2lDKEA0UQMZQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Dawn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:01.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1058, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3lnjdd8bye13tx2sv12v4bisy.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,Uz2hrptCa0erjqyPoiZxew,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Void)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:02.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1500, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4dh2u6nja9oh06bc83r9s4pqb.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,PQTvQiYu-ESRUldnxgm5BQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Jungle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:35.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2209, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8thOMYf1lUybc3FN_bY8WQ.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,oNtLjorrZ06t4yIdWqKCcQ", + "FriendlyName": "Headphones (Wooden)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:26.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2795, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SbQkirL3sUidVCVTEkiS0A.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,pAlEt5sqD0mKgFd_0b7RgA", + "FriendlyName": "Floral Headphones (Blossom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:16.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2796, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OjJJcLC_l0SxYuXsaT4blg.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,PNH0VNS1qk6nWVecYmVDDQ", + "FriendlyName": "Floral Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:22.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2797, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "snOakm9_R0upwnPGZDeEuw.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,sXF1BZr8X06AYnWUrWxy1Q", + "FriendlyName": "Floral Headphones (Lavender)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:25.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,e5954339-a379-4510-a54e-18f64e29ffe5", + "FriendlyName": "Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1452629529 + }, + { + "AvatarItemId": 1083, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c0aetz7tzn1vg6rqcopztwq0i.png", + "AvatarItemDesc": "akAhnN0cFkecj8q3spixkw,", + "FriendlyName": "Notes Headphones (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c21406a-1cdd-4716-9c8f-cab7b298b193", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-12T01:24:56.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2375, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ONlwGgbS3EG8In6YslOpGw.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,", + "FriendlyName": "Headphones Mic Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-30T17:42:35.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2376, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NY7YBusIgkCX1gRBRn_KDg.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,M9ZKWX59e02037Qy4rgmAA", + "FriendlyName": "Headphones Mic Hat) Headphones Mic Video Contest Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-30T17:42:37.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2565, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "46_hhqT9P0isgoM6SBDAVw.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,u2nh8EAZT02TxPTWVl59UA", + "FriendlyName": "Science Headset (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:01.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2151, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "730oGQopMUGTrJNdUPTkeA.png", + "AvatarItemDesc": "1db08f02-d559-40ea-9bae-dd11046694a7,", + "FriendlyName": "Video Partner Headset", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1db08f02-d559-40ea-9bae-dd11046694a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:18.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r0K8LjSj0U2lRyRwptpOoQ.png", + "AvatarItemDesc": "fbf74c15-4b42-4cd4-94dd-d0e86f11541b,", + "FriendlyName": "Heart Antennae", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fbf74c15-4b42-4cd4-94dd-d0e86f11541b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:00.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1955, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1uytf301ssh2nyytxcnm0r2v1.png", + "AvatarItemDesc": "5944a66e-b386-43d9-a4b6-7b4a8e987fd8,", + "FriendlyName": "Heart Sunglasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5944a66e-b386-43d9-a4b6-7b4a8e987fd8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:15:27.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2239, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6Wo-wGbct0y9F6OdVvsZog.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,", + "FriendlyName": "Anime Hair Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:26.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2557, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5VAj7sHGqkWRLz4ktf2ibQ.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,YfRfaC49J0yW0wnSJf5DwA", + "FriendlyName": "Anime Hair Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:50.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2555, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gK3kKpiWpkeCTN3muHzInw.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,0WW04NMMfU2LLahCq0jQ6Q", + "FriendlyName": "Anime Hair Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:45.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2556, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HPGiZzkfbUagfHZSMVdpIQ.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,4kYWnAwZo0SDZFmiU8jJwg", + "FriendlyName": "Anime Hair Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:48.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2576, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d7RSXQeGNE2FITMNS4x9lA.png", + "AvatarItemDesc": "f09be0ec-19eb-4443-843c-b387b6731e48,", + "FriendlyName": "He-Man Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f09be0ec-19eb-4443-843c-b387b6731e48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:32.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2577, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KeJx2mtu3UWduFaoj3_lzQ.png", + "AvatarItemDesc": "28f00eee-d3d1-4cac-ac70-fca064fcbaf0,", + "FriendlyName": "He-Man Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28f00eee-d3d1-4cac-ac70-fca064fcbaf0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:34.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2578, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YgFKySGtSUOhM_WsMBYbNQ.png", + "AvatarItemDesc": "9ab4ea8a-1b16-4969-9b80-8e36b1f782ca,", + "FriendlyName": "He-Man Bracers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab4ea8a-1b16-4969-9b80-8e36b1f782ca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:37.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 56, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "37a5o4unxmplzke25isjr0n4n.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,", + "FriendlyName": "Flower Hair Clip (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 485, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "chwhfus6b1dkb3vtb59wuymig.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,ce465d53-3807-423f-baa0-bdba2a75ceb3,65b89eae-347e-4eff-9ff3-7f25cace0125", + "FriendlyName": "Flower Hair Clip (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:05.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 486, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "23uhduy56xwn0r399ed4oscrw.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,596097a8-d545-4256-959d-1d3cd96fc64c,65b89eae-347e-4eff-9ff3-7f25cace0125", + "FriendlyName": "Flower Hair Clip (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:07.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1763, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0lhij515w2a2pi1bjg8pqzztr.png", + "AvatarItemDesc": "d15139de-5e57-423b-b8da-335ad7a4df5a,", + "FriendlyName": "Fae Horns (Hidden Worlds Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d15139de-5e57-423b-b8da-335ad7a4df5a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:29.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ehfh8lkiq0cqwio8dxk9kdg0y.png", + "AvatarItemDesc": "f51abeae-7c5d-4065-aa2a-b2e7025b3fcd,", + "FriendlyName": "Fae Tunic (Hidden Worlds Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51abeae-7c5d-4065-aa2a-b2e7025b3fcd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:33.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2251, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CHOox5OrjECPbDwbhliATw.png", + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:05.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,N-C_xBTZeUq1YsdELln53Q", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1435331028 + }, + { + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,G4MuMrpiKEWN0uEBcGlX4w", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1797238548 + }, + { + "AvatarItemId": 2252, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nonED7GaH0qPJ67vtt7QxQ.png", + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:07.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,VfSI4zfHbECwcrQmcbelTg", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1842931803 + }, + { + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,qRIbIelHBUOU71eoK2XQeg", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1992229999 + }, + { + "AvatarItemId": 2253, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwCEXZw8HkeluG48ThKdoQ.png", + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,", + "FriendlyName": "Hiker Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:11.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,IcgjR69rYUuNhsb3s9qjYA", + "FriendlyName": "Hiker Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1494615195 + }, + { + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,yMkN0om7l0-_47gDrBWxiw", + "FriendlyName": "Hiker Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1664824131 + }, + { + "AvatarItemId": 57, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0ky7ceo16g76hoi2vudcp4u2d.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,", + "FriendlyName": "Hockey Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 639, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7rm6qzjybqpssdvxptzb3cq12.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:11.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 640, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ap6srpnsg5qje8yjd0k6xoq5v.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:12.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1718, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "08ly4egzp8znsf9rr74dqyzom.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,qdA34j6p_UqphqChTCHEYQ,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:09:30.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2078, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HfG5sUm4_EC1LqHmdMhF4Q.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,", + "FriendlyName": "Hockey Helmet (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:16.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2715, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yVFjcpLMhUGC2rFphSKjxg.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,Z6o5ySTsmkmwAGslDH7k4w", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:06.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2716, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "l33ddE1N5kSh2m4vYm-lhw.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,cLKDeDSwd0OvF27bW_wBWA", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:08.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2714, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hGYlm15Q8kS3Qwi0ugLJdw.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,m4t_QKkV4EWTVEH8biQuEA", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat Purple Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:03.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2079, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DLbcAnoh6kOcI8vlUfbceg.png", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,", + "FriendlyName": "Hockey Jersey (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:18.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2702, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "R1XZo-34Wkm75mdVSeCFPw.png", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,c3Su0qnh-UK-ymcGaUy4Uw", + "FriendlyName": "Hockey Jersey (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:34.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1547381390, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,zhgbWHad00aE2KNFM3FNWQ", + "FriendlyName": "Hockey Set Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.507777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 186, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3c1iifcjs873h8qwb9pekkmpu.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,", + "FriendlyName": "Hockey Shirt (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 496, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8j3mq83a7m2bcd1ue5tzv6r2o.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,fcbdfbfd-254a-4538-b119-43e23404b8b5,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:19.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 497, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bkzswe4hfhdvak21tbjhe6icc.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,b1ea158d-1663-4db1-bb09-9847f6e9d1e6,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:20.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 498, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "91cioy61054580v8ruloocj3l.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (Red, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:21.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 752, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "964z64p0djst7okp5yy4qnb5n.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,", + "FriendlyName": "Rec Room Hoodie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:24.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 787, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9d2j5ikkdbngspuc7tki5lecu.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,mvMeyStxGUGlNLSYNeSubQ,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:22.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 786, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4jf3ogeghh4h207uwsgs53ay.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,u_Gzj9Nro0WS48xKnH9few,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:19.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 796, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "14fpnws4q76d3svlzzp3ogr29.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,JTgXKeT7w0K2Mw4Jk2HCqg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:26.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 793, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ccdrg5p5ytm15zierfd33d677.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,rNhOQoP3XkOBHV28ze2NpA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:50.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 878, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "89cwe9sw6lx2jml1bc48d9io1.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,TEgugZRs80iJbhL87RdYwg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:54.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 877, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "81kofnq5cb3fvgyozz15k1ro0.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,ytmSzjhCr0Sm58MSNbwJUA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:39.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 876, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6w9zh5x30uv01aj0rnudzyicy.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,yM5xRrd9OkmDGyQddwOP6g,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:37.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 875, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2xo1n1x3mq2iry1ptnih904dt.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,-Nf7WM2TeEKujB637h3tRA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (White - Starter Pack)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1612, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ngrs0bphj486q6105peoygyq.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,LuYelZY8NkqmOp1pnxPXBg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:23.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1809, + "AvatarItemType": 0, + "Tooltip": "For earning $100", + "Rarity": 50, + "ThumbnailImage": "5yo24l49i1ul66r6i2yec4ggz.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Z10TTlOeW0mumezbqXKGzA,Ht0WRvpguUaLZ1tigCzCMA", + "FriendlyName": "Rec Room Hoodie (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:33.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2153, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9ZVUjjO21U2ujrZzoNX9SA.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Ist3SXFwKkCCvxirWLLjng", + "FriendlyName": "Video Partner Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:21.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2202, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6TEq-Xvb8kirly5K2g96vg.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,9jB8U3yxjE6hanEZgMdvtA", + "FriendlyName": "Rec Room Hoodie - AAPI (AAPI Heritage Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:02.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Ebi3A88h2U2yiygzWBBL_w", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1692692324 + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,gPPg0QrOlEmVmvEc_OheXA", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1976989717 + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,d022fcd7-850e-490b-893e-eb713e5848b5", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1155409075 + }, + { + "AvatarItemId": 1375, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aojr333s0451ovrb3sccpajtm.png", + "AvatarItemDesc": "5362d685-fc55-4b43-85a5-cc3d6cad3f69,", + "FriendlyName": "Rec Room Hoodie (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5362d685-fc55-4b43-85a5-cc3d6cad3f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:10.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2245, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9EjzWesDb0qc78bYLR4gXw.png", + "AvatarItemDesc": "ac957e1e-676b-45c9-a39d-b238d2329a03,", + "FriendlyName": "Horseshoe Bracelet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ac957e1e-676b-45c9-a39d-b238d2329a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:58.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2246, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ewhdfsr2ocppmfgs5a2ddeb9h.png", + "AvatarItemDesc": "9c4976c1-a0eb-4a1e-b5cd-08d44a4b3011,", + "FriendlyName": "Horseshoe Earring", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c4976c1-a0eb-4a1e-b5cd-08d44a4b3011", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:00.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2247, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5vzoo1Ope0ayRkVlKiJDZg.png", + "AvatarItemDesc": "cd77c8ed-ab60-4f42-9bee-0141c7e3e626,", + "FriendlyName": "Horseshoe Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cd77c8ed-ab60-4f42-9bee-0141c7e3e626", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:02.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2377, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "Vv4sv_u_EU2qi4FLQuoCmg.png", + "AvatarItemDesc": "b6d10952-d492-45e5-b54c-de3ab138be0b,", + "FriendlyName": "Hot Dog Hat (Mustard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6d10952-d492-45e5-b54c-de3ab138be0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:39.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2378, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "N2RqYRINKU-RJEE5tp5BNg.png", + "AvatarItemDesc": "b6d10952-d492-45e5-b54c-de3ab138be0b,2KCpbel8JkaeAGQRgvpt2w", + "FriendlyName": "Hot Dog Hat (Ketchup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6d10952-d492-45e5-b54c-de3ab138be0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:41.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pR32NwZrJUWfiKEdw3CYeg.png", + "AvatarItemDesc": "4c52478a-b2c9-4520-8f88-52f2262cc6bf,", + "FriendlyName": "Hot Dog Suit (Mustard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c52478a-b2c9-4520-8f88-52f2262cc6bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:43.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Oy7uq9Y6VEi4oYPxwG-aMA.png", + "AvatarItemDesc": "4c52478a-b2c9-4520-8f88-52f2262cc6bf,YR7P-KQLGUKPF7JbW85K7g", + "FriendlyName": "Hot Dog Suit (Ketchup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c52478a-b2c9-4520-8f88-52f2262cc6bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:46.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b5221aee-22ca-4855-81d9-0b4491229060,", + "FriendlyName": "Hoverboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b5221aee-22ca-4855-81d9-0b4491229060", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1868649629 + }, + { + "AvatarItemDesc": "2c09d6b4-039e-4475-b013-0630740d820d,", + "FriendlyName": "Ice Cream Truck Box Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c09d6b4-039e-4475-b013-0630740d820d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1514562461 + }, + { + "AvatarItemId": 952, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9zxq5okpcuze5r8vwhknknnfd.png", + "AvatarItemDesc": "W7AJ4UYlVkWF5nLbmpmRBg,", + "FriendlyName": "Ice Queen Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e109b05b-2546-4556-85e6-72db9a999106", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:39.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1939, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8tqs6ckiohne11n3sui6d7dvq.png", + "AvatarItemDesc": "W7AJ4UYlVkWF5nLbmpmRBg,5Hk7UR60rkquUNxpQ8O0Gw,zMXDVNsKn0GhVd2Tjho0-g", + "FriendlyName": "Ice Queen Crown (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e109b05b-2546-4556-85e6-72db9a999106", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:31.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 954, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "da9hnrgqj5nbv2mz69que9ixy.png", + "AvatarItemDesc": "ilDgmey2ZUWlFiuEAN7YoQ,", + "FriendlyName": "Ice Queen Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "99e0508a-b6ec-4565-a516-2b8400ded8a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:50.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1941, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4n3khnydgbrm5og1sy02ak2rp.png", + "AvatarItemDesc": "ilDgmey2ZUWlFiuEAN7YoQ,K9wPZ9LHo02GPMtf02rANQ,U7jMLMX4_kKIy1K9IcHOXA", + "FriendlyName": "Ice Queen Gloves (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "99e0508a-b6ec-4565-a516-2b8400ded8a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:55.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 953, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7lqx31eaisz33b3nvpajuxr47.png", + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,", + "FriendlyName": "Ice Queen Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:48.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1940, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "edfuldgko8416a06hqug8ckhi.png", + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,iqSyLD37IUejWYa8ndvqnQ,qrXO8rdEaUqUCXC1QVkhsw", + "FriendlyName": "Ice Queen Gown (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:48.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,TRpbSTiI9Ua2E6H7XiT0lQ", + "FriendlyName": "Ice Queen Gown Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1042519908 + }, + { + "AvatarItemId": 2695, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5YfnXd6wCEGAI8JfuYuzDQ.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,", + "FriendlyName": "Ice Skates on Neck (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:23.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2698, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vZa1r1ZS0CNjfN_JffQ5w.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,ENYQugZRP06SDh6d-oJcjQ", + "FriendlyName": "Ice Skates on Neck (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:29.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2697, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VLP_xmNQBkWJGvDaXrvrQQ.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,KdsZQVCA-E2TiEozsSUv7w", + "FriendlyName": "Ice Skates on Neck (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:27.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2696, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cdkQ8Fl7h0-dBHorHAVq7g.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,uQjh0DySZE6M6DzD2VUPgA", + "FriendlyName": "Ice Skates Neck) Ice Skates Neck Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-21T19:38:25.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1505, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "21f1vt9sfsmvu7i0m9fa5v25j.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,", + "FriendlyName": "I Heart Rec Room Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:52.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1586, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e2e7acypymae4je7p6sgzp5cj.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,4oywwjb3-kifr-um9x87Pw,11ZEZTFhy0ONpoXb3kiewQ", + "FriendlyName": "Suspicious Goblin Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:38:26.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1695, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dpd8v8j4srhnyyau92pk9e677.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,i4Zu9uZuWkWwm8IVGZwkbA,RmjxL1O9bEGOuYxY9kzHEA", + "FriendlyName": "I Heart Rec Room Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:32.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1694, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a4crcmj7ke4jvau32zw1jzrfl.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,HQOKcDCvVUCItkAKo2ygGg,RmjxL1O9bEGOuYxY9kzHEA", + "FriendlyName": "I Heart Rec Room Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:29.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1922, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "dygizmsl47ijdpie6x1q5tspr.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,Rd-iQjCjUUW8_o2kIb1rFw,Ez3OIIGl0Eqys2JGnSnQHw", + "FriendlyName": "Pumpkin Pattern Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:09.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1988, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5nmucss74wx9jq66yxgs5bstt.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,lYsxZfxd0EadrnjliEj4Ug,xnZj7ipl60WO8e_BXK-mlw", + "FriendlyName": "Lunar New Year Sweater (Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:43:34.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2778, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GVwVHoCBbEqlEsScL_SXfQ.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,ymRNNF0h3UOv-yH0ILxhiA", + "FriendlyName": "Rec Rocks Sweater (Ethan Bortnick)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:39.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1514, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "36ltpv6ah0ck4rjqp3q1ypw3k.png", + "AvatarItemDesc": "5873814e-c9a1-4042-b96d-2aa0e0d52e2a,", + "FriendlyName": "International Thief Coat (Mystery Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5873814e-c9a1-4042-b96d-2aa0e0d52e2a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:57.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1385, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ev03rmx75zjo4wrysupitq82o.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,", + "FriendlyName": "International Thief Gloves (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:15.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3hl7xtr4md2vxqqi3d3z5zgxe.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,DtFYrQwaCky5C1ZycqZbiQ,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:17.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1387, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3gmgmoqsor5tij57yfuqtj9i0.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,9SAPupKy_EO6YeG0SMkI5A,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:19.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1388, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "29vqlnj8n33qtfh0u4ka6qbtz.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,hiMYlmk5sEqh-sElOY3A9g,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:21.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1381, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7oh8ujd818cghfg99enk8x1d8.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,", + "FriendlyName": "International Thief Hat (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:54.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1382, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d0flbz3gdxvalp78ggvuj2x8p.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,RBRFHN8_rkuImbyDAfcHWw,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:58.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1383, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2jtzah4nvld7knjcyuxcgml69.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,QJSPgq2KXEOWNynY8uqQvA,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:01.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1384, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0bjo5i6bxmha24uhgq15xq71t.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,4RH8_Z1mqU-JIVvx31wOsA,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:05.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1515, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "87ayi0litn96xap15w1n2iq85.png", + "AvatarItemDesc": "d12e5738-00c7-47e7-9334-8399eec5aa89,", + "FriendlyName": "International Thief Hat (Mystery Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d12e5738-00c7-47e7-9334-8399eec5aa89", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:19:01.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1377, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "alszurr07un4fg5ta4aw0z10o.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,", + "FriendlyName": "International Thief Coat (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:44.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1378, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzgwcv1v83e9lql441lco77up.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,YB7kXMyLJ0mF_5WidhmH8A,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:47.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8r2mn0ynnf5yr8p2yifhlpskz.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,bQyNgFWAe0SPttPri6hxyg,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:49.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5lo58vo2p2eebv8s1aunra4xy.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,wRPoHTsXFESK5pjkxVMqGA,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:51.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1588, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0h4tnqaliavr973k1vxic8j21.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,", + "FriendlyName": "Jacket Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:15.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1589, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "688mit0sflv394soqbkisg5u9.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,v_5qFi-vAUSDd1fLNL7pvA,X0_GvGrZGkSP9Ub81CHgqQ", + "FriendlyName": "Jacket Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:21.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1590, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e1v40txlz8i12cmhyjswztu53.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,y5O8zRJy4kuhnXzATSWKew,X0_GvGrZGkSP9Ub81CHgqQ", + "FriendlyName": "Jacket Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:33.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2492, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4DTZdxI0zUWUUrv5xARVCA.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,tLbmGIoldE2CC5Qi1oyqyA", + "FriendlyName": "Jacket Dress (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:08.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1656, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b8nie0lojas6v6embnj3tteiq.png", + "AvatarItemDesc": "85e03c16-4288-45f4-a579-c911437d27d4,", + "FriendlyName": "Hoodie Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "85e03c16-4288-45f4-a579-c911437d27d4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:20.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1284, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7b7sjloem429tv2xpco7r45jw.png", + "AvatarItemDesc": "105a5100-cb57-410f-a26b-1ee21002f067,", + "FriendlyName": "Jack Frost Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "105a5100-cb57-410f-a26b-1ee21002f067", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:48.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1754, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "36iyxpz2ym6qs0k43ssehyv05.png", + "AvatarItemDesc": "105a5100-cb57-410f-a26b-1ee21002f067,prEZHWvOM0ysaNfAe06ywA,q0nBwoIx_UWPe93wHDWTYA", + "FriendlyName": "Jack Frost Coat (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "105a5100-cb57-410f-a26b-1ee21002f067", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:03.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1283, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xvu0kz1tmmaoifwn7fvdtl6o.png", + "AvatarItemDesc": "e6216087-fb37-46f1-bda4-a88c4640f339,", + "FriendlyName": "Jack Frost Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6216087-fb37-46f1-bda4-a88c4640f339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:44.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1755, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6swgo4j116b5mqtoe4dl3pyh1.png", + "AvatarItemDesc": "e6216087-fb37-46f1-bda4-a88c4640f339,KwhdQ1deNE6Y6jzO9vaAnQ,byvLynH8x06OQ7GNCXOVYA", + "FriendlyName": "Jack Frost Hair (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6216087-fb37-46f1-bda4-a88c4640f339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:06.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1282, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7e934oy5eg0xzkuoeyk090u8w.png", + "AvatarItemDesc": "fbdca48d-4ae6-41ad-8c33-b70a9e33fd77,", + "FriendlyName": "Action Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fbdca48d-4ae6-41ad-8c33-b70a9e33fd77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:40.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1285, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "extmqwfwviflhb677k7y0j5vw.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,", + "FriendlyName": "Jack Frost Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:52.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1753, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f5anyvib4zotf2hlzp6twhrpq.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,H6fcuLxeBUSY3oERFc-plA,oGrNKZLpMU2bLwNEe4MPnw", + "FriendlyName": "Jack Frost Wings (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:02.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1851, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "23wddlluexp562bjt32f4humv.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,dpfTnOvsrUePTVs0XZD9qQ,gOt7BYXvw0uUS-U53KThAA", + "FriendlyName": "Carnivale Wings (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:59.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,3XVjOeAlF0K23sB1krWo7g", + "FriendlyName": "Jack Frost Wings Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1956877502 + }, + { + "AvatarItemId": 84, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "en7nrykz2qqj8buybdl5qjmo7.png", + "AvatarItemDesc": "49397e16-1027-4286-9bc4-b59eae565ff0,", + "FriendlyName": "Side Pony Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49397e16-1027-4286-9bc4-b59eae565ff0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 187, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cbcx8aneit1a0djh7mi7a9pd1.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,", + "FriendlyName": "Jazzercise Shirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 652, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ln4084il57kp9glk7af3u8u0.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,3b2cfc85-4e2a-4e0d-b738-c296ae8692f8,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "FriendlyName": "Jazzercise Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:39.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 653, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "axojfsb5b2u9dmz817cfa6h7z.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,78eb0969-f246-4cac-acd6-295df7c092e4,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "FriendlyName": "Jazzercise Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:40.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1850, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dhyvjxkfgg2fqqwac0tztvlo1.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,oF_wUv-ShU6GP34Ch0MnlQ,YTd0lIQLcU2Y2trwuLhJTg", + "FriendlyName": "Carnivale Shirt (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:46.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2159, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HIXUSEOjTEymO-YYuEyRWw.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,zwKRET9pP0O5uhPU_yHrlg", + "FriendlyName": "Ballerina Leotard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 236, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4tf3fmonukbqbhbj46tlqwovk.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 237, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6e2rsyvdnxb0lqz1t1x527ji2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 546, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6yhybki2x1u4frp9d3mj5057z.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:33:21.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 548, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3404czywuocyttg1yeivg4l6d.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:29.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 549, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bjb4do6miad7c1uhkxlvmvtdu.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,d2b40639-5d34-45b6-904e-6dd29030ff44,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:31.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 550, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "68baneyontwji279r6ya132th.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,724c79a3-822c-422f-9462-a5374ee0211c,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:33.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 551, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "88z32iu1vqx5t79u8rbzxb2jh.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,c72911d0-453f-4732-b1bd-dad520220a06,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:34.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 552, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e8kd5l3itqg0vfla6oyfwyug2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,90bc2d31-1715-4d1a-813a-7c57e66cb017,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:35.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 553, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "00krwbc9mtk0c5c3rczo0ba1i.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:36.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1666, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "elfjxtuklom8p26dxh4behbx3.png", + "AvatarItemDesc": "1631eb78-6718-478c-9b62-abbb0537e09c,", + "FriendlyName": "Jester Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1631eb78-6718-478c-9b62-abbb0537e09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:30.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2745, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pRrGRfj8XE-CVSQQ4C_hmg.png", + "AvatarItemDesc": "1631eb78-6718-478c-9b62-abbb0537e09c,9Yj55hS4gEOB34bggoRnzw", + "FriendlyName": "Jester Hat (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1631eb78-6718-478c-9b62-abbb0537e09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:03.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2029, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0sChD_Bp9Eq36jZRpwRRUw.png", + "AvatarItemDesc": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,", + "FriendlyName": "Jester Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:50.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2747, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YNqKRBfsS0CFwf6ZTMjyvw.png", + "AvatarItemDesc": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,g_jYTusUbU6W_6jHif-CKw", + "FriendlyName": "Jester Shirt (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:11.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2030, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "v__U3oLXhEWjGpKh_L8MoA.png", + "AvatarItemDesc": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,", + "FriendlyName": "Jester Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:53.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2746, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "FhANeW3-fUqUVfzO1F7Ujg.png", + "AvatarItemDesc": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,YzJL2s9S0Umv5XH_g5PG_g", + "FriendlyName": "Jester Gloves (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:06.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "96781359-a8e3-4cfb-be5e-79456ead88c6,", + "FriendlyName": "Jetpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96781359-a8e3-4cfb-be5e-79456ead88c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1129156756 + }, + { + "AvatarItemId": 2187, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wsQ3uMHglEWj0o4orNrwvg.png", + "AvatarItemDesc": "17e64798-f6ca-47b9-84ed-0c83b8678056,", + "FriendlyName": "Karate Gi", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17e64798-f6ca-47b9-84ed-0c83b8678056", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:12.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2188, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "oPruDc0j2EGwzv9I9FWS5g.png", + "AvatarItemDesc": "01107a3f-9a4c-4fb9-8918-e6195e346d42,", + "FriendlyName": "Karate Wrist Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01107a3f-9a4c-4fb9-8918-e6195e346d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:15.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2276, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "abgo5oixia5zua1xh9pr9knb6.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,", + "FriendlyName": "Back Keytar (Pink)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:08.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2277, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "8jjzm1iwi6v9h63b4saad9462.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,njOH4bbntk6THrxbd9TDDA", + "FriendlyName": "Back Keytar (Orange)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:11.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2278, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "aiaec0zo6aruhuugqrw4xr6p2.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,0aNp_2sWHUS_08TMe-_AzQ", + "FriendlyName": "Back Keytar (Purple)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:13.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 238, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7fmex9oqxz42zd52m3gbxb3l6.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,", + "FriendlyName": "Knight Armor (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 239, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2p5x227st2nelqfya7ywpcppx.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ed134bee-d199-43eb-98bd-206571603f40,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 240, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7hajn9eszpqh00q654d0iqds1.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,59866578-d4e0-417b-9483-233ab108b1ac,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 241, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4d9p7yhef4c8w1jrj3cd61gbp.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,22dc463a-a89b-46ad-9a0d-557c742b4a80,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 242, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ce1v4ja422emiw67599tvacqd.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1113, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e00pks3p8szzbwf082p7udhb7.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ITH2OpzjP022vZ5JgxV_iQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:17.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1312, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58uqy0e4o8nv424g5aory3woy.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,PMa9370LPEig_7pKwCceEQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:08.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 117, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "23zm48ii99k5gviu6wd03hbo7.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,", + "FriendlyName": "Knight Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 118, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6zfzza7fqr56kicscf9e2lil6.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ed134bee-d199-43eb-98bd-206571603f40,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 119, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7rnan20ml6o94ezwuvfv1cpqu.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,59866578-d4e0-417b-9483-233ab108b1ac,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 120, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1r4tyyoovhhxpcatrb3ixmuwl.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,22dc463a-a89b-46ad-9a0d-557c742b4a80,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 121, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d1fd8ubrjvta346f7rqtixtxy.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1119, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3dqqaqnta15d1edl9k3vlzu5f.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ITH2OpzjP022vZ5JgxV_iQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:56.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1308, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6hsc92hhcj6jhxh9avyxdqvj.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,PMa9370LPEig_7pKwCceEQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:46.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 276, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "85cm7w6an4z42wlzdgr9k9d3m.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,", + "FriendlyName": "Knight Gauntlet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 277, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bohc8ayxvepselhmkomofmhb4.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ed134bee-d199-43eb-98bd-206571603f40,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 278, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "37g3go1ko7zez3ua49s4tjpbf.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,59866578-d4e0-417b-9483-233ab108b1ac,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 279, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1q79g6q9p9to9xi0qtaj9a9s3.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,22dc463a-a89b-46ad-9a0d-557c742b4a80,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7fy3687o43lmb87xgdmjslpcy.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1109, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bvdx04lp2rrweg7mjd5hl2bnk.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ITH2OpzjP022vZ5JgxV_iQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlets (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:07.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1318, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9tsbb3bjtxr44158yh7ncrn5n.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,PMa9370LPEig_7pKwCceEQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:47.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 58, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0gmqaftgdlsoph92078ffbhpq.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,", + "FriendlyName": "Knight Helmet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 59, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "acqok07g1d8homim9mnjivxuf.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ed134bee-d199-43eb-98bd-206571603f40,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 60, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0r1snvqxftybe0m39s41incq1.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,59866578-d4e0-417b-9483-233ab108b1ac,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 61, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a4l2zz1yv4o8nlxgjjcyczfza.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,22dc463a-a89b-46ad-9a0d-557c742b4a80,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 62, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c2t0mn4y8uzp3mnvpdd9z0unb.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1122, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "15u1a4mvdkbujoe41brggozje.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ITH2OpzjP022vZ5JgxV_iQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:07.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1302, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eb444rmgn7q2ygy1kaxe6tsh2.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,PMa9370LPEig_7pKwCceEQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1917, + "AvatarItemType": 0, + "Tooltip": "Survive any game with the Squid King watching your back. Go Krakens!", + "Rarity": 50, + "ThumbnailImage": "5gf9eq3qeqfuxeh3zi34kmtt1.png", + "AvatarItemDesc": "9176086b-a0b4-4298-a7fb-eb91ea421e89,", + "FriendlyName": "Kraken Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9176086b-a0b4-4298-a7fb-eb91ea421e89", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:19:29.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 243, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5zxnu4wl529tzcip7nwk8im0o.png", + "AvatarItemDesc": "39278302-b52a-48a8-a566-ed756a5fa1a9,", + "FriendlyName": "Lacrosse Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39278302-b52a-48a8-a566-ed756a5fa1a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 244, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0pmiv1zb13od6nl9giu03eizj.png", + "AvatarItemDesc": "39278302-b52a-48a8-a566-ed756a5fa1a9,e3454bc7-c88d-4958-8b81-db3fe5472a1e,da36bbcc-2a9a-4ba2-9e4a-6f52c9b1ef74", + "FriendlyName": "Lacrosse Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39278302-b52a-48a8-a566-ed756a5fa1a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 4, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0eekw2et1by72nmm01x4haan4.png", + "AvatarItemDesc": "7134c60b-0640-463a-8d97-caa9184bef93,", + "FriendlyName": "Lacrosse Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7134c60b-0640-463a-8d97-caa9184bef93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 5, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6cznt9a4cfpxfot00vquk2gb5.png", + "AvatarItemDesc": "7134c60b-0640-463a-8d97-caa9184bef93,e3454bc7-c88d-4958-8b81-db3fe5472a1e,7e688b30-ed13-4790-9ad8-c49885d1b7e8", + "FriendlyName": "Lacrosse Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7134c60b-0640-463a-8d97-caa9184bef93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aqhhqebivzflcpyzyf70m1608.png", + "AvatarItemDesc": "c859a371-5d34-4e53-a96f-a6f7f3377aba,", + "FriendlyName": "Hero's Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c859a371-5d34-4e53-a96f-a6f7f3377aba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-29T23:24:59.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2155, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kNf3tlW1kEGHxU4EDJABMQ.png", + "AvatarItemDesc": "c859a371-5d34-4e53-a96f-a6f7f3377aba,aMOwLebcBU6SuPigtovUYg", + "FriendlyName": "Future Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c859a371-5d34-4e53-a96f-a6f7f3377aba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:24.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 576, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "500ajcxzkdjvi2db9ev74rstc.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,", + "FriendlyName": "Laser Tag Visor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-28T21:55:28.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 580, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "81x4cfhnckh1a4ni8mxn9trzj.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T19:04:48.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 740, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9z5r4i4elegluux9nocggv8lh.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,oT1vLrHW6UC2hWLIT3f_SQ,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:02:48.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 741, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1mu93bk5rhp7yqekc2k5aukn9.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,-e6bmXnwZU2EVwZ8RupKbg,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:02:52.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 574, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0mhwz2ogugpedmv34mtp8p649.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,", + "FriendlyName": "Laser Tag Sensors (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:34.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 575, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1gwx6x19onp32q7iolw1hdeck.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,08b7c12d-3317-4d27-9b40-34737a709454,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:35.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 742, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4b2c3lu6x2lix8wwu23es1z9t.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,jkuw9EhCpEKn-2lKJXWd3Q,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:05:11.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 743, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2mtxs666msasqmqw57iol4efk.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,2Z_FxSPiyUODtlFZWF9DFQ,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:05:17.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 579, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "15nrb536jxz09ck6emxavel7l.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,", + "FriendlyName": "Laser Tag Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T18:30:43.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 581, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dzm37a1dmgdsj0aetwi19afes.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T19:04:55.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 744, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0qqunbtr2dxrzl797zjamcpg9.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,EC0ttvk6eU2HEMacLAx2Iw,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:06:17.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 745, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "13cf7bho89ewryskk93gh33it.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,mhsglVEtREmElF5wuLw3NQ,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:06:20.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2248, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eYDxcWNR4kOlLWhyBLjM2A.png", + "AvatarItemDesc": "665d4fe9-88dd-4fae-97ad-201884a6ca02,", + "FriendlyName": "Lasso Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "665d4fe9-88dd-4fae-97ad-201884a6ca02", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:04.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 958, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dl9x5bl0fz2s03jn0xnqwzao0.png", + "AvatarItemDesc": "mfFLpKQRE0anhrmeUvW0bA,", + "FriendlyName": "Laurel Crown (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44bf199-11a4-4613-a786-b99e52f5b46c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:24.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2608, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KgPqo5yaDkibaNuclBpcJA.png", + "AvatarItemDesc": "mfFLpKQRE0anhrmeUvW0bA,p5TStfnDXk-EY7k6UdHxnA", + "FriendlyName": "Midnight Gala Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44bf199-11a4-4613-a786-b99e52f5b46c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:04.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2855, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5Zki1DsZ0UW_pMecyP3JoQ.png", + "AvatarItemDesc": "37b68987-73d9-4aae-8e3a-edd24ae890a9,", + "FriendlyName": "Layered Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37b68987-73d9-4aae-8e3a-edd24ae890a9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-03-10T22:40:41.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2856, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_uhKCE9kyU6LOgc4Sixxqw.png", + "AvatarItemDesc": "37b68987-73d9-4aae-8e3a-edd24ae890a9,UCxz68a5E0WaZkocDfEf5A", + "FriendlyName": "Silver Slugger Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37b68987-73d9-4aae-8e3a-edd24ae890a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:43.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2199, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZN_075PMnUq0_A0OEIo51g.png", + "AvatarItemDesc": "32b7a724-1112-476d-9ddc-2d04655c9df0,", + "FriendlyName": "Leaf Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32b7a724-1112-476d-9ddc-2d04655c9df0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:54.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 759, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b89jss76zlj72ms617ah00h3b.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,", + "FriendlyName": "Letter Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:24.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 874, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6m42sjjil9iqwyjnymvy516a7.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,apSQiQ1eO0mEzRsbugeu9w,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:32.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 873, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6mcbxskzp5pfwcnetac762guc.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,VpqHNpPiY0CfRJEjRRM-mw,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:31.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 872, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eq8wyyutlzcjeneqryph1cfzk.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,Z1F9rjKqCE-Hd2-Ip5g3gA,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:30.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 871, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4xku2dldqk5zfbjc6m9uw2v07.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,iufpnteVCEiiZu_m-yPovQ,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:28.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 762, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0luffpfh5ls3knh5u8r6w0x5u.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,", + "FriendlyName": "Letter Jacket Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:27.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 937, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "amfc2bzt0jj15t76n2lnhv25e.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,UUyjVU4mvECPhI9xTMYR1Q,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:13.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 938, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4hbdjfasjaponwtrx2tmjnwa3.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,HWc25-d8dUmNN2F3n8mV5A,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:15.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 939, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "39guqwt3fqru9qq6psoua1yff.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,Y0e7o0_BKUm7XJSLediN5w,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:16.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 940, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8uiva9ffm7qrizcvqx99frs29.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,MS7EPNLJEEyMLC6_yHQYtA,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:17.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 756, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7hwrwj6lty2rur9opjp05fgej.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,", + "FriendlyName": "Plastic Sunglasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:21.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1641, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8oaudlg81glmnhd3cqy8t650s.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,z-yaXP9ImkysLOGuCQiblw,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:46:59.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1642, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ec4tf5t9oqy8loqqvqfrw3rl5.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,SmRcrycMZ0qYNsHuQQOORA,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:01.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1643, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "egy0vqy333fqme7jsrxachsgt.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,0svDrGn2NkmhUjC1VJUgRg,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:03.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1644, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0hvhuyqd3i33hptb6ljvlazeo.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,Aq4fjEW5gEyzetXMoG2tdQ,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:05.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1832, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ew0vvmubkulgd7xjvlqhfuc2.png", + "AvatarItemDesc": "affdc9ad-8312-4421-8f56-e35dfdaeb63a,", + "FriendlyName": "Lifeguard Whistle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "affdc9ad-8312-4421-8f56-e35dfdaeb63a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:16.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "e97f05da-57d3-4927-aa22-c124a94d8432,", + "FriendlyName": "Life Jacket Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e97f05da-57d3-4927-aa22-c124a94d8432", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1703069062 + }, + { + "AvatarItemId": 1900, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ct8ojf0jqnnc7an3o2t5zs1a5.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,WWVPdeMMi0S4_Dmn85yEqQ,FRUp0aYZTUmAXsybauOs7g", + "FriendlyName": "Director Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:06:09.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1417, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "75icni7eg78l4eyuy6hwwrzzk.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,", + "FriendlyName": "Luau Dress (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:21.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1418, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "63oehgwwbgnddpx5bauhi2ztm.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,ZXeI9jooq0-dQaX-FlkNuQ,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:23.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1419, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "08kjv45btrzy2apeo276vys08.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,IzdUC8x0F02i3BhPfEUOYw,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:24.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1423, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "du4ay6j6cuhxjyg0f72e9dwtt.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,nPUQvBsxu0mzhQg9YnWEgw,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:51.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1422, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67fl0ih7xgt9itoq9vvq1op9o.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,wyKAZSZwT0u9cuSaQ_-IHg,vUgpsaXrO0ak7huhkHqlUA", + "FriendlyName": "Luau Dress (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:48.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5uifh9814ayb3sywh90qxniz4.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,_OM7UNZENEejgO5h7H1ztg,vUgpsaXrO0ak7huhkHqlUA", + "FriendlyName": "Luau Dress (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:46.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2236, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "jWeFkQl2_Uu7fhibuWdtXA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,D9U3j_XgSUesfHtSNoAdLw", + "FriendlyName": "Luau Dress (Cow Print)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:35.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1149680221, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "s-VTHTHagUa_Xh0Oa3PAJA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,I8SCbauJX0OLAKWOjy78yQ", + "FriendlyName": "Strawberry Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1876125901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PO4F_PVCVkSGejhtWCvxVg.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,SiraUm3ZaUOCeXCekunJuA", + "FriendlyName": "Pineapple Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1965281381, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FVH53dlIV0yvAH9GU2tgxA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,9Z95fSKS00GscDxK8-ARlA", + "FriendlyName": "Blueberry Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1480692753, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0_EW1WuF1U-UBFVVcVjAGw.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,wLpOk1g2rUyDeCKb5uOeSg", + "FriendlyName": "Gamer Dress (Raspberry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1421360180, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aKVQYVQFDkCP9Sy-U3FWXQ.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,RKftA1Fw30-hriuZNMGsoQ", + "FriendlyName": "Gamer Dress (Grape)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1341801044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "A8eUc7t5xEK6b8yAZPGUdQ.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,P6w6X7-Dak2G6O8HDRwlVg", + "FriendlyName": "Gamer Dress (Lime)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1444, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9hmp8axdgbs7r6mungzjw2kzt.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,", + "FriendlyName": "Luau Headband (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:25.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1445, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0gg1ctgab27af7zjuvqitr0zi.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,fV4H_1ZXx0-OVtlasI-xGQ,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:27.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1446, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7nuouk86sz6hwwhfoh6ca9e3a.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,B9DJPqTeYUuPnPHRbKzEJA,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:29.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1447, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1u22xuxo0l7hbu4dipegf91f6.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,yXZO-donI0SHB9-TT0jUdw,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:31.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1448, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4sf5uwmo0j53djcg0konx31j5.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,J5VsmblfWU2MeUjNo0fz1Q,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:32.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1449, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "afqbc9aj7hwgkwft5kyonc1xk.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,Asfe0ZRi50eryD5wlJzgzA,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:34.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 189, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "beio49fzwv1l86rp7tcvhz8t3.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,", + "FriendlyName": "Luchador Singlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 499, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eazhrt40qa5kdt73v7vcqhsdm.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,916384ca-e209-42a0-bd64-c64f5a40b56f,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "FriendlyName": "Luchador Singlet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:23.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 500, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6m59lwkz3pdm7ylfg5q1hwbzf.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "FriendlyName": "Luchador Singlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:24.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1733, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28qzx00rr1mcsdfj73tvv70qd.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,TLrw3oncCE2MeuVifXSWTg,-_ViYCHHRUip7oN_AnH0Og", + "FriendlyName": "Luchador Singlet (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:45.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1513, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0dhyx3nk36z445fgi9v0vyvce.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,7Jx8mrxoi0aQtZEb03CdRA,-IgxYllxkEyr7V1gosWXUA", + "FriendlyName": "Luchador Singlet (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:37.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 122, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8dvvtd9gw9krmqu0dvvlk1rv0.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,", + "FriendlyName": "Luchador Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1734, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "72gv1b98ofujgx42ctd0lwdf3.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,_VfdBTi7PU276qzsYKayow,qSsEcadcm0-twR1TvVcgYQ", + "FriendlyName": "Luchador Cape (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:53.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1512, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2xop370rhid69r8mypwbzzbe1.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Dtis2HpVzkelZb6M9mW5TQ,iqGv6iRJ-UGbofv8-aHKVA", + "FriendlyName": "Luchador Cape (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:30.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1609, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6gheyta77ei8tn3jvtdoimp48.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,xNjX_6-aREm0cYZObVLiAg,OE-PtL0-0kWcJPwn1wvuVg", + "FriendlyName": "Luchador Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:08.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1610, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "32o16d0w2ku6u0tavwqbw9lq2.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Kcyp8sv0202Puym1Wf86xg,OE-PtL0-0kWcJPwn1wvuVg", + "FriendlyName": "Luchador Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:11.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1679, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e1cquzpfu1rcke1xofv7ks1oj.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,r_QZLd-Aw0SsdQb_uYroog,eYGWiwqSGUG7Hx1R3I-11A", + "FriendlyName": "Luchador Cape (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:24:48.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 63, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bgxz7thjzppr81bia4pwvg2qh.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,", + "FriendlyName": "Luchador Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 487, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4jk1xibdjjddzpvrwcw05usiu.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,916384ca-e209-42a0-bd64-c64f5a40b56f,96b36c98-cba1-4337-91dd-c74c00316f97", + "FriendlyName": "Luchador Mask (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:08.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 488, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "59dj5f6fpgmoilwpcpugnwomn.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,96b36c98-cba1-4337-91dd-c74c00316f97", + "FriendlyName": "Luchador Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:09.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1735, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cwfcqra4nyurw86ksh4qv0cjq.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,RA7cizxxYE6J8yV_0XwPxA,VSLcLM0TJU2kJU5gS4KZGw", + "FriendlyName": "Luchador Mask (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:56.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1511, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a6g3l9h3i3ads39fgxf0lnwaq.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,5_UnQ7vZ9E2T-EhJ7M-mQw,jL-LAsd03EeN1J-Zp8opew", + "FriendlyName": "Luchador Mask (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:23.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 64, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d7reyk0b2dqeh51xmv3wcihld.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,", + "FriendlyName": "Lumberjack Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 829, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cjqz06mkg3l90vu87fnh371yt.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,7amHTgEv-UOJSGl0ofo4mw,lb5tuJOqnUKp9prjjlgHmA", + "FriendlyName": "Lumberjack Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-23T21:21:48.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 830, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "drjic5tpew7fpjaelvkqowvgg.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,cKHAetNcgUOxFfDkAPPF9w,lb5tuJOqnUKp9prjjlgHmA", + "FriendlyName": "Lumberjack Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-23T21:21:49.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 245, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0t92rww5x13bmx7jh4n78jqtx.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,", + "FriendlyName": "Lumberjack Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 775, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8u0ou663lluquhfy88m3nuvu9.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,7amHTgEv-UOJSGl0ofo4mw,CUCKsNVnIkavHjcspWAv_A", + "FriendlyName": "Lumberjack Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:58.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 776, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ba5xrtahc2k6jzt6i6q17wg7u.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,cKHAetNcgUOxFfDkAPPF9w,CUCKsNVnIkavHjcspWAv_A", + "FriendlyName": "Lumberjack Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:00.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 107, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6fn7llrs7vzf0mr1pws7ys253.png", + "AvatarItemDesc": "61ba3c90-c81e-4deb-bc79-50c0f1fe3e83,", + "FriendlyName": "Lumberjack Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "61ba3c90-c81e-4deb-bc79-50c0f1fe3e83", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 768, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "301onvpom3avfdh6fxup3q7fv.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,", + "FriendlyName": "Anime Tiara (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:49.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 922, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2oaezhhf77m3tlzy73ehvtapu.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,lhfOKGDAgEKWucerCmH7Fg,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:56.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 921, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dzietotul0k7x0rgoxmeqn53w.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,kLv_wsbbGUmlfIUO1QLYLw,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:55.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2oen9cny4wzp5qnb6rdx5nchw.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,f0pjsV0XMUWoE_z2kqOlSA,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:54.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "79soc27032oiz7ezyzb7bj6w3.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,EDLxuGjEVEGdOPXDqyN-xA,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:53.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 771, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d9jbl9u6drfoj1reb16xtye8f.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,", + "FriendlyName": "Anime Bow (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:54.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 903, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "13c35i4i2yh37p6u3kwbblvup.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,Ot9EG-FfKEauT0u6pSZuWg,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:34.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 902, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "54xi44ymvq9amsim1078bvqlt.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,i3rD_hnQG0a-5f6Ve4AfWg,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:33.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 901, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "35wve61we8efcghfumwpogrnp.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,S9uymuvraUCOM8gmB7bnEA,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Dark Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:32.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 900, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "31o9bg37u28wwxm2fpf5h095x.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,_uh3kj6hKUyBKOSR3R8dog,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:30.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1822, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dc2oyr445lq5s6rcxguz7tjdd.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,kaRBW1u3dkODGjPMePW6Uw,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Rose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:49.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 777, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ouv3swjffh6u9o5pks8yim6q.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,", + "FriendlyName": "Anime Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:01.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 870, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8pxavigoe6em3oy4686ym8ue2.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,VQjkyb5aoEKHKo_eZsq9ug,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:27.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 869, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1b7zb62caqbw5yuafz8hg7nxn.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,kTP3xKhzBkuQF3SqvmYFKA,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:25.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 868, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b79pggvoulv5z0dkn70z5syru.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,hYu9itGe1Ee5Neanhj9VtQ,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:24.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 867, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3unj20ah5pwz86qh67lqz9qvh.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,FE7_gQNBt0CXzMN6di5HWA,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:22.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1820, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c52ga28yxp7v5z0t8ow8e4rtw.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,dYPaOkwy3EGAfvl8RyN4pw,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:39.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 781, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "by0enw83tqi7qq14gmpytpavy.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,", + "FriendlyName": "Anime Glove (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:06.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 941, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1qj8ezhvwgdu6g4waborypyet.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,zaSTWOUy_k6cnoVVYzypWg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:18.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 942, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6tq3bm6g7idt1uv2itnhf5qb9.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,1FcyrVGyGEC9dmOq_51nFQ,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:20.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 943, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6qwl1qaaam1kkaku033yyeppd.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,xpkEQ6yjhU-WfFPSjvPdHg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:21.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 944, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2dr1z7istjp1d0bnqdoebk8ne.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,N9CBuYaMtUOrCks5UcOycg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:22.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1821, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9y4m58z1b4kasw620juo4nihy.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,LDjFJtM-WkeYhtwA7cyDOg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:43.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 190, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dl8ieqkdftxjtgfey8gb41yid.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,", + "FriendlyName": "Cheerleader Sweater (Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1645, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aidgb349qpp5nx0tlmqvfmxvy.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,6secgNPvmUyFnI3NC5dIYw,5V_1JEDrRUa1UPKhtrSB2w", + "FriendlyName": "Cheerleader Sweater (Victory)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:50:04.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1646, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "120gze52rf81cvsx2extomyi8.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,X5SuBHIGBE6wy8N8KDop8Q,5V_1JEDrRUa1UPKhtrSB2w", + "FriendlyName": "Cheerleader Sweater (Rally)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:50:08.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2424, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zwCsgKFNm0KCFbci6h6_ag.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,bc-aVFn__E61pyA4SvUoGA", + "FriendlyName": "Space Suit Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:51.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1952325815, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,dDSR-6ck8UGOeCSXR8Xnug,OFIqUvE4r0yMLHSMnlWMhw", + "FriendlyName": "Male Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.513779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1964834389, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,EaJMu_M0OUyUZ_3x-X-d7Q,OFIqUvE4r0yMLHSMnlWMhw", + "FriendlyName": "Male Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.513779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ckirjfll5885ahrmczp8ry2l.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,", + "FriendlyName": "Class of 2020 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:08.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1890, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dhfwe25d3r0oxtup5xd695cyf.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,oMz5rMzCxkiw9690ZkvlGw,gW2zFv0sFEmiodfw8p9P-Q", + "FriendlyName": "Early Access Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:00.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1903, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "7c0xvai9f3e3orr592s3eq4da.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,zlBPAWMbOE2RuKoIy1Ps8Q,UwQiLub0-kqf4dNvQc-R0A", + "FriendlyName": "Rec Con Shirt (TFO)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:31:04.757Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 192, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "au05f75bl482johdfb0fvjxjw.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,5864d23d-1a86-40a6-a52c-46ee670e4508,2ee3944d-e9fb-4959-973e-570a7fc6cc57,167ba0d4-4fac-4143-b5ae-1dbd8e02dc76", + "FriendlyName": "Class of 2016 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 336, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01iubrr0qyuaj94by5oj4xg3m.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,14afd295-bcd0-4595-80dc-d477618b9ab9,2ee3944d-e9fb-4959-973e-570a7fc6cc57,a74e6c0d-1300-4928-9141-977a711b71c4", + "FriendlyName": "SFVRCC Promo Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-27T19:16:00.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 337, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "eyc48682dl6k4obf67w3822z8.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,f8635fe2-90bc-42c8-9bbe-3387b5157e18,2ee3944d-e9fb-4959-973e-570a7fc6cc57,49934059-cc45-46ce-8277-f96959e1f6ba", + "FriendlyName": "Helix VR Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-27T19:16:03.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 362, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e03fl2rpw0xd46aawjspj2i92.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,922f229c-124f-4f07-b4e3-19138420ca19,2ee3944d-e9fb-4959-973e-570a7fc6cc57,ceb07e4f-50a9-4ed0-b50b-c59d92ba66b5", + "FriendlyName": "Birthday Shirt (1st)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:02.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 565, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b1k80npgw88paq57cw285zhgv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,0c496f32-0011-4c4d-9778-59446f70c032", + "FriendlyName": "Rec Room T-Shirt (Crew Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:23.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 561, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2stqqgiaqmpxhz7kyzokxny1x.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,c0ff7719-c56a-4934-831b-506ad0f51cca,3259238f-4a9e-4e06-99de-6e7324fa0894,e38294cf-6279-4863-9aea-1a19aeaf8476", + "FriendlyName": "Laser Tag Alpha Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-08T18:59:57.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 562, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "69q96bwnfq08dvg7z8i3423dd.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,381a9f13-5b44-48e6-8a49-221422a963f5,0440f08f-ef1d-49d8-942b-523056e8bb45,9e782751-71c6-4208-87da-3a3cffa6a4f0", + "FriendlyName": "KO Skull Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-08T18:59:59.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 567, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5fseh1kw7c5ej962dbiugr5ns.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,befa2f11-b9fd-457c-b1d5-9d82e90b70ce,0440f08f-ef1d-49d8-942b-523056e8bb45,770e7674-6f9b-41f8-b023-ae79738db0d3", + "FriendlyName": "Laser Unicorn Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:26.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 566, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bp1j53987blav97zjuw9die57.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,bde08851-a9e4-4faa-8f04-a0a661672472,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "FriendlyName": "RECS Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:25.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 578, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "afeyng27plnoeojutrac3dvvj.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,5e7a48ed-8f08-4990-a3ae-05722c9f59a8,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "FriendlyName": "RECS Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-29T01:25:03.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 568, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "92ns2hqejpkog0560clmw3qjv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,4d52bbd6-ef74-45ae-a6c2-bde10ab8eb16,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "FriendlyName": "Robo Logo Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:27.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 569, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3tf587nd10cq6wv4dr1lk0r9p.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,bda7a57e-5227-49f4-a119-e3f166604738,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "FriendlyName": "Robo Logo Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:28.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 570, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "85nms7ezyti5i4wp8l9azl90l.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,6c395d04-2aa6-4a3e-8c2c-695932bc9736", + "FriendlyName": "Squad Goals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:29.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 571, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "19u0u4m2c6wsyaocczd4hv6pd.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,703ff56b-560d-4ff4-8c63-d195e879a328", + "FriendlyName": "Alpaca Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:31.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 572, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dckjq7bzoptwnhc7299yac8sm.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,fe304452-b57b-45bc-9c1c-b85b70bcec12,0440f08f-ef1d-49d8-942b-523056e8bb45,6a007c73-d930-408a-93df-041a715e0dd4", + "FriendlyName": "KO Skull Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:32.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 573, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "eu5x1j13qp2b88ukdkh7eax1v.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,8f19d7f9-4f28-48ab-a1c5-d8f4f788514c", + "FriendlyName": "Hang In There Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:33.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 594, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dtf1nt1wah54rmo77jxzohkg6.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,75162f12-44e7-499f-bc6c-9b0612e9164b", + "FriendlyName": "Class of 2017 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-11T18:54:38.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 623, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "26xtdwd2iy25ifneaxeal9nji.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,ba6b6e1a-a09a-4ba0-9523-552869f03336,0440f08f-ef1d-49d8-942b-523056e8bb45,d461ca71-45c9-415e-8e09-ba93e8d73450", + "FriendlyName": "Happy Tree Shirt (Blue, Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-04T17:02:17.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 731, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2w7aitx6z9si6pezgai622y01.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,rnM1qDlYBU6_Dl1bjg0J1Q,0440f08f-ef1d-49d8-942b-523056e8bb45,OZb8bb5FNUuFYOkwANvxAA", + "FriendlyName": "Rebel Rose Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:48.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 732, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4yagacwve48lrxa8yoo14k83r.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,0qh4mcxJH0Wsrb3nz8UeGw", + "FriendlyName": "Emerald Syndicate Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:51.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 733, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "70j1efvr06uolk29w6i31vbbq.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,Ib_iMwvUm0CJ-t_Thwt77Q,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "FriendlyName": "Cyber Trash Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:53.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 734, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8o1cue9ixf7cfijq9eqg5xk8j.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,yaQ7AlUc3kGq8_RW3eTSmQ,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "FriendlyName": "Cyber Trash Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:54.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 735, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2ad06ofz4ewso7onvvyfzerog.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,dDLQbD9jtkOmw3PR5QJdfg,0440f08f-ef1d-49d8-942b-523056e8bb45,2tFUNn-4kkK6ICo_Zp065g", + "FriendlyName": "No Laser Ham Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:56.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 736, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "aqkgdsqeztkohtgzqy9eu3ee6.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,g4diCmk0wEmIKUVozk9tDA,0440f08f-ef1d-49d8-942b-523056e8bb45,nHg4Uifww0GEX15L27WonA", + "FriendlyName": "Laser Graffiti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:57.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 737, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8q53ymq8bhq82a6s9fqt8a814.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,MPfe2wahQECCDGfhZE-drA", + "FriendlyName": "Hack Graffiti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:58.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 738, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5k6xiv9l2oaeteyfht5ak4epq.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,nLRxEIBhUkyQsfrk3nzsDA,0440f08f-ef1d-49d8-942b-523056e8bb45,n4hqSCv9OUewI30KnbPhFw", + "FriendlyName": "Cyber Junk City Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:16:03.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 739, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "agqloaqzftkyi0wa9v9j439d0.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,M1JVd5ur7U2ZtpvhEhPNhw,0440f08f-ef1d-49d8-942b-523056e8bb45,tzMFIJyxZE6cZNuLdaKs7w", + "FriendlyName": "Hyper Noodle Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:16:09.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 965, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58ls1z246ivu3x64bsenly4v9.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,6kPcLZtEqU2Hs4yBFvc_sA,0440f08f-ef1d-49d8-942b-523056e8bb45,NxMm0sSBdUatynU16o0_Zw", + "FriendlyName": "Class of 2018 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-22T23:43:30.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "978ivwbrlhtfjt9lpibr7eqx3.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,TtncjvYcK0qHqWN-IMMoqg,0440f08f-ef1d-49d8-942b-523056e8bb45,hHhjZeLcaUGN1OyFoYHznw", + "FriendlyName": "Happy Tree Shirt (Purple, 2019 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-04-05T19:47:41.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1130, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a9mzwqn1pzmy5ptemp2sjk6r3.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,fwIgUAM_C0WhKds0O6DyPg,0440f08f-ef1d-49d8-942b-523056e8bb45,fJVxsLWuTESd-2xTCJPZ2w", + "FriendlyName": "Recollage Shirt (Pink, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:28.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1295, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ansni4asvqpm50bcuomwb4i4l.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,ZNPzHmrU6kGVQW7Q5ei03w,0440f08f-ef1d-49d8-942b-523056e8bb45,fJVxsLWuTESd-2xTCJPZ2w", + "FriendlyName": "Recollage Shirt (Yellow, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:42.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1246, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "971lp3mdb0j453fkjy0cckqud.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,d3-sxPTJOk2rKo-Jgy3wvw,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest T-Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:36:17.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1563, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ded2g4m0rj9eastu0comvx8bw.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,v12eJ8louE-cW7-pCGzVLQ,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest T-Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:32:35.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1kt3y6b06rh6qdox2huzn0c6f.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,6kPcLZtEqU2Hs4yBFvc_sA,0440f08f-ef1d-49d8-942b-523056e8bb45,br9hzhDoZUmIevgl7SJkmg", + "FriendlyName": "Class of 2019 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:11.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1369, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3d9h318l8ubxs4x703bpvjqcy.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,TuAjeDmPuUmFr_ESx4q6WA,0440f08f-ef1d-49d8-942b-523056e8bb45,AXtRUZcYWU6g1ZtM-L4vfQ", + "FriendlyName": "Video Contest T-Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:07.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1499, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "anq7dknee5z82yqq5bymquby0.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,LrQ1z-mTHE2vIePBwm8l_Q,0440f08f-ef1d-49d8-942b-523056e8bb45,omS7gF10C0ec0ozEoJGJXw", + "FriendlyName": "Recollage Shirt (Blue, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:19.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1839, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0jdw82n61uuapunyx297i85ng.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,owoIOIEM70e6WxnYdlKYWQ,0440f08f-ef1d-49d8-942b-523056e8bb45,t2do8WSejE-Y0aiEirKw9g", + "FriendlyName": "Video Contest T-Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:19.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1840, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "84fxnyn9eqldulf6rpki5uuz7.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,eb1vYTP_KEGQH-f33_whQg,0440f08f-ef1d-49d8-942b-523056e8bb45,t2do8WSejE-Y0aiEirKw9g", + "FriendlyName": "Video Contest Shirt Reward (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:21.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1841, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zhwxqyn2zie0fpfnaf18z5ve.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,evgH6VWUgkKJNews32hUxQ,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest Shirt Reward (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:24.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1889, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4dsx1k2farwthtgsfgem735gx.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,UUlB4S3GVUGyil6ZK-EbWQ,0440f08f-ef1d-49d8-942b-523056e8bb45,omS7gF10C0ec0ozEoJGJXw", + "FriendlyName": "Recollage Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2021-08-18T21:22:47.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7zgp3njcd8tpgi1f8j1679bwa.png", + "AvatarItemDesc": "c8a97aeb-e8dc-40ba-b6bc-a7c7be907381,", + "FriendlyName": "Feathered Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8a97aeb-e8dc-40ba-b6bc-a7c7be907381", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-30T20:20:27.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1324, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ucp3apcgf7mvka7m2albckl8.png", + "AvatarItemDesc": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,", + "FriendlyName": "Mardi Gras Mask (Orleans)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-31T00:38:06.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1651, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3lmgppocqd4j182ijbbxl0eo0.png", + "AvatarItemDesc": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,mkkk2N_2-EKX3pVfk7U5aA,UQe6v62i6ESImdpc7gB_8w", + "FriendlyName": "Mardi Gras Mask (Parade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:35.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1325, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjo835jgyqtwaijuhly8otljo.png", + "AvatarItemDesc": "43354e59-4938-44bc-81e0-4fba47d0ac12,", + "FriendlyName": "Mardi Gras Mask (Carnival)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43354e59-4938-44bc-81e0-4fba47d0ac12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-31T00:38:11.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1652, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9x7rlsmit2ca70h5xx2vgtwez.png", + "AvatarItemDesc": "43354e59-4938-44bc-81e0-4fba47d0ac12,4xp-xZyNWEi_jJCIhAPI3g,Ulie-RkcJEmblLyoucx-JA", + "FriendlyName": "Mardi Gras Mask (Revelry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43354e59-4938-44bc-81e0-4fba47d0ac12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:38.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 725, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bt8tu5acrblwkf3nc68uhi3jl.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,", + "FriendlyName": "RecFly Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-27T22:48:20.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1084, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "66rxidv3887cwdjojesos240j.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,kkvNbVRjcEKtChpy3W1bqQ,nbhzQy_Hdkeohh4S1IezwA", + "FriendlyName": "RecFly Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-12T23:52:26.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1671, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4gn26corqk34q37e2hx5ouc9q.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,GVoh3mdWf0WNA9lfqL1GEw,w9qG5gHlAUqh8f9ZgnezsQ", + "FriendlyName": "RecFly Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:05.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1672, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cyarqq8aeox95hehu1py5nbbc.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,x4PPvuhg6USE6GSFgZZnCg,w9qG5gHlAUqh8f9ZgnezsQ", + "FriendlyName": "RecFly Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:08.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1681, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eewujb3it57bm2qlk725l2aji.png", + "AvatarItemDesc": "d4f86b2b-0fa3-4264-9eac-cf15b62463bd,", + "FriendlyName": "Resistance Shades", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d4f86b2b-0fa3-4264-9eac-cf15b62463bd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:33:42.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1680, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ekvm0nia4j7hfolms572ortek.png", + "AvatarItemDesc": "c4622dca-80dc-423b-ae22-2065174e817b,", + "FriendlyName": "Resistance Trench Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c4622dca-80dc-423b-ae22-2065174e817b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:33:36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1931, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3iw1qqexx0h5n5ksh78jfpf4h.png", + "AvatarItemDesc": "c4622dca-80dc-423b-ae22-2065174e817b,pCEBoSP2pESLl9ih-J-55g,8SzXFoWD8k20cekFe6cLKg", + "FriendlyName": "Resistance Trench Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c4622dca-80dc-423b-ae22-2065174e817b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-20T21:24:53.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2279, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lQCcPfTrxkuFrwpWyZRiiw.png", + "AvatarItemDesc": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,", + "FriendlyName": "Mecha Helm (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:15.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2703, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b-rwdReQyEOex38lYG4k7g.png", + "AvatarItemDesc": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,GhulL0k9q0euEooMTUKTHQ", + "FriendlyName": "Science Helmet (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:39.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2487, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "PaCn_gU9XUWR1kli2ZdIYQ.png", + "AvatarItemDesc": "40258a3d-0d5b-4b67-bb99-4bafc6410311,", + "FriendlyName": "Science Helmet (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40258a3d-0d5b-4b67-bb99-4bafc6410311", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:04.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3clewgmvjgao6vqaxfjuzx41z.png", + "AvatarItemDesc": "b74ef8c3-405a-4d68-a2bf-01579a9cc397,", + "FriendlyName": "Mecha Helm (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b74ef8c3-405a-4d68-a2bf-01579a9cc397", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:03.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "deey8zsy6ga64seu2ecdgbscy.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,", + "FriendlyName": "Mecha Armor (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:11.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2289, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8u2ve7984kGPN_F3hbDzFg.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,eG1Lf1vG60qNQiCBRsAhqA", + "FriendlyName": "Mecha Armor (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:51.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2489, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "zYe-zV8o90qtz6ryq0Q3aA.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,a9oUAXZpHUaW1_ncqhkYZg", + "FriendlyName": "Science Armor (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:09.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2706, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wWzp1pLLbEq0Zrt_O6yaMA.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,CeebPv2TZUixq8cmVD5lUA", + "FriendlyName": "Science Armor (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:48.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ar7fmx8roe1n7n2hcnjomo65l.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,", + "FriendlyName": "Mecha Wings (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:05.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2283, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QGWG7sL090695CW4JWWvvA.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,cS-oBNlLE061SIB40QjiDg", + "FriendlyName": "Mecha Wings (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:30.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2488, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "JEZj0fwAP0a4qOcclGw70g.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,rFcMAp5UvkuGalprkr_ARQ", + "FriendlyName": "Science Wings (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:06.757Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2704, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sQ5AG_RCAk-j6MKfyvnriA.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,oSoW8v6emEW4g1IduUNzOA", + "FriendlyName": "Science Wings (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:41.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1466, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vpmmt96jkto0ifnjtv9nwy2a.png", + "AvatarItemDesc": "16359b51-96c4-4be0-b598-319c7549e18a,", + "FriendlyName": "Mermaid Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16359b51-96c4-4be0-b598-319c7549e18a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:44.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "16359b51-96c4-4be0-b598-319c7549e18a,3ya4VDfxLU-krfBh_H0VAA", + "FriendlyName": "Mermaid Crown Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16359b51-96c4-4be0-b598-319c7549e18a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1142113680 + }, + { + "AvatarItemId": 1464, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5rcywgkgsd6v3ov5jeez13xtz.png", + "AvatarItemDesc": "fd7774bb-b29c-40b5-84b1-5c2bbe574255,", + "FriendlyName": "Mermaid Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fd7774bb-b29c-40b5-84b1-5c2bbe574255", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:35.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "fd7774bb-b29c-40b5-84b1-5c2bbe574255,499irXZltkSvv9pWsdBSyw", + "FriendlyName": "Dress Mermaid Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fd7774bb-b29c-40b5-84b1-5c2bbe574255", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1339048088 + }, + { + "AvatarItemId": 1465, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f44q2mf0bi5g9hcmaw85jm3j6.png", + "AvatarItemDesc": "c8c00394-4cd9-4bad-9774-a825ded78f80,", + "FriendlyName": "Shell Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8c00394-4cd9-4bad-9774-a825ded78f80", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:40.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c8c00394-4cd9-4bad-9774-a825ded78f80,Qevoxmtsa0eNzGss_aG2gw", + "FriendlyName": "Mermaid Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8c00394-4cd9-4bad-9774-a825ded78f80", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1547269846 + }, + { + "AvatarItemId": 2067, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "k7-wf2BSPUW8hyIsgm1DCg.png", + "AvatarItemDesc": "51b85276-3a40-4c40-93a1-9df307c3e78e,", + "FriendlyName": "Messenger Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51b85276-3a40-4c40-93a1-9df307c3e78e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:24.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1958, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wtce4myle20jpvnkao5yy6q4.png", + "AvatarItemDesc": "745fd5ef-705b-40f0-bfd7-dbf4af5a82e8,", + "FriendlyName": "Mighty Princess Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745fd5ef-705b-40f0-bfd7-dbf4af5a82e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:09.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1959, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8icc0glgl3g068z6zkzlc81e7.png", + "AvatarItemDesc": "7652db86-bc67-4665-8937-f5199d27b5c7,", + "FriendlyName": "Mighty Princess Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7652db86-bc67-4665-8937-f5199d27b5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:12.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1997, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1rltz0xd7rtu9knzw8dxe4tnf.png", + "AvatarItemDesc": "7a5cb589-5eab-4d95-9040-e9bfdc9590fc,", + "FriendlyName": "Miraculous Cat Noir Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a5cb589-5eab-4d95-9040-e9bfdc9590fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:14.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1996, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bkqrk31ohvuvc5euhf8ftmopj.png", + "AvatarItemDesc": "dece6d88-b6b1-4e01-b341-8ca4171353cc,", + "FriendlyName": "Miraculous Cat Noir Ears Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dece6d88-b6b1-4e01-b341-8ca4171353cc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:12.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1995, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2dpq3dcn5tt6gohq1e92zn647.png", + "AvatarItemDesc": "f0abcb90-d8e7-414a-b81b-a2aafacd4dac,", + "FriendlyName": "Miraculous Cat Noir Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f0abcb90-d8e7-414a-b81b-a2aafacd4dac", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:10.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1994, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "56uyxkfexq3rv8uwgrvrv1uc2.png", + "AvatarItemDesc": "e441a297-22ba-4a43-92ef-3549c6cc45c1,", + "FriendlyName": "Miraculous Cat Noir Hair Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e441a297-22ba-4a43-92ef-3549c6cc45c1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:07.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1993, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2qk8k1ry8kme3fai13fs7y5na.png", + "AvatarItemDesc": "fe85784d-070d-4b33-82a2-e0875da053d6,", + "FriendlyName": "Miraculous Cat Noir Mask Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe85784d-070d-4b33-82a2-e0875da053d6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:03.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1998, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0swzhsru2itov2t18lj5ptr8y.png", + "AvatarItemDesc": "19d83cda-692f-4ec1-8da0-15211f3a0c34,", + "FriendlyName": "Miraculous Hawk Moth Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19d83cda-692f-4ec1-8da0-15211f3a0c34", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:17.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1999, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "01v0cofrfhvsecj4d83kd6qn6.png", + "AvatarItemDesc": "a28d3819-e5e0-457f-8363-09aaed6a1382,", + "FriendlyName": "Miraculous Hawk Moth Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a28d3819-e5e0-457f-8363-09aaed6a1382", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:19.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2000, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "53f1e75rivkkk1v3yiqcezzwd.png", + "AvatarItemDesc": "696f46ae-eaf3-43b5-9635-70a78972826e,", + "FriendlyName": "Miraculous Hawk Moth Mask Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "696f46ae-eaf3-43b5-9635-70a78972826e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:22.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1992, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ddhkud4mfi72rzypwa7k6q892.png", + "AvatarItemDesc": "26b8c05a-e960-4039-9e50-4e27befaa44c,", + "FriendlyName": "Miraculous Lady Bug Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26b8c05a-e960-4039-9e50-4e27befaa44c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:56.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1991, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "e055b8bmrxrpkco54wa2vg5e9.png", + "AvatarItemDesc": "9815999a-c4d3-4741-a34f-2545b0cb8e11,", + "FriendlyName": "Miraculous Lady Bug Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9815999a-c4d3-4741-a34f-2545b0cb8e11", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:54.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1990, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4f78dqx34vgtcv34rpbpetpea.png", + "AvatarItemDesc": "34023e02-f59f-4e90-b245-e298cbc1a6d8,", + "FriendlyName": "Miraculous Lady Bug Hair Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34023e02-f59f-4e90-b245-e298cbc1a6d8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:51.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1989, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6yostvo78m555vx70ouaibzah.png", + "AvatarItemDesc": "ad21788e-1995-4c2b-9c7f-610bbc1019c4,", + "FriendlyName": "Miraculous Lady Bug Mask Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad21788e-1995-4c2b-9c7f-610bbc1019c4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:46.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 348, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bgvzostookysj7axrphthrkbq.png", + "AvatarItemDesc": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,", + "FriendlyName": "Mirrored Glasses (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:05.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 392, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0p43fda4clqqc4kkkt9xq6vsz.png", + "AvatarItemDesc": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,f841cdfa-45ba-4f09-a117-503eef34cb4b,9c55b609-b7dc-4070-a3ad-6351a0054a06", + "FriendlyName": "Mirrored Glasses (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:30.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1160, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0y8i0p2err8v094d39pt4hrsl.png", + "AvatarItemDesc": "a4c26527-8f75-44da-b150-7aaf8ef739c1,", + "FriendlyName": "Stunt Shades (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a4c26527-8f75-44da-b150-7aaf8ef739c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:54.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1136, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "12z70obzqdrm08ikd55tdgqj6.png", + "AvatarItemDesc": "d57cf97e-b5d2-4e47-8231-a226a161f6e3,", + "FriendlyName": "Stunt Shades (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d57cf97e-b5d2-4e47-8231-a226a161f6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:15.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1139, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9t35j4q9ykj4qblmofdh0c5vg.png", + "AvatarItemDesc": "87792b6e-1468-45e1-b28c-83f27ada7299,", + "FriendlyName": "Stunt Shades (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "87792b6e-1468-45e1-b28c-83f27ada7299", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:26.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1135, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5e5ro3jz91njip3g49yxza47y.png", + "AvatarItemDesc": "973dc863-b9a5-40d8-8f15-4096e75bd2cb,", + "FriendlyName": "Stunt Shades (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973dc863-b9a5-40d8-8f15-4096e75bd2cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:10.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1138, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0bc165hvvokoljesm0z26hcxx.png", + "AvatarItemDesc": "e2620c2c-c28e-45ce-a83c-0588329b9c81,", + "FriendlyName": "Stunt Shades (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e2620c2c-c28e-45ce-a83c-0588329b9c81", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:22.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1140, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6k0yaoht3qdepv964z904hrm7.png", + "AvatarItemDesc": "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5,", + "FriendlyName": "Stunt Shades (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:31.913Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 557, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "02t4ai6twkgrds4d3p7uhoirv.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,", + "FriendlyName": "Mobster Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:50.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 626, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5durkqoejr41mlqicbjoralgg.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,40283127-2ffc-4989-82f6-29c3381bf9e2,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:45.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 627, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bcipyj02mi2pt13ubgaeou2w9.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,1421caa9-9a0d-42c2-b847-1500ba2e483a,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:47.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 628, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cyemgjkr87sifsyr57umkly9v.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,390a476a-5545-48eb-955c-1bf1a16012bf,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:49.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wy0v5yb2753kb3b6g2r6wmdp.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,46970903-9799-4d25-b90a-7162c1f5dbef,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:09.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1129, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b4yf4gpicr0pa3pz4cdefjtu3.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,PZx7Iae0pkiDaxT9t3-vgg,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:13.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1462, + "AvatarItemType": 0, + "Tooltip": "A hat given to the ethical hackers of Rec Room.", + "Rarity": 50, + "ThumbnailImage": "cjcge17sgy7ehaobbb1d90lb2.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,8XajS2adOEOqzqnLy6-zcQ,P12CUkIg8EuFihGQsvPlxg", + "FriendlyName": "White Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:11:31.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,_Fl6LKUUykSBrdEgaN-GFw", + "FriendlyName": "Mobster Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1396740312 + }, + { + "AvatarItemId": 558, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28199zgfnoo8pyinqru3bjurg.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,", + "FriendlyName": "Mobster Suit (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:54.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 559, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28rrxlerexeuvbdhwah6qw82t.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,531b4297-b38b-4362-9854-4f2139ea43be,33ee376a-586e-4aff-a7e4-4213445a3e92", + "FriendlyName": "Mobster Suit (Herringbone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:58.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 716, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cqin0ca6oahx1odk9wqkjoqzv.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,46970903-9799-4d25-b90a-7162c1f5dbef,c2052d8c-fbcd-4462-8f08-bcf5b3dfde2f", + "FriendlyName": "Mobster Suit (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:17.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 966, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a6mox6qxfhus2uphu75sycnf6.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,4VkPVbBVLE-eJHilwGMAbQ,HTjlzAlWZ0ezY6w3ngSQSg", + "FriendlyName": "Mobster Suit (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:12:59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1131, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2ohuwbw1u9rnkyon9psv94x54.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,newGzX_zA0eGUX3Iayq5Ag,6Jv4AJGBYUGP_n4sDcz2Kw", + "FriendlyName": "Mobster Suit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:30.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,352obeXyMkyLIv62h2g-Ng", + "FriendlyName": "Mobster Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1791921697 + }, + { + "AvatarItemId": 560, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "76jy5kgg3ywcce4irqjp40e7o.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,", + "FriendlyName": "Mobster Cuffs (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:35:08.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 717, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5qsejkrvzfabn61cbd7tzrav2.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,531b4297-b38b-4362-9854-4f2139ea43be,c2a9c007-2748-4fa2-ab9b-0081eef792f1", + "FriendlyName": "Mobster Cuffs (Herringbone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:18.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 718, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bo4sinu9h61ugjoyoi0qu6aqc.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,46970903-9799-4d25-b90a-7162c1f5dbef,05cff8b8-d437-485a-b4b8-41d35becd022", + "FriendlyName": "Mobster Cuffs (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:19.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1132, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7oklc7x4ju4r3afyx70po80cx.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,newGzX_zA0eGUX3Iayq5Ag,QBzMp7MdAUOLIofvddui1A", + "FriendlyName": "Mobster Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:32.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2825, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1Z97coFGmUSgGU9P-pU5KA.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,6ItbfT4igECHl2eLuhGLIA", + "FriendlyName": "Mobster Cuffs (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:41:02.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,326QgcQbnUGJBs2xzFKVOA", + "FriendlyName": "Mobster Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1214487053 + }, + { + "AvatarItemId": 87, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2l73l9kt7me20f6m5vhkstse2.png", + "AvatarItemDesc": "e49d5d78-157e-4bc8-b878-ff7c4963fdc4,", + "FriendlyName": "Mohawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e49d5d78-157e-4bc8-b878-ff7c4963fdc4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2212, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kykH4YwaaUSlrQTBZsT8tA.png", + "AvatarItemDesc": "e21b9dd6-c3d1-42cb-a90d-1b60ffb826fd,", + "FriendlyName": "Monkey Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e21b9dd6-c3d1-42cb-a90d-1b60ffb826fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:30.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1806, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25fe9p8vszllgyike2qzq66fc.png", + "AvatarItemDesc": "5f6a7dee-0b44-4138-b3a7-a079dbc63e83,", + "FriendlyName": "Monkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f6a7dee-0b44-4138-b3a7-a079dbc63e83", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:23.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2291, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "emdth023j0ib2692bu9zs42fg.png", + "AvatarItemDesc": "3bceb51c-ef29-4942-8b8b-a15ed4a41451,", + "FriendlyName": "Moon & Stars Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bceb51c-ef29-4942-8b8b-a15ed4a41451", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:37.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2292, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6l7ba33mqkha8zbtzg8cty02.png", + "AvatarItemDesc": "3bceb51c-ef29-4942-8b8b-a15ed4a41451,4lwF00Rvb0Kr3FJz07HlOQ", + "FriendlyName": "Moon & Stars Earrings (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bceb51c-ef29-4942-8b8b-a15ed4a41451", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:39.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1509, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7gjdy5ae0sascpzzv1vx2hn97.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,", + "FriendlyName": "Moons & Stars Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-08T21:59:42.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1930, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1453pgb9cjdtfwanmhqks0fn.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,eutz2irupECSYGMtru3NMQ,Gruqp7EGM0WJbAM_F8hWbQ", + "FriendlyName": "Snowflake Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T19:00:38.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1700877762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qhUHWc_oWEanxQNBSskKJQ.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,4PI6XssggEKRxkGlWeo8KQ", + "FriendlyName": "Jigsaw Moon & Stars Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.517784", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1209731358, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "APU0Kt0HrE6E_zMnX0MMJw.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,Mp_TzaiSTUaULweziW9_AA", + "FriendlyName": "Dress Moonand Stars Dress) Moonand Stars Dress Shirt Jigsaw Aurora Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.517784", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "eb91dbb0-0dc1-497e-b6d0-b7cd4482bacf,", + "FriendlyName": "Moon Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb91dbb0-0dc1-497e-b6d0-b7cd4482bacf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1407377184 + }, + { + "AvatarItemId": 2857, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CXVseWtLVkWtwM2EQ4f2Rg.png", + "AvatarItemDesc": "ab446412-84cd-490d-84e0-2e666633b72f,", + "FriendlyName": "Dirt Bike Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab446412-84cd-490d-84e0-2e666633b72f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:44.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2858, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "p0HdGaW4sE2gqP6I_UnOTA.png", + "AvatarItemDesc": "3028ba83-0c85-4e27-a47a-56609fa58b33,", + "FriendlyName": "Dirt Bike Chest Protector", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3028ba83-0c85-4e27-a47a-56609fa58b33", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:46.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2859, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Mz1ko5b7O0GghTIcaFyRTw.png", + "AvatarItemDesc": "40c1f20f-182c-4751-afd1-b572b0fd061e,", + "FriendlyName": "Dirt Bike Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40c1f20f-182c-4751-afd1-b572b0fd061e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:48.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 359, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2o15q3ks9rbgmv8h3mf4bdpj3.png", + "AvatarItemDesc": "2b7f4dae-51da-4af0-b009-12d6005c85c6,", + "FriendlyName": "Mullet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b7f4dae-51da-4af0-b009-12d6005c85c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:58.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 815, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "056efjnqghgyjgzcgyuz5vbq6.png", + "AvatarItemDesc": "Lli8-aqt1EmCmyXwBRQaYA,", + "FriendlyName": "Mummy Head Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f9bc582e-adaa-49d4-829b-25f005141a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:39.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 816, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "549e5xz7cvnjwkcrv8qnxssm6.png", + "AvatarItemDesc": "oRUh266ohkWQil14uxcEdQ,", + "FriendlyName": "Mummy Shroud", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db2115a1-a8ae-4586-908a-5d78bb170475", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:42.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 820, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "32o1jvjp0zv2e13meoz6ly7pc.png", + "AvatarItemDesc": "zpjkkb1HtUORoPaODFuuxg,", + "FriendlyName": "Mummy Hand Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "91e498ce-47bd-43b5-91a0-f68e0c5baec6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:52.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2760, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70axtgsjoojk0df4289594v6q.png", + "AvatarItemDesc": "25e76b00-4c56-4259-968f-046c14fb36fd,", + "FriendlyName": "Earrings (Musical Note)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25e76b00-4c56-4259-968f-046c14fb36fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:06.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2761, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bah-hC34O0mkCKiY2pH72A.png", + "AvatarItemDesc": "60cc933c-5bc8-49c1-aa6b-9c2adfef088e,", + "FriendlyName": "Necklace (Musical Note)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60cc933c-5bc8-49c1-aa6b-9c2adfef088e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:08.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2160, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CCrSa0uGc0u9LuDrRzUoeQ.png", + "AvatarItemDesc": "f4f372b8-33a1-462e-a9f7-8a0e85b2c0d7,", + "FriendlyName": "Mystic Mantle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4f372b8-33a1-462e-a9f7-8a0e85b2c0d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:37.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2119, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hVhS_lAUy0O3ga-alhshaQ.png", + "AvatarItemDesc": "b79a6f2c-d960-4d8a-b05f-24e818ec5f60,", + "FriendlyName": "NBA Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b79a6f2c-d960-4d8a-b05f-24e818ec5f60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:38.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gq5kauTaAU2sDicXqi98Qg.png", + "AvatarItemDesc": "fe2721b5-16c8-4d9d-bca5-0233ab008fbb,", + "FriendlyName": "NBA Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe2721b5-16c8-4d9d-bca5-0233ab008fbb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:40.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1270, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "brfgb9cf9tp8tgva2k3qzm6iv.png", + "AvatarItemDesc": "5e9bbda6-d681-42b1-9cc4-5758acf34bc0,", + "FriendlyName": "Formal Necklace (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e9bbda6-d681-42b1-9cc4-5758acf34bc0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:55.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5fwhywu2l5mxei877fhrhwhq1.png", + "AvatarItemDesc": "b95678aa-351a-4929-8a0e-0274a183eb18,", + "FriendlyName": "Formal Necklace (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b95678aa-351a-4929-8a0e-0274a183eb18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:51.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1274, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0rq3ue0rxap0g0375himga11m.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,", + "FriendlyName": "Party Hat (Shindig)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:12.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1564, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aal6en3akiu96g363ok04sx99.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,AfqXBWLBQEiiag4cEtoEiA,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Hoopla)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:37.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1565, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aiqpa9xefa8z86zduzd2gyjw8.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,RROvzwtshUGyRudVIBn8bw,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Celebration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1566, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e9evvmcwo9jnfy5clp9ztr4vz.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,69vv21jfTUu1E_LjK5bytg,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Jubilee)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:46.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,IUDG__cu9EG9wK57ajRTHQ", + "FriendlyName": "New Years Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1348963899 + }, + { + "AvatarItemId": 1273, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9alk9hbmjw74ccjkcwn2i7s3a.png", + "AvatarItemDesc": "86dd9ff9-643f-4f11-8243-93ad948db4cc,", + "FriendlyName": "Happy New Year Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86dd9ff9-643f-4f11-8243-93ad948db4cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:08.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2611, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cbi2t8gd1f5tzytciebrrxndh.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,", + "FriendlyName": "NFL Arizona Cardinals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:35.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2612, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxeoecs08mv3iy5dgdogqbb94.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,G9cFrBRvw0S-nDp-Fn494g", + "FriendlyName": "NFL Atlanta Falcons Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:35.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2613, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7n3s5s977ulc5z7jhiencdnrw.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,KcsFLjkhCUmg8HbamtChbg", + "FriendlyName": "NFL Baltimore Ravens Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:36.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5n2ngxovy8uj9lfz3essaxn29.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ye7GYcd6PkObqAsla6BJaA", + "FriendlyName": "NFL Buffalo Bills Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:36.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2615, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eslxy84me9m2oxtnk8o18n5nn.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,xVPRKfkYG0-u4MFhJYGe9g", + "FriendlyName": "NFL Carolina Panthers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:37.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ah2mm6n1rgh6pvzk4fx9rnj6.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,84OVMNAiv0mQwX2bpkw2yg", + "FriendlyName": "NFL Chicago Bears Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:38.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2617, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "831h9zt2yylg0ao93kyxmy4t3.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ishXZTP3bkeVNk68PBZAiA", + "FriendlyName": "NFL Cincinnati Bengals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:38.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2618, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cz3bgo3si4042vf9xb2dtypxf.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,cLswsDJEmkCqYDGI8DJmPQ", + "FriendlyName": "NFL Cleveland Browns Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:39.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2619, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "220fwlypah2hrdvg8fi0gswf0.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,fdmR0dt0hEakaNp8mpjGmg", + "FriendlyName": "NFL Dallas Cowboys Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:39.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2620, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ca68ce9flmrpl7ig3zwhbqrv4.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,5mFA4uHLmku8oCc1rDJmXw", + "FriendlyName": "NFL Denver Broncos Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:40.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2621, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7g7ym06cw3m98l2f9ziwdlpia.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ry0fWzcysk2fsr-zIvzjUg", + "FriendlyName": "NFL Detroit Lions Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:40.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2622, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1p6ky76pvprr7cwbobzngw01f.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,S9YQnGF2WkaFSG07d-5TUw", + "FriendlyName": "NFL Green Bay Packers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:41.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2623, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bmav2pqie439kaf4223zzgxlg.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,pcEwV_ZPxkSKP39UgPh6fw", + "FriendlyName": "NFL Houston Texans Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:41.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2624, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f2apsx992xa88goodejq341lr.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,nHa0WjWfl0-g-sknorb10g", + "FriendlyName": "NFL Indianapolis Colts Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:42.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2625, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "68ou7wz2ezhyh6y39hxqeyihw.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,w_8Bff5J_Uej5iCAz2WTBw", + "FriendlyName": "NFL Jacksonville Jaguars Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:43.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2626, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dz7a5njd87o8hv2l5etgyisnj.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,kdExSysXREOAYqobK_Wzpw", + "FriendlyName": "NFL Kansas City Chiefs Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:43.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2627, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "351j7myhryxcppvy8gvdz4oix.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,XlkkIjP4EEKq0ObjXYGtBA", + "FriendlyName": "NFL Las Vegas Raiders Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:44.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2628, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ddaagd1il03gi4nrec2vd1xei.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Rlxn533MdUqMefvUSoZrrQ", + "FriendlyName": "NFL Los Angeles Chargers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:44.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2629, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ndck6ysf75lg3kvjiz1bhh3w.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,_BV42pO3n0e0V-eOf9CyGg", + "FriendlyName": "NFL Los Angeles Rams Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:45.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2630, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5uj5qvvm6izou0whgm634hnme.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,g6PcjAOH0UinJtrGmxVuyw", + "FriendlyName": "NFL Miami Dolphins Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:46.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2631, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d32jj9kz3n5jceagnlpmq5vpa.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,i1e3GavcdEu7EJFzwpBXvg", + "FriendlyName": "NFL Minnesota Vikings Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:46.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2632, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "44ohtkl0bud9zkb1hd75qdgph.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ebjoqQ_pnU2qdFVR1yDIzA", + "FriendlyName": "NFL New England Patriots Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:47.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2633, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8t9dbig6zpecho4qe8scj3bpt.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Fr--3nBIUUuD6EYmj1iklA", + "FriendlyName": "NFL New Orleans Saints Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:47.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2634, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b0joufd9x01y01brs3e7krk43.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,hrTGnJLb4U21P9PCi7u3xw", + "FriendlyName": "NFL New York Giants Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:48.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2635, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwlxb29ug9oqs1apya0icyjsc.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,coRfJXTxhUKsFs1tzSBgUQ", + "FriendlyName": "NFL New York Jets Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:48.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2636, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxnxsmeklw8dkemz9vrpajd7t.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Y75a0D5Wj0qmpfxG18LvWQ", + "FriendlyName": "NFL Philadelphia Eagles Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:49.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2637, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5t2128zn5sfwk2uc2qclkhlb9.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,VExSaZXZ9kan-0qjP6YPzQ", + "FriendlyName": "NFL Pittsburgh Steelers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:49.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2638, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "du30i4p3o8j2i1t6evwaw6x3b.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Yec2FLXhpEmrocbVUOkhhQ", + "FriendlyName": "NFL San Francisco 49ers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:50.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2639, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "40mqyx1i4dzjcumjlzblnr5jl.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,EziQI39wZUWqIy8zs5I7LA", + "FriendlyName": "NFL Seattle Seahawks Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:51.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2640, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5gvpye8hcsb60wbuk15bi7r5t.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,PWMkuXHWFEKGKfSiT8rWNg", + "FriendlyName": "NFL Tampa Bay Buccaneers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:51.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2641, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9ftzba4l162233dbbcqiaszc4.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,xdO8lwFuG0e1v2FRmza4dQ", + "FriendlyName": "NFL Tennessee Titans Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:52.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2642, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "30ubxldr0r27wafqn2ejkmtgv.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,vtQ3bx6n7kSFdES121M5IQ", + "FriendlyName": "NFL Washington Commanders Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:52.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1413, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "09zvd3ohdxjul97glak50v2zt.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,", + "FriendlyName": "Nightcap (Zonked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:09.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1414, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5oixy16bep4bh46cp4ma645d0.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,2vawoQvJPUegnClmr2RNAQ,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Zzz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:10.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1415, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wpy1gh40rv5ibcltdnowpd05.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,kIcd-yEnsEqxoeogquvmRw,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Dozy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:12.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1416, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0415gb7q368nr2qadddcg1i0k.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,qHyKfCsrbUqSWomjLXf7Hw,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Sandman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:14.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2064, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "g4VIHBisj0-q7bWIQ6IMfA.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,ZvUwd0aVfk6SjA7oyouXUQ", + "FriendlyName": "Starry Night Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:16.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1569, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6gfimjrsbl126ir365lqnmv8n.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,", + "FriendlyName": "Nineties Outfit (Da Bomb)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:09.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1568, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1oix7x6qvyo7ps4kk7lftxq9j.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,ulOBBvFEJ0ioFA1qmL390A,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Aiight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:07.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1567, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1czdhhr33h8ztob05qmon2562.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,V4CVhCB8eU-nrfYgticVOA,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Booyah)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:05.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1920, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eakv6vkwmx6lwc95u5k9tvb07.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,6ZtRAbD-xUufWNLCrlrrCg,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-29T23:25:09.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2566, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e1-lO7c1i0m5eg3UkCPczg.png", + "AvatarItemDesc": "a600fa80-ed4e-4acb-8d6d-72fa87d2bef3,", + "FriendlyName": "Nutcracker Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a600fa80-ed4e-4acb-8d6d-72fa87d2bef3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:02.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2567, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6-Sz9f-O00uY5pWih9NC_w.png", + "AvatarItemDesc": "beee4e09-9d93-4023-bc78-c5dc551a0ab8,", + "FriendlyName": "Nutcracker Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "beee4e09-9d93-4023-bc78-c5dc551a0ab8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:05.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2381, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "pEnzLpycJU603AzcfO2Mnw.png", + "AvatarItemDesc": "9b636782-47f0-4218-9a1d-be65e550dc44,", + "FriendlyName": "Bongo Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b636782-47f0-4218-9a1d-be65e550dc44", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:50.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2382, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "YzXiKP_YPkW_dray4uvVpQ.png", + "AvatarItemDesc": "96ca8d50-0721-47a3-a988-018a1c157677,", + "FriendlyName": "Drumset Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96ca8d50-0721-47a3-a988-018a1c157677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:53.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2383, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "I_TDOXrCVUe1V3HKxNvzAw.png", + "AvatarItemDesc": "c8d2503e-5d43-4828-8d1d-40c64a577753,", + "FriendlyName": "Harmonica Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8d2503e-5d43-4828-8d1d-40c64a577753", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:55.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1838, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5aq3d9icptakxqxo8zitb57dn.png", + "AvatarItemDesc": "caf1e229-9660-4abf-8074-f706709f4097,", + "FriendlyName": "Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "caf1e229-9660-4abf-8074-f706709f4097", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-23T17:41:53.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2860, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "A6Gyszd5SkyByb7oryYm_w.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,", + "FriendlyName": "Overalls Skirt Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-03-10T22:40:50.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2863, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bGS9YZZvhkSZTQaLBEnbYA.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,sCj73U9p5E6DQreOsbutqw", + "FriendlyName": "Red Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:58.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2864, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "jAr4ImFmQ0u4xa689IeaXA.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,HDpCyFn40kOxJ0P_xJr4-A", + "FriendlyName": "Yellow Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:41:01.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2862, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T3O9LKgezU2qgWjCAoDPfQ.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,OZ1v-s_XFUyfMA5mfd47FA", + "FriendlyName": "Green Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:55.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2861, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LP6loSF2uUmBukTEKkAN5Q.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,e8aNxGFlBUWy59Bfrvvv7w", + "FriendlyName": "Brown Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:53.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1561517472 + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,08593d9f-aaed-4662-973d-0f30e77f1955", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1085700839 + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,e002caa2-3447-4102-8d9f-570d1942101b", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1362900866 + }, + { + "AvatarItemId": 1351, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "729s6a9tw6yv1xyun76b5zwkz.png", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,", + "FriendlyName": "Paintball Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eda29bce-5a07-4439-a2a7-cafa11e2ed62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:51:06.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 105, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "42cr1t40qzbgiloer9d71sb28.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,", + "FriendlyName": "Paintball Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1946, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "52yxsadxjnh09e02kzlza70z7.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,kS7jek5tPkSkIgjIF369Vg,_Jx5VqGbVUCPwl_SaUCTRA", + "FriendlyName": "Paintball Goggles (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:35.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1947, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4bndj29hiu03hkt63qwabedlj.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,3HAMDzfFO06jtYNXm2Bsjg,_Jx5VqGbVUCPwl_SaUCTRA", + "FriendlyName": "Paintball Goggles (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:38.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 281, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "agzh2svq1rj75i5f37dk13i34.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,", + "FriendlyName": "Paintball Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1948, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "841fudm9eezzeqyfx2wqdrpj2.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,-Moek0heVkWtKpTw9oZ0EA,FsirX90YM0y5lk7mnqDnEQ", + "FriendlyName": "Paintball Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:40.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1949, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cvkzqrgyihp9bv1n8b6ie95fl.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,D-XJyw1BPE-m0gXpvcXrQw,FsirX90YM0y5lk7mnqDnEQ", + "FriendlyName": "Paintball Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:41.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1332, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1xj0gybaafqrogg7hnsgqbscb.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,", + "FriendlyName": "Mechanic Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:27.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1333, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0q7ysb2tsjy0mbxx1nes33jdx.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,C9cPjOoiCUCHrCBi809CMQ,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:32.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1334, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20bhcftlu43jwrw06imhg37ox.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,24zVmiGiaESrM5qcRfF6gA,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:36.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1335, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0v5t8d3m8790n1lzjjp3qd9z5.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,LOeHr4xoik6VOv1fOeImjA,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:41.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 197, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "94farwa7bctza7e4db865is8x.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,", + "FriendlyName": "Paintball Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 654, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "274io1pn2peum3e1dm08i0tlj.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "FriendlyName": "Paintball Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:41.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 655, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0hdenghs8yfyhmtnz229g0hd1.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "FriendlyName": "Paintball Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:42.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2941, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b4RkTVS6UeA0lrNcM_BAg.png", + "AvatarItemDesc": "07116e9b-b182-40b4-90c2-99738e7aedd6,", + "FriendlyName": "Parrot Purse", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07116e9b-b182-40b4-90c2-99738e7aedd6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-24T15:25:33.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1255988719, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "74e6878f-94e0-41f2-93ea-de6412238385,", + "FriendlyName": "PartyBand (1710)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74e6878f-94e0-41f2-93ea-de6412238385", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.519776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2568, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fuCyF2LQ6EaCTanw2VqOwA.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,", + "FriendlyName": "King Rat Cape (Rouge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:08.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2571, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VoZTTqgfBEOCWqIxKoS-Pw.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,gYXppJSpDUG9MB0xej4-kQ", + "FriendlyName": "King Rat Cape (Jaune)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:22.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2570, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_Hj47OdDmUeTB3wTf2r2Kg.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,VF7QrfNdC0ex5p62bkdt7w", + "FriendlyName": "King Rat Cape (Vert)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:18.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2569, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WEL2fL-6q0qFtXp51G5Wng.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,eEsEJ78Me02dXnp6gTuZRw", + "FriendlyName": "King Rat Cape (Blanc)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:12.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 760, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3r88aoow0xky1yenciez1enyj.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,", + "FriendlyName": "Peacoat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:25.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 791, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2i0jzvjgsic4vsv7uod1p0o5k.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,fvoaMN4M10WS6jiA6XlOJQ,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:46.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 792, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6fdpcq0zhuz9xr09wlcj3j7bx.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,PwsLxCnzOkSW-RGz3tNb1w,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:48.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 794, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "beldcz68emei41q0ndhzzbtr7.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,uiBklotTUEqRI2F67vyJyA,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:11.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 817, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dhqntf98nka1vp9oh5hi5ybtn.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,jXBi28cRV0CEQgKIrpD0_A,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:43.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5db418c3-1aae-42d3-af59-4ad03a9a3bf4,", + "FriendlyName": "Peacock Gown Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5db418c3-1aae-42d3-af59-4ad03a9a3bf4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1042258582 + }, + { + "AvatarItemDesc": "e457f946-b985-417c-9dec-0d8d3a7da71a,", + "FriendlyName": "Peacock Gown Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e457f946-b985-417c-9dec-0d8d3a7da71a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1725552604 + }, + { + "AvatarItemId": 1271, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjtkko8cs4inet2bswk24bky0.png", + "AvatarItemDesc": "7be2eafc-21ea-4266-ae64-8888c06798d6,", + "FriendlyName": "Pearl Necklace (Freshwater)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7be2eafc-21ea-4266-ae64-8888c06798d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:00.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1272, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4kmcbo8n4ht1kadrspll7f1oy.png", + "AvatarItemDesc": "7be2eafc-21ea-4266-ae64-8888c06798d6,sqPMH0LhLkeG-4XkxpU-sw,URWHffwjGUKgXeV01upePA", + "FriendlyName": "Pearl Necklace (Saltwater)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7be2eafc-21ea-4266-ae64-8888c06798d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:04.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1479, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6a428adukettbzkckyu9updp2.png", + "AvatarItemDesc": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,", + "FriendlyName": "Pegasus Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:17.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,RRmRsm6R6kOyn5HfbXNJWw", + "FriendlyName": "Pegasus Wings Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1841210169 + }, + { + "AvatarItemId": 2603, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zzqrxKCL00W27uhhyfZuqA.png", + "AvatarItemDesc": "b66cb611-c079-42f0-ae04-6f51384dbe90,", + "FriendlyName": "Penguin Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b66cb611-c079-42f0-ae04-6f51384dbe90", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:46.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2264, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Av7hkeKA7Ei1Stn-Fk2txw.png", + "AvatarItemDesc": "3cd52c5d-0c46-41c0-8254-c4542805da31,", + "FriendlyName": "Pioneer Bonnet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3cd52c5d-0c46-41c0-8254-c4542805da31", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:14.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2265, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2o_-7a2C30eYta_KTWhvIw.png", + "AvatarItemDesc": "ad55bb62-671a-4a29-aec8-36a1e3104f05,", + "FriendlyName": "Pioneer Cuffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad55bb62-671a-4a29-aec8-36a1e3104f05", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:16.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2266, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "-__TuY2TCUu0TPihPV1SqQ.png", + "AvatarItemDesc": "bb6ee7fa-7a31-4a16-a97e-be6eac42c595,", + "FriendlyName": "Pioneer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb6ee7fa-7a31-4a16-a97e-be6eac42c595", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:19.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 600, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bu0ii93b6dmvvc93szjfnal02.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,", + "FriendlyName": "Pirate Captain Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:24.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 611, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0rvymvsae95r30k0psb64iavr.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,0f71b9a2-dd88-435c-a571-4503d12dcf2d,9b2102f0-e16b-464f-9f1c-f068de52bb89", + "FriendlyName": "Pirate Captain Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:47.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 612, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0pyrhi595qqyqin50b6zfj2ws.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,f4ad4b9e-dd42-4711-9437-5beae51966f6,9b2102f0-e16b-464f-9f1c-f068de52bb89", + "FriendlyName": "Pirate Captain Belt (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:48.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 599, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dl4lc9lujy638xdphs121fg3k.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,", + "FriendlyName": "Pirate Captain Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:22.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 603, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5o6eyu7tmzl4ffq27fn0ucofj.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,17040a88-87f1-4613-b439-3316e33dbd48,71fd06e3-cac8-4923-a09c-ef91183712d2", + "FriendlyName": "Pirate Captain Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:32.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 604, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b90z3pydklkb08tidovz5jd1g.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,f9aaa7c5-59bc-486c-9a6e-27a8ac618f84,4cc0ea9f-65af-486f-8319-02b85ab5197f", + "FriendlyName": "Pirate Captain Hat (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:37.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1128, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7zbbv8oqo767y92w7mp1p6brm.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,jQwNBXyoU0Gk4FyNw6jFxg,4cc0ea9f-65af-486f-8319-02b85ab5197f", + "FriendlyName": "Pirate Captain Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:01:03.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 601, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5ynt63fbqq9vg0dnp6obm10jk.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,", + "FriendlyName": "Pirate Captain Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:26.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 607, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5ftzyq0wpssi4a8hsrmla5jml.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,556141f7-6fbc-4065-9faa-8d8240a93d7f,00ac4e37-6eac-4ed5-b46c-2c075038bc3f", + "FriendlyName": "Pirate Captain Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:42.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 608, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "69yr1pl69y8de2fpn7tvc0f34.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,d81d0b62-e052-46c2-9252-4bc5fb219f53,00ac4e37-6eac-4ed5-b46c-2c075038bc3f", + "FriendlyName": "Pirate Captain Jacket (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:43.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1112, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bm2ft9q0v5z0q0i1x8nutm0yw.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,xGmnUGF-Ck-jP43r8tVVjg,52053afd-9f12-4fec-a358-7eb3fe8c19bb", + "FriendlyName": "Pirate Captain Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:14.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 602, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cwpa2fe7uhjhy2fg8z2bjoqqf.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,", + "FriendlyName": "Pirate Captain Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:28.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 615, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1ouywwp83tgc11yi3x9wro2bd.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,556141f7-6fbc-4065-9faa-8d8240a93d7f,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:52.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5384s8w1fosxnurfe756wgd0n.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,d81d0b62-e052-46c2-9252-4bc5fb219f53,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:53.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1105, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cijsojywwytastkrfmnur8m2l.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,xGmnUGF-Ck-jP43r8tVVjg,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:48.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 619, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dfy3wgi2sfer51uhs3af8y5da.png", + "AvatarItemDesc": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,", + "FriendlyName": "Pirate Eyepatch (Skull)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-28T01:18:41.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1121, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dkbvx8r9yxtlxb2uhcv5qn1az.png", + "AvatarItemDesc": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,clFrpYWFQEmDkE14f3o_Ig,DE1C-RJkuEWIvpR0q1i42g", + "FriendlyName": "Pirate Eyepatch (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:04.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2884, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "j7uEUd9qPEerGchhJ6rmaw.png", + "AvatarItemDesc": "11b3413f-2c18-41cb-8ef2-55fb825ed476,", + "FriendlyName": "Plush Bunny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11b3413f-2c18-41cb-8ef2-55fb825ed476", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:59.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2885, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B08PvxHhZUOcI_bnrefOFQ.png", + "AvatarItemDesc": "11b3413f-2c18-41cb-8ef2-55fb825ed476,DNGqrCGe8kKrNmJaGzjjew", + "FriendlyName": "Plush Bunny Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11b3413f-2c18-41cb-8ef2-55fb825ed476", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:02.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1823, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0xxjg720qofphga2otdghooga.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,Ris_kNVcgU-EK4SoMckC2Q,74J7skzs9EaIq09bZArjiQ", + "FriendlyName": "Reclympics Shirt 2021", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:53.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2017, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6JmHNL-jsE-M9URUaMcvuA.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,PkMkvHzXek-yKyqJESZ3tw,nbgwLqKBnUyvieWX0i47dg", + "FriendlyName": "Scrubs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:10.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2018, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bCXA7saMUUuD_C4_Qy0t5w.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,P49CJ5imfEuxH1rryrfFRA,nbgwLqKBnUyvieWX0i47dg", + "FriendlyName": "Scrubs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:12.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2685, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "efVnvXfV-0-RgOpfnNi_bQ.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,logtSrZSTUqwEmBw9i0qjA", + "FriendlyName": "Rec-On T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:00.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1884, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eafwn78k3h4uaugzy9kay26kr.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,tMsCUhDslkmdNBfogeSz7w,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:18:04.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1883, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "crpsa73v3nvk6saa71tdx8k9u.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,cypMpMaVeUuRjm4FLH4czQ,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:18:02.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1882, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7h6z4ycgbt833y56ccc3rtl3p.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,whWKAUSGp0qprsJomq9Nwg,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:58.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1881, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "84jgglt9m8uawzj1481xfk4fw.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,LTXy6g4xe0abuFzOLpVDUg,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Dark Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:56.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1848, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4hpfukjpch71fzmfokc3tx3s6.png", + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,", + "FriendlyName": "Poodle Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:44:57.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2558, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BHgotDTbX0umzVt4i36gBw.png", + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,tPAJqFHiX0yUQpDOE0MJ6A", + "FriendlyName": "Planetary Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:53.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,zhLs4Hgw1UCrcNekAWd4RQ", + "FriendlyName": "Poodle Skirt Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1708534829 + }, + { + "AvatarItemId": 1070, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5sb23yvxjkh7ozjfothb1ul6f.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,", + "FriendlyName": "Floatie (Creamsicle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:59.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1071, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agoyhlxxj14bkqor8pthssxge.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,LRv1X0ZrjkqO7azGpZ7uMg,sx10-Hsfl0OS62pIFvHH8A", + "FriendlyName": "Floatie (Donut)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:01.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1461, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c5zj93bwe1qme6eb14dtwbcjt.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,NMmOn0zQZ0mJLxCMnsbIVw,v1ZOE9G9gEWAqNJX1A9f2A", + "FriendlyName": "Floatie (Watermelon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:09:35.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1844, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2iyjpxumims1bzvq7x6kqrmqx.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,1w_u4d4AjESxw5dxRkKHrw,CylZksC7KUGXF2VeJhlLSw", + "FriendlyName": "Floatie (Cheeseburger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:37.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2158, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dVpzRz2OY0avaXnWbmbm_w.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,6wCu0bPODE6tayAY_aicRQ", + "FriendlyName": "Floatie (SummerCamp Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:33.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2244, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "n9H1eWtdiE25SebY5g4Ukw.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,7BMBl3iA6USKijNCKfCXqA", + "FriendlyName": "Floatie (Neapolitan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:56.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2299, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SlacPLiWtE2CpmO5WHd-RQ.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,akqD9mYQQ0qks9SJe80MbA", + "FriendlyName": "Science Floatie (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:38.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E_MFRZ-dI0GiU60k_m8LSw.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,RCit-BlLyE6cvzpbfCVShA", + "FriendlyName": "Science Floatie (Invasion 2)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:28.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2817, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QMhjhzpkIUOPw6-mpbsclQ.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,1Z24Obg1MEm6dC_blOy4IA", + "FriendlyName": "Floatie (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:39.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,KrNRgSR3qEq9f6hfcsTFQQ", + "FriendlyName": "Pool Floatie Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1941160248 + }, + { + "AvatarItemId": 1072, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6o6o3vze30qmak32kdlkeus4l.png", + "AvatarItemDesc": "awOFE0_GSECr8GBNBo6a_w,", + "FriendlyName": "Floatie (Giraffe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1385036b-c64f-4048-abf0-604d068e9aff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:03.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1073, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aasr0sd35kmv6vhgbs9znxafg.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,", + "FriendlyName": "Floatie (Flamingo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:04.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1074, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxj3pn647bzr2jpxm9myh6ret.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,k3dbqP5z30KU37MVPpsEPQ,GtoHkTpyA0iGIHsxphwivg", + "FriendlyName": "Floatie (Dino)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:07.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1451, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8t2f1nmayxxozgvqs90t59neh.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,hM0OmxjWLESbsJ-M0Z3QvQ,qZzkrGXpPkGq_nnGLZorwA", + "FriendlyName": "Floatie (Parrot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:05:42.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1824, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bs25xo0gnja1akbu4eg7q8dor.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,9WpjdWJbhkCoEgDljqZkdw,Uyn3QOl1zkGR2_OUOhNWjQ", + "FriendlyName": "Floatie (Rubber Ducky)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:36.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,zxDWWqb0S0uDJgYeVQQ3oA", + "FriendlyName": "Pool Floatie Bird Head Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1424015386 + }, + { + "AvatarItemId": 2106, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LHx48xU5E0OkqfpqJBes6A.png", + "AvatarItemDesc": "2565d597-9d17-4c47-a66f-b683583f0cf8,", + "FriendlyName": "Floatie (Bunny)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2565d597-9d17-4c47-a66f-b683583f0cf8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:19.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1825, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bdeaw4skm15u253skgs4rtpe9.png", + "AvatarItemDesc": "efac28ae-ff44-4322-9bc0-61eaaef95399,", + "FriendlyName": "Floatie (Dragon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efac28ae-ff44-4322-9bc0-61eaaef95399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:42.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1453, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9srp5d5aba29z5vzoj3qkqmpg.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,", + "FriendlyName": "Floatie (Zebra)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:11.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lLR7SX5N30af5zpgdD5M6A.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,p379ObEL9Eea0DX9Ld4qnw", + "FriendlyName": "Horse Floatie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:18.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1158295129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0vpwih9r_UeG9tov9Mm6VQ.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,ljbO0JAKVky8M97MsjKnPg", + "FriendlyName": "Floatie (Armored Horse)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.521776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2190, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dOpBUgepYk6c0zUHhvZxfw.png", + "AvatarItemDesc": "187569d3-23b4-4118-862a-ace7f50fec45,", + "FriendlyName": "Rammy Floatie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "187569d3-23b4-4118-862a-ace7f50fec45", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:21.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1471, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "chtv23889bkxqdhplm9mrfmxz.png", + "AvatarItemDesc": "7a98503f-0460-4d25-a0d5-72a32db8f4c2,", + "FriendlyName": "Floatie (Turtle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a98503f-0460-4d25-a0d5-72a32db8f4c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:25.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1954, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c0c3e51sc58rcze7q10j1aiq5.png", + "AvatarItemDesc": "7a98503f-0460-4d25-a0d5-72a32db8f4c2,A8ht7G1iqEeLvP0SW6SwyQ,QppxPdf550-jT7dK7XrTzw", + "FriendlyName": "Floatie (Penguin)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a98503f-0460-4d25-a0d5-72a32db8f4c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:15:14.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1075, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "64nsdc03lld08y8ouiugpbt3a.png", + "AvatarItemDesc": "UGkQa4atNUe1cE5TN79s4Q,", + "FriendlyName": "Floatie (Unicorn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b106950-ad86-4735-b570-4e5337bf6ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:08.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2231, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "47gm8nyvqd8aiokq2iddqfdko.png", + "AvatarItemDesc": "e633d56d-5121-4492-b606-40759108a00e,", + "FriendlyName": "Ice Pop Earrings (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e633d56d-5121-4492-b606-40759108a00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:03.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2310, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d54dk7h9h3ru312k9t4ncpscd.png", + "AvatarItemDesc": "e633d56d-5121-4492-b606-40759108a00e,6CFwXqWT50mH1v6WueFsjw", + "FriendlyName": "Rocket Pop Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e633d56d-5121-4492-b606-40759108a00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:03.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2579, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rDwV9FzDSE-8tcpVeQsaFw.png", + "AvatarItemDesc": "7a97838f-24ac-41a0-8390-0c9308644eb1,", + "FriendlyName": "Power Sword", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a97838f-24ac-41a0-8390-0c9308644eb1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:38.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1843, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1lnhgu9j4uf3ypg48ra7v0by1.png", + "AvatarItemDesc": "76a73866-5b60-424d-b751-a105e030f46e,", + "FriendlyName": "Prankster Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76a73866-5b60-424d-b751-a105e030f46e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:34.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1842, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aijv697pz04ho70d9l3lxcnvd.png", + "AvatarItemDesc": "5b3cae00-88bd-4179-b75a-9444c5a52fa5,", + "FriendlyName": "Prankster Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3cae00-88bd-4179-b75a-9444c5a52fa5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:28.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2060, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XSHhlJsxRU2OwZD3eTZWJQ.png", + "AvatarItemDesc": "5b3cae00-88bd-4179-b75a-9444c5a52fa5,ZM6W70pGt0aAPLYf4DykeA,icHpJch4uUeGdhoLkawyyg", + "FriendlyName": "Prankster Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3cae00-88bd-4179-b75a-9444c5a52fa5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:55.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2019, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wf_PLXUTzU6UXiDZCDulRw.png", + "AvatarItemDesc": "06475461-f3ab-4e92-ad92-243c00b19ff0,", + "FriendlyName": "Prodigy Princess Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06475461-f3ab-4e92-ad92-243c00b19ff0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:25.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2020, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "SdP7P425CkCbPhAIfZCtaA.png", + "AvatarItemDesc": "076e98b6-dc1b-48f8-bb02-aa65d26b33d1,", + "FriendlyName": "Prodigy Princess Bracelets", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "076e98b6-dc1b-48f8-bb02-aa65d26b33d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:29.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1456, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e08mipx3bu50m127eoa5c3js9.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,", + "FriendlyName": "FNAF Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-19T23:01:58.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1587, + "AvatarItemType": 0, + "Tooltip": "2020 Rec Con", + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "27ormucbs1iox5kvr9tka0fpc.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,DOY6LpHq8kui8yDWGsNgZw,48yVsRt8sU6jf3P4wlPA8Q", + "FriendlyName": "Rec Con Hoodie (2020)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:11:27.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1783, + "AvatarItemType": 0, + "Tooltip": "2021 Rec Con", + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "0w037o55hb7xrlbbz5ndkt3qk.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,HUxqkzeE-Umq96TgSn0Hbg,48yVsRt8sU6jf3P4wlPA8Q", + "FriendlyName": "Rec Con Hoodie (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:47.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2162, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IezDzyHOWUy4vzhhR8erIA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,QMScBauef0uOE48-YcDcCg", + "FriendlyName": "Dino Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:31.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2237, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "oYY2GYyfUUmrxi__mx94Gw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,25X6s0sTZkyzUOKiIt4cGQ", + "FriendlyName": "Creator Conference Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:39.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2284, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "sg_RSadtBUepyNJrDn81nA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,tkuP0sNwjEO3XH5Lf_KqkA", + "FriendlyName": "Rec Con Hoodie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:33.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2733, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "84yMtUUfQUGCcvFe82Y2DQ.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,G_QPd8YBe0-n8ywAGLMKBg", + "FriendlyName": "Hoodie (Jessia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:25:00.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2684, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ja4C7EX3zkKj0VnySl1Muw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,Kuh-HrhzTUmuGaw7cpLUMg", + "FriendlyName": "Broken Heart Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:56.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2705, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "JpkOsmJyvk23HfK4fWMwJA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,SJ7DV0meUU6QjksIcAAeRw", + "FriendlyName": "Pullover Hoodie Shirt) Pullover Hoodie Shirt Color Block Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-21T19:41:44.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2799, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "grtmSgDkekKbMf5XDtoA3g.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,bS-St6XLHkSwKt41J7BmKw", + "FriendlyName": "Alo Yoga Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:29.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2823, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cDunfbR1wE-D8-LUSD_mdA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,jir883uk5k-cEgyAPDtQww", + "FriendlyName": "Pullover Hoodie (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:55.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1177382318, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pPyQA-HfpUirnUHijRJeYw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,a2mUuKOr-UOM5nPpR8dSMw", + "FriendlyName": "Hoodie (Em Beihold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.521776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,501dae9f-2fc4-45fe-a276-f606b93f434f", + "FriendlyName": "Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1545540299 + }, + { + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,b8cfa1ab-13bb-4e71-afe0-490820aef449", + "FriendlyName": "Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1335772728 + }, + { + "AvatarItemId": 1522, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "44boqpgj0k38m24uqczos4upt.png", + "AvatarItemDesc": "9ad40310-7cb9-473d-920c-5c18d7972030,", + "FriendlyName": "Pumpkin Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ad40310-7cb9-473d-920c-5c18d7972030", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:17.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2368, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "KFjbHzLz7UezIItKG7vasQ.png", + "AvatarItemDesc": "9ad40310-7cb9-473d-920c-5c18d7972030,mQTpoW0-AEufdFmApqtRXg", + "FriendlyName": "Pumpkin Cap (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ad40310-7cb9-473d-920c-5c18d7972030", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:12.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1523, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "9vaxzwj4b0l1ty30cspscr3di.png", + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,", + "FriendlyName": "Pumpkin Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:26.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2369, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "BtXG-VrTYU-1n1VDsKOgjQ.png", + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,Be2v1bpBSEu4lwKJePscPA", + "FriendlyName": "Pumpkin Costume (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:14.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,p4X1ilkxU0OjEWXdmQrVwQ", + "FriendlyName": "Pumpkin Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1015871793 + }, + { + "AvatarItemId": 1524, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "asfkh306x4wyzvnccvqkhby0d.png", + "AvatarItemDesc": "7e6e24b6-6f9c-4315-b457-73eba52d824c,", + "FriendlyName": "Pumpkin Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e6e24b6-6f9c-4315-b457-73eba52d824c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:31.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2370, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "F7i3Au-z5UmKBdm2ZZxm_Q.png", + "AvatarItemDesc": "7e6e24b6-6f9c-4315-b457-73eba52d824c,Mnpd7r3eu0CNmZYSPTMASg", + "FriendlyName": "Pumpkin Gloves (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e6e24b6-6f9c-4315-b457-73eba52d824c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:22.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2445, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "94mq42cmonwiia61g4kx9jsmu.png", + "AvatarItemDesc": "07bebdb7-bbe1-49ea-a746-c1d00e11f174,", + "FriendlyName": "Pumpkin Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07bebdb7-bbe1-49ea-a746-c1d00e11f174", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:30.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2446, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "0qumzncb3mqqivji6bma4w0vd.png", + "AvatarItemDesc": "07bebdb7-bbe1-49ea-a746-c1d00e11f174,ciwVtCaRHEO72BFPe4Ujmg", + "FriendlyName": "Jack-O'-Lantern Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07bebdb7-bbe1-49ea-a746-c1d00e11f174", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:32.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2800, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ae49cxd8vs4g8bpfmd9l2uiv.png", + "AvatarItemDesc": "cfce35b4-d182-4505-a631-5a8e5011f550,", + "FriendlyName": "Pink Pop Rock Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfce35b4-d182-4505-a631-5a8e5011f550", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:33.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2801, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZOOhVVu5AUmtTX571xSvXg.png", + "AvatarItemDesc": "98e4c868-df73-4755-84b6-c4d12723b811,", + "FriendlyName": "Pink Pop Rock Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "98e4c868-df73-4755-84b6-c4d12723b811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2802, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lA0R461bQ0ST6HUFBbaWCA.png", + "AvatarItemDesc": "cb061cf3-05fb-4494-be5e-091ddb080271,", + "FriendlyName": "Pink Pop Rock Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb061cf3-05fb-4494-be5e-091ddb080271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:39.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 368, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8zf6s3vsds90klh8gnb66rq7t.png", + "AvatarItemDesc": "ce250e13-8137-4f62-9e52-2b1cebe7d0ad,", + "FriendlyName": "Punk Hawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce250e13-8137-4f62-9e52-2b1cebe7d0ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:07.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 372, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d4xgo6yv2c0ciprp97kwmfjbz.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,", + "FriendlyName": "Punk Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:12.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 373, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f4451n0e2f0tbm5fmb0h59n72.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,4169bfed-8403-4590-a29d-4941275ac0b6,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "FriendlyName": "Punk Jacket (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:13.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 374, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ovn4a6rs6eyhc8cqwowjycsp.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,0724b166-9ace-46ff-a191-b13f135b00a8,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "FriendlyName": "Punk Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:14.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2755, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "rlFO-9ZKCE2yfkHlavGzig.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,MFsWxydybE-dw3KO2hkuZA", + "FriendlyName": "Yellow Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:36.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2750, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qEYJHcXWwkKASHqpFug-TQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,9bb0x4qgokyQLbt1jA25Wg", + "FriendlyName": "Punk Outfit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:22.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2752, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d21ms4sSZUe8LXA5sCgB6Q.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,YceWQmfB7U-R7OeBIr2Ogw", + "FriendlyName": "Punk Outfit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:27.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2754, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "30PBljeph0mO3nz4ycwRAg.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,G2ncsS9U8E6fyjvliFqdOw", + "FriendlyName": "White Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:33.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2753, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "XTqgvYBK3UeAHs9ot0OlpQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,AdJ3ExsUq0ixzaKDHK5RUQ", + "FriendlyName": "Purple Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:30.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2751, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f4hUyaoXvUGqD6pHI4u8WQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,TC79rbThdkCZHGWaxk-qYA", + "FriendlyName": "Olive Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:25.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 376, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4d1njsjeb0jzje2jyt4qwnpjf.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,", + "FriendlyName": "Punk Wristbands (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:15.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 377, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1px8h9mdfc4sqsqsgszmxrx0h.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,a6796264-9c7e-4605-830c-06db8d0dc780,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:16.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 378, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8b0gvz7zqfujt7dgbozqj2clf.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,f3b2330a-b981-4c7e-902f-1f57382f2388,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:17.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 394, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0kiv90a8q8p0qlzhtvo821u6h.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,baffdefe-de21-4551-9330-b50873965bdd,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:30:01.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 202, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2amyw5vvgk1c9r6w9pdloakyg.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,", + "FriendlyName": "Long Coat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 472, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6nqdiagvhy4jttofvcepefq2y.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,b42d903b-b393-4f49-af5e-38d0fb6c42b0,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:40.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 473, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cdi7d06wp2qv0t93m630vtp9s.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,8cab8cc6-9a2d-434c-8b72-e66edbdd223c,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:42.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 474, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "asgwfydw5ghc9650zs32gm2b3.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,657f28d5-672a-4f27-86c6-3cb00f25bb84,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:43.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1628, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3q0iocg562i3lzbn5n65t13lw.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,w_aK2K9bHEeU6oSQ7BK0qg,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:28.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 969, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agu0otilxkbnfpbo3ad7xiozv.png", + "AvatarItemDesc": "e2ObiQFqCUOdKniKL9a1EA,", + "FriendlyName": "Cupid Headpiece", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899b637b-6a01-4309-9d2a-788a2fd6b510", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:01.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 971, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d3mpn71rzkxnchgl4zc2c29lm.png", + "AvatarItemDesc": "BqpRbr_T1kO_YJqB1787kA,", + "FriendlyName": "Cupid Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6e51aa06-d3bf-43d6-bf60-9a81d7bf3b90", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:24.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 970, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78t1ffywj0qqumsmn2pc9yr9b.png", + "AvatarItemDesc": "PryEX4MEvkGKwST6dqJHjA,", + "FriendlyName": "Cupid Toga", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f84bc3e-0483-41be-8ac1-24fa76a2478c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:10.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2899, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nSSGXZLjwEGclxA93atFUw.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,", + "FriendlyName": "Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:25.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2903, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MD5DVsHC4U2RMM-e36mYDQ.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,pqtwCB_c20yRmOEs6jlH3g", + "FriendlyName": "Purple Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:38.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UhJ8FHTUiEWTy2_6oM5T3g.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,SSstW2ZPk0SB47qYlgbZzg", + "FriendlyName": "Black Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:32.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2902, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dkNHvcM-PUeaHpc9SxCaTA.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,p5zYbKLGU0m0ZutOicfjsw", + "FriendlyName": "Blue Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:35.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2900, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eJWJ01dRW02aA6rSCX8mJg.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,wgIwYsXqiUekieE6pSt1Ng", + "FriendlyName": "Beige Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:28.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1321, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "357bwxxf0kvoz6yd778gmm1k6.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,", + "FriendlyName": "Regal Crown (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:46.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1322, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8kgbf2gxx8ajp1uw1zrx306an.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zljmtj5WqUaRvB3XQTgJXg,XFuSiGupZ02Fut1Dxum_Iw", + "FriendlyName": "Regal Crown (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:52.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1323, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5zb2ksxrxwp3n2qlecml4u0l5.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,rmq1AOQvtUGOHvbPUximwg,XFuSiGupZ02Fut1Dxum_Iw", + "FriendlyName": "Regal Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:59.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1895, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "18orrg4k9zm7dsjfj9k2dz23q.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,rxMCIkLb2UO0Z3tsqtYuSw,x8nPf0xEwUiRdBQU00Bu1g", + "FriendlyName": "Diamond Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:55.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2423, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "jlklbsk_UEW29bsjWRwmUQ.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zu3hTm6JyEa0sE72KtVGXg", + "FriendlyName": "Space Crown (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:49.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1296, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "52v2ad9kyewcmaefknqmm6l5i.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,", + "FriendlyName": "Quilted Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:56.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1297, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8je25aruza2779n4njv1gmsgd.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,QBglgj80zkuvUkk55Y5Feg,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:00.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1298, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bldx6nf85lql3l8l0v9q3wmag.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,-HYrY2A5Tk2UWMi4_hmYbg,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:04.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1299, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7i0g7hs6aziwfqxrm0xnv67gg.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,IjRMopsYlUqSE6327I9TIQ,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:10.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1936, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ntv07mm6o9xzz2hcltgnxkur.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,zTsJHduQ5kirH9-6mewV4g,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:36.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202199258, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "GriWpcHg50eSLAxY5yc47g.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,ons8s4ILoUmkdtliouAscw,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quiltedcoat) Quilted Coat Shirt Teal Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.523776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2559, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "2GOgnQwKxk2NkTSIxV9t_Q.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,j3I0vVz0yUOYLX9wCOMyyA", + "FriendlyName": "Science Coat (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:56.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1675, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d4c3lyj93p5m9r7r9lvo7z2br.png", + "AvatarItemDesc": "be75b46c-ce22-4758-8810-c409c5aa1c50,", + "FriendlyName": "Puffer Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be75b46c-ce22-4758-8810-c409c5aa1c50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:38.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2008, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "87cdvvpwz27kl5fqj4a53il38.png", + "AvatarItemDesc": "be75b46c-ce22-4758-8810-c409c5aa1c50,ZLaDizWS7kOmnHVGlLLJ-g,TUZ21v0C9UK9GPsfbRwpsw", + "FriendlyName": "Flannel Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be75b46c-ce22-4758-8810-c409c5aa1c50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:04.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1853, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chi1z36iwxalp98kkl4coqu8h.png", + "AvatarItemDesc": "45695fba-5abc-4c5b-b9eb-949d5174d1f5,", + "FriendlyName": "Racing Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45695fba-5abc-4c5b-b9eb-949d5174d1f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:12.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1899, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7trthor2vpnt3r9e6aywi0ohc.png", + "AvatarItemDesc": "45695fba-5abc-4c5b-b9eb-949d5174d1f5,oFjt2tSsa0CJ7j0s3P6b4Q,XD9dH8LF_ECmL0TwXJW1zg", + "FriendlyName": "Racing Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45695fba-5abc-4c5b-b9eb-949d5174d1f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:41.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1854, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5ll88rg9iiispe9c611fj575e.png", + "AvatarItemDesc": "9fb62d0c-67ff-4675-85d3-a2869c2330aa,", + "FriendlyName": "Racing Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fb62d0c-67ff-4675-85d3-a2869c2330aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:22.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1896, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b5jt7jrhmi5ch4z1axfan8u95.png", + "AvatarItemDesc": "9fb62d0c-67ff-4675-85d3-a2869c2330aa,ijCbiOeGD0C4uznegq9CXw,VzyoPsQyCUaW6RSusgVUGQ", + "FriendlyName": "Racing Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fb62d0c-67ff-4675-85d3-a2869c2330aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T20:34:43.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1852, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7lyaxr5m3766nt3tdurlv2gik.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,", + "FriendlyName": "Racing Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:08.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1898, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1ohpxd83hqnj4iz7zqf0eqy3w.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,lhqUg1VMNkC8nrsGZF4edQ,uFERkSfSl0i9BPFBVOICeg", + "FriendlyName": "Racing Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:38.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2115, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qvzY9bthF0SXys1g8FCeNg.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,yO5pgI2quE2i3tNBlvJPGA", + "FriendlyName": "Racing Suit (Sponsored)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:25.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2484, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8uEeIbBPq0eGrt1TFDEHTg.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,jGNpSO25HkO7KbR3v05nkA", + "FriendlyName": "Racing Suit (Astronaut)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:36.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2002, + "AvatarItemType": 0, + "Tooltip": "This item may come in handy at concerts, afterparties, or in dark, forbidden lairs...", + "Rarity": 50, + "ThumbnailImage": "es10uj7wkevzcr8uv905avgbr.png", + "AvatarItemDesc": "e4888429-6f5b-4f57-9b9b-2559d3a54856,", + "FriendlyName": "Rainbow Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4888429-6f5b-4f57-9b9b-2559d3a54856", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:38.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2928, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZZVGN49VWEOBaIP-aV-9gQ.png", + "AvatarItemDesc": "24781418-38ad-411f-925e-2321fe616b51,", + "FriendlyName": "Rain Cloud Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24781418-38ad-411f-925e-2321fe616b51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:16:03.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1357, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1c69e4rsxwolxiw7iq522lr5k.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,", + "FriendlyName": "Raincoat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:41.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1358, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2y3vygtoygwgkb4vvi4ueelz8.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,GH9q8_0O9EqjGdz7lGWD2g,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:46.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1697, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7uwfpp71odpm2zjf7dopjufm1.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,M-N9vQt0CEefqgmeLp3l4g,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:43.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1696, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "647nsozzr00smthvpc1qrymze.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,q_HcScqHgEG-x70g1wtfLA,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:42.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1482, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9oppuuz5leb1br8st56q8pzgc.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,gDP3y0J2EUayzVvBT9-92w,dMDMDExSmEKOF_kQIHaowg", + "FriendlyName": "Raincoat (Getaway Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:47.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2809, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6MqtAecx3EmgN2hug-kDWA.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,kBmmFoqo-0eLSSAhen-VLw", + "FriendlyName": "Black Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:53.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2811, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "zxxN4bCyXESVjKRUDYv7iw.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,tVbu4hUFHkmEKEz38GG2Nw", + "FriendlyName": "Pink Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:59.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2810, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3yrQDfBRi0i3HHeiqNPY3w.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,m8x6fiLEw0m4pJywntEpzg", + "FriendlyName": "White Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:56.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1359, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5slbcv7tlqy8kpl75ol21jidq.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,", + "FriendlyName": "Rain Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:50.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1360, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0ds3vsjpe01l47ks32nxbst5q.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,v3af60p1Q02NpdCqIRO9lw,1hVOI7IuY0mKvtr2PZXLww", + "FriendlyName": "Rain Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:57.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1481, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agse5pe0hx0ij3ujgprl3xcsz.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,kX7ocqn8X0-4M1nw-KrwFQ,qC66Hu6yKEu2Res2o2z1Kw", + "FriendlyName": "Rain Hat (Getaway Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:44.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 688, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eabi5cm8xoboqpioo9h705aih.png", + "AvatarItemDesc": "f277af6a-0807-4716-8948-1bee236ffb74,", + "FriendlyName": "Ranger Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f277af6a-0807-4716-8948-1bee236ffb74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:08.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 697, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "58ktghs1gt48t0xa8cgyrvrhr.png", + "AvatarItemDesc": "f277af6a-0807-4716-8948-1bee236ffb74,d0bb468c-b78f-4e80-916d-65fec3ec0010,0272eb19-fc0e-4ad6-8db9-cc0cda2528b0", + "FriendlyName": "Ranger Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f277af6a-0807-4716-8948-1bee236ffb74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:47.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 693, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e2oxz7ylbhkwldqs5mqnxnqzb.png", + "AvatarItemDesc": "fb907eb2-91c8-45ff-84a5-462bb86ea7de,", + "FriendlyName": "Ranger Uniform (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb907eb2-91c8-45ff-84a5-462bb86ea7de", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:18.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 700, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6r3jp2iifvxn0dpu7pmhfzw0a.png", + "AvatarItemDesc": "fb907eb2-91c8-45ff-84a5-462bb86ea7de,b415fe60-3e9d-4f4d-a53a-ca15eac6af7d,d21e1b6f-9c9e-42b4-bad5-201ec43161e9", + "FriendlyName": "Ranger Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb907eb2-91c8-45ff-84a5-462bb86ea7de", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:51.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2572, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "i5YnDBV6w0ef6GvGX7uWBw.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,", + "FriendlyName": "King Rat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:26.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2575, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ma2zBsTYbEu0Crl3zZaDJw.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,RTrcYpLtukm83lwAfWi5WA", + "FriendlyName": "King Rat Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:35.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2574, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "m90YcY3tGkWS_3aDgwpp9g.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,3oar1K99PU2OTGaT0i7Hag", + "FriendlyName": "King Rat Hat (Vert)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:31.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2573, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T9OIhzV2uUiPCPipuBhCkQ.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,T5z4W4H8sU-2NxpepVH0Qg", + "FriendlyName": "King Rat Hat (Noir)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:29.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1769, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "dt1vtd0wu908uat9x5cnwqgmz.png", + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,", + "FriendlyName": "Rec Con Lanyard (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:29.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2152, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "QyumHCBqFkSaxEc2T7bMaw.png", + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,cxwdcbGTfUS_LYQoXpcQng", + "FriendlyName": "Video Partner Lanyard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:19.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,b6b5aad9-1211-48ce-ac2e-8053a879ccbd", + "FriendlyName": "Rec Con Lanyard Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1103472285 + }, + { + "AvatarItemId": 1829, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1efy3zkdik26gdvivvvd0ap3k.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,", + "FriendlyName": "Reclympics Medal (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:00.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1830, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vqxnzgn42vejyf08m7zft4f3.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,zNLzEvpTpkOIi0J3fIvh1A,OqC4ny2rk0yJJOHemspV7A", + "FriendlyName": "Reclympics Medal (Bronze)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:03.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1831, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "alczfhgjdnbcvf9orafqcp6ag.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,mVj9t_XVCEqdx89v2zdjHQ,kJ_pNxSWKE6HZX4Sc7Dv0g", + "FriendlyName": "Reclympics Medal (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:06.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1667, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aya5t0pnayzq9jz4mfs6jidhc.png", + "AvatarItemDesc": "9656a307-0c79-4b8f-910f-e67c454faefb,", + "FriendlyName": "Rec Pods (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9656a307-0c79-4b8f-910f-e67c454faefb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:46.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1668, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0f7hl0uorhvk24n6ag35m0pxu.png", + "AvatarItemDesc": "9656a307-0c79-4b8f-910f-e67c454faefb,mG0HW8fJkUOE5u7Whg4kag,3CLmVNB_p0Gd5N8dN74TBw", + "FriendlyName": "Rec Pods (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9656a307-0c79-4b8f-910f-e67c454faefb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:49.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2037, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X2KqX8rMj0WzdutVcCYmhQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,", + "FriendlyName": "Rec Trooper Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:54.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2038, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4iczDP9r4EqWpK0IGEcJ8g.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,WkI_v-Sua0WlhmMf8R4aGA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:56.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2039, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nmOyhPcHDkG33U0RQ55jZg.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,3bqY_T4_HEy406HcXrfLuA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:59.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2040, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rVOFYXLD1UOM6Gx-rJT_-g.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,I4lNZy7lR0uyUN5Vk3fabA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:01.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2041, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1825duyZL0qQprGQokp2Lg.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,SMeMLRUF-UWrOcXYJzgScw,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:03.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2042, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "I1pEVWdT3E-DLY2hkN88qQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,aaaG7Fg8fky2dkrwVsA30g,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:05.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2081, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5lE9WIsg6U67neLntaKrvQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,GSO7t0cNs0SIbU-jQjnX5g", + "FriendlyName": "Rec Trooper Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:22.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h3kstogj0skzqb4uky1jpnsn.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,", + "FriendlyName": "Rec Trooper Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:07.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b94th4lvtw4iu7e3pxussoiwk.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,WkI_v-Sua0WlhmMf8R4aGA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:10.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2045, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "07vec7ppqlcv6a60pn9voj1va.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,3bqY_T4_HEy406HcXrfLuA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:12.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2046, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3v252zqwrqkn4wxjcllebsnwx.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,I4lNZy7lR0uyUN5Vk3fabA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:14.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2047, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "afspou17ntsll0egoozdkp3md.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,SMeMLRUF-UWrOcXYJzgScw,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:16.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2048, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25yoikgkjnlbo9c84x1r7rvuf.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,aaaG7Fg8fky2dkrwVsA30g,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:18.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2082, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "14cf5es3wgsam3e771iwjkzbm.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,d0D2htQZvEKajd0oAesNow", + "FriendlyName": "Rec Trooper Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:24.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2401, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b5ur52zpzgwpmifcd9wxjdhq.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,W7E9KfRTLEK-1EZTNKM3Yw", + "FriendlyName": "Rec Trooper Vest (Space)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:39.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2031, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "glEms9DPgUePBYRHEkwVCA.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,", + "FriendlyName": "Rec Trooper Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:39.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2032, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cHR8FF5MZkaGrOTZcAvR-A.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,WkI_v-Sua0WlhmMf8R4aGA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:42.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2033, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3kbfjdmvbkav8SJ0HVhIfw.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,3bqY_T4_HEy406HcXrfLuA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:45.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2034, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GLCx4lFj6E-Z-FE9wkiDoQ.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,I4lNZy7lR0uyUN5Vk3fabA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2035, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NVXRDfl_ZkOY9c5Nq_W1Wg.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,SMeMLRUF-UWrOcXYJzgScw,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:49.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2036, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5bl8JvLg7Eqp39q9_7m5dw.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,aaaG7Fg8fky2dkrwVsA30g,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:51.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2080, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ziuv9nfKPkOE__m_o9Hvxg.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,9ir4Es5d0Ue-8bn2EUcCZQ", + "FriendlyName": "Rec Trooper Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:21.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2400, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NXhmwZeIG0SGdfTzbBGsaA.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,XaNgcjkwP0uxS5_S1JaQrA", + "FriendlyName": "Rec Trooper Gloves (Space)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:35.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 203, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3q137204ani35lep2nt9a2rx9.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,", + "FriendlyName": "Belted Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 204, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cm21tbv0v6i0hf0k70bpbha7b.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,5b58d879-2bc4-47ed-a996-1af2ace16425,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 205, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "74y7j6vz7569lkpcqb2fje2p0.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,d320ad9b-ba5c-45ea-b2b8-0d3e409a9db1,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 206, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2ikovokydhafpprj55cpdyk5m.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,d61711fd-589a-4669-b991-9408a58fffd9,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1607, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5l9wcvzyx9vvzrqe84ohl0s72.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,oJFfs62tR0WVRry2Smt8yA,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:39.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1608, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8ur3yl22591shtrv2px510hqi.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,MIfCkcrFOEasJqfDmxThdw,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:43.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 207, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "djd1hn8umzgdrbijc8oam3pqa.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,", + "FriendlyName": "Flowered Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 475, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e9vo4xrmjapg3t56dbabbraqj.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,1acb1f56-4509-449f-9c6a-065bdeaa9ee3,2d7361d2-31a2-404b-90da-10e89114e1f2", + "FriendlyName": "Flowered Dress (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:45.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 476, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "930kpdjun0nt4qnb2awrj922c.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,e98c3f98-7844-4d6e-a21c-05027033c18c,2d7361d2-31a2-404b-90da-10e89114e1f2", + "FriendlyName": "Flowered Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:47.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1790, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4i7ompfsh19q8naq86uvyacc4.png", + "AvatarItemDesc": "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4,", + "FriendlyName": "Red Panda Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-05T17:42:31.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1964, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5m6k9qeuysh54wzun74y56isp.png", + "AvatarItemDesc": "51e5467f-d310-44f8-80c3-17d7e27450bf,", + "FriendlyName": "Retro Gamer Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51e5467f-d310-44f8-80c3-17d7e27450bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:31.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016431692, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "06VrP-0hN0-WkSvHS02YfA.png", + "AvatarItemDesc": "51e5467f-d310-44f8-80c3-17d7e27450bf,pue_FwobQEiaVubgc5eMUg", + "FriendlyName": "Dark Retro Gamer Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51e5467f-d310-44f8-80c3-17d7e27450bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1963, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ehoq4v330bykz8stgk9bf58j.png", + "AvatarItemDesc": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,", + "FriendlyName": "Retro Gamer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:29.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1134686645, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IFDGFDNDlUCCJx3FM5dBHQ.png", + "AvatarItemDesc": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,ZYjUekG8JkC9uPkm4xH8ww", + "FriendlyName": "Dark Retro Gamer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1962, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5y0de11wldvhcpeo87sdq0vyq.png", + "AvatarItemDesc": "352b49fc-99a2-4f05-a688-866ade1cd394,", + "FriendlyName": "Retro Gamer Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "352b49fc-99a2-4f05-a688-866ade1cd394", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:27.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1132364555, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "q-bZ8zTCIkOyfHecJwDDng.png", + "AvatarItemDesc": "352b49fc-99a2-4f05-a688-866ade1cd394,PabSuDB_GEejMahhLvLkJg", + "FriendlyName": "Dark Retro Gamer Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "352b49fc-99a2-4f05-a688-866ade1cd394", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1961, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ehevha4vehw8jlosk83rag8tj.png", + "AvatarItemDesc": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,", + "FriendlyName": "Retro Gamer Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:25.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1933117342, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uxAsLya8MkWuyusZMQmV0Q.png", + "AvatarItemDesc": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,EM1x0p66L0Gf6BNBP8-1kA", + "FriendlyName": "Dark Retro Gamer Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1960, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "em7ibip6ffvp7q0jpp5qcx4j5.png", + "AvatarItemDesc": "4885683e-a87e-4d27-990a-c16f1df7a28f,", + "FriendlyName": "Retro Gamer Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4885683e-a87e-4d27-990a-c16f1df7a28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:22.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1138935250, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_uzRLVwLNku8gCel1chHsg.png", + "AvatarItemDesc": "4885683e-a87e-4d27-990a-c16f1df7a28f,hKBSHovOeUy84yCbvutkOw", + "FriendlyName": "Dark Retro Gamer Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4885683e-a87e-4d27-990a-c16f1df7a28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "e036f042-92c1-4c35-ab71-e9c7d5fbf301,", + "FriendlyName": "Ringmaster Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e036f042-92c1-4c35-ab71-e9c7d5fbf301", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1683507107 + }, + { + "AvatarItemDesc": "73994140-58f1-40b8-a7a4-baca020ab2fc,", + "FriendlyName": "Ringmaster Mouth", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73994140-58f1-40b8-a7a4-baca020ab2fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1011431567 + }, + { + "AvatarItemDesc": "53a73a1a-70d1-447b-a594-3c96890f52ab,", + "FriendlyName": "Ringmaster Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "53a73a1a-70d1-447b-a594-3c96890f52ab", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1084249225 + }, + { + "AvatarItemId": 1807, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "duw3kiu7wtbc2uhma5er2l480.png", + "AvatarItemDesc": "1fed823a-22bb-4225-bdc4-491da6eba875,", + "FriendlyName": "Apocalypse Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fed823a-22bb-4225-bdc4-491da6eba875", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:25.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1802, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5dszhnzwez2lmqbracuxzqjsf.png", + "AvatarItemDesc": "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a,", + "FriendlyName": "Apocalypse Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:49.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1804, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3k1od2jqyltyokb1zgvbyxoll.png", + "AvatarItemDesc": "3e307eca-0850-4d85-8cc1-f5c176bfa7e9,", + "FriendlyName": "Apocalypse Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e307eca-0850-4d85-8cc1-f5c176bfa7e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:03.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1808, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dqdlvwys8ok8v51xsyvdtnxs.png", + "AvatarItemDesc": "23944856-b567-4075-815f-261cb4655cff,", + "FriendlyName": "Apocalypse Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23944856-b567-4075-815f-261cb4655cff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:29.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a6631bxfoa0zyvs7fxyzubtji.png", + "AvatarItemDesc": "23767c66-31de-4e6f-81f5-b17769a0fd5b,", + "FriendlyName": "Neighborly Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23767c66-31de-4e6f-81f5-b17769a0fd5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:13:49.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "7f4317a3-0f47-4fcd-bfeb-ff62e33d37c9,", + "FriendlyName": "Robot Arms Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7f4317a3-0f47-4fcd-bfeb-ff62e33d37c9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1505598127 + }, + { + "AvatarItemId": 1719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "446uvbhzordij4cso3587acsi.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,", + "FriendlyName": "Robot Gauntlets", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:10:59.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2111, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "shxxE1X6sE6_4lH3hEXGSg.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,gIDTSqjSvECgxxd4YtOZIA", + "FriendlyName": "Alloy Power Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:27.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2110, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SIkjnmq_P0mIn1aA3HyRTg.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,BoaDgMfzEEycbxVmWneHNA", + "FriendlyName": "Alloy Power Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:26.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1874, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4syp3adr5p8obc2cv6ef22vvr.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,", + "FriendlyName": "Rogue Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:34.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2442, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OzofyASWI0-3nEAN_uzMww.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,C64nh9MHv0mlUEP9-s7sJw", + "FriendlyName": "Constellation Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:17.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2444, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "K8ehibem7USe2u0ui6wmDw.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,ay9ktLnHfUKdj0-q52Gvyg", + "FriendlyName": "Constellation Cape (Sunset)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:24.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2443, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "C3GGuNb9x0qmewSkRHP33A.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,WJ27X_UnfUmFKpY6mRk_Rw", + "FriendlyName": "Constellation Cape (Aurora)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:20.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2560, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "zW972rrvQ0-DezwkPcNYvw.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,YVkcCfJZekq7GvdMUy2IWg", + "FriendlyName": "Science Cape (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-07T21:47:00.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1870, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5f0fab7drkewxktsldpgux1wv.png", + "AvatarItemDesc": "4b72736e-6fca-4961-98f2-1d4e199eb1e0,", + "FriendlyName": "Rogue Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b72736e-6fca-4961-98f2-1d4e199eb1e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:20.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1872, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "edcczm6mc1nc1pcanunxzgprq.png", + "AvatarItemDesc": "155c13b9-3c72-4314-85ac-6fbac19cdc76,", + "FriendlyName": "Rogue Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "155c13b9-3c72-4314-85ac-6fbac19cdc76", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:26.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2803, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dWn_mPxj7kqBYwHjUkgI8w.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,", + "FriendlyName": "Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:41.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2807, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7TM3VMXrDUSYrIPIMneOSA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,P8w3-vcH5kaHaMEsFsKDjw", + "FriendlyName": "White Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:49.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2804, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-PKF_-QCLkiKRwC3epBnZA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,KUSVg33DSUK8aYXxJzOTdA", + "FriendlyName": "Rose Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:43.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2806, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "myhvbSN3lEG_bApVoStEcA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,g8h43tuiN0CCifW9XbnFhQ", + "FriendlyName": "Red Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:47.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2808, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kABVMxEknUWZArqSsxRLFg.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,T0PMM2iQqkCCMZoznrdkug", + "FriendlyName": "Yellow Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:51.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2805, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FWfZEwnuIUSoM_qCk6O_ag.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,sA0KRBVMikOo6BBfk0dC1g", + "FriendlyName": "Blue Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:45.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2285, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4h1x24hz3beovhoamej45mz90.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,", + "FriendlyName": "Rose Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:37.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2286, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bf3m7280dfcfs560g1sg22rd1.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,4ve7AWD5pkGjvmtC6JRdiw", + "FriendlyName": "Rose Earrings (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:41.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2288, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "63yh6x5s2jp8owgzlji3dlq1b.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,10nOM4HBRU-e7Bb05SrtLw", + "FriendlyName": "Purple Rose Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:46.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2287, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "044d8yuos4umj6mmduxqxzgkx.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,F3m52YaKyEqwbgUyPVfFxQ", + "FriendlyName": "Rose Earrings (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:43.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1221, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "emi0fu2fmbr4yo7ar29206bpi.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,", + "FriendlyName": "Round Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T23:28:26.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1915, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "conhirr4han3hgcb2cd702mbx.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,2pnfLqkYZ0-GbpeE8g0vVA,hGtyDIIWckGoRgCaSG6GBw", + "FriendlyName": "Round Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:29.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1914, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "99d174z75khtsr89u9oy0e8ac.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,vw2iLomK9keTfEWsqJrmhA,hGtyDIIWckGoRgCaSG6GBw", + "FriendlyName": "Round Glasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:27.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1913, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8i5jwartl3iqnxvt1wo4k0n5h.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,t_y2xmFYLU2v6Jb6PX6v_g,4J9GPTKjjUOjyLVPmJyxWg", + "FriendlyName": "Round Glasses (Tortoiseshell)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:26.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2180, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gsJJq-rFYEqEYVYeX8ig2g.png", + "AvatarItemDesc": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,", + "FriendlyName": "Basketball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:37.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1806077292, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Jo3RL0WeG0e_NtvrpG87MA.png", + "AvatarItemDesc": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,G_fqC7KPaUWFM4Dk3stJYw", + "FriendlyName": "8 Ball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.527778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bf58ryhvrbk2aecunq5n3d31i.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,", + "FriendlyName": "Royal Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:08.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 772, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxnimeiegto1m9zphsy7qlcz3.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,5_-QLURc_0W8qeeN3Q3iGQ,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:55.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 899, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bi9ik7y9m6zg5fxaf10v08uj.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,mqdncbYcuUSGFvFqEU0z4A,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:29.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1309, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "auudmnyvmvid9ohssear4bwrb.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,MbusQ9bte0i9zirlxtNkSQ,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:51.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1310, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7h38lx0795mk0b8xq52d9qxpl.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,_9QlnTPXuEO5OQ3H0CwFkA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:55.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1503, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ttt8tqohiquycu310oyhuoql.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,cKLBS6pbf0SFuavWyeZ7GA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:04:18.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1758, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bwspz3moa7kwzc2t95xaqy4m.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,DZyW-gOG9U-QLGOMFBhOoA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:31.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1805, + "AvatarItemType": 0, + "Tooltip": "For earning $50,000", + "Rarity": 50, + "ThumbnailImage": "7na5kk0hvnqgktgkcfeeae46d.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,pEiU6yZm2U-X9ip_CM1SAw,PIV6RtD7Pk-UTcUp2wF1ww", + "FriendlyName": "Royal Cape (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:07.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "76b2m8j7wi2u4yhfscwdacit8.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,", + "FriendlyName": "Royal Crown (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:00.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 962, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "auqhtl8pre7pp954pnbtgfdm6.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,E1GfXDLUaUKaG9hTLAfFPA,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (Silver, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:27.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1303, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6e9fx1i4uk389yevktgtc9s2e.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,wCe2yC2U1kyYvPd8Z1rX2w,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:15.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1304, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aq5apzhwgqow1ci3zzpz4gmlg.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,QkfA71-1z0qOiZAVrbzhtA,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:20.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 721, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2awsas0vcecc7a11lr9fprbmy.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,", + "FriendlyName": "Royal Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:10.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 866, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a73qlfdd1y677wh2wfqn4hheb.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,0ps19D7b_U2iejdKQFORyA,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:21.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 865, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4chofv403g5ompt8exr51v7ai.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,t1MeoroFOUioYmPfT9mCeA,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:20.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1313, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "856n9ywg3kp1796sqc8zm9yvz.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,pjkTGVHkIE-BzCVi1KAesQ,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:12.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 963, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4sv3incjefp9fawup30vx3ih8.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,QP38EMygY0S-KIvdGatARA,m5Vw_MMCWUiSOOCL94QO0w", + "FriendlyName": "Royal Dress (White, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:35.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 722, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c7mqflmgoajhkoupate9o25ll.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,", + "FriendlyName": "Royal Tabard (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:11.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 864, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "247il28eoprdj9km80orhaqg4.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,5TJPDmvEBEOkP82ZWLFqtg,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:18.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 863, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9q4xzeoi13tu32l6zxmci5daj.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,rCeRk8Pdl0W5M47JAQPhpw,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:17.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 964, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6i0vxy286tiuc0bmfbvghl17k.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,8zXWUxwZHU6tThCV4p5iGg,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (White, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:37.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1314, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eq5nxte0f2lf5p4vau1f14d8j.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,s2kCppLsN0uY7Hy6qjQc-g,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:16.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9s3qbhep0iagvjuzfxnb10ab1.png", + "AvatarItemDesc": "418ed21e-766f-4c69-9d57-6f866dcc7feb,", + "FriendlyName": "Safari Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418ed21e-766f-4c69-9d57-6f866dcc7feb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:19.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 389, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ah0cj8axzd6ex6de2x2474x3m.png", + "AvatarItemDesc": "418ed21e-766f-4c69-9d57-6f866dcc7feb,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,0dabda0d-842c-4d39-a6b9-a8ca3ec20825", + "FriendlyName": "Safari Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418ed21e-766f-4c69-9d57-6f866dcc7feb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:26.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 375, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1v72ao5s3ko2tz94379807tki.png", + "AvatarItemDesc": "1041d745-c5e9-4218-a3e1-198f5c9c418c,", + "FriendlyName": "Safari Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1041d745-c5e9-4218-a3e1-198f5c9c418c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:15.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 393, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "702vaqfirb8tw9cqhy7977bcq.png", + "AvatarItemDesc": "1041d745-c5e9-4218-a3e1-198f5c9c418c,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,84c016ad-133f-45e3-a412-cac35612540b", + "FriendlyName": "Safari Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1041d745-c5e9-4218-a3e1-198f5c9c418c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:29:49.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 370, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "28rrsgo2cs68zome98ef6on9s.png", + "AvatarItemDesc": "f47f81e7-fcdd-4687-9b99-013147bef30f,", + "FriendlyName": "Safari Belt Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f47f81e7-fcdd-4687-9b99-013147bef30f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:10.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 385, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9d9fdme5qc72avvqaj8m7u2b1.png", + "AvatarItemDesc": "74365234-cb72-409d-8cf5-9a7209f707e1,", + "FriendlyName": "Safari Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74365234-cb72-409d-8cf5-9a7209f707e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:24.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 395, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cpfts5y9qfb5p04u9nnjxia23.png", + "AvatarItemDesc": "74365234-cb72-409d-8cf5-9a7209f707e1,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,fa9a3d8e-16c9-4a7e-9166-3226e0d13c39", + "FriendlyName": "Safari Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74365234-cb72-409d-8cf5-9a7209f707e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:30:09.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 382, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e4rsuhl9ws520phjximdfsour.png", + "AvatarItemDesc": "02a5b48c-abba-4385-967b-ba900424829e,", + "FriendlyName": "Safari Monocle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02a5b48c-abba-4385-967b-ba900424829e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:22.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 246, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bbp0mhypju37l680pb4b7mrdr.png", + "AvatarItemDesc": "2c679f89-c76e-4cfb-94e9-448c8fd44d55,", + "FriendlyName": "Saija Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c679f89-c76e-4cfb-94e9-448c8fd44d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 282, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6mjsnrcz0704f1ablk7vhwper.png", + "AvatarItemDesc": "50c9c6f8-2963-4ef3-95d5-e999a898269f,", + "FriendlyName": "Saija Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "50c9c6f8-2963-4ef3-95d5-e999a898269f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 65, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "98wzkbq0ttvls6my69cezxa1o.png", + "AvatarItemDesc": "274cb9b2-2f59-47ea-9a8d-a5b656d148c6,", + "FriendlyName": "Saija Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "274cb9b2-2f59-47ea-9a8d-a5b656d148c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1904, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "670kc3b5s4g784sztj0kyl4xe.png", + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,", + "FriendlyName": "Samurai Belt Sword (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:42:56.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2054, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mFZE17grJkezYN_vmo5ZnA.png", + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,XIluUn9vQEabkEpqyYBG0g,9sSCSLRkYUCs4CCHK8FfAA", + "FriendlyName": "Samurai Belt Sword (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:41.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,d6525663-1028-463a-be52-7b8ca5de99ba", + "FriendlyName": "Samurai Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1465178662 + }, + { + "AvatarItemId": 1905, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f278x818nbgl8s8jjebtz9s9z.png", + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,", + "FriendlyName": "Samurai Helmet (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:42:59.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2057, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "huMYQIkj9kGAZLeIjvaUTw.png", + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,tdECtZp1UUuRRKHV60coyA,PIg3ywG2J0CiGUPpm1A6Kw", + "FriendlyName": "Samurai Helmet (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:48.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,689e15e9-7e7e-4fd5-927d-c015d69c1178", + "FriendlyName": "Samurai Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1144250768 + }, + { + "AvatarItemId": 1906, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6u8vpdm9um3l371w3nl9q0j0g.png", + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,", + "FriendlyName": "Samurai Armor (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:43:02.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2061, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tcgeftujX0my-JOBuLu7kA.png", + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,c7wiHNZ-X0KazEW4cY9PeA,5fVeR6S6R0yo_NcT-TB1lQ", + "FriendlyName": "Samurai Armor (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:57.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,5d21a059-fdc1-4edc-898a-5fef98eed137", + "FriendlyName": "Samurai Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1797755030 + }, + { + "AvatarItemId": 1907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0n5obvj9pnbfm6a7x8t010av0.png", + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,", + "FriendlyName": "Samurai Wrist Guard (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:43:05.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2062, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AShCqsZgOUmxqJasgumjXQ.png", + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,h-uMOz2Hz06ymEjtz4wuzQ,4Ooi0h3SrkObClvS2cGk9Q", + "FriendlyName": "Samurai Wrist Guard (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:04:00.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,ec8b5518-1fda-450c-ae57-d0cf06700532", + "FriendlyName": "Samurai Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1208756254 + }, + { + "AvatarItemDesc": "f0fd87b4-4891-40a0-b463-7ce1da2c8929,", + "FriendlyName": "Sandcastle Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f0fd87b4-4891-40a0-b463-7ce1da2c8929", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1197877174 + }, + { + "AvatarItemId": 1934, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "8fzadq5f6ni6nboo4jmt4z0wb.png", + "AvatarItemDesc": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c,", + "FriendlyName": "Jolly Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:06.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1935, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "8qodk6xu0694kgb6xwrvd2pb9.png", + "AvatarItemDesc": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c,veF50UiWeUS8ugHSm6g_3g,7KDavHUPDUKTJI0fFzJHpQ", + "FriendlyName": "Jolly Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:13.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 596, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4b0c6i1j57butbjhen02dvnxd.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,", + "FriendlyName": "Scallywag Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:13.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 613, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3rvrjylez3infpqjran7vpvjs.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,53dd9e44-7096-438e-a7c3-11d7c307426f,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:49.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 614, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a8hpjaq8s1osstq81y66f1yk0.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,c3c9f89c-580c-43c4-b38b-07394044e0e1,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:51.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 595, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d1wurf50yoyooz47syoelb2ri.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,", + "FriendlyName": "Scallywag Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:10.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 605, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7qkfqsjmmjcd9v4hxmipdbu36.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,53dd9e44-7096-438e-a7c3-11d7c307426f,a4903da3-249d-4f0a-a746-b547fda371ff", + "FriendlyName": "Scallywag Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:39.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 606, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3llu49o19577q8gvi3uv7qu29.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,c3c9f89c-580c-43c4-b38b-07394044e0e1,a4903da3-249d-4f0a-a746-b547fda371ff", + "FriendlyName": "Scallywag Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:40.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 597, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a6uf8rc48kcjj4lj2fsir9vsr.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,", + "FriendlyName": "Scallywag Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:14.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 609, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3m6lx9rzs88omzsy182j1yuxb.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,85eb7f47-bb31-4c66-a8b3-562d35028e6c,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf", + "FriendlyName": "Scallywag Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:44.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 610, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00u595ofjuu0ky28diplmypee.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,39f04eee-3a58-4e53-9f5e-7e995d46ed71,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:46.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1111, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "80jjzxrcdq2pf4nuv4ev13nx0.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,6y_fzLhG7E65Px3-CYiyXQ,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf", + "FriendlyName": "Scallywag Vest (Crimson)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:11.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 598, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6ko9exiyd39fsdh9zptnhw8ba.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,", + "FriendlyName": "Scallywag Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:16.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 617, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "f0s9ro8uu6ibrjgn1bicm5vf8.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,cdca593a-8840-4140-9145-fe962732040c,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:54.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 618, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cxo3ckfyyp09zazkttzy0mjz4.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,62bd5dda-08da-4528-83c1-f0ca3812dc6e,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:55.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1104, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00nybvpdwpb8lqxe6gkzo9y7x.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,X3MkhL55Rk-0UK6h4dw0Eg,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Crimson)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:44.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1178, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cbojb5iebhb2p1tra7gh61j7y.png", + "AvatarItemDesc": "d960b47d-f8bd-4c2e-813c-a6a09876ea18,", + "FriendlyName": "Scarecrow Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d960b47d-f8bd-4c2e-813c-a6a09876ea18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:03.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1179, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5saxy9nrha61uuej237wtnnsh.png", + "AvatarItemDesc": "d960b47d-f8bd-4c2e-813c-a6a09876ea18,TZ3oQBp_3kWY4Vzscgv5Wg,xCpdHx1iM06Gg0tYoGv0DA", + "FriendlyName": "Scarecrow Hat (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d960b47d-f8bd-4c2e-813c-a6a09876ea18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:08.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1174, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "acu72xlzacl1k4mnepqsfd2td.png", + "AvatarItemDesc": "1fcaf4fd-9012-407e-b691-fbc3ccf22421,", + "FriendlyName": "Scarecrow Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fcaf4fd-9012-407e-b691-fbc3ccf22421", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:25.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1175, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9mad5t9bu3npfwut2s2mhtvkp.png", + "AvatarItemDesc": "1fcaf4fd-9012-407e-b691-fbc3ccf22421,S0AKRB_m2k-I7snHnUigtw,uTkbswCedUGyuZcaX0W6Ww", + "FriendlyName": "Scarecrow Overalls (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fcaf4fd-9012-407e-b691-fbc3ccf22421", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:31.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1176, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6vc9gb07nhld11lkb3agtr3xq.png", + "AvatarItemDesc": "3c9acb9e-3353-4359-aa5a-ae9512b086b4,", + "FriendlyName": "Scarecrow Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c9acb9e-3353-4359-aa5a-ae9512b086b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:52:54.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1177, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8otr4d3cafso2wpwxnuskm5o5.png", + "AvatarItemDesc": "3c9acb9e-3353-4359-aa5a-ae9512b086b4,dro7VLV8BkGjqpPAbJheYA,bNA44H2wzUy-FkWgyOq1Kw", + "FriendlyName": "Scarecrow Wrists (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c9acb9e-3353-4359-aa5a-ae9512b086b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:52:58.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 94, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2t2ujgsm8dnv8j9ax7bmlsnsd.png", + "AvatarItemDesc": "f17a2f9e-1eab-44e8-b10a-1f53c5d0778c,", + "FriendlyName": "Scientist Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f17a2f9e-1eab-44e8-b10a-1f53c5d0778c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 247, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "17egpclh4qfjwpauvfot65edy.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,", + "FriendlyName": "Scientist Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 795, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xvbjj5mp2xmnu3w4g7m7c9px.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,KTidYfH0ckWxJGs5qzUBTQ,w07bkcUoQEiNJTVDeSywzQ", + "FriendlyName": "Scientist Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:23.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1100598754, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "oDUFSXJI5E6fmcsxJqMskg.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,X0nzvhAtgEKqzjQPb9d3qQ", + "FriendlyName": "Science PPE Coat (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.529776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,8OdkUQPXzkGqXFa3LNXupw", + "FriendlyName": "Scientist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1664244968 + }, + { + "AvatarItemId": 99, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9g1cba0qnig6whctstb0qaxyc.png", + "AvatarItemDesc": "11e508a7-fe7d-470e-b270-2dba33e2f7a1,", + "FriendlyName": "Scientist Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11e508a7-fe7d-470e-b270-2dba33e2f7a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "11e508a7-fe7d-470e-b270-2dba33e2f7a1,chxai_T-BEOc0-uZv6OAQg", + "FriendlyName": "Glasses Scientist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11e508a7-fe7d-470e-b270-2dba33e2f7a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1577926849 + }, + { + "AvatarItemId": 283, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3sd1nkxa7ilnnjgnwm2xtqw6b.png", + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,", + "FriendlyName": "Scientist Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 802, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6r1rr4giperexziw91ocwu6f9.png", + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,KTidYfH0ckWxJGs5qzUBTQ,VSZX99mSt0e7gPRM2_XzMw", + "FriendlyName": "Scientist Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:45.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,mRNobpJiWUCpih1zETyaZA", + "FriendlyName": "Scientist Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1306056020 + }, + { + "AvatarItemId": 1510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aun7sd8qe3iqtu86eeyec9uq1.png", + "AvatarItemDesc": "380a5e35-b4d0-4119-9721-8ce4c0840fbd,", + "FriendlyName": "Starship Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "380a5e35-b4d0-4119-9721-8ce4c0840fbd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-08T21:59:46.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 102, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0lm206mtws500pk9e4v0zaxuv.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,", + "FriendlyName": "Space Visor (Nova)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 909, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "av1v53eoyk53w8zi6bwd633xb.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,SXG9rdEkxkOR31Nk0aTOww,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Quasar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:42.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 908, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c9qgvf1jfrdwzsjud14bqa9pp.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,FFIpxInE-EKbk2GY-pIkyg,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Nebula)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:41.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1800, + "AvatarItemType": 0, + "Tooltip": "Unlocked by visiting ^WelcomeAndroid", + "Rarity": 50, + "ThumbnailImage": "1x302shgihlyh5d4ewddkiln8.png", + "AvatarItemDesc": "8691fa78-3069-41e6-8431-01c294e8d56b,", + "FriendlyName": "Space Visor (Orion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8691fa78-3069-41e6-8431-01c294e8d56b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-21T18:25:26.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 783, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9cfmiay0o7q5lwqa8wx2pm6ug.png", + "AvatarItemDesc": "KNZ1DvCLC0WHyH4opYAXtw,", + "FriendlyName": "Space Visor (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0e75d628-8bf0-450b-87c8-7e28a58017b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:25.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 914, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "80gor8nb4uaucnbfr9tmpumkr.png", + "AvatarItemDesc": "KNZ1DvCLC0WHyH4opYAXtw,FwiizGjYnUaM1VJ53R1cCQ,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Eclipse)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0e75d628-8bf0-450b-87c8-7e28a58017b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:48.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2483, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "BUTSu7FlzEOaiirSftT3Aw.png", + "AvatarItemDesc": "54ab4eb0-e28a-438f-9977-1ae8d642d136,", + "FriendlyName": "Space Visor (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "54ab4eb0-e28a-438f-9977-1ae8d642d136", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:34.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1235, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4eqc3t4t4z5gnf92sqf844nea.png", + "AvatarItemDesc": "1736a60b-6fb6-49bd-948a-fd3f3fc1be50,", + "FriendlyName": "Space Visor (Neutron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1736a60b-6fb6-49bd-948a-fd3f3fc1be50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:22.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "66dcdd7c-7dee-490f-b2d6-1113daa782a7,", + "FriendlyName": "S Cloak Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66dcdd7c-7dee-490f-b2d6-1113daa782a7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1696752929 + }, + { + "AvatarItemDesc": "50721e23-67e6-4f16-8e70-b0eccc2d9c3d,", + "FriendlyName": "S Cloak Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "50721e23-67e6-4f16-8e70-b0eccc2d9c3d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1144628819 + }, + { + "AvatarItemId": 690, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1x9c7tsi208t2ykfb3m4oc9je.png", + "AvatarItemDesc": "f60e093f-33be-4527-b827-060adc77b1f4,", + "FriendlyName": "Scout Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f60e093f-33be-4527-b827-060adc77b1f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:13.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 698, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "29hnpj9lbliny7rio545fdave.png", + "AvatarItemDesc": "f60e093f-33be-4527-b827-060adc77b1f4,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,464fcf20-361f-4dc9-b074-150c3bc27523", + "FriendlyName": "Scout Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f60e093f-33be-4527-b827-060adc77b1f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:49.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 694, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1p79pekt3n19qb30ijvjrm1pn.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,", + "FriendlyName": "Scout Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:28.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 701, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "46gvey2edecat2kkb5igdbv3b.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,5814a331-6155-4df4-b921-3ffc6059e44c", + "FriendlyName": "Scout Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:52.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2390, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "O9Zd5keRSkKyg2NA2V-QEQ.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,OX1ituawfUqYC-caC1WcCg", + "FriendlyName": "Zombie Boyscout Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:38.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2403, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "hsrSnSftIEikNWVCh1v1GQ.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,5owofMF_Y02WX2vwVL2aew", + "FriendlyName": "Zombie Boyscout Shirt (Mutant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:48.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,b3LMvviE_0-ObF1x5s1dtw", + "FriendlyName": "Scout Neckerchief Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1494988446 + }, + { + "AvatarItemId": 689, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5p66sv7hukzmxmyh14z8b5781.png", + "AvatarItemDesc": "7c1552a6-bc36-46e6-884c-1de6b8111a92,", + "FriendlyName": "Raccoon Cap (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7c1552a6-bc36-46e6-884c-1de6b8111a92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:12.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 704, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3x3ft6vz0rl2rj8hpyhovqfpw.png", + "AvatarItemDesc": "7c1552a6-bc36-46e6-884c-1de6b8111a92,926ae943-76cb-46dd-9c66-a6ee44ea9194,f1cd10fb-3d0b-436f-a173-65bc0d88d8bf", + "FriendlyName": "Raccoon Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7c1552a6-bc36-46e6-884c-1de6b8111a92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:08:41.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 692, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "10h045rqcvgang1sy8hxigaa2.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,", + "FriendlyName": "Scout Sash (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:16.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 705, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9bcfdhumbtjizrtjd0clcoxgy.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,67490e3b-0f9d-4bf8-84fb-54957b981c8c,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:49.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 706, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1reomg1o3fiimjissaj3cz8gd.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,b520f3bc-599a-498c-8a4f-97cc25aa741f,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:52.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 707, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e9pz1n0sr3t8a3p5llhnnh5l8.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,639f5e2c-6fbd-4ab4-aa10-7772e0932798,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:55.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 695, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8usltv63g871msvqw5fh4v055.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,", + "FriendlyName": "Scout Skirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:30.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 702, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "636348tvnldqryf6iouybgnoy.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,2bb65868-ee12-4871-b50d-64bbc9066f0b,e65530f7-543c-43e9-bf12-b6cda2bc755c", + "FriendlyName": "Scout Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:53.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2404, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "KhdXc-odtUiy82uWA9Hyfg.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,XxVqGAlweUKta__sKdMP3Q", + "FriendlyName": "Zombie Girlscout Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:52.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2405, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "3krD9e7_gUezvWyLO30zZQ.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,VL_OnsbYNkGgGAV16x5tQA", + "FriendlyName": "Zombie Girlscout Shirt (Mutant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:56.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1242, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "768szgyob0dtu1479hlls39oa.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,", + "FriendlyName": "Scuba Goggles (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:39.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1243, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3uvxr9vhe7g8piy0o8yyctuu7.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,DRS8xu3AqUOy2L0RKF3f7g,A7Y52vGIN0ijEKsWR_GQLg", + "FriendlyName": "Scuba Goggles (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:40.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1244, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1qslsywz60fw9vjpnsmkdf7gi.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,ZSOuWuKVCkqaIKc5d23G8Q,A7Y52vGIN0ijEKsWR_GQLg", + "FriendlyName": "Scuba Goggles (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:43.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1236, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4bal97uh9so459loudao1d5pk.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,", + "FriendlyName": "Scuba Wetsuit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:24.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1237, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6n0p0vohzjctpgowbi5u1zyvq.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,VqtOYBiKRkaSW4xAlBxQgg,xTqtLPQVyEKjAQZYl6HGow", + "FriendlyName": "Scuba Wetsuit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:26.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1238, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0xnsnma7zo835x2f5qjbn78m1.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,J-K34UX_B0yObjQI9tn0vQ,m0R4Jtf4HES8krxMnwIWFg", + "FriendlyName": "Scuba Wetsuit (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:27.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,ib2g1jTBgEeuj_s_uD1LKA", + "FriendlyName": "Scuba Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1519911932 + }, + { + "AvatarItemId": 1239, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7f4gij6i4zbxcp7r843b74p12.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,", + "FriendlyName": "Scuba Tank (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:29.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1240, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "29lilphy1aywcuix2e39k2ll3.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,V_duN2oWaUmmao1xHhzCJA,Uvarm_CwTUWWQYZvzI8OaQ", + "FriendlyName": "Scuba Tank (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:35.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1241, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "64h9st85tv92639yx7n2hvkgm.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,GWi1rEebqkCV_TS2WzQhcA,4zPm9miF5UOouEWFpOxYZg", + "FriendlyName": "Scuba Tank (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:37.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 66, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ajleo52n1fi9sfdklgfwqj0fo.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,", + "FriendlyName": "Captain Hat (White, Blue, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 67, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "abjufv7nbd5ja0vwk6hsiu952.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,f5403c89-9769-479b-8a05-c71607f9b189,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (White, Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 68, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ifixksiv9byqkl1zzr14g6m8.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,36e63797-5a42-4779-a5cc-ef5c2e7d17d4,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (White, Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 69, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8v6gk9097bsjdumvvi0xe4uk5.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,70112887-dc8d-49d5-96ed-0845204e0b58,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (Black, Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 554, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6hqhj2g8w7mvm85ibz82mgre5.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,2738ab5f-8c94-46b3-bac6-4a8111c41c21,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (Pink, Black, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-26T21:31:46.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "af4dh1nguqj29w27x7rk4ev5y.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,hhs9GY3XZkWb-8TXODvo6A,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:17.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 208, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20un14zo8e1w83oiatx99lwpt.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,", + "FriendlyName": "Captain Jacket (Blue, White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 209, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "41op70bhqlf5s2l320femq7fa.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b9f51861-ad10-4878-a9b1-d849bccf532a,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 210, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5iuznrq2zpc14fctl8tnp4odk.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b5d62931-e756-4a8a-8f3b-760beba49319,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 211, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9c3baiau5sgxnhlikff1pph09.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,6145292a-9002-41c6-8cd5-43679b6f542f,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 212, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "45br9sqobjiwgoh4alz5l9qf8.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,7c1aaa7e-503b-46ef-b1c5-0c61a75b916e,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 213, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "00jh263iuxouyk40imb4wjv0o.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,27af51bc-8226-4911-8d47-22d28ea8e378,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Blue, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 214, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7ergraacf4272gslnvieytiw3.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,857fdcd8-704f-43d0-92b9-bca26619b153,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 215, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3jnhwwxklxb0ojoszaexejbfx.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,3df5d13b-ba77-4b51-90b2-7a946e4d0be7,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1245, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3jysjn20nxxud7yahb9m78toy.png", + "AvatarItemDesc": "d86e2e65-5d87-466d-b885-8c52ac2eeeb0,", + "FriendlyName": "Captain Jacket (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d86e2e65-5d87-466d-b885-8c52ac2eeeb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:45.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 111, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5whnrhprjsn09q0089w2ztby2.png", + "AvatarItemDesc": "c161dc73-6ce3-49c4-8d15-45e6911f84e4,", + "FriendlyName": "Sea Captain Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c161dc73-6ce3-49c4-8d15-45e6911f84e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1871, + "AvatarItemType": 0, + "Tooltip": "Drops occasionally after Rec Rally games.", + "Rarity": 20, + "ThumbnailImage": "aqft3txgw2utq98o392d8z647.png", + "AvatarItemDesc": "9b62cbca-694f-4a32-b910-14da66bee9bb,", + "FriendlyName": "Settler Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b62cbca-694f-4a32-b910-14da66bee9bb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:24.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1869, + "AvatarItemType": 0, + "Tooltip": "Drops occasionally after Rec Rally games.", + "Rarity": 20, + "ThumbnailImage": "dcmbdw7xs41bspzcqqgmm66z0.png", + "AvatarItemDesc": "8cc01536-2f43-4de4-ac63-7304d8beb73c,", + "FriendlyName": "Settler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8cc01536-2f43-4de4-ac63-7304d8beb73c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:13.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2834, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1jTfJYB_7EGuMfrAABvtrQ.png", + "AvatarItemDesc": "e27d33ca-8a81-4c16-a3c1-ffdbc9104d7f,", + "FriendlyName": "Pincushion Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e27d33ca-8a81-4c16-a3c1-ffdbc9104d7f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:44.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1133, + "AvatarItemType": 0, + "Tooltip": "Doo doo doo doo doo doo", + "Rarity": 50, + "ThumbnailImage": "8y0zbo7xaq7ymt78orz2j8b42.png", + "AvatarItemDesc": "13e70c80-69a4-4ad5-90e5-4dd5e03da677,", + "FriendlyName": "Shark Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "13e70c80-69a4-4ad5-90e5-4dd5e03da677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-07-16T18:20:43.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2197, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "znzGBCjJOESSUfgrMXG-dw.png", + "AvatarItemDesc": "13e70c80-69a4-4ad5-90e5-4dd5e03da677,ln5aTG458UOVh4sR820FJQ", + "FriendlyName": "Shark Hat (Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "13e70c80-69a4-4ad5-90e5-4dd5e03da677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:48.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1877719043 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,ppkByMiIi0inxbXKX4fu4A", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1679852489 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,KaTmQ95fV0KAUM-nvOdHwg", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1933742111 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,1GKZY3pku0Wuq4_vdiO21Q", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1603771490 + }, + { + "AvatarItemDesc": "45e9bd7b-b1df-4751-b3ac-994652a5745a,", + "FriendlyName": "Sharky Fin Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e9bd7b-b1df-4751-b3ac-994652a5745a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1869424493 + }, + { + "AvatarItemDesc": "c0e3d978-c4c8-4606-a1af-b37de8995dbc,", + "FriendlyName": "Sharky Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e3d978-c4c8-4606-a1af-b37de8995dbc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1467914069 + }, + { + "AvatarItemId": 723, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "597y9xwas90z92z2w9b6wwif0.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,", + "FriendlyName": "Detective Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-21T01:05:53.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 747, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73q1jtkz7ele0ps0zjryw0veq.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,fEPwvuJCWkGBe4IekXpmLw,axPBGoqorEe77Oqqjfzb5w", + "FriendlyName": "Detective Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:11.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 748, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5x3p1zbvxjej9uzfpfkzw5g75.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,MQOxmUk-mUG-Ee9m9nX31Q,axPBGoqorEe77Oqqjfzb5w", + "FriendlyName": "Detective Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:16.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 724, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "abhyjwyie97h1rlznlk29up36.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,", + "FriendlyName": "Detective Coat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-21T01:05:55.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 753, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0dwm2kiido15zo6nhtcwi0vma.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,Z1Ib9r_fkUqgoZk0uid_Xw,9ZP617DQk0mZ10J3d5q9oA", + "FriendlyName": "Detective Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:25.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 754, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ec3q3cbtla7cn866qybjjfdaw.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,msOFHoFN9UORkeXXtdqBsw,9ZP617DQk0mZ10J3d5q9oA", + "FriendlyName": "Detective Coat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:27.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 959, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4dej37re0qtqjy0r8z93pdoo.png", + "AvatarItemDesc": "pNOfTDOgJUGQvGAlTjBoCg,", + "FriendlyName": "Wide Neck Tie (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c9fd3a4-a033-4125-90bc-60254e30680a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2215, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "62A9TZ7RIEOpOwFfOh9vXA.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,", + "FriendlyName": "Skateboard on Back (Skull Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:38.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2233, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IVFZ9-hSgUC7-LCqkg6e_Q.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,RDkLPQ8OukGIuLgTwnyqfw", + "FriendlyName": "Skateboard on Back (Vaporwave)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:25.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1511682151, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QhCcLdIsJE6qCUuMXJNKzA.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,7WzGZTf51EWClLTLNhhGVg", + "FriendlyName": "Skateboard on Back (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.531778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,655a48e6-ab7c-4c77-9c52-262f5bb8cbc2", + "FriendlyName": "Skateboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1710365958 + }, + { + "AvatarItemId": 2910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0GJO9X6LHUCt29vIeqSe6A.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,", + "FriendlyName": "Yellow Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:52.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CuNGqjAKSU6GCIYRVZDJjQ.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,y6NrbfS070WvASkKmPK-9Q", + "FriendlyName": "Burgundy Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:57.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SbLa3ck7dE2KhA1vlS01Hg.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,MVXeErsEE06KenrYaEm7Kg", + "FriendlyName": "Blue Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:54.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2914, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1kMIu6ND0CfZWLprY8CHw.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,7IYi1twr6kO3xJiQo5CB4w", + "FriendlyName": "Skater Fit (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:02.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2913, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hvNdEmSkiUWJ0ctgd3uXlw.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,qwQ8OV3E60WG_VEl9ECHoA", + "FriendlyName": "Green Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:00.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1182, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "21qmin6ucgfg45pvh611izmvq.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,", + "FriendlyName": "Skeleton Mask (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:30.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1183, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "08urzyypklh9ip5h98tczkuaj.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,s6DQxJ3gY0CG8L7JmcmRTQ,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:33.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1534, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "02lomjpecz0msvhsrrykghrca.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,DJnKyNXtuEW1tbKBFscIng,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:38.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1535, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "965z4xrxm4jg3xkv7a7vkdam0.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,ZY0rVNemjEuEMGMjuPNZyA,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:40.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1536, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "23626wp454o5pqka9wlihhylb.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,jhPUIkf5bkinMSsqPTK3fg,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Yellow Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:43.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2307, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "Z6ugLoGUCEiPuvn_pWpROA.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,FO32QjZ17E66PiSEDyf0xA", + "FriendlyName": "Cyborg Skeleton Mask (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:56.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2303, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "KlT8BALxYUisE34P3vbNZg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,rv8cpfd8kUCK5NbwjNmExg", + "FriendlyName": "Cyborg Skeleton Mask (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2304, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "i_i6gp2yhEyV8tCehNVavg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,kHPSqeM7JUqzMDtVxBGyBg", + "FriendlyName": "Cyborg Skeleton Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:49.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2305, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "4Sc2MIKVHkWz1i42WTaUvA.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,7BrllPDzNEyD9UZgdgM1Dw", + "FriendlyName": "Cyborg Skeleton Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:52.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2306, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "LKhhC901_Ei4OyKUfa82Hg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,Ftonil9tzEWQnmRQuTxwxQ", + "FriendlyName": "Cyborg Skeleton Mask (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:54.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1184, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "4yv9lieuu39ybfs061keni4bw.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,", + "FriendlyName": "Skeleton Glove (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:38.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1185, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "5yfuk6ffcuv1z2vqg6ffd7b69.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,s6DQxJ3gY0CG8L7JmcmRTQ,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:41.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1537, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "ejkvm0j7lbzn3f6m3h8vzq464.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,DJnKyNXtuEW1tbKBFscIng,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:47.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1538, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "bac6n0vzhk6sgr3r9ltyf5xm0.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,ZY0rVNemjEuEMGMjuPNZyA,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:49.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1539, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "dl5czy3ptauoh49mbo0ae06zs.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,jhPUIkf5bkinMSsqPTK3fg,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Yellow Glow Skeleton Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:52.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2322, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "5hcwuha7qbinh2y6q3kskmmp3.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,GYeYHzKcekODxx_zbtk8Qg", + "FriendlyName": "Cyborg Skeleton Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:44.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2323, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6g7hoxuvnw5065l9r0fptljij.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,lwyGrpMk-kGZwIkioHr8Sg", + "FriendlyName": "Cyborg Skeleton Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:46.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2319, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "3clh44ctdz1digv1oiwv505ae.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,KWYMlbpZiEWRUb0F7CYDjA", + "FriendlyName": "Cyborg Skeleton Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:36.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2320, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "369wpmjr71m46vryx3x1ynzm7.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,CsPK8sqkHUSV1u9d4LijHw", + "FriendlyName": "Cyborg Skeleton Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:39.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2321, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "7x0z1jskh28m0i22yy1hel8xd.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,dPM-UHMtSEKC3rN8l4JsUQ", + "FriendlyName": "Cyborg Skeleton Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1180, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "1b7g9kq7p2543j7d970f1bkw2.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,", + "FriendlyName": "Skeleton Hoodie (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:21.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1181, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7hrs6xlvsngnvndq0px6erei9.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,s6DQxJ3gY0CG8L7JmcmRTQ,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:24.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1531, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "0s8dsvnn6kc6q646bv4ipcqzy.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,DJnKyNXtuEW1tbKBFscIng,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:29.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1532, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "5z8kh7zr2w90iw14n6828rdeu.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,ZY0rVNemjEuEMGMjuPNZyA,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:32.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1533, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "8duxsgrggyqxtmflb6g47toll.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,jhPUIkf5bkinMSsqPTK3fg,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Yellow Glow Skeleton Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:35.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2314, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "fU2o342vgECqLjsyiP8vCw.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,x_UvGO3yQEmprFEXhPExDg", + "FriendlyName": "Cyborg Skeleton Hoodie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:18.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2315, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "uHnG7V2W1Uu8H2VhCYGJgA.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,WRi0rK1O60qEpDPgOH5P6g", + "FriendlyName": "Cyborg Skeleton Hoodie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:22.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2311, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "cFJR0iw4gUGSumlZz_qxXA.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,NvnHqZ-nb068DOSrxZPYIQ", + "FriendlyName": "Cyborg Skeleton Hoodie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:04.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2312, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6Oec9FiiLE-7m4bafgo2Jg.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,2mwfYNx7iUGbSf6u8rRXwg", + "FriendlyName": "Cyborg Skeleton Hoodie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:09.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2313, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "nRtJQf3QR0iu2fICET7LtQ.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,xtjgHdnYREm_gx_tLsTpvg", + "FriendlyName": "Cyborg Skeleton Hoodie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:13.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2580, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "k9Vy4pHmyUWpUmPbGE0xuQ.png", + "AvatarItemDesc": "6936d8e0-9d4a-4e22-99b6-04e5182c1109,", + "FriendlyName": "Skeletor Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6936d8e0-9d4a-4e22-99b6-04e5182c1109", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:40.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2581, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SAPYQD9BqUWYP73yXsGZkg.png", + "AvatarItemDesc": "ba96458f-84c3-42b7-acc0-e4d96f31ec84,", + "FriendlyName": "Skeletor Hood", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba96458f-84c3-42b7-acc0-e4d96f31ec84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:43.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2582, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2cM1bj5ynEux57OhmNIcfw.png", + "AvatarItemDesc": "7ee02f33-1135-4adf-a626-44a1b6d1b0cb,", + "FriendlyName": "Skeletor Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ee02f33-1135-4adf-a626-44a1b6d1b0cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:45.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 946, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "81f46rtgrs77zl3to2piot49a.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,", + "FriendlyName": "Ski Buddy Goggles (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:16.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1276, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "948ckejd6ibxj5plgmycvqr20.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,fXNkMWFMV0aXalK_y_8SHw,SSY8ZYMgaE2Bl4oFmMjKQA", + "FriendlyName": "Ski Buddy Goggles (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:39:56.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2597, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4doxnMsj30aGV6CnhP58sQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,n741vKj02kyt8B5fT6s2og", + "FriendlyName": "Ski Buddy Goggles (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:30.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2598, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eqLzMORoeEGX-z-yYZtUsQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,iDnij3VaEkSAnpt5QRevig", + "FriendlyName": "Yellow Ski Buddy Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:33.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2595, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "VFE9I4VMiU6FDWdRZaZlRQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,oCB1ZeT0hU6uQGhHNsEvkg", + "FriendlyName": "Ski Buddy Goggles (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:24.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2596, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-KcCp_tuMkalrEo6T6ql_w.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,IQMO6E5ofkyC3S-IgrKyIw", + "FriendlyName": "Ski Buddy Goggles (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:27.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 947, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b3xepw6yux7925w5til94vmmw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,", + "FriendlyName": "Ski Buddy Ear Muffs (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:19.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1277, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "502z6klf3k4u48eow06006kxl.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,KwCEye0nsESFs-yj4h9sjQ,c_t9XDbn-Eqml1O-wC5zJQ", + "FriendlyName": "Ski Buddy Ear Muffs (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:02.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2601, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aiYHJsbXHku2qBmKihSApw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,m2znGrdH-EOhrbLbFsmyOg", + "FriendlyName": "Ski Buddy Ear Muffs (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:40.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2602, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "JHSLD9otmEODYVGGix00gQ.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,ojrXnWkUkEOGM3_oT6V29w", + "FriendlyName": "Yellow Ski Buddy Ear Muffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:43.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2599, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Cl-lyYb_7EyL8pudY8bgOg.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,sipxCGA1uUu72KV07vAWFA", + "FriendlyName": "Ski Buddy Ear Muffs (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:36.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2600, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cEOrlpC5uEi63XRZAeiOxw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,1_vRe9nJMEWcNX8kAwgh9g", + "FriendlyName": "Green Ski Buddy Ear Muffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:38.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 945, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7e765tmsry0a7xzqwbwtih0ga.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,", + "FriendlyName": "Ski Buddy Jacket (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:14.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1278, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2rr9ojhz8lb65xzu05oejy4i4.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,fXNkMWFMV0aXalK_y_8SHw,HQQWccdyakaEmntPhvC6CQ", + "FriendlyName": "Ski Buddy Jacket (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:15.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2606, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zPU9jyLBykuvxdbYwUE6Sg.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,1BQdC-mTdUqfm4vUf0lx-g", + "FriendlyName": "Ski Buddy Jacket (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:57.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2607, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Tk070G0_FU-z-K1AbYodRw.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,S7C2A8nga0SlIeOEkeYaUw", + "FriendlyName": "Yellow Ski Buddy Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:45:02.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2604, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "h2VEGWuIQke-wT_k7W0sTA.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,vpxBkYv6LUSVV4NhYAS1Fw", + "FriendlyName": "Ski Buddy Jacket (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:48.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2605, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "klVYrqwIukW57n4kEMxC6Q.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,0mwtvRpkmECaT5_4aqCttQ", + "FriendlyName": "Green Ski Buddy Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:52.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2721, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "iCZlDOC3T0yMCT2lIaEPrA.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,", + "FriendlyName": "Skis Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-10T00:14:03.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2723, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nT7I-G6ixUSQ6Om25gsUGA.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,sNtmG8EODkOoiAdOi5Hetg", + "FriendlyName": "Ski's on Back (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:10.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2725, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5I7iBS_W3EG0-nf--ECAJg.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,HQ1M8kINaEu-0HJtz7f8Gg", + "FriendlyName": "Red Ski's on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:16.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2724, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LdbyV0Td6Uuj5MUaG1720Q.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,hPVbRnQq-EW3g0q17N6G5w", + "FriendlyName": "Ski's on Back (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:13.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2722, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BP7vd50iWE-mLN5Go0g5hQ.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,OScwXIzJHUKzb-NC6gUnxw", + "FriendlyName": "Ski's on Back (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:07.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2726, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lCMwxmYnFE2_25yjGunDzg.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,QVsGyxlefEeomtJO-dFAOw", + "FriendlyName": "Ski's on Back (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:19.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1201, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bb4i0uki9qbf8p96v4jfo349e.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,", + "FriendlyName": "Trucker Jacket (Sleeveless, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:49.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5hykjpkvkk03yf3qjii3sm1ii.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Sleeveless, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:53.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1203, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0x06arb0upzvorpzdr0ci4qjo.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Sleeveless, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:58.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1204, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3t7w6vu3h429ps4eurav4diui.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Sleeveless, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:01.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1205, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dn6y0cqf5ukxig24sgivzxa2i.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,4Eck_dNL1UKlIZWsA1YppQ,WeyKw_WS70Cx3NZdOaxGeA", + "FriendlyName": "Trucker Jacket (Sleeveless, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:04.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2763, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AmL-fduRx06s08e_Pzs1tg.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,wMo3Bq1aXkCMFSRJft81BQ", + "FriendlyName": "Trucker Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:13.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2272, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TulnHZMqkE2HIMXRNacnsQ.png", + "AvatarItemDesc": "a79e855a-c4fc-4ffa-be2f-9f4fef0ab1d5,", + "FriendlyName": "Outlaw Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a79e855a-c4fc-4ffa-be2f-9f4fef0ab1d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:19.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2273, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3zjJ5Sl4WkCfXJUE--1kTg.png", + "AvatarItemDesc": "cc30f7f7-6c12-4a96-acfd-11aec984c2e4,", + "FriendlyName": "Outlaw Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc30f7f7-6c12-4a96-acfd-11aec984c2e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:21.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2274, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4nxCvy9s-EWMBpdXowp34w.png", + "AvatarItemDesc": "86e0a4cf-b112-40d2-94b9-7ef35abe5a61,", + "FriendlyName": "Outlaw Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86e0a4cf-b112-40d2-94b9-7ef35abe5a61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:24.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 70, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ero4520aj78qgzdiz2knkg422.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,", + "FriendlyName": "Small Brim Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 489, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1z40s482g9xq9sirwfx26sfce.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,67fc9269-8dd8-4d6f-b594-c77e716f2482,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:10.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 490, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "azus8th8ecez3uybgfgvqq0o4.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:11.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 491, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "77ytai8z0lk4qz5x8il84ybvx.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,d6823e01-69f0-4f85-b94a-74894356a2cf,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:14.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2481, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "R5_LD0bgPkWiDdYxa1A_mA.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,08r7prRREUi-Hvp1Xze-wQ", + "FriendlyName": "Mini Droid Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:30Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "C2N_ATMr50e2W4aeVk04iQ.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,7qVilU94X0Kldpvku2mcng", + "FriendlyName": "Alo Yoga Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:27.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 310, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "16fi077y627ijhrcmqr0t298e.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,", + "FriendlyName": "Smuggler Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 311, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zkofpgk5o77fd65ks8x95cxv.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,e821aa74-2d47-4388-bcfb-1893b8a83a52,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 312, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3seplqtowg3fi113bsetxytpu.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 313, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5iogm3iyqg7sp05x3hgihphdy.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,90869e86-cbc5-4024-9d08-fcdc41c64eb5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 314, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c02xchxpap45jxgdksgsouq6h.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 334, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ed58vssma43zkdcb28nce7hhe.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,002b07bb-4256-44ce-8ca4-25510e70b6b5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1110, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ce642u8nppm5zamly1hq59fs.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,bzYdPb8GWUS-CgT4wrziIA,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:09.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 321, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2bncfuckcjmwaa7wano2sn1od.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,", + "FriendlyName": "Smuggler Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 322, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "648nagff1d7nh5vgo6tx3tpqr.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,e821aa74-2d47-4388-bcfb-1893b8a83a52,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 323, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7mr1xqf0fhgcv6v8p96yba2sx.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 324, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "97ynsh86vqwidiuno90esdvqs.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,90869e86-cbc5-4024-9d08-fcdc41c64eb5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 325, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7botyxuklwv4dbcddyrwdpcbc.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 333, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9vn2nq6cpu4mjyomplb7mld09.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,002b07bb-4256-44ce-8ca4-25510e70b6b5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "de8u98lwjl6796f9eeq8g7zfr.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,bzYdPb8GWUS-CgT4wrziIA,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:41.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 305, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aipia6o2kym88ur3brk8ufjud.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,", + "FriendlyName": "Smuggler Goggles (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 306, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3940162c53c4psfxm652thn6s.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,e821aa74-2d47-4388-bcfb-1893b8a83a52,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 307, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20x5x09hzrg6c4zcpys1iykz4.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 308, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "72lfd8j7a44mhv0viedavv2ht.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,90869e86-cbc5-4024-9d08-fcdc41c64eb5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 309, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7a494od9prmwajsibg29z7jaq.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 332, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ayx5bk4f6mojyvfiarqfavc3n.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,002b07bb-4256-44ce-8ca4-25510e70b6b5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bkz14lxmqlga9phjxk98i0j7.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,bzYdPb8GWUS-CgT4wrziIA,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:00.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1068, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d8r0drrwrxs7ot1bgzz145sm6.png", + "AvatarItemDesc": "rJ9zPPGdlk2i24sxLfkqng,", + "FriendlyName": "Lifeguard Uniform", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c739fac-9df1-4d96-a2db-8b312df92a9e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:51.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1063, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5bzqn0b3ouxa0kemvmre0a06k.png", + "AvatarItemDesc": "22pyI3Kz-UullhcebC6DLQ,", + "FriendlyName": "Snorkel", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23726adb-b372-4bf9-a596-171e6c2e832d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:25.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2707, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lhJKy4WMsEKemSq-lsUulg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,", + "FriendlyName": "Snowboard on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:51.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2710, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Gb6e46akCEKEXpH2qSvTlg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,PI3_sfTcNEW5TcAmHbsUOw", + "FriendlyName": "Snowboard on Back (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:57.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DsRdBkQ4c0WsfkMYTXQsXg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,K1-5YGg2rEiJFRWUKEfEMA", + "FriendlyName": "Snowboard on Back (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:59.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RlqSyYcF8UeGktdGy3dvmw.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,DSZKCNhXAEiAxMM1Gqfhsw", + "FriendlyName": "Snowboard on Back (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:53.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sf_MJ_CbKUCFSn6Iky3HxA.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,E6md4xTNYkS9geB8zkNRmA", + "FriendlyName": "Black Snowboard on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:55.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2712, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rfwXR61Q1k2Kyd4vTEsAyA.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,2dGY5MHfxEunzr7jtSFRVw", + "FriendlyName": "Snowboard on Back (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:42:01.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1660, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3bu1frdg68km45i2m32p4tuf5.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,", + "FriendlyName": "Snowy Owl Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:56.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1s8l5fozxazwvdbf7an9mewb0.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,W83EASFUS06FBVMfL80ruw,Ydd6tdb9dUGooM6WAczNjQ", + "FriendlyName": "Snowy Owl Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:38.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0mjop3uc4xo79ox3fcqpa6wjj.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,8M72ydR_uUSsli43PH2Eyw,wQxO4xmXCUGeC9ZHfDNGTw", + "FriendlyName": "Snowy Owl Cape (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:43.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2257, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U8zL4biQO0utrAL1LCl_Zg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,F0o3eeWbCUaUAGRUdI0z7A", + "FriendlyName": "Floral Cape (Midsummer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:51.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 5880, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6crHGcPzUUefWWZmQFnzOw.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,NnqAnqqLmUypWnNR2s-_sg", + "FriendlyName": "Cottagecore Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-10-10T17:15:03.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1543394911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5Rk5icw_oEqqERltIynyTA.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,qvK8xgbyuE-dW3--Ku-mvw", + "FriendlyName": "Floral Cape (Midsummer Obsidian)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1633983614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2jE0M2FxUUKRdwB-Z981Dg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,hRl4UTGoBUi5dmu0f4s76A", + "FriendlyName": "Floral Cape (Midsummer Verdant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1144281523, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4YKttOYfZUCJWTM47OGSsg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,07qFFGmwm0qmIikemncBkQ", + "FriendlyName": "Floral Cape (Midsummer Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3li2gob8aeawnh0bgtt1lfs2q.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,", + "FriendlyName": "Snowy Owl Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:58:36.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1710, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "48ikxhz5n438dh6imyqdnqqdx.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,I_3bHRSG_EmC6novYyy2dA,Cq8BOWcHb0GTDLvXl1iIcg", + "FriendlyName": "Snowy Owl Mask (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:46.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cabsqekth6kemqe8zn7s45v7k.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,HWAWBHI3zkGsh06he9CoPg,UAeSz-XeZUaZsk9FNlPX5w", + "FriendlyName": "Snowy Owl Mask (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:49.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 509, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2vqxpnzd5r6nuyacqrsrw5loh.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,", + "FriendlyName": "Soccer Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:35.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 778, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9bshjyb8x1qk9xkany8c3ngke.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,OnQeVxdstkS2zGxLBAuM3Q,i8bvbGAKREmAEbjKO-geOQ", + "FriendlyName": "Soccer Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:02.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2316, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "rRsmZUSOC0q9ITRtTz45Zw.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,-v3ddzUMF0aKxJR82SDevw", + "FriendlyName": "Soccer) Soccer Shirt Recpro Orange Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:26.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2317, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "mJIUWEd9WkWLnKurCXXFEA.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,mtR99SoHQ0aJLJfXPsKW1w", + "FriendlyName": "Soccer) Soccer Shirt Recpro Purple Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:30.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 513, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0we6tjqz2a38v2ojhop58m34f.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,", + "FriendlyName": "Soccer Goalie Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:39.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 782, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "azz8fjlgu2rjy5q07n278fb0b.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,whEHo6Rhr0CxsDBsBD32WA,av4PwLz6YkmpBRImQZgn4Q", + "FriendlyName": "Soccer Goalie Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:07.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2324, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ZftdQQ7kwkKCDZuTV-DWFw.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,3by5X5XZ4EW7eaERsaBJKg", + "FriendlyName": "Soccer) Soccer Wrist Recpro Orange Combination Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:48.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2325, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vupu6d_gGEaywmPePUvf1A.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,l2g5qO9dokiNepH92oPmug", + "FriendlyName": "Soccer Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:50.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1059, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bij80e36orzdtzehef7fmjeru.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,", + "FriendlyName": "Soda Clerk Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:04.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1326, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "crm789xheizu2cbu14b3wavpq.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,XFCpIdeDXUqeYCIgh4fGtg,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:47.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1327, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8etl00yx6u11l07jed4sl7xnc.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,oKxmH-1s2EyIDkZoOQSQQw,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:52.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1328, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5tdjwtkcesfgqls3gzy24y3hf.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,MxnAq_XOw0uTzQ8IIxqb_Q,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:56.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1069, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7x52aisqe0e84v27jn0madibj.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,", + "FriendlyName": "Soda Clerk Uniform (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:54.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1329, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2cvsw2qbbncs9zl5vsv7mxqkk.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,XFCpIdeDXUqeYCIgh4fGtg,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:07.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1330, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "447rhi816e4hfny2r4ktjynv5.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,oKxmH-1s2EyIDkZoOQSQQw,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:12.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1331, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "di4jb7lfnmlgt0x975ppcx8h0.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,MxnAq_XOw0uTzQ8IIxqb_Q,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:16.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2384, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0MfRmLg2yUGhr0zEhMC8Pw.png", + "AvatarItemDesc": "341dcb4a-818c-4d7c-9ae3-1dabbb79519a,", + "FriendlyName": "Solar System Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "341dcb4a-818c-4d7c-9ae3-1dabbb79519a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:57.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 315, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "d0pc71o2msynlo2gg6zblsdb3.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,", + "FriendlyName": "Space Marine Armor (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 316, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bn2zkvbfzcikfewpjgtd2bgl2.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,c8cc9589-4831-4e73-a865-ef8c139a2ba7,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 317, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9matrdtcoznln9t9wjrus86kx.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,a089bcc6-798d-4777-98e2-7afd290dc6b6,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 318, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2z7ww93ccq8rsr8l3bu6m7ntd.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,a36d252b-16ac-4a82-bece-de145147e7d9,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 319, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1a78ihg1rn6i5jh196fh166vv.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 320, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "90qc2umq7k4h8g9c9wgxcr18v.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,68273c9e-96d6-4ddb-aaab-d99229e864d5,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1101, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5rzpff3hf3m92tijpthgokni5.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,4ElTihrMA0ipcgeb9UG01Q,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:32.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2490, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "tmcuRuyNAkaX-uyikLs9ww.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,1pJN1AfxgUmUN_bTaH3rng", + "FriendlyName": "Science Iso Suit (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:12.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 326, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7izb4xifehd19st4qgaf70jqg.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,", + "FriendlyName": "Space Marine Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 327, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "anqd573o831mzengh9221nawt.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,c8cc9589-4831-4e73-a865-ef8c139a2ba7,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 328, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5zeohu1rh1ox5hoix80fbfl50.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,a36d252b-16ac-4a82-bece-de145147e7d9,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 329, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3qre9n3k1ubvucbztpra48in4.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,a089bcc6-798d-4777-98e2-7afd290dc6b6,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 330, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0k5d0e2dbwkixi838nrtd06ll.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 331, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0u3e41638g5e5afvuyqxror2x.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,68273c9e-96d6-4ddb-aaab-d99229e864d5,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1106, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0crijq94h4xfrkph110y254b5.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,4ElTihrMA0ipcgeb9UG01Q,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:53.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2326, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lmmMFWBKYUqm66trPr35Yw.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,LVnNy7-nFkyvi19nNcdFgQ", + "FriendlyName": "Science Gloves (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:52.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2713, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9nzqt8ExHU6STyCJinXlUA.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,uWe5nxA4qkaicmXm3tMw9g", + "FriendlyName": "Science Gloves (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:42:03.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 299, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2blhcp3hyn511tz9gxh7s982l.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,", + "FriendlyName": "Space Marine Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 300, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6wlpi8vp4n0f8yrjuywdx8dzr.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,c8cc9589-4831-4e73-a865-ef8c139a2ba7,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 301, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2xmq1ji9y74k37fj1ft9nmnvf.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,a36d252b-16ac-4a82-bece-de145147e7d9,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 302, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3v8rhp57dz9s41uhg5l4rytbv.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,a089bcc6-798d-4777-98e2-7afd290dc6b6,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 303, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8syjfjzce4brzrmudgrhv1mvo.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 304, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7dx5tt4aplt5y8oxs4j4ojbqc.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,68273c9e-96d6-4ddb-aaab-d99229e864d5,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1086, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ebj5kkpndee5u9d3b5fcifo7s.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,4ElTihrMA0ipcgeb9UG01Q,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:20:13.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2679, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KnhWwAmT6kOTA9oXrhmGMQ.png", + "AvatarItemDesc": "bc0a1c68-2eb5-4dde-a242-c4e19940699c,", + "FriendlyName": "Space Monkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc0a1c68-2eb5-4dde-a242-c4e19940699c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:29.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1507, + "AvatarItemType": 0, + "Tooltip": "Unlocked during Xbox launch", + "Rarity": 50, + "ThumbnailImage": "6t5em8ljarz9lnn8erh5ndc1b.png", + "AvatarItemDesc": "b37582a1-7904-4288-a47a-7a63a3cb28ae,", + "FriendlyName": "Space Soldier Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b37582a1-7904-4288-a47a-7a63a3cb28ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:06:05.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1508, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ey0vb0vehvdu8qhub0g2unvrw.png", + "AvatarItemDesc": "88ae6d12-6d39-4770-ac2a-f8c4602a10a5,", + "FriendlyName": "Space Soldier Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88ae6d12-6d39-4770-ac2a-f8c4602a10a5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:27:18.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1760, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ba7xldqaol3atwgv9n84yszra.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,", + "FriendlyName": "Gunslinger Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:19:11.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xe9d7zlk5k1ybhox7zl3n47i.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,jXRXO3CVE0-H_iaxbMhFhA,VZyHWGKNJEGSxDT2GO3ebA", + "FriendlyName": "Gunslinger Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:46.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1812, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8e5uyhuugy9bj5wqx2g8ucwlc.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,YSnoHFmJDUa_XCK6JMMh2g,VZyHWGKNJEGSxDT2GO3ebA", + "FriendlyName": "Gunslinger Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:43.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-ejrJeBy7kaQ8QEETufdXg.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,JilOXG1I_Ei65mw6TzFWgg", + "FriendlyName": "Gunslinger Hat (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:11.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1761, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "11cf76c54n13whwtqa3rjs7s1.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,", + "FriendlyName": "Gunslinger Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:19:21.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1817, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2dhxtutyxumnzuwg5rz5nbm1l.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,1-_ssucgeEalgr6EDL3S9g,Ru4HN3stVUutQ9rUh7ro1g", + "FriendlyName": "Gunslinger Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:59.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1816, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2mqqtndrscn1090v5nnmhizyz.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,cbn6vE0gDUOuklVhJv1vNA,Ru4HN3stVUutQ9rUh7ro1g", + "FriendlyName": "Gunslinger Vest (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:57.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2271, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "x5M_A1sHQE-nk0Uj7ghTzA.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,2eTBbHTkf0uxy9JqA507Hw", + "FriendlyName": "Gunslinger Vest (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:16.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1759, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "el2614e47hfl8u1fca9qvtyp2.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,", + "FriendlyName": "Gunslinger Poncho", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:18:35.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1815, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5yb187ueehdy6eu34my5nep7x.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,Gep43Ix44EWybJ80XAyDbg,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Gunslinger Poncho (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:54.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1814, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6m083hwotwj7o3sa08b291vyg.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,EmTrJKm8cUy8_BHAvP8Meg,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Gunslinger Poncho (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:52.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1403059708, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,VnRSTH4HqEqADLtryqSQNQ,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Spaghetti Cowboy Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.537778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2270, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BEUiX6fKYE-xt4747ftpcA.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,T670e7nOakqKl3L6LTlTtA", + "FriendlyName": "Gunslinger Poncho (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:14.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1572, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4shtbpjixq5n883npkahv8c5v.png", + "AvatarItemDesc": "e29d044e-79fb-4280-bca9-aa1e1257e968,", + "FriendlyName": "Martial Arts Gi (Speed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e29d044e-79fb-4280-bca9-aa1e1257e968", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:38.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2203, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "-yUmlgdAekKg0XMSaP8K7g.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,", + "FriendlyName": "Sports Scarf (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:05.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2204, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bnXH-F2F6EG-wTVhwSmaaA.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,ggpNxFtZMEaHlvWXsDl9Tw", + "FriendlyName": "Sports Scarf (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:09.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2812, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KiAiCi-3AkKa90OHtKGapg.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,Jd_ahlwRskehLggn1T-GdA", + "FriendlyName": "Sports Scarf (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:02.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2109, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XMdVSPwYlEm9q62jRsWEOA.png", + "AvatarItemDesc": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,", + "FriendlyName": "Spring Bloom Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:24.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2504, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nRaGYENtIUSgiJpKpJtfdw.png", + "AvatarItemDesc": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,MtbewwIGSEu-F9Ks_Ibb4g", + "FriendlyName": "Spring Bloom Antlers (Solstice Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:53.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2915, + "AvatarItemType": 0, + "Tooltip": "First stage of growth", + "Rarity": 50, + "ThumbnailImage": "QrS8jVr1HU2v-T0h7SMZgQ.png", + "AvatarItemDesc": "c2df65e0-2e85-4666-bea1-e8efa654b155,", + "FriendlyName": "Gaia Clip (Sprout)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2df65e0-2e85-4666-bea1-e8efa654b155", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:05.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2916, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "PXVbvtMXvUuHbJItHMqazg.png", + "AvatarItemDesc": "d1de5e76-0367-42f2-be88-6f28a140e5f6,", + "FriendlyName": "Gaia Clip (Bud)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1de5e76-0367-42f2-be88-6f28a140e5f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:10.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2917, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "QbAsltVVv0qUrxJJRoYbRw.png", + "AvatarItemDesc": "4fb6e083-ae10-4c71-898a-6a55138a68a3,", + "FriendlyName": "Gaia Clip (Flower)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fb6e083-ae10-4c71-898a-6a55138a68a3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:12.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4t9vou7x5a8b92tyr0k4wzzza.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,", + "FriendlyName": "Square Glasses (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 646, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2p1yfnex9n9efclfrdg5v75cm.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Square Glasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:30.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 647, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9rp7mz69wmhupgeve23u4kbt6.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,c5884778-a87f-4612-9717-86fbb65d440f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Square Glasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:31.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 750, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ccjied0pmdne9fu4abyc5giwn.png", + "AvatarItemDesc": "kghEf_iMKUSxPO-h5iRhSQ,", + "FriendlyName": "Classic Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7f440892-8cf8-4429-b13c-efa1e6246149", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:21.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9q6v1yay56q7ckhj5we9h1ksn.png", + "AvatarItemDesc": "899f1d17-eee1-4b7d-a112-fbbba597e738,", + "FriendlyName": "Squid Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899f1d17-eee1-4b7d-a112-fbbba597e738", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-07-30T22:14:05.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2198, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "x-RovcwIAUOk4uruqtQEUg.png", + "AvatarItemDesc": "899f1d17-eee1-4b7d-a112-fbbba597e738,t8kKXvBU-kSM_MHaqZYpkQ", + "FriendlyName": "Squid Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899f1d17-eee1-4b7d-a112-fbbba597e738", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:51.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1655, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c5xy43pxfjwaaum08f6rtf6um.png", + "AvatarItemDesc": "34f45c5b-fea7-40b4-add7-bc7a37000ab5,", + "FriendlyName": "Star Captain Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34f45c5b-fea7-40b4-add7-bc7a37000ab5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:02.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2168, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PbHPcqrg4EqJp9-zB9HpRQ.png", + "AvatarItemDesc": "3ce86805-987e-4958-bcc9-57d89c4a5b1a,", + "FriendlyName": "Star Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ce86805-987e-4958-bcc9-57d89c4a5b1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:49.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2779, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Qs_cSEoU-Uy3_fGkJeIHKg.png", + "AvatarItemDesc": "3ce86805-987e-4958-bcc9-57d89c4a5b1a,PGZihr096kC4XZvHfmVMzA", + "FriendlyName": "Star Glasses (Popstar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ce86805-987e-4958-bcc9-57d89c4a5b1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:42.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2780, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bsf3ipTBlUC2I-jUSFPw9Q.png", + "AvatarItemDesc": "b4c93141-f387-4e42-9b04-12b7a97c9416,", + "FriendlyName": "Star Glasses (Popstar Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4c93141-f387-4e42-9b04-12b7a97c9416", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:44.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 784, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bts86kl55yir0z5wcudycjvob.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,", + "FriendlyName": "Steampunk Topper", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:45.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1079, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eb13jqd3eks4b9bwsooqjxlp4.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,AAbGHOOitUu9enweOXbrkw,fXnd_COSzUeznwe7P4ZYRg", + "FriendlyName": "Steampunk Topper (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:53.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1827, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3gjby0eo4e1tmv2wuecnz6rnf.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,Jy2TN1KBTEeoVehy_x32Ng,fXnd_COSzUeznwe7P4ZYRg", + "FriendlyName": "Steampunk Topper (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:17:50.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 805, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dmp4y6ycyxupuom25j3hzjnwn.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,", + "FriendlyName": "Steampunk Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:54.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1081, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ozwlaf5193fr6v3ysxikeo2e.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,fD5vtKxBWECH54Jj4FbnUg,C8gCmBZ8RUG83vvDw_UYew", + "FriendlyName": "Steampunk Vest (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:57.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1835, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8b66w4zmh5zq171rxofxcml3h.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,gy-xeYKKREaBlx9fUmuW2w,C8gCmBZ8RUG83vvDw_UYew", + "FriendlyName": "Steampunk Vest (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:35.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 803, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b1x1g3wbr7kr9cld4mtw2wn1t.png", + "AvatarItemDesc": "sOREgAGs-06CeGGsG-WIkw,", + "FriendlyName": "Steampunk Pauldron", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8044e4b0-ac01-4efb-8278-61ac1be58893", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:48.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1837, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9a9ta5xg1e227zgwpmygwy1cz.png", + "AvatarItemDesc": "sOREgAGs-06CeGGsG-WIkw,rSzG7s1ocUmx9_VrJLOcHQ,ISycihytUUyoypVb-kXN_Q", + "FriendlyName": "Steampunk Pauldron (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8044e4b0-ac01-4efb-8278-61ac1be58893", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:43.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 806, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7p4x9oq3c0f415llgnjzustxv.png", + "AvatarItemDesc": "qxovV6Vym0ufL2cTCyKjOQ,", + "FriendlyName": "Steampunk Cuff", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "572f1aab-72a5-4b9b-9f2f-67130b22a339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:55.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1833, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "en36simwv139x0vu9nj5hit04.png", + "AvatarItemDesc": "qxovV6Vym0ufL2cTCyKjOQ,64Hvmzrb3Uu9X_dCA3M3Jw,rPpYm5fIzky4vqVB26guAw", + "FriendlyName": "Steampunk Cuff (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "572f1aab-72a5-4b9b-9f2f-67130b22a339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:20.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ajxapi81nzit5fp49tbhidav5.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,", + "FriendlyName": "Steampunk Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:39.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1080, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4e35e3ndy0mrxjrfnrs3kay0.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,vuSwk04zykCe3ANKN838OA,Uhw5NBQy-EOIiCWiZFiL7g", + "FriendlyName": "Steampunk Hat (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:55.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1828, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3k0gmjgtjhawlaqbc76p3n7c4.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,ZUMIXcyOpEGiHgslbHQbwA,Uhw5NBQy-EOIiCWiZFiL7g", + "FriendlyName": "Steampunk Hat (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:17:53.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9a1u3xocxzaogyzpudo3nopsn.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,", + "FriendlyName": "Steampunk Corset", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:48.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1082, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bcv3r4hoktif6vyvq5xi14rez.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,A6VnGtpY9Ey0atf3zaLo3g,GJRG8sCM7kSGfUd9gLwpgQ", + "FriendlyName": "Steampunk Corset (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:58.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1834, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d34cbgen23yzb0kmri6koo0j4.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,rsUfUnTwbU6zrjrHWps1yA,GJRG8sCM7kSGfUd9gLwpgQ", + "FriendlyName": "Steampunk Corset (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:23.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 765, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0dl5ao69a8mzsn6jlyoei1qmd.png", + "AvatarItemDesc": "CdeM6RJmPUumXtdmdM3RXA,", + "FriendlyName": "Steampunk Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e98cd709-6612-4b3d-a65e-d76674cdd15c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:41.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1826, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bu4jy4ulmhtqc9baxhaqtabrf.png", + "AvatarItemDesc": "CdeM6RJmPUumXtdmdM3RXA,mZ2ASMrs80axStLp9g1enA,dGzmSibucEe7i1HD7lPcNQ", + "FriendlyName": "Steampunk Goggles (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e98cd709-6612-4b3d-a65e-d76674cdd15c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:49.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 804, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bq9tt6z6r54gl79e7oun3l7dw.png", + "AvatarItemDesc": "iBQ-v6xaqkipUlfzkSHEeQ,", + "FriendlyName": "Steampunk Satchel", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf3e1488-5aac-48aa-a952-57f39121c479", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:52.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1836, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f1t23z0rwzoefrj56j19gnhgr.png", + "AvatarItemDesc": "iBQ-v6xaqkipUlfzkSHEeQ,bH3dgwnwt0KEw8oCDVIcEQ,erPtGSgq_kmqfeSlZWw4VA", + "FriendlyName": "Steampunk Satchel (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf3e1488-5aac-48aa-a952-57f39121c479", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:40.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2727, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tP86FtoqYkyI7ZU9u21Ryw.png", + "AvatarItemDesc": "42f1a780-1fe1-402e-bf11-176b768e33b9,", + "FriendlyName": "Strawberry Side Purse", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42f1a780-1fe1-402e-bf11-176b768e33b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:22.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2728, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mAe4p7EG9kGOltV99JqDCw.png", + "AvatarItemDesc": "42f1a780-1fe1-402e-bf11-176b768e33b9,jzGTu3tSB0WM430AiMGAAw", + "FriendlyName": "Strawberry Side Purse (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42f1a780-1fe1-402e-bf11-176b768e33b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:25.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1571, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "89it9c90iy4bi8otctbxs4kl0.png", + "AvatarItemDesc": "25769d1f-4ecb-4d92-b692-322b38c013b5,", + "FriendlyName": "Martial Arts Gi (Strength)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25769d1f-4ecb-4d92-b692-322b38c013b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:33.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1279, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "3faagp8a1v4tthau00hha26mx.png", + "AvatarItemDesc": "b795c631-f075-4d32-ac87-c732b36cb932,", + "FriendlyName": "Light-Up Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b795c631-f075-4d32-ac87-c732b36cb932", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:21.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 23, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8vw62vvj318mjoz88zhu69qod.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,", + "FriendlyName": "Sun Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2210, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "SwnYTGNYiUG4Ua78QnfFEA.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,YS-fXtVLTkG-wl40HPBLOg", + "FriendlyName": "Sun Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:27.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2211, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FoMbe_63g0WNL9Sl7y_rJw.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,dzH_40F-MUCy_7gATqL4PA", + "FriendlyName": "Sun Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:29.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1483, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ijpwku25zvpdgkp4y12e3yyv.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,", + "FriendlyName": "Sunflower Sundress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:41.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1484, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9iwljaztbqbk7v3c1w2zryiah.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,OJYpuFw810Sg4bTw0XQf-g,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Lodge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:47.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1485, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d1rcizuvnlnebb16r91lfnd5i.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,t4gMAKp4L0WBaTGf_rtqFw,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Yacht)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:53.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1486, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4j8oqc693u6bw3exf1fz40gqg.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rGMFdPKmK02_OvIIFYlAxA,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Veranda)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:57.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1487, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "17odpv44phqhpwrrvimszr6y6.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,wxvSDMommkenIl5hTY2fdQ,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Seafoam)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:04.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1488, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8vqcr3rvn7ctiddmrvtkcq1oc.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,-sqfCQpzpUyjEShEXH9JYQ,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Sunrise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:09.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1489, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7lu5zwmfs5awwdn6oo40jy7z0.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,as9fjqDWiUi1w7yYavKZXA,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Sunset)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:13.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1490, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "96vw242yfg6iw9nr4lt97iqqg.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,sz-J7RjesE-ke3bKwumzZQ,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Meadow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:17.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1498, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5ov29z2kh47zws4vv1r89tx28.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,QlZt51mdn0WuiqKoJaIRyA,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Vineyard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:10.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1491, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4iobr7kq8wwwa2wclut3agqo3.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,6PyJEQxyd0K9TPRnvt_86Q,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Estate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:30.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2262, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "EPFpkkBDokyWw7QwCJe9Hw.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rsh2uKli90OoEnJZRFc2Ww", + "FriendlyName": "Boho Sundress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:09.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2826, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3e4iynr3546eem9i3x1apmhk4.png", + "AvatarItemDesc": "e0c4c93d-ea3e-420f-9e8f-7890b5bd4b6f,", + "FriendlyName": "Sunflower Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e0c4c93d-ea3e-420f-9e8f-7890b5bd4b6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:54.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 104, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f0xdfrv8p6p8tly3s9tdqx0kh.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,", + "FriendlyName": "Sunglasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1877, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ebmp0xvyd2y9i4tqvuzl5ns1v.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,dZM1bs6_MU2F6Vz1mH0QKg,KfM7veOVlE-dNZmsbDNZBA", + "FriendlyName": "Sunglasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:47.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1876, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "59lin822pwng10iwcthz1bb9d.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,0Ytm7G4_lkmZWPA_YfaIeQ,KfM7veOVlE-dNZmsbDNZBA", + "FriendlyName": "Sunglasses (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:45.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1468, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ainuo5affylw7gah48hs640la.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,", + "FriendlyName": "Hero Gal Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:11.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2012, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "diaifnoquzrh4x2mmkz1mf8zh.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,zw-evwOmg0q14P040EYYiA,a3I7EyRNVUilLfE6hriYtw", + "FriendlyName": "Hero Gal Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:24.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2100, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wBjDMVQgx0uxNDVdCLu4qQ.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,bhYZTPD1nEaEercLIZRucw", + "FriendlyName": "Hero Gal Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:08.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2101, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "P8mS2NTJS0e0H0VqDKvZUA.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,_C4RxcDBQE-mHh2oyL9vGw", + "FriendlyName": "Hero Gal Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:10.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1470, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cls959ibdrc0xabl1rbe41fux.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,", + "FriendlyName": "Hero Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:18.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2011, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aiplwveih9o7f5q4ok7hr4a2t.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,-K940hWJXkee1HDrlurepg,ItJCR1utoU6i-syjJpxQqw", + "FriendlyName": "Hero Crown (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:20.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ejV0XPkWEuQwpYQ8UBwQg.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,SdSjmCkq-ECoboR2PwxJfQ", + "FriendlyName": "Hero Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:15.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2104, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vXnwcxkJbE-7MComJenMwQ.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,iGhiA5wU8kG3NH6P9NZvGg", + "FriendlyName": "Hero Crown (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:16.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1467, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6lr4h9o5e2b37vr6d9nn7tkw9.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,", + "FriendlyName": "Hero Guy Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:06.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2009, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "56tgu6gfzoy6bro091y5liea7.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,JY4zXsTbX0W1sU6g1o4kuw,Y-XQzHOjIkC_-it1j9m9jA", + "FriendlyName": "Hero Guy Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:11.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "iVc5S7VHGE6_DP6V_NdRXg.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,W_AVltwf7kSzmftSsGdijw", + "FriendlyName": "Hero Guy Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:20.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2108, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bGfb7o8B7U2W6pcvZdElGw.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,trFPTiehdUm2Q3bdvBQr7Q", + "FriendlyName": "Hero Guy Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:22.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1469, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d6wng88epebl3fbw0v5p6llyl.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,", + "FriendlyName": "Hero Bracer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:15.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2010, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "duphs7n0pfrpd3ttpnqv1dzot.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,oCeStQfVkE2ZwCcQjtV1bg,r94RX2106E2TRK2wQ-u8-w", + "FriendlyName": "Hero Bracer (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:17.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2112, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dfP4cWSPL0qZYIPW3aBHmA.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,nNJ8Tkg57k6pTS_0HSLkUQ", + "FriendlyName": "Hero Guy Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:28.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2113, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "xukCmMKpjUaYrqcBdb18OA.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,VmHQJIt7HkO4INJEXCBbNA", + "FriendlyName": "Hero Bracer (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:30.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ffddbd79-0158-4f93-b8ec-dd5573c035a7,", + "FriendlyName": "Surfer Puka Shell Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffddbd79-0158-4f93-b8ec-dd5573c035a7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1672191014 + }, + { + "AvatarItemDesc": "5b2d0a6c-238d-43a7-a20b-bddcf14315f1,", + "FriendlyName": "Surfer Skimboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b2d0a6c-238d-43a7-a20b-bddcf14315f1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1582373149 + }, + { + "AvatarItemId": 730, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cg6s53psdqilkxbzgkx72gri3.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,", + "FriendlyName": "Poindexter Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-11T22:30:21.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1647, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8c3gd19ccfwts3cqzv6pojvyj.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,lq59KZwElEStrviDEQLLcA,olyS074RHU-ojE0GzBBYJw", + "FriendlyName": "Poindexter Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:24.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2214, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gYtC-UGMRkC9gKrg3689Fw.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,PSpysyn5gEKXCEi7f6gDoQ", + "FriendlyName": "Suspender Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:35.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2888, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IMgfGlThV0q8ilEF8MG5ew.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,", + "FriendlyName": "Blue Sweater Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:10.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2890, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6Ry5ZkLGke8F2jv7aYoxw.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,LVuUiGBtn0mp4Wfs-2F5EQ", + "FriendlyName": "Sweater Scarf (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:20.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XrXf6JrQaU6tG5MV94TFSQ.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,R8VYB2EGPkmQX4dbmGLuRA", + "FriendlyName": "Sweater Scarf (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:27.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2889, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HGZ5flmXY0Ow5pQH-q3L-A.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,wCCq14_wc06hkmjg6TTFxw", + "FriendlyName": "Sweater Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:17.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2891, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MsgTxG94f0-timzwekDXSg.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,0JVWqxXYoUqgI-5ORIr0bA", + "FriendlyName": "Purple Sweater Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:24.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2756, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SWIYABA3u0KSKjMg4tbN1w.png", + "AvatarItemDesc": "97c8e2f3-e786-42e5-8852-4abec5dc645d,", + "FriendlyName": "Sweet Tooth Hat (Milk Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97c8e2f3-e786-42e5-8852-4abec5dc645d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:39.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2757, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WNWMQVjKaEC9CXqo_esiGA.png", + "AvatarItemDesc": "97c8e2f3-e786-42e5-8852-4abec5dc645d,_8OmgC7c80CsvQN41U6XyA", + "FriendlyName": "Sweet Tooth Hat (White Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97c8e2f3-e786-42e5-8852-4abec5dc645d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:41.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2758, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tGRQjD2mEKhf-jbbGpp7w.png", + "AvatarItemDesc": "39854685-4793-439b-9c64-c98825456a03,", + "FriendlyName": "Sweet Tooth Shirt (Milk Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39854685-4793-439b-9c64-c98825456a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:44.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2759, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NRd9D3di1kKciSpywTnJwA.png", + "AvatarItemDesc": "39854685-4793-439b-9c64-c98825456a03,qVeFYveZQEmobBTEMg1RTw", + "FriendlyName": "Sweet Tooth Shirt (White Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39854685-4793-439b-9c64-c98825456a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2583, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "wy-_Y3jSdUC9bKLa63iIpA.png", + "AvatarItemDesc": "a928c6dc-442f-4cbb-b705-cc5a3969b34a,", + "FriendlyName": "Teela Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a928c6dc-442f-4cbb-b705-cc5a3969b34a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:47.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2584, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yCbSGCAoBUyP8osA23LjDw.png", + "AvatarItemDesc": "f159e878-7563-46db-b59d-c838612a0a78,", + "FriendlyName": "Teela Tiara", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f159e878-7563-46db-b59d-c838612a0a78", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:49.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2585, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HwOW8GYsaESO0xHdf6rxQQ.png", + "AvatarItemDesc": "38e7e182-5166-42d9-9ed3-b525b98579c1,", + "FriendlyName": "Teela Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38e7e182-5166-42d9-9ed3-b525b98579c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:51.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 366, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c6y5y8wwcc2dcp81jcdqvac5r.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,", + "FriendlyName": "Tennis Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:06.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 441, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "22s0jdhj08deg2hm1agcg0ap2.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,304e5cf1-5c94-481f-8e49-e6a943e6b300,90e0da8b-1941-4ca8-b7e5-b4e31921873c", + "FriendlyName": "Tennis Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:11.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1370, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1eu2yq3p1x77hmixyzmubdux6.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,NpMkoIK62k63QoLVEXId6g,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:15.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1371, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "duxjnjbu2n1zbarvejwuhhzgn.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,PLRhiOhhb0KhatFH7f3pwA,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:21.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1372, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8zfsf3e3asjbwc8dz1kq9acae.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,RwgnKTfMEkWrAp_OMGy__Q,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:26.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4g834pzncxe2djcfe8ri5ow6t.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,", + "FriendlyName": "Thrilling Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:46.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1529, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6iumwjj0hu0bnlky89lc0xdjd.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,7-lAnMa7M0u0n0nmeIINgw,6GfSjqIb4ECCvIPC8vO8_A", + "FriendlyName": "Thrilling Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:25.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2318, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "inGiSDO8JESERdMjtdk9-g.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,8HR0DwDl2ECtHf5aEkqSHQ", + "FriendlyName": "Thriller Suit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:33.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 821, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5zwxtidy5x8bl647kq837917l.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,", + "FriendlyName": "Thrilling Cuff (Red) - Available again until 10/15", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:54.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1530, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5tmz1m7roox1cl2u7cf3bgrnc.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,Q1knTl-n-EeAUwotFspNew,FHB5XHwNWka7aUt5km3LNg", + "FriendlyName": "Thrilling Cuff (Blue) - Available again until 10/15", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:29.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2327, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "FqT9BAyTM0qDBFE5vkveUA.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,B1IotYmwbkiUAfdZiqzq8w", + "FriendlyName": "Thriller Wrists (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:54.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2293, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "O2g6r5QKqk2REyXvLqLyeQ.png", + "AvatarItemDesc": "7b568bd0-ce59-456f-af69-537364a52f69,", + "FriendlyName": "Lightning Hero Suit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b568bd0-ce59-456f-af69-537364a52f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:40.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2391, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5MjNgKFX1EuMIDbmbkqxZg.png", + "AvatarItemDesc": "7b568bd0-ce59-456f-af69-537364a52f69,_fabGqrxrkW2mfx0bejHsA", + "FriendlyName": "Lightning Hero Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b568bd0-ce59-456f-af69-537364a52f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:42.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2294, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ww6XZOkMQEGPfHNxwJ9pbg.png", + "AvatarItemDesc": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,", + "FriendlyName": "Lightning Hero Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:43.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2392, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kk6TuKH2gEeN-PCQlSGzGA.png", + "AvatarItemDesc": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,46WaeyRZnUCuLkEpxECRMg", + "FriendlyName": "Lightning Hero Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:45.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2295, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2RiLsCbQ0Uubmw-2hVgnPg.png", + "AvatarItemDesc": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,", + "FriendlyName": "Lightning Hero Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:47.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2393, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "JLbQE-oojUqVUNpFNs_Cxw.png", + "AvatarItemDesc": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,8NY4MevLp0C2lP178fIZ9w", + "FriendlyName": "Lightning Hero Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:48.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 133, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "66fwuamhzcs860kylwr1qfv8m.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,", + "FriendlyName": "Tied Scarf (Striped, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 462, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bcknoanm16wf9xuuxd8n236ez.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:27.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 463, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6qg6j1yialrvz8slxa9jeyu87.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:28.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 464, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ehbtcde8j9yfepf412zdnzsqs.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:29.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 465, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "15m86aopz28g8iezgelpzwej3.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:30.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 466, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9dfutygcqtdm7xo7lv2j8y5fy.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:31.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 467, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "a6a5lj41oooaly6njbbnnox5o.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:32.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1267, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f561z4j5s60ih6h3mp3me6ath.png", + "AvatarItemDesc": "05ec4e87-088f-4281-a118-2e63c2585200,", + "FriendlyName": "Business Tie (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ec4e87-088f-4281-a118-2e63c2585200", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:43.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cm7rzgd535abr6dvbm4wfb061.png", + "AvatarItemDesc": "05ec4e87-088f-4281-a118-2e63c2585200,Wm30oV5BtEuFQZFk3dPK_w,tfiSfpRez0y_R4QjAhO0bQ", + "FriendlyName": "Business Tie (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ec4e87-088f-4281-a118-2e63c2585200", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:46.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1265, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "76fk7csv3rzv0frnesf1d575f.png", + "AvatarItemDesc": "0d4e7ebe-efd9-4e68-9900-5645916e7596,", + "FriendlyName": "Business Tie (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d4e7ebe-efd9-4e68-9900-5645916e7596", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:35.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2781, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yn-2YY0OvU-VMl9fBtdbuA.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,", + "FriendlyName": "Tiny Backpack (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:45.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2786, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TQGXsPW7Hki2VKZiU0b-HA.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,AtF5be-huUeADedbRurRcQ", + "FriendlyName": "Tiny Backpack (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:58.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VzzHEVAKM0ChjhmxaHnhow.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,T9XdU5xAFEqX3QV__SjXYA", + "FriendlyName": "Tiny Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:55.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2783, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e-QC6uq5zECcdfkIRxtpiw.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,cxWuO4qBtUOKYmCXI0D5Jg", + "FriendlyName": "Green Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:50.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2782, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9xjIqLGxDkOZsXIexIon3w.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,FIZdMEUgykOHb7fVlOfa5g", + "FriendlyName": "Black Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:48.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2787, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lfkspyvSk0CIkZx0gXhMog.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,5GEhQr7Iy0-kB0yCA3uJ6g", + "FriendlyName": "Tiny Backpack (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:14:00.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2784, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "y91-mA5ypkiHUOTxxjG6iQ.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,wrobkXV-10CD1JJtgyy6Fw", + "FriendlyName": "White Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:53.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,8e5280a8-7771-4278-b985-c325f2490f9d", + "FriendlyName": "Tiny Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1327578016 + }, + { + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,545ed5e0-beab-46d8-9d14-7de0420ff90d", + "FriendlyName": "Tiny Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1500824834 + }, + { + "AvatarItemId": 1504, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "49614bvc1m6rzznnvix44dnbb.png", + "AvatarItemDesc": "00f0b914-aa22-4d14-9af1-996e1af61f4d,", + "FriendlyName": "Tiny Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "00f0b914-aa22-4d14-9af1-996e1af61f4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:46.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2068, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "higzQQvRGEa6dDK9f0TTvg.png", + "AvatarItemDesc": "591af332-a01c-454d-89dd-5ed6ae3ed09c,", + "FriendlyName": "Lucky Clover Top Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "591af332-a01c-454d-89dd-5ed6ae3ed09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:25.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1897, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "56r92qv64w04tvm8920bhyjlv.png", + "AvatarItemDesc": "2f77696c-618c-4fb8-9220-33839cd3bfcd,", + "FriendlyName": "Tiny Top Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f77696c-618c-4fb8-9220-33839cd3bfcd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:30.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2730, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "K_CvxmWWCkugsiOwB8nZAQ.png", + "AvatarItemDesc": "0bd750d9-82ff-4d77-9f2e-7e2651316f51,", + "FriendlyName": "bwahhh", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bd750d9-82ff-4d77-9f2e-7e2651316f51", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:30.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2729, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "UNu6rZ-7CU-S5MeZzybZKA.png", + "AvatarItemDesc": "74902dee-08a5-4a8b-a113-80f450a0de0a,", + "FriendlyName": "Rec Rocks Shades (Dark)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74902dee-08a5-4a8b-a113-80f450a0de0a", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:27.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2731, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EwmmD_7_NUeiFXLFKImuQw.png", + "AvatarItemDesc": "b0a2e988-9942-4c52-a714-f1bc12fa6290,", + "FriendlyName": "Rec Rocks Shades (Tokyo Machine)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0a2e988-9942-4c52-a714-f1bc12fa6290", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:33.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 72, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7beqslha3807spvktpk1rat13.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,", + "FriendlyName": "Top Hat (Black, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 645, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "d8wvm262ivsmterpwyjcf0d3i.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,bd4a84e2-b67a-4269-a26a-17fb23ddb09e,ccf1ccc1-e229-4157-bb74-f2cdef01e547", + "FriendlyName": "Top Hat (Black, SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:28.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 915, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e8kcw6vyqqzb5temhwr493mtf.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,DzgN67HoikexE2rTlZLdYQ,vm1ESK9ls0SaxSwHplNALQ", + "FriendlyName": "Top Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:49.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1732, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3nihpsb8d8hrh8ovb5pzd5f1a.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,xeK_acj2hE-sYGhsMRwjhw,HwhHShudGkGjXagJxCN2uA", + "FriendlyName": "Top Hat (Patchwork)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:16:15.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1801, + "AvatarItemType": 0, + "Tooltip": "For earning $10,000", + "Rarity": 50, + "ThumbnailImage": "5948oo28re9hcl2wswbr309wb.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,Em__GeYWaU2pAEqWYCBKpA,DG83ruIdI0Wu-GSRg4icYQ", + "FriendlyName": "Top Hat (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:46.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2482, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zcove5uhGUSvwxXtkvlWow.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,n8t0gIYFbUaPMhyQeWkPSQ", + "FriendlyName": "Top Hat (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:32.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1374, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zss1xn4q3bwumq65zgy4xq5g.png", + "AvatarItemDesc": "7dc6c916-b887-4929-93b5-dcea3022e6a9,", + "FriendlyName": "Top Hat (Wonderland Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7dc6c916-b887-4929-93b5-dcea3022e6a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:11:18.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1356, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h9wa7v6ma9wzhwdgvumqlyp3.png", + "AvatarItemDesc": "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03,", + "FriendlyName": "Top Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:48.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 972, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ki0gazw736edmje4kpkv8bvb.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,", + "FriendlyName": "Traveler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:40.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1430, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "31sglupqv6cqh6rzb8embhfwz.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,Mt2a9ygWPkS-DyXj1q-Kvw,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:41.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1431, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5eujgf1g27oi4kl20v8si25ry.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,LEUQnDz-k0m-hY-AX3Z9Sg,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:42.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1432, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1dcjmvlujpg9zj2zccxxccpcw.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,cSFt9MWLj06CDzIzATqrTg,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:44.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1433, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9ozq264dxzs2juj87d64edd1i.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,B6qNcZQ770eJel13RskuIg,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:46.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1434, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6qw0otr02xea0hnbor3sp5kas.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,p3g8YAOhTUKAiE-Wh-X-aA,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:47.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1435, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5seeahrp9r3hyie3y148dclsb.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,SqHAYcqBXU2PtF2urWGruQ,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:49.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1731, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "can59da4g2r699415y5w5vr03.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,Pu_1I6Zq-0G1HBMHa5MrKg,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Tourist Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:57.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1730, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6v72ng5qy6zmim5nx1i6k7hy2.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,7Rw_gt9IUEOYsvVcOTbVBw,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Tourist Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:55.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1729, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e25fr88d2412i6acn3s2e2khj.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,aiBfwCNwpUyJ0rYKCMa7qw,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Traveler Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:51.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 974, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eati57vuo2tgumb24q5t0je5y.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,", + "FriendlyName": "Traveler Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:53.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1424, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0x7hflju5oo8crqrrj3r0b5so.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,otQaNOsODUSTfHlCX0IVOA,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:46.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1425, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bp1h0205pg13g8p8aj2j9vhdi.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,NKf5-r2S_0iP0SaO9bjvaw,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:48.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1426, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chd2q6f8set0nr8jh2vr2m7qd.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,adJVA26PyUSVvmZgxkCAmQ,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:50.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1427, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "56zylqig2v1wahn347cxlipyg.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,4ijeE3LqUUuSplKrMto6Kg,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:52.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1428, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9896kxku8xyotg3kpo30sep01.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,d27e5m1BLUWI42Pxnj6F4A,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:54.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1429, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5a104x4ogdqr0eqzaqirydl55.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,RZFWcTzxPkOfUuTIQ1mzkg,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:56.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1725, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3gp6gte16x8khurqqu7nzhfhd.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,XT_txjZfIUOT400UPHLaRA,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Tourist Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:05.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1724, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a744qz6lz4fslmbrp602yan1l.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,iEvS6I9LjkSaGAmjcxz8uQ,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Tourist Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:02.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1723, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "87fumlw57etytmbtflbzgbrxm.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,gwdtq3NiEEmMKYtn1VPoiw,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Traveler Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:57.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202549920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_a0Yxn41bUmWYogStG-J6w.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,6taO0KRM_kuKoaTnOIsHRg", + "FriendlyName": "Raindrop Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.543783", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,7WLoulmRgEKX3Eshtn_cxw", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1718528065 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,BzzPuw8i0km4VQLd77mXEg", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1462034470 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,er8BswhSkUqh5TVV5fDAng", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1696359360 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,zUZHddmmhE2rXfCVuz_GGg", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1381816991 + }, + { + "AvatarItemId": 976, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "35n8yj17afa5u5gc5w40oz7de.png", + "AvatarItemDesc": "cbN5e-t_jEKa4hzir5JAxQ,", + "FriendlyName": "Traveler Shades", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b79b371-7feb-428c-9ae2-1ce2af9240c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T19:10:59.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2154, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BO1szcANSECxRSF0upetrA.png", + "AvatarItemDesc": "59bbb9d4-4fdb-4400-90d5-244643dca4ce,", + "FriendlyName": "Toy Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59bbb9d4-4fdb-4400-90d5-244643dca4ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:23.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2485, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "9aC9wW3rVkKmZlMNJSwZkg.png", + "AvatarItemDesc": "d834be2b-b107-49aa-b0ec-b54bdb4c61aa,", + "FriendlyName": "Science Iso Helmet (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d834be2b-b107-49aa-b0ec-b54bdb4c61aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:40.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2004, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70dggr2u22pi1bpl844m6ozue.png", + "AvatarItemDesc": "2b7a32da-eda1-45e1-b28a-f9f882cda7fa,", + "FriendlyName": "Toy Jetpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b7a32da-eda1-45e1-b28a-f9f882cda7fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-12T22:10:14.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 251, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5gm9mbr01cwr3bmzl4nsv5ko7.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,", + "FriendlyName": "Tracksuit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 501, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "essmk8w84jurmnzu791bfueh2.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,026ca50e-3a81-43cf-87f5-950687b7bbdd,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:25.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 502, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bvsa18aen05ajtkzw2h9y672b.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,6584dada-499e-4d7c-958d-466955b20640,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:26.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1648, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8v9x6a68s45783jrnnqykohcu.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,uUOO-k5KS0mU7B5BLaOpnA,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:41.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1649, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "222uif2yybv04a5a1ialb8rze.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,Ijf-j7EgEkGYOKo9SQNLRw,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:44.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2182, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SPNmVkKb1UWv0spykc088w.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,J7l1YwWjqkutybwykwih7w", + "FriendlyName": "Tracksuit (Squid Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:41.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 763, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1265j9ozr0rlgadvfmpq5yyzv.png", + "AvatarItemDesc": "-SQLtL4rdEWTJdw2IroQCQ,", + "FriendlyName": "Treasure Hunter Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b40b24f9-2bbe-4574-9325-dc3622ba1009", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-23T00:08:02.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1704, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4nzqsmjss5f6hoabn1ihimj3.png", + "AvatarItemDesc": "-SQLtL4rdEWTJdw2IroQCQ,CoSVCyrI-0qg-EkqB2R0hg,JO9lJOQSREWbPyDBZH_cyg", + "FriendlyName": "Treasure Hunter Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b40b24f9-2bbe-4574-9325-dc3622ba1009", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:20.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 779, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5jvdbqiv7unk1h9fed6w0xzm1.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,", + "FriendlyName": "Treasure Hunter Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:04.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1698, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4mi4ucsloz64q4qidq36cfyf8.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,H0sqS2_VPEieVrHSyMNo7g,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:47.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "05ockzuu7hohv73owaltasco3.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,TjeZwaUp10uhM72q8j4R0A,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:49.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1700, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6gg5wzc84qgzuedb40n52oe0i.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,ZlO_DqkSKUStV4-gGPvQOg,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:51.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 773, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5h0gufhuvlgih1xqqy2oje03x.png", + "AvatarItemDesc": "t_66r_j5oUSh9awIPmdUKg,", + "FriendlyName": "Treasure Hunter Bandolier (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afbafeb7-f9f8-44a1-a1f5-ac083e67542a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:56.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1705, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0s4l4l3cvdmfvz1eza6ts5dia.png", + "AvatarItemDesc": "t_66r_j5oUSh9awIPmdUKg,YTlnBuEjhUG1hSfAs8qwuw,IcR9Z_MxKkS9uh4uFYuFzQ", + "FriendlyName": "Treasure Hunter Bandolier (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afbafeb7-f9f8-44a1-a1f5-ac083e67542a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:26.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 774, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5vu32ph9qbrum8aq8hkvaa7d1.png", + "AvatarItemDesc": "ImwG0snB9ECc_gqRRBoz6A,", + "FriendlyName": "Treasure Hunter Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2066c22-c1c9-40f4-9cfe-0a91441a33e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:57.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1706, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7p5hywqgv256aipqmufecvfc4.png", + "AvatarItemDesc": "ImwG0snB9ECc_gqRRBoz6A,lAjUeUtTLU2-3bG2OXBukg,LFB_YqTym0-tQmUIj-YcYA", + "FriendlyName": "Treasure Hunter Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2066c22-c1c9-40f4-9cfe-0a91441a33e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:30.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 780, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d6no5z0xu0alfa3sfd7tchss3.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,", + "FriendlyName": "Treasure Hunter Tank Top (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:05.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1701, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2squicogn0nbezwccj6ahi6we.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,3qu7mxSNpkKRrYMhsWHVGg,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:52.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1702, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5fzlk5daghmydxowkt8y6x3ba.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,Ij8MXrl1Okip4M7y-gF23Q,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:54.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1703, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ets8ha47bzly0bvux6s0lw1t7.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,qEojxjSXm06CX52hRDJAew,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:56.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ffdda4d3-a768-4335-a883-aa3f9dc897bf,", + "FriendlyName": "Trihawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffdda4d3-a768-4335-a883-aa3f9dc897bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1335410714 + }, + { + "AvatarItemId": 982, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bfcff0ql8rwt35vnexuh55sc1.png", + "AvatarItemDesc": "ReqOfohe6UamzaiHG1x3Ow,", + "FriendlyName": "Trucker Hat (Pickup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e8eea45-5e88-46e9-a6cd-a8871b5c773b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:18.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1365, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4dquic4wqv5qwn7664v0i7fpi.png", + "AvatarItemDesc": "ReqOfohe6UamzaiHG1x3Ow,lDXambcP60CeY2C8qeGDFw,uiAwVt3HK0mRVtWp9B-gTg", + "FriendlyName": "Trucker Hat (Flatbed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e8eea45-5e88-46e9-a6cd-a8871b5c773b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:31:41.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1206, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8b8w29yppr8ire3rmnz7dihi2.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,", + "FriendlyName": "Trucker Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:09.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1207, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xzrzv7jtrqilh8yio1uf4b27.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:13.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1208, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c5h60lx2j7phvk8hmvih50s2o.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:16.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1209, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "71nsut92sx3eet8hzyon7q6mt.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,o2WfRyjJy0CKoUg8YIxNjQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:20.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1210, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3ko6st266abg6sg11o1nsxdxj.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:23.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1473, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a0lqwqfuolj7hkvhf1glhqc15.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,sscD2O3LEUOAKkqjGpGe8g,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:33.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1474, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6v3w4z6b6gofs8vtz0up7olo8.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,hRxyLsKfsESFAsXAyZQjoA,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:37.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1475, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1c2vx4eqcpf3ulizhf8xymy5k.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,kgCKjtSjFEmFq9ez2llxKQ,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:41.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1476, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "22a6bzdqbg2mnctnuw24oqszc.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,KaZqNx5VKkarRrOJcTuD9A,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:45.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1722, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3oy21va74rnfmipzz83x24n3q.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,eOJkmpSL4EypK-AQz7j8gQ,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:48.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1211, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "evjppeeirfwnfcqr1gdexa1og.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,", + "FriendlyName": "Trucker Cuff (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:28.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1212, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9z0jwf8oh2kcafax37xcxp68x.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,yA0KhVg19kWqG1UkyxQogQ,yBNsYcZXzEiuR0u3tWnnWQ", + "FriendlyName": "Trucker Cuff (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:32.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1213, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3d4u0uq00scnglmtqdwyvp55u.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,P9bBSL0YLEyjN8PxMfgbKQ,yBNsYcZXzEiuR0u3tWnnWQ", + "FriendlyName": "Trucker Cuff (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:37.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1214, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "emod4iboh8jp8hcuu07ylugks.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,o2WfRyjJy0CKoUg8YIxNjQ,sHyNs-dpc0-_Ah-HJVczcw", + "FriendlyName": "Trucker Cuff (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:40.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1215, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4hs50cqx2o2w24tay05gzsxem.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,bgRKH1wfqkWVHcuSnFYnlQ,sHyNs-dpc0-_Ah-HJVczcw", + "FriendlyName": "Trucker Cuff (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:44.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1347, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bqm1574wf7rpdwjlok49b0p77.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,", + "FriendlyName": "Mechanic Overalls (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:45.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1348, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ka8fpkswhsrqk4yzqu9zq6hx.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,iokdlBfjuEuAwqIMOQwxdA,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:51.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1349, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dvw88qyim2kmbtb852qiitri8.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,nKauYAyWhkm2AMHFyC-fLg,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:57.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1350, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8l97dx6qhjdo9t7s2dpksymii.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,EiGgN7uVukqoRE7UJcMLTw,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:51:02.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2021, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "deVNr6Y5KU6p3WI9O9RCpw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,", + "FriendlyName": "Tucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-26T17:42:24.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2022, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yD9WlEoR7Uuxoz4pegMXnw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,iIjl6PamKECL_JWvrZNHfg,2IFVtBYV8EqQXs8XHqeqdg", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee Shirt Latinity Mask Tucked Tee Shirt Latinity Swatch", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-26T17:42:27.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2049, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ubK_rEh-IEm7I_Dm8X_6hg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,vtXUyu-xmkaYtOCL9r3PsA,VtepaI2fvUWhcvNmpQ8r6Q", + "FriendlyName": "Class of 2021 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:37.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2069, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fH7sFkQfakOQNGz1LNIeDQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,r4O0JuB0dke1IoOEqW4nkg", + "FriendlyName": "Art Festival 2022 Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:27.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2070, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HafOMtY0B0eYptZba4mZ2A.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,UJRSCFfPq0K0yV0ETS3uSQ", + "FriendlyName": "Art Festival 2022 Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:29.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2157, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3JZYjcH1rEilhQIrqVialw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,3bWtQstEmUeHJXngec632A", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee 2 D Contest Gold Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-30T20:55:50.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2156, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "tgdvdzDpgkOt5c2sU-g6og.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,NEU8mYk_TkqS78l0zY9GAQ", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee 2 D Contest Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-30T20:55:48.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2096, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5xSIXFJJUEm8gEi413DBeQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,K4_AIRWX-U-f-nXsBOc82g", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee Shirt 8 Bit Logo Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-09T20:24:32.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2250, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eAx_UfHqTEmu_aRnqNXzGA.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,9lPZEIVbbUK1hdR1aVPLwA", + "FriendlyName": "Cowboy Boots Shirts", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:09.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ffb9sMGT0EqSNh85meeoJQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,W_RwnySBMk2GlrknM7NDrg", + "FriendlyName": "Class of 2022 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:05.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1489631510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B0Y_RojdNUWlUY10xCmzCg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,LXR5H_-7JUO7F3JkJh3ouQ", + "FriendlyName": "Strawberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.545778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1413305249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_TRYe6zmekyTjU6_Sa9d5g.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,mKWQMgBuTEq9EwXBTusuJw", + "FriendlyName": "Tucked Gamer Tee (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1176199818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qHJWmNTPqUag0af__vphuQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,N2ofIXeSBECHTBAoP2yQ6w", + "FriendlyName": "Grape Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1198761907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LmTBToLyKUyYe-lIJ4jKiQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,rg6sMjE1XkijY4Pq73pjcg", + "FriendlyName": "Lime Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1755749293, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DTvdUNjKGkmhKbNFYXzdkg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,t9-7AjmLgEWawcyWCD4xVw", + "FriendlyName": "Raspberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1511681798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qEEw3iKEAkmL_Kmj7syfCw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,5Q9E76b580CnMWIg6zbPwA", + "FriendlyName": "Blueberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1234, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "3qareqet5g5gwgynxe4tw9gub.png", + "AvatarItemDesc": "9b5406c4-8589-47e4-9f69-4fa4733615e7,", + "FriendlyName": "Turkey Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5406c4-8589-47e4-9f69-4fa4733615e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:19.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2486, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "mt3RP76mEkGxwfRV3p8aoQ.png", + "AvatarItemDesc": "9b5406c4-8589-47e4-9f69-4fa4733615e7,yXDNXmcJvUC4eCx9oGItcA", + "FriendlyName": "Turkey Hat (Snowy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5406c4-8589-47e4-9f69-4fa4733615e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:01.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2531, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "PcxjLeS1vEKxB0-e2aPilg.png", + "AvatarItemDesc": "5da97d5c-0d1b-4b0d-a2be-d255871d833d,", + "FriendlyName": "Turkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5da97d5c-0d1b-4b0d-a2be-d255871d833d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:04.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1570, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "4g2r02n1g5w09re7hl1ba2yyi.png", + "AvatarItemDesc": "002a0f2f-1a24-4439-b578-470818ef8325,", + "FriendlyName": "Turkey Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "002a0f2f-1a24-4439-b578-470818ef8325", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:27.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2491, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "NjxKDOlshkq09A4VSq5gAQ.png", + "AvatarItemDesc": "002a0f2f-1a24-4439-b578-470818ef8325,d-scjeOeCESC0rQMKsozsA", + "FriendlyName": "White Turkey Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "002a0f2f-1a24-4439-b578-470818ef8325", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:14.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b5a83be1-fd78-4797-b3e4-e5fa9730fab8,", + "FriendlyName": "Turntable Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b5a83be1-fd78-4797-b3e4-e5fa9730fab8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1787211725 + }, + { + "AvatarItemId": 1193, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cccfr6msg1shob4s8fngcwqa6.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,", + "FriendlyName": "Turtleneck (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:53.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1194, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "18z0oxaf9r5hu5604hnfsb5b3.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,9b9t8_aKH0-gQXMhZycaOA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:58.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1195, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aafnbk70rbkwsh5xsq4e8fk2j.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Zz4XatKfUEapReRAQHxKKA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:16.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1196, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "biyvmvzdwez09j9045jip174p.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Jt0QqNrAp0qzUR_RQoivVg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:20.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1197, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8o2jy3y3nfrfunsiqadtg7716.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,lBePHNL9LkyaAjEFyFE8Sw,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:23.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1198, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4aqh17g64pqv044ldezph3z37.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G4HIPUjekUigiU0-Kr9mdA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:27.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1540, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9d1oi3isvlv9xgg27pjqx7xbv.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,5mcp7-2ZEE6n3-zN3_CoWw,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:26.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1541, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8hdi7uyevqrhqw4zfu5knwqb5.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,R0UCacpdhUS906Xf7l142A,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:30.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1542, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ackem1gqqmknsbpbqr1r0opsk.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G-VKgepBkkeATcwCaS6Rwg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:33.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1543, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7nm5wdsxclddyf0y5up03zpg0.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,1LonkCbt4kykeAScYKpwyg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:36.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1650, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7sv7ea1zgexf9hov8dp1ky7im.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,IXWd396grEOWHt32L7fCpQ,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:12.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1087, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7swwjtbh6ko8afp6d0nmwspsn.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,", + "FriendlyName": "Tutorial Cap (Bachelor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:20:17.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1127, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b0ck22y2gp6w3ppasjughn453.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,gDH1WmW5rk2RdY4wJYYZow,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Tutorial Cap (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:58.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1126, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5y6bqiyhj3hhqy92tqzvjxo6l.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,wHJnbDOo8ESmUbQDr0k31w,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Tutorial Cap (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:46.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1247, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b9enslfg9k5mul9t9zukxjibd.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,GK3yolCNlEm3B_5DtHkE8w,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:15.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1248, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "amll8glk5rg14851jcmr390x4.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,U2ZbHn8WJEySUXVpzAY6ig,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 1)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:17.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1c3y650gb6irf7rhmbs9cf457.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,L333w3dfIEWDbtt1Yn1gBg,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:20.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1099, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "37ulwbdd8vskdogo0hv1kascn.png", + "AvatarItemDesc": "ASHnWS0h9kOJhbOUwxWKFQ,", + "FriendlyName": "Tutorial Gown (Bachelor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59e72101-212d-43f6-8985-b394c3158a15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:25.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1256, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wia57rfapa2pl00ngn2vtv2t.png", + "AvatarItemDesc": "ASHnWS0h9kOJhbOUwxWKFQ,U2ZbHn8WJEySUXVpzAY6ig,6nmHu9zj8k-tEz0tBHblnQ", + "FriendlyName": "Teacher's Gown (Level 1)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59e72101-212d-43f6-8985-b394c3158a15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:54.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1098, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7x241yml5e8ojjul82gsjcu8c.png", + "AvatarItemDesc": "MJQiQ3LATEOymxCC8wfSjw,", + "FriendlyName": "Tutorial Gown (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43229430-c072-434c-b29b-1082f307d28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:23.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1257, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ac5q83c1bed0a7fr6fqc9zz7e.png", + "AvatarItemDesc": "MJQiQ3LATEOymxCC8wfSjw,L333w3dfIEWDbtt1Yn1gBg,lSDrKR69rkSFULy9GA__1w", + "FriendlyName": "Teacher's Gown (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43229430-c072-434c-b29b-1082f307d28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:58.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1097, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bogc32ezqq4e260fqnwn6hunt.png", + "AvatarItemDesc": "0yFzgVESokC3gFd3hrS8bg,", + "FriendlyName": "Tutorial Gown (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "817321d3-1251-40a2-b780-577786b4bc6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:21.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1258, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bsih24pcifs39vkgb3nj2y01r.png", + "AvatarItemDesc": "0yFzgVESokC3gFd3hrS8bg,GK3yolCNlEm3B_5DtHkE8w,yKb2A8M_m0GQDhFWCRUHTQ", + "FriendlyName": "Teacher's Gown (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "817321d3-1251-40a2-b780-577786b4bc6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:04.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1118, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5frgjfs2olhxlc6p6k1xzi1gc.png", + "AvatarItemDesc": "vP9EKTMXP0yni8U7uCz2-g,", + "FriendlyName": "Tutorial Sash (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2944ffbc-1733-4c3f-a78b-c53bb82cf6fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:43.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1252, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3sux0cg60z82am4w6irvfph44.png", + "AvatarItemDesc": "vP9EKTMXP0yni8U7uCz2-g,FsATqzRVekO9hQ3u_q7keg,Q7hqvEo7DUyD9AqIOunWWA", + "FriendlyName": "Teacher's Sash (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2944ffbc-1733-4c3f-a78b-c53bb82cf6fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:31.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1117, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f20t6rhmbvzsu0byj0ag8itk7.png", + "AvatarItemDesc": "O8VqC11xNkSKpDh6Omp_0w,", + "FriendlyName": "Tutorial Sash (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b6ac53b-715d-4436-8aa4-387a3a6a7fd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:40.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1253, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9y51i5ld8baokfpueqd67oc07.png", + "AvatarItemDesc": "O8VqC11xNkSKpDh6Omp_0w,mvj5RDTuh0qoql0OyBFI9g,YQQamy-tQkCkpZPXKepUrQ", + "FriendlyName": "Teacher's Sash (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b6ac53b-715d-4436-8aa4-387a3a6a7fd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:37.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2084, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rZx9UCnz6kWaUo6z050exQ.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,", + "FriendlyName": "Tutu (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1738760043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ts-p4lQTo0qU3LNY7YIW3g.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,UjSxxUK7z0GGdjvI_NsJ3Q", + "FriendlyName": "Butterfly Tutu (Monarch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1968011120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ItXnzo230qoec69LEGbBg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,lBkOSQNDZUOvoknntnEMew", + "FriendlyName": "Butterfly Tutu (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1920210778, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NpnrKR1oZke3h0i8odmHGg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,sq5Qv4nVAUqzhMdB7U2MVQ", + "FriendlyName": "Purple Butterfly Tutu", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1044129999, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kafLNepzrUqLnxh9kG5Nxg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,6fp7nDdMvU6Ig-EmvjMpIA", + "FriendlyName": "Butterfly Tutu (Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 858, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b1blz138qua11xtiprlgui282.png", + "AvatarItemDesc": "jUpFzO5MPEGTbS-E0a99Nw,", + "FriendlyName": "Dracula Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc454a8d-4cee-413c-936d-2f84d1af7d37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-14T01:26:26.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2610, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Wir5kzMxjkaVf8C17Tg5wQ.png", + "AvatarItemDesc": "a994fc19-974e-444e-ae9e-c2f6c32432bb,", + "FriendlyName": "Midnight Gala Tuxedo Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a994fc19-974e-444e-ae9e-c2f6c32432bb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:21.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "246e6caa-c94c-420d-9805-9f0282c6d052,", + "FriendlyName": "UFO Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "246e6caa-c94c-420d-9805-9f0282c6d052", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1580435119 + }, + { + "AvatarItemId": 2918, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "m4UIuZjNzEWsCP1gpZBgjg.png", + "AvatarItemDesc": "95a519de-f2cb-429c-b014-508477f20d42,", + "FriendlyName": "UGC Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "95a519de-f2cb-429c-b014-508477f20d42", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-04-07T17:07:07.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2184, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "KXfytDhXzES2yco-rwqDSA.png", + "AvatarItemDesc": "c5d70cb4-71dd-4fe4-b719-34fe2073c611,", + "FriendlyName": "UGC Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5d70cb4-71dd-4fe4-b719-34fe2073c611", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-04-19T23:40:30.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2195, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5K9fUPIZ1kaQahcIYrlZCA.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,", + "FriendlyName": "Umbrella Hat (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:36.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2267, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qcRLmi1ST0OCkd8H8YzgYw.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,Ii2WNQxqtUeoIfGItUkVbQ", + "FriendlyName": "Olden Umbrella Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:23.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2828, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_ER1egNWrEmQDiXRt-SAmQ.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,U-KT3nJlW0-JT8GSQuQXhg", + "FriendlyName": "Umbrella Hat (Mushroom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:59.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2827, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "D69pyDco3kKsZJf_pN2FHg.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,3L-AGgM50kCCz2V3UJtErA", + "FriendlyName": "Umbrella Hat (Ladybug)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:57.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1060, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6xa1zsivxbibzmgrkhclxi34k.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,", + "FriendlyName": "Unicorn Horn (Spirit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:16.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1061, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "43eqpst8khtmcw02hx4g56yts.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,FNe9LkWJMUeC3frYJMyEaQ,nW_fBsuZA0OfIjTMXm6ZCA", + "FriendlyName": "Unicorn Horn (Harmony)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1062, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8txs4ivnem6d79jibl2zf6qtx.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,mudt-Z5-5USdnIq-xaDgOw,nW_fBsuZA0OfIjTMXm6ZCA", + "FriendlyName": "Unicorn Horn (Joy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:21.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2116, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rgbXleaBw0Wb9fZ3vTUtbA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,", + "FriendlyName": "Rec Room Untucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:28.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2117, + "AvatarItemType": 0, + "Tooltip": "Mr Beast's shirt", + "Rarity": 50, + "ThumbnailImage": "CF81YKO-nUGbzKI3slBEtg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,vjO5ctMe0UC8GN6p61IC4A", + "FriendlyName": "MrBeast T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:30.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f4svRkfHhE-6a9L_W0YSwg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,hbF6ChMyEk-L8O4f6o_l6A", + "FriendlyName": "Sports Fan Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:53.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2169, + "AvatarItemType": 0, + "Tooltip": "Rocked out at Rec Room's Prom!", + "Rarity": 50, + "ThumbnailImage": "LA32UPI01kqXUcm7xSrlmw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,rQaQALql2E-zdM8072gfCg", + "FriendlyName": "Prom T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:50.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2216, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NkEwfQcQFUiQ_C-0VKPJhQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,y78S5o6VXUGva1Wpd6sp3A", + "FriendlyName": "Untucked Tee Shirt) Untucked Tee Creator Reward Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-05-11T19:02:40.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2241, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E9mf1x4jp0yyg72cw7s-OA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,S_dLd3dzvk2hM6ZA2DKTWg", + "FriendlyName": "Tennis Ball Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:38.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2240, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LvZbawPbAEWmg5aVGi-M_Q.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,GU267AQ5T0m1jco3mNw_uw", + "FriendlyName": "Maker Pen Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:30.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2298, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "VasCRGTf-E-zXE53kLLl1g.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,SV2UmeQDjEaMK_A2Q4tt3w", + "FriendlyName": "Rec Royale Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:56.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2296, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pVwmecVkBEm37eRY3Jc_Qg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,V-vCW0ICl0Ke3QPAGo9GSg", + "FriendlyName": "Bee Yourself Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:49.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2297, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dH9FOddSYkSjUWt2Ux_kSA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,g_f9bH8tykGq3yRwWuQPOA", + "FriendlyName": "KREQ Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:52.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2688, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZOvGH1iQ_kqzVB8EkYBpeQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,Eh1jgOjxpE-AEModcPAFMg", + "FriendlyName": "Jazz Fluence T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2687, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VT-6wtBaC0Gs78iXxiZS1A.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,_aQEMQzp20eMT_Izfc64wA", + "FriendlyName": "Retro Cassette Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:07.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2689, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2_H41S2JX0a9J5AqYS7boQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,ppH8miE39kGaSuACQv34WA", + "FriendlyName": "Untucked Tee Shirt) Untucked Tee Shirt RECDLC Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-12T21:02:17.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2686, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0UwHPWuWWEacVUf3ynSV8g.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,hZvsroTOnU6ew9HBWaqxHg", + "FriendlyName": "Rec Rocks Shirt (2023)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:04.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U0QKGodCxUOP2Vvs0UCeKQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,QQVh8FdTakO26iOFdzGoBQ", + "FriendlyName": "Rec Rocks Tee (Akintoye)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:18.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2744, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UDBwvAX1qUWFKJmxqFGxzw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,mzu-a8fcsUCW591ENABAgg", + "FriendlyName": "Rec Rocks Tee (Haven)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:58.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2843, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XapimDoowkSNnhZVot3imA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,JpaGA1tG6U-VLsHH_vklzA", + "FriendlyName": "Fourth Friday Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:31:05.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2765, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XEh7w_iBSEWOPDJHeE18nA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,42YaouiGVEe3zSjTsvyfoA", + "FriendlyName": "Rec Rocks Tee (Clinton Kane)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:22.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2814, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WtNLFBwRdEqYa6RZtR82Lw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,fzQzxcpDNEW8Rx122zmjAw", + "FriendlyName": "Rec Rocks Tee (Charlie Curtis-Beard)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:09.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,b11GS6Jd4E-UhO8wxhfQXw", + "FriendlyName": "Untucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1640435160 + }, + { + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,cd9d37fb-51a9-45dd-8e87-2756eb62f48b", + "FriendlyName": "Untucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1312778606 + }, + { + "AvatarItemId": 831, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6dn83vim1zahdfgjcl98s0u6d.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,", + "FriendlyName": "Vampire Hunter Necklace (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:46.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 836, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7pryvhofcoe894jd6lj9k3fpt.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,lvDIOeSI10SNxnCO_M_9zA,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Diamond)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:40.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 837, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0e9k0ehhqbwmok3pfnwg4a9k3.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,0S7Nqm2kv0qMDTh-5Wy0MQ,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 838, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8dp37nmbznt4ddn3fpjb7i882.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,poqpmOloPUKotHH9p0YAyw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Amethyst)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:43.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 839, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dhvs55ab5etr8whu54dyu63mq.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,F7luQ18ZXEaITFiCqqwkpg,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Topaz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:44.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 840, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bmf0h0l2wd8hhxzq331rdtd2n.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,okpg4ftsak-wc8Zu83uytw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Opal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:45.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 852, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2qjoypuya1mw4gd0o6jhz9c0k.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,OfenZaa1-UGdQ8YVOrXjzw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:56.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 855, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "czzdjcyf31hpujiah45yuse4n.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,WOxMXWKGrUWhnNInttZ-Zw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Amber)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T21:38:59.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 833, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "84ln91je4llsa10o7904ovuwq.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,", + "FriendlyName": "Vampire Hunter Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:48.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 843, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b6rtey8gih1qt69aci1qc2op4.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,Z8Bfk0MTb0evZehvvgzuBA,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:48.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 844, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "csm6i7gnanlmwjohok0nf865k.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,9QG6qp5FBU-u0-JK-yMMqA,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:50.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 856, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b80np2tvmtp3siyl63qj51enc.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,CavRDqIlmUeTZBtCLn1hZQ,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T21:39:01.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 835, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cimomkml6k4toyowd1voh7hqm.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,", + "FriendlyName": "Vampire Hunter Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:50.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 847, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzkxvaged4oeb5kres30cpf1l.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,D0tP58wC30mJ00vNxw8hqA,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:53.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 848, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5colkn1hn7j9z19pn2vmuyjq5.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,Vxq6v55a_kGPODzwB5MC1g,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:55.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 849, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "79l7swn4ouu72xp8nmx7n1meu.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,iJW8XGJfU0G12iXarjxhdQ,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:56.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2275, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TtA9vfjAmUSSZ0HliWFsDg.png", + "AvatarItemDesc": "3ec19cb7-d0a3-4c35-a7e0-dc1f8609d373,", + "FriendlyName": "Pretzel Vendor Tray", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ec19cb7-d0a3-4c35-a7e0-dc1f8609d373", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:26.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1231, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4ktwnvhbdb6vrphngnp2a7pne.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,", + "FriendlyName": "Formal Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:10.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1232, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3h61nzryygyv3hlalnxl445sd.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,ffHKr5EcIU24PbAtWSnalw,bVrkIyADCUiGNYWdmP_HPw", + "FriendlyName": "Formal Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:14.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1233, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8a8z4irf5f2f0qemw44a9ksy3.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,m4W5D0Oa9kaWa-8I96YYzA,WF_fg80f30qvHit0vGBwCA", + "FriendlyName": "Formal Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:17.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2050, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "l-xQVuiMukee1yhQpZD6Dw.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,9RPbhHJWBkqwpDbMg-QumA,Qzklnrvw0EOiZ1mrj_nAYw", + "FriendlyName": "Formal Vest (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:41.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2561, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NMm13SJapEefUXYk6zAzcQ.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,bJXcVtY7qUK101QUAgr05A", + "FriendlyName": "Midnight Gala Gilded Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-07T21:47:03.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2001, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6ik8yccgzd9kcci799wrss8n.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,", + "FriendlyName": "Vigilante Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:33.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2205, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EsJbgLpY9E2nOTvBpQupHQ.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,mO5wjf7_cEWNCHFR6ymGxA", + "FriendlyName": "Vigilante Cape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:12.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2407, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FTP3W6-lVEm0dHlsJU-OAA.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,EjNmQJBRnUet_ovEQ3HnDA", + "FriendlyName": "Vigilante Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:04.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2532, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8__P61NSO0yRenF0XuCZVQ.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,oy67Rd9BskuB8yloqSv0Jw", + "FriendlyName": "Vigilante Cape Shoulder) Vigilante Cape Shoulder Green Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-21T16:42:06.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2408, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3e_aTt0gJUOLEV5MPnaBFA.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,", + "FriendlyName": "Vigilante Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:07.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2410, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hNQCYaLS6ES3S7MsMnbjYQ.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,eBm7gRXomkivUz4Qg_ab8w", + "FriendlyName": "Vigilante Glove Wrist) Vigilante Glove White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:11.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2409, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0seg4oV6PE-6014-iF3t0Q.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,agkPLQG_Mkmqf3WkDaFaWA", + "FriendlyName": "Vigilante Glove Wrist) Vigilante Glove Blue Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:09.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2543, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yCllwTBTl0yS2I5e-uCk5A.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,", + "FriendlyName": "Secret Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:08.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2548, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5cJBLmj_M020xDeOBjTGfg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,l1cBXE49rkKTDEl1NvmWog", + "FriendlyName": "Secret Mask (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:21.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2550, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dbd2B8AH_ESr4B382zo8zg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,MYCVPJDuz0Od_aZJekuWGA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:25.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2549, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "G-NyjtfwGk6tNxEwpb3r1w.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,kfZalp0wCkypX-QDhF0YoA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Red Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:23.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2544, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "udVHYjEfKUWkjL-2WA5Etg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,2U3ZEvBzoESCb7kk4zSokQ", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:10.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2545, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1oQmhaLu1USd36z3vIXSkw.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,S5IPVluRPkWzapjf7Bdtxg", + "FriendlyName": "Secret Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:11.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2547, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "zAnC5sdLVE27dsVXv-pKug.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,oDkSnfL7Vk64ZdxkuenEmA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Pink Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:20.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2551, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wAPE0uRkEkivSTVbtq2xDQ.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,8kAqQHAgnUypHdxnwVSQwQ", + "FriendlyName": "Secret Mask (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:27.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2546, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WGlj0EddtEuIpKKzqJ0XVA.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,pUHlnnttXUmqP-9Rab8YaA", + "FriendlyName": "Secret Mask (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:13.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2414, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qsnBFyRE20CfNh68iWd0lA.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,", + "FriendlyName": "Vigilante Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:22.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2416, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Xxv_F0hPeE-3CmWWVz9lgg.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,rixBVVHS1E2Q4Y43ix4ySA", + "FriendlyName": "Vigilante Suit Shirt) Vigilante Suit White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:27.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2415, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "jgtZjEvX30q8Y9obf4VFqg.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,zvs7t0P2Q02wIELJzMa2TQ", + "FriendlyName": "Vigilante Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:25.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2417, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "scABHZiEIEOr3uyq1HhN5A.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,", + "FriendlyName": "Vigilante Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:30.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2419, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wWHYtq4i-EeTTmy0RHGJqQ.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,0Ua_WtxcNEWc_KoRuuHhtg", + "FriendlyName": "Vigilante Utility Belt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:34.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2418, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_pw8woutPUabGOMaUuOihw.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,PXssHhcJsUmUKGA8T4u85A", + "FriendlyName": "Vigilante Belt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:32.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 71, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "drqcby4q8dncgmtl1ndocn4kj.png", + "AvatarItemDesc": "c87239ca-a255-4b13-bbc5-1b38236fb4cc,", + "FriendlyName": "Viking Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c87239ca-a255-4b13-bbc5-1b38236fb4cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 24, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bpe9ub501z2dr6rgv1b5zhi1u.png", + "AvatarItemDesc": "8c51fc1a-b1ad-4002-a13c-4313abd92c7f,", + "FriendlyName": "Visor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c51fc1a-b1ad-4002-a13c-4313abd92c7f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1497, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6f370azhmwpl1vkwy7fwf4kzo.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,", + "FriendlyName": "Silly Shorts (Donut Dreams)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:04.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1496, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "65sqa2rlvhzcxamj55t9w8684.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,nu_UGtkJOUK-5MWaW83MUg,pgBFpOtl00K2zsTJcfrefQ", + "FriendlyName": "Silly Shorts (Watermelon Rain)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:01.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1495, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8u3din8ubpb13mug1b7ls4lnw.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,sJioq5ARDUyMHFji_6OxFw,bjXO36Nu6EWD2dU1UC4Uwg", + "FriendlyName": "Silly Shorts (Pina Colada)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:58.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1494, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2lgy30wam8vhg848fzjhiys66.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,56qchovVVEaVwL9S52kneA,BAjMoTHbD0mSCjoG9jmzjA", + "FriendlyName": "Silly Shorts (Sour Popsicle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:49.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1493, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e0hiapycpgqmkydxsy7vminkj.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,zJMD-_HR80WCAUqYGe7_NA,rAzhaLh1nEWMd9v1D-HWcQ", + "FriendlyName": "Silly Shorts (Lonely Island)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:41.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1492, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "49x7h4tmx4xozv5opw2vrtmnl.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,9P8ARRUYdk6hCoGMKhix4w,o__JORNtYEiJEyyoBIimPA", + "FriendlyName": "Silly Shorts (Pink Flamingo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2167, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nIUJ-iQCTECRCeCD0vn96g.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,Pg2cVQonrkmbXZdsNcn-KA", + "FriendlyName": "Wacky Shorts (SummerCamp Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:45.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1463, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "08qct3wo7gai3uz597vre1rid.png", + "AvatarItemDesc": "767684e7-0eec-4f6e-ad58-3d5ae715206e,", + "FriendlyName": "Retro Sunglasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "767684e7-0eec-4f6e-ad58-3d5ae715206e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:11:46.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1339, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "drpze5r42trhdfe0kkmbirisr.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,", + "FriendlyName": "Server Apron (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:05.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1340, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9f3l01qhr5bcg8ed2l0i4d4wy.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,HdwZhjrqxE6wuf68MnDhDw,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:09.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1341, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6k82rsbhcoow0xzix9ty8q06z.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,b7oobDcDtE-69fhM-5g5AQ,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:14.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1342, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3on8a1l5752ih0y8i5uu1zha8.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,Blev_VFjRUysY_nymGWAVg,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:18.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1343, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6840rnvh9m72ksb1xw4y69cp3.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,", + "FriendlyName": "Server Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:23.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1344, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9cewy2sbl6u8ip4456pu20bad.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,GQSpXmD0B02_gwzpq6StFA,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:28.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1345, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2vipnhbzbw003kcmye8831wbk.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,YvPeCbQS2UOcLjPfoIML_g,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:32.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1346, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8657c4mdn19tdzvke8uu9mztu.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,zGOD45zNAk2i6P_zPsNPfg,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:38.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1794, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5c3hj3unazrmeie5zbuj4d1yk.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,", + "FriendlyName": "Wedding Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:09.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2511, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KXWrEO68d0Grp3m1mwkRjg.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,nuR8wvvCXU-EMXjBZuTXJA", + "FriendlyName": "Sugarplum Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:05.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2513, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "of8r-gp3HkCGzkv5aES9aA.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,TqhSo1hbPE6wdB_6lvLhFw", + "FriendlyName": "Sugar Plum Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:10.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2512, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PUAM3Iq0rUqQ6Pi5AYQeYg.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,SExtD3FBk0GwseMZDdGJlw", + "FriendlyName": "Sugarplum Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:07.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,96556f7f-a6e8-48f4-a4e4-3fad9be5df4b", + "FriendlyName": "Dress Wedding Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1724539158 + }, + { + "AvatarItemId": 1521, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "5gcobmtf2n9884p336ea9z10x.png", + "AvatarItemDesc": "05b7af56-71f2-45ba-a377-c105bf6a6f7a,", + "FriendlyName": "Werewolf Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05b7af56-71f2-45ba-a377-c105bf6a6f7a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:21:05.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1520, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "dqy4lj2b3ng70db6wvws3916n.png", + "AvatarItemDesc": "833e8ab3-9b54-4e15-8f4d-ee1bac5823c9,", + "FriendlyName": "Werewolf Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "833e8ab3-9b54-4e15-8f4d-ee1bac5823c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:21:01.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1519, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "abqsaidncml8v5e6w3cus33fc.png", + "AvatarItemDesc": "16f4dc71-d35a-4269-b925-1a0cd85a43dd,", + "FriendlyName": "Werewolf Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16f4dc71-d35a-4269-b925-1a0cd85a43dd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:58.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2394, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "xlMaAOhYFUiyeB9g8Ynt_A.png", + "AvatarItemDesc": "67d8d599-4611-428f-98cd-98d8a527ace5,", + "FriendlyName": "Wicked Sorceress Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67d8d599-4611-428f-98cd-98d8a527ace5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:50.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2395, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zgDeoqR2GkqwnMftPbASfA.png", + "AvatarItemDesc": "67d8d599-4611-428f-98cd-98d8a527ace5,4qSWIO-cy0q5JWHp7tagHA", + "FriendlyName": "Wicked Sorceress Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67d8d599-4611-428f-98cd-98d8a527ace5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:52.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2396, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2591QLU31kirjhVvKJKqcA.png", + "AvatarItemDesc": "6d3be838-85cf-46db-b33d-0985aec9609f,", + "FriendlyName": "Wicked Sorceress Cowl", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d3be838-85cf-46db-b33d-0985aec9609f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:54.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2397, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "89fpSMNTb02Y1qYpw1xf6Q.png", + "AvatarItemDesc": "6d3be838-85cf-46db-b33d-0985aec9609f,Vn2gH3yjiE6bwBRgh5ntZQ", + "FriendlyName": "Wicked Sorceress Cowl (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d3be838-85cf-46db-b33d-0985aec9609f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:56.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2398, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6wrK2BA4GkydRnaEpYN9DQ.png", + "AvatarItemDesc": "730453b8-a352-4e6b-9573-54294690c2ad,", + "FriendlyName": "Wicked Sorceress Staff", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "730453b8-a352-4e6b-9573-54294690c2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:59.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2399, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_Hv84dtkbkuHQuNy36QnrA.png", + "AvatarItemDesc": "730453b8-a352-4e6b-9573-54294690c2ad,4yMvQ3-hnU2zZQf_XT9r-A", + "FriendlyName": "Wicked Sorceress Staff (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "730453b8-a352-4e6b-9573-54294690c2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:12:00.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2505, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pSMu4GZwPkaYjHeVvrdP0Q.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,", + "FriendlyName": "Winged Headband (Frosty)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:56.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2509, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9quIlEDlukSdbauAxP4n8g.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,g14i4cTzkUu6RCOELBZEZA", + "FriendlyName": "Raven Winged Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:20.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CHZHjVfzp0mycor85z3G_Q.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,JUOrYcAd7U-ChkFZRiBeKA", + "FriendlyName": "Winged Headband (Fire)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:22.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2508, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7cUt9iO5Y06r9k3KfGPwGA.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,yxBqgXjlVEeRUOC_6iv77g", + "FriendlyName": "Winged Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:18.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2506, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yMQVOtm_EEeDJjnd-mgvQQ.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,6JihVI8pm0eJ-wDWuXm0sg", + "FriendlyName": "Winged Headband (Hawk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:58.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2507, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "F5yTEJXfq0GxW0BX6wKrmA.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,wwT8NdC-bk-aAWLoJvef5A", + "FriendlyName": "Winged Headband (Ice)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:13.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6a9x79mupkr34kq5jdirl6jkn.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,", + "FriendlyName": "Winged Headphones (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T20:59:45.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1789, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "92zq8rl5cyj5107xzihrijuss.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,9Zm7zOFBXkGRHrtp0JnaeA,oEsjaKTnKUq-Zgg4vm-EfA", + "FriendlyName": "Winged Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:07:04.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1784, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "e0ivt24nvmykvpkg7ivnet274.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,0jnL66i2_kGjfzDBc8ZNSQ,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:54.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wipr83z3k4w166t8juiq5q2x.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,pY3RdcA2lU6H5rVXp8csfg,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:56.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1786, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a4nb7rytqdlq8f433qn0cieme.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,1Cbw9819NE-Fmv1NQIYKow,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:57.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1787, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "295vi4pw9jv6gz93ql5te9zw3.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,5JQPQ2JBUkSFyXeYWvr4tA,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:59.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1788, + "AvatarItemType": 0, + "Tooltip": "RecCon 2021 exclusive", + "Rarity": 50, + "ThumbnailImage": "3agkplk68iooewqp7l2qckznf.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,omnSzDLD5ESCU6ndropzPQ,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:07:00.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 6613, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "EkFVi2G8T02B9WxMBsz-hA.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,chqfCVxsEUuuUfp6qIadrg", + "FriendlyName": "Winged Headphones) Winged Headphones Hat Emissive Blue Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2024-03-27T22:53:46.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2440, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "Mx131-oxr0WEOEUxE3QvYw.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,VtsT6sKDYE24bAU-PKkIDQ", + "FriendlyName": "Winged Headphones (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:13.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1544, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "16g4r2mkpxrh4o9sywb6mqjwm.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,tXhgU1o_wkaNQ-7P3KXidQ,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Kingfisher)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1545, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bwisaai6w6aje9vyxb5oi2qbp.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,nn86ATWxekWdIGlPePPgGQ,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Vintage)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:51.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1546, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "22evbpym92jss1q2zzd8hgkb7.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,Rf7CDh2bg0-HdG5n7phGaw,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Thornbill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:55.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1559, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8czrpcicgmpn30oiwunhjkdov.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,oxi26T5a4U6L73pF-Pgrhg,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Grackle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:55:59.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1558, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dym59p1zihb18nmd4gfuzr226.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,6547pqf6vUm2L89AG6D91A,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:55:55.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 590, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "byuimkku1xbrkury4h0dloo3g.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,", + "FriendlyName": "Winter Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:32:56.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 951, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7c8t5ha57jkfu3nt2csays2c9.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,6vjA_vHq70Gclypll_GS_A,nalaCGqtL0SFA4Itwqmp8g", + "FriendlyName": "Winter Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:17.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1669, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aqnqkk7wewefq5ptgg8ky4ll8.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,3UxfTkPsEkezOXO__wz_mg,nalaCGqtL0SFA4Itwqmp8g", + "FriendlyName": "Winter Hat (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:04:21.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1223, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5h97nfxudddfga6xddftkdv7n.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,xVwzV9-m6E-8PVpkg4D-AQ,NH0L99bd00OVKMyltuSnHQ", + "FriendlyName": "Winter Hat (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:30.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1224, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ujv4qof9f4vkfnmm5zhc3yqp.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,mraJrTA4V0eARb_Kq7yYVw,NH0L99bd00OVKMyltuSnHQ", + "FriendlyName": "Winter Hat (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:35.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1596, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d2wg3243jobc3bipqut3250ii.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,09Cg6dOq2kC2-tI5yyGSYg,YFZimVHKuk--lXS8RCrZTA", + "FriendlyName": "Winter Hat (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:29.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1945, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dq0ld9pvcoy11h7778869v6w4.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,u1JNHHTxSU-N53Bs9pRCoQ,0zHD3DaMAUOogqOqyd59SQ", + "FriendlyName": "Winter Hat (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:24.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2677, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ogsxq40HrUGm_XGRT-bDrA.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,w7ybaJQVo0WCZFDPCJAtWw", + "FriendlyName": "Winter Hat (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:21.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 591, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "byw8djj38cmq7xhypade0jdsi.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,", + "FriendlyName": "Winter Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:02.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 950, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8oldwy2zk4m8splglpnb00px6.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,2lD-mjl3pk2wWv24RY2tbA,dUu7b-JFOky9c2lIVCNmcg", + "FriendlyName": "Winter Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:14.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1225, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7vhlvcgeu2orqfj1v5ym2tw6b.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,OrqqGMhK1UKGowFxpJi2mw,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:39.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1226, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c374omg9e5qg2vfc2ferpr6rs.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,-tDd0gdxQkuvL2id10AweQ,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:41.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1670, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c9kkdeflt9if8ses9jc4zceuw.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,TDcZZ2ynEU-Mgmju_qIVIg,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:04:43.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1597, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6vj33whkr0o942bwju915h7tv.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,09Cg6dOq2kC2-tI5yyGSYg,eUktNKKK3kqUR8-gA3rNxA", + "FriendlyName": "Winter Scarf (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:35.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1944, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dg0fgutjfxtnt68jv0ixfe86l.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,cj9OYinX5kyzlhBUQwZKog,AKWH3_SDqkG8SRm5sauROQ", + "FriendlyName": "Winter Scarf (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:21.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2678, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "K2uufB0PC0S-SCX_3wmmvQ.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,NJcJw51-xkq0phT-nlJw2A", + "FriendlyName": "Winter Scarf (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:25.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dll3aenct2b8668hjuionbuff.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,", + "FriendlyName": "Winter Sweater (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:04.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 948, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "779n1128jegvy95s8avlm32fs.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,LDqcPkvNXE2gFXxbpNkfhA,Too5jpb1w0iQenkjRRS-QA", + "FriendlyName": "Winter Sweater (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:03.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1227, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5e6rkamgfx1r7o0j31z8lkrc8.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,XOBIPrKCr02xa4tKj8wN9g,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:00.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1228, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f0y7tht9axiudht3tlhqeysgc.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,i33RmJJCwUem-ADkOwMNQQ,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:02.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1673, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "44uerp68eefsysf8d98xb2sj1.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,fq0xC9BjLECbsQIZfAvK9A,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:17.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1595, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ajy0ulpcn5j6v22cwvub3mj4g.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,09Cg6dOq2kC2-tI5yyGSYg,3fQj_7meDEeQqm_7RfEfxQ", + "FriendlyName": "Winter Sweater (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:26.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1943, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aa8mp2wzw823pnrmg36w1gge6.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,fTWOc1eEl0aVKXwTYYjA9Q,31nK13dsTkaIf3tGFrZlJQ", + "FriendlyName": "Winter Sweater (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:19.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2609, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ZjDS_OAMk6uv03QVqK4vw.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,PG6H6px5HkO4X_xPRd1idA", + "FriendlyName": "Winter Sweater (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:13.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 593, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0hlts8hq83dvp3ifo504f57oz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,", + "FriendlyName": "Winter Mittens (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:07.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 949, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aiitap49m6e7fam5fzxlgszq0.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,SWvJWvMDpE-it5PFoPgERg,jQjfiz2i6kaPfrkjnBGxBA", + "FriendlyName": "Winter Mittens (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:10.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1674, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0iiuzgowl2qneslonnzfktvaz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,83jS7d-cUUC_GNDcGpiDxw,jQjfiz2i6kaPfrkjnBGxBA", + "FriendlyName": "Winter Mittens (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:24.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1229, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "00oq8k2l0ag3pn1lp8zpc36rz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,afWY5VGiF0isGn6qpo4KeQ,_yvss6L59US4-8QYZgcCdA", + "FriendlyName": "Winter Mittens (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:04.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1230, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9uo8iwvaprweq4i5xoa11qb42.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,tyzdtSkR9k-YCnjNvPAT6w,_yvss6L59US4-8QYZgcCdA", + "FriendlyName": "Winter Mittens (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:08.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1594, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9k6nv0za82bel8lvz8220tor9.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,09Cg6dOq2kC2-tI5yyGSYg,xq7bizx6UEGaMTHB9zI3pw", + "FriendlyName": "Winter Mittens (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:24.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1975, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6jg4ugpje9gdl4iyf223r1e0.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,MmGNESXgOkiZ0xCG41E9ig,Nc6jwNeK_kyajH5xKzCNVw", + "FriendlyName": "Winter Mittens (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:21:21.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2680, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zwdzcRCLDEeGDboAcpAzOQ.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,e6hTzFc-fEiJe0L4jDVxRQ", + "FriendlyName": "Winter Mittens (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:33.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1300, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1hz9fyly8abux6bynwxt67g6.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,", + "FriendlyName": "Winter Cloak (Nightwatch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:14.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "90opi030nb86oj7r0um5a2iip.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,pXrZhtzjV02_qvIQoWEFWA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Hearthstone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:57.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1617, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dtxdon4brg2jmucu6b9sga2wh.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,VK4YBLXO10G2_aEl3ZQUGA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Whiteout)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:00.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1618, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8yf569v4byj5czra0vsottv9w.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,A7l6Q2clsk2XyKf62D3dHA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Ironbound)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:02.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 826, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "36zngdpuj89crp4vmobfqwgk9.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,", + "FriendlyName": "Witch Hat (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:24.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "byiqri4xizdtch72sub8lx40z.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,PcUZrdKXhk2XHa5Gy0YnnA,dpBx3DxB7kKJphxCvfmKKw", + "FriendlyName": "Witch Hat (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:50:36.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1516, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dam6qhnya0lx1zj3twhobzuo1.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,0cP9EcZRFEuMOvmZ_Kx9dA,smmfK-N2R0uoDiuIHrhImg", + "FriendlyName": "Witch Hat (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:19.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 827, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "avd2s93fmxs9qkst7n3y7zcxq.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,", + "FriendlyName": "Witch Dress (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:26.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1173, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "csihpdjmklkvoybyrc0do3hiq.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,uTHwo8qd6Ei3Oy4gz_j9Ow,AK4CoAB2IUGkwqECNaO75g", + "FriendlyName": "Witch Dress (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:04.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1517, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7olbww29lo0vqi3gal2gidvgz.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,rZ45-lecBESqHaLX0c7NAw,iDxHI0T4Gkifs3_GY5gdjQ", + "FriendlyName": "Witch Dress (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:39.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 828, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4zz4132pzz51bxxjxf570c1d3.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,", + "FriendlyName": "Witch Cuffs (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:29.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1172, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dab6kmwjuwqeqhmjfdhdx043o.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,Hk6svQ5Sr0GDKLxOyktk9w,IQvHmZ8ei02hVuXZiXb6TA", + "FriendlyName": "Witch Cuffs (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:00.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1518, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0p6vqm6iz2rilzafurlc4bbfd.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,0ZKR_qxrWkiWkjpot8s6Cg,5aX524Lfok-0dC3UFv-OkQ", + "FriendlyName": "Witch Cuff (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:47.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 515, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "17c4sjyyimn5d2gw0f556bubi.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,", + "FriendlyName": "Witch Hunter Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:51.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 516, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4quvkumfq6a1nznmdsezcpwxq.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,64545dcc-a8eb-4c69-a539-78ee73a2d327,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:53.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 517, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4110szxveghcz05ilxuwglfb9.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,e02838f8-4029-455a-b35c-dd40f972b05b,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:54.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 518, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6z583ji4uuq0r1nymbm4h8ou5.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:55.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 519, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2boublopz20zq6qu6ncbx97n7.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,909ed249-ce09-4304-8b02-56794fa7567d,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:56.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1125, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "04egovo7xe0ud9w0z1a6z2glf.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,Tbrc2MlSlE6mKua8qSoluQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:44.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1305, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3wjvw25jvmg34uniq3dxxrv37.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,aBQ3R9boek-mYeSYSNvDbQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:33.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 520, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c6y6kr8pkktfh9j9jrwbdbiop.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,", + "FriendlyName": "Witch Hunter Torso (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:57.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 521, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dbbbxrdwflnaiaj1nx1ovh07r.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,64545dcc-a8eb-4c69-a539-78ee73a2d327,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:58.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 522, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "40hz8pm5dpxvycg72a9eqhqu4.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,e02838f8-4029-455a-b35c-dd40f972b05b,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:59.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 523, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6crngu0sszkwju23mkpdiihjs.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:00.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 524, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cfce1ejdgdy1jvubqb4vyv8k0.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,909ed249-ce09-4304-8b02-56794fa7567d,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:01.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1096, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9hmvdv7ncpyo5f6agy7dh9rxv.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,Tbrc2MlSlE6mKua8qSoluQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:18.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1315, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dci33hnvmb7vrbgaa8u6zvrlg.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,aBQ3R9boek-mYeSYSNvDbQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:26.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 525, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aifzs5jdida9x4pu9dc5p3lvb.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,", + "FriendlyName": "Witch Hunter Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:02.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 526, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8kbnanl6xae6f5zssdhow3qi6.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,64545dcc-a8eb-4c69-a539-78ee73a2d327,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:04.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 527, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "71i7ci2n620of8lk3jxy8p0xl.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,e02838f8-4029-455a-b35c-dd40f972b05b,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:05.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 528, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "99lamaljd63j9ijdm4si9n6nz.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:06.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 529, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eiseqfjnni496s1qlbkfv63gw.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,909ed249-ce09-4304-8b02-56794fa7567d,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:07.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4baeboc0ifrxpbd0ko0ohkilh.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,Tbrc2MlSlE6mKua8qSoluQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:56.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1319, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ao77s83vraqs2p4xiwkoxdk8o.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,aBQ3R9boek-mYeSYSNvDbQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:57.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 530, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dvw439bvcm7yg6u5gwrrduckq.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,", + "FriendlyName": "Wizard Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:09.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 534, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6tvxvhjodc7ixkaqu2ncy473u.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,357eb160-360b-4a63-840d-65d11cc8ffc2,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:35:59.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 535, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7866xyln6cpoizvv5eps253jn.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,a38254c0-00e3-429b-9d5d-b9e6a9812e69,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:01.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 536, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2mgmt7so1oho67truxxyy5key.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,d3a9b576-5433-4305-bd96-b08820c09212,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:02.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 537, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ts6u23lp3vvh9g7zmcwgm6yd.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:04.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1124, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cgrpr9ydok6zdsitoa7a14oie.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,RKmRHk2N_U2gd91nybo6vg,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:32.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1306, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "19rp9i0wdr1ua9vm9hgjiy1f2.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,d3KmqVuMQkCYK6sVTMOTOA,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:38.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 532, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4u8g9x7yeqkmpqikqud6p0889.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,", + "FriendlyName": "Wizard Robes (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:12.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 538, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "a814kjf5tknfmtt221aq0rmcg.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,357eb160-360b-4a63-840d-65d11cc8ffc2,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:05.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 539, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6xg4k21qpb73l0qe9wq3xs507.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,a38254c0-00e3-429b-9d5d-b9e6a9812e69,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:06.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 540, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "351pq2gydedb34tytcs9n87b7.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,d3a9b576-5433-4305-bd96-b08820c09212,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:07.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 541, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c1nbb1yfgeptsgsxxakgr3ejf.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:08.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1100, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dlznhq7lrq71suzc0045grre.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,RKmRHk2N_U2gd91nybo6vg,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:29.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1316, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "47ngqkmtgcfvbds2nhtksozc3.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,d3KmqVuMQkCYK6sVTMOTOA,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:33.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 533, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "42c0usoynxjhmimj6coo11rd3.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,", + "FriendlyName": "Wizard Bracers (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:13.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 542, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4kheccxh9b10611a6leuimejf.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,357eb160-360b-4a63-840d-65d11cc8ffc2,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:09.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 543, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ejoni0f6t3kbisv094qlswolb.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,a38254c0-00e3-429b-9d5d-b9e6a9812e69,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:09.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 544, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4s49nshq56cwk2ptse4evh4p.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3a9b576-5433-4305-bd96-b08820c09212,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:10.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 545, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cz0n5qaa95cagk71hmmypg87q.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:11.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1108, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1n7pr3zdkb6nskzxhpvouxscx.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,RKmRHk2N_U2gd91nybo6vg,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:58.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1320, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0vwlyjwom9p9ef29kfe7nowjz.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3KmqVuMQkCYK6sVTMOTOA,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:16.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 531, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "633t2xbzngvd2f2x91gj77qth.png", + "AvatarItemDesc": "27cdecd2-88f1-40a8-80a9-87abd827c3c5,", + "FriendlyName": "Wizard Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27cdecd2-88f1-40a8-80a9-87abd827c3c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:11.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1873, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dachlbubxeuputz2ewly8je7l.png", + "AvatarItemDesc": "fb2a0e86-3363-4579-9f18-f41138dc85ba,", + "FriendlyName": "Wolf Amulet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb2a0e86-3363-4579-9f18-f41138dc85ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:31.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2121, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aTYlG6TI1ESQrh8Tyzi-bw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,", + "FriendlyName": "Atlanta Hawks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:41.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2126, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vndWqkhOjEiG1tfAoDZoig.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,dpxEW-2Un0eeVIsg5_o6Xw", + "FriendlyName": "Boston Celtics T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:53.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2137, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cTeuIowAx0KzIVy0-WH2cg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,4UTuNYISo0OP70FuGzmOsg", + "FriendlyName": "Brooklyn Nets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:22.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2130, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1KoWvmLOa0m99qKETLTIOA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,yVNoqFjSeU6mPJxEJnzRLg", + "FriendlyName": "Charlotte Hornets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:04.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2124, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r1RzRtaGuUKZ-8XAY3Dh2A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hE5scNMqJ0igY_jhufvNmg", + "FriendlyName": "Chicago Bulls T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:48.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2125, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YC4ouQXwGEOyXXjSiDhPkg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ibmxOUIFs06gOpRYLe58xg", + "FriendlyName": "Cleveland Cavaliers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:51.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2136, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "up6nS7ge3kWq1ROeCQ9GTg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,LpUOG6fHz0uQPtmOLtUlKw", + "FriendlyName": "Dallas Mavericks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:20.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2138, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NMpKzkwrjkmapWagtUmY4A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,sVeZH49mP0yBRKrbVr-0LA", + "FriendlyName": "Denver Nuggets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:25.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2141, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-6_8xc5kqEyRKQ1vuvbNYg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eAq9xK51mE-Nu4RQY0ds4Q", + "FriendlyName": "Detroit Pistons T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:32.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2149, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3ABGwcegD0CZluvQKynPlQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eCEF5P5N7EC_cEjM9-5iUA", + "FriendlyName": "Golden State Warriors T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:52.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2143, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2t_BBgqutkWjsMndffuawA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,OpWc8pumTk6M6x1WSw3a6Q", + "FriendlyName": "Houston Rockets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:37.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2139, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QTk8hWoyH0KjtihHXsQ3ww.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ow_9g2xNj0G33-zWIhjW-Q", + "FriendlyName": "Indiana Pacers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:27.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2127, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-Ct_cbd5DUeGp8zl21aLMA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,2MUexARJAU-Vc-Qebqqepg", + "FriendlyName": "LA Clippers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:56.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lrUa6D6QQUec3po_q7y2-g.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,9bQaIrqI00qtjdtkIkj_3g", + "FriendlyName": "Los Angeles Lakers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:15.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2128, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kf_qLY9VIE2q2gSCp5L1uQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,pvnZdyL9NEaYYX8T12AgVA", + "FriendlyName": "Memphis Grizzlies T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:58.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AGZJkBZVWUy1O-nlqtYI3A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,clStIXndXk2ayewq-1LKng", + "FriendlyName": "Miami Heat T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:01.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2123, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ako-ThG7XEqcnjIt9OZnlw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,oflbr1qvHUyvwUxdFZMcmA", + "FriendlyName": "Milwaukee Bucks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:46.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2147, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "W9BC4K_6K0G3J8tY9fyJeA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,j3S5Gk92mEOKlD424seciA", + "FriendlyName": "Minnesota Timberwolves T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:47.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2140, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NNC2C7t7sUaTbsPQB8wOmA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,JBFu1kZ8C0ya5JZLDh0ynw", + "FriendlyName": "New Orleans Pelicans T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:29.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2133, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MKQctCVd20ONb5z9deG31g.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,a4r3MYNjT0Ok0uQX9BYH6w", + "FriendlyName": "New York Knicks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:12.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2146, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PLv73v1H3EaMsBsQJzBVqw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ZDsylFpkEUi5PijTLSfYNg", + "FriendlyName": "Oklahoma City Thunder T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:45.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2135, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kOzNsMW2mUGpmPEeT0AnUw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,B3wjSG3Ui0ipGgN3h7BFiw", + "FriendlyName": "Orlando Magic T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:17.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2122, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yjtuZ5Fb5kGrGUDMaeoyEQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,H_lV3FQG0EC4ebW5vfnRIA", + "FriendlyName": "Philadelphia 76ers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:43.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2145, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HUAmHnGyBU-bYA17m4hILA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hU2rt41rvUqulLM1H5NX4g", + "FriendlyName": "Phoenix Suns T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:42.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2148, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1lch9FUPrk23Vdmwl61lHA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,49mpP0duKEylQ2v5ZU9SYg", + "FriendlyName": "Portland Trail Blazers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:50.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2132, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Qficbcavm06029T8l_tmgw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hoXpo41nnEOPGq20Tq-t9A", + "FriendlyName": "Sacramento Kings T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:09.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2144, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KwQTRfiStkqFs1xlwiPgUw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,RddbstjtZ0ClxgY-s66tTw", + "FriendlyName": "San Antonio Spurs T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:39.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2142, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ss8R7vuL6UqFDQeWpt-fxw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,kk271nI1MUu-gZjec9djrQ", + "FriendlyName": "Toronto Raptors T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:34.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2131, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kgk79zh1rka-JRcVXk1xnQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,tkHJl8TQHEiSeD-seDV4jg", + "FriendlyName": "Utah Jazz T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:06.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2150, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lqK4K0ia5kCA6osf4p36eA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,M3vXSuDRw0GLNh9A4dF4vA", + "FriendlyName": "Washington Wizards T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:55.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2183, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "K5HHE0xLXUyHcne6GNjNig.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,nEBw9Nh1-kWcOynsm7P8Pw", + "FriendlyName": "NBA 75th Anniversary T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:44.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1894, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2tkpst7269jmrhwyhewrh4t18.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,", + "FriendlyName": "Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:43.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2837, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78b4yr_R40a5Z_V4E85knw.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,B5WV1io6fkaAJdm4GbzM_Q", + "FriendlyName": "Monarch Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:52.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262588196, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RGy9sbGCrkuEWsufq0SbUg.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,bNWm_2_VE0-4fdcF6S0aIA", + "FriendlyName": "Purple Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1968469639, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8VDdpE56dk-9OeT9sGG-dA.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,q9Nganq9nUaV3zuvx9s3nQ", + "FriendlyName": "Butterfly Wrap Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1600494107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DWzLX_RUc0yrZ_icbuAJgA.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,y8yTimlV2EWhJMU-Ay9e8A", + "FriendlyName": "Mint Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1657, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5tzmcps62glt2hqx42rvrgotc.png", + "AvatarItemDesc": "9231a9c6-7638-4fe9-bd2f-eac6c943009b,", + "FriendlyName": "Championship Wrestling Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9231a9c6-7638-4fe9-bd2f-eac6c943009b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:31.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1855, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4510ib9vx62wtdf62c22d4ll2.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,x3kGXCD0UEWkjfXqu_kuxg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:58.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1857, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6utyv84yov0n8z090tcquifk4.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,fWxn_AarpESryNA9UN8wvA,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:02.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1856, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a2n00ipkx8cd6po7pkjti1rsw.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,sEKIlMfbJ0eED6VtcXcQrw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Army Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:00.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1858, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2b2f6frv89cqaeuswhoqzwvfg.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Dg3DxHr8GU2e190TGveXqw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Baby Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:05.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1859, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c4c0hhlbwpkuvdbt67tg68guo.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,5rLtRfNWO0qT7X-0sJkUXg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Lime)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:07.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1860, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bl1czxrbjmc23vhg7hy87v385.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,DiJGFIO-5Ue8rkEhvF5-Mg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Sienna)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:09.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1861, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5s7gwmw9faljog1rm2hdo358p.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Sg5yKkqJEEulQeUvRhFgJg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Prussian Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:12.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1862, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8w15137vxkyf85ljd9clb74kk.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,M0nOQl44DkG_GjTXzAEQCQ,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:14.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1863, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4zk2f1zx0df30rx3r2rafuhsi.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Ai85jH88H06ze74VdWolfw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Pure Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:16.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1864, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0saykzdj1mlrn24gfq57ps3yu.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,NRjJuinZ4ki8qaEPvP84tw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:19.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1865, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8k3ikhmifw4vcf0qeayqmlzby.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Z2L6AqDdcUqxiN1mVw-X9g,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Light Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:22.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 564, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "btwnqsv2torue6ekxyp3zudoe.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Wristbands (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-13T20:56:59.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1866, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "43wffwjx8jpzi7e26q188u0mg.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,j367nSYyg06rOMZ1GmfmFA,FS7kgc4DpkKuar24J9dW8g", + "FriendlyName": "Wristbands (Plaid)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:25.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1867, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0vdopp5i9bqzer09tpkfgi76a.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,2qYs9IkE90eppx1DggNlsA,6Y43EyTpiEejGp0ODWTSjA", + "FriendlyName": "Wristbands (Moons & Stars)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:28.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1868, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c2ypxn1ve868vb7jw2i5vn060.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,JV60jZGerUWuzQ2rRnwquQ,gSb8iLTEZ0mi3g5-jZXifQ", + "FriendlyName": "Wristbands (Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:31.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2788, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0AD75TjNekWbBVUQv0B09A.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,gOP1m4pVHkmlR2k8RrcdmQ", + "FriendlyName": "Rec Rocks Wristbands (Ethan Bortnick)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:14:03.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2072, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "kuIHwuTu10Cr8dCY4ZBlzQ.png", + "AvatarItemDesc": "d9c025e1-561b-4997-9f69-c96eb3c00786,", + "FriendlyName": "Wrist Warmers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c025e1-561b-4997-9f69-c96eb3c00786", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:34.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 256, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3szxfcxg8z8qj8wcaoubbty0z.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,", + "FriendlyName": "Poofy Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 257, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "00voe87lgaogwdjrgkt66bxew.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 258, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9z43yb4xdivb6qyou9niixqtn.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,64850553-cdfe-455a-ac00-dafbe63d613e,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 259, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9c3ziz00oalv0ji0f7tke3j7w.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,31ac9d4d-3d6b-48f2-85d5-dc4b8165a17a,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2051, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kq1OCF490kKmBUv0YF5PPw.png", + "AvatarItemDesc": "459ff0a0-ad68-4337-9399-57e3b03e4bf6,", + "FriendlyName": "Yeti Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ff0a0-ad68-4337-9399-57e3b03e4bf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2052, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "y_99GWjFbUa8UCqUY9X7FA.png", + "AvatarItemDesc": "7524b3f3-130b-41b1-83f6-9c241bd92bb4,", + "FriendlyName": "Yeti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7524b3f3-130b-41b1-83f6-9c241bd92bb4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:49.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2053, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UHgH78a4iEmYmvBe9cKI7w.png", + "AvatarItemDesc": "184368a6-17fa-4cfd-bb57-a2862f6e17bd,", + "FriendlyName": "Yeti Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "184368a6-17fa-4cfd-bb57-a2862f6e17bd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:53.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2118, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f5pXQnYPQUK8GU0CzpeWig.png", + "AvatarItemDesc": "f5b01307-e495-469c-afac-c10f375cffb0,", + "FriendlyName": "Yoga Mat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5b01307-e495-469c-afac-c10f375cffb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:32.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2420, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "xFNT1OMmg0O3cQ3aagkA8Q.png", + "AvatarItemDesc": "f5b01307-e495-469c-afac-c10f375cffb0,lIURXMnmn0GcYFUVSXHzWA", + "FriendlyName": "Yoga Mat (Boogie)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5b01307-e495-469c-afac-c10f375cffb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:36.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1189, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "c9bcud5i7blg02i1i21anii66.png", + "AvatarItemDesc": "9168b3f9-c7e5-40b6-bb2c-9e708596e675,", + "FriendlyName": "Zombie Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9168b3f9-c7e5-40b6-bb2c-9e708596e675", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:07.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1190, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7q88u3fe1zgi5smic4akp7ow9.png", + "AvatarItemDesc": "de19b239-3ed0-4f03-acb6-da69b5aa52ea,", + "FriendlyName": "Zombie Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de19b239-3ed0-4f03-acb6-da69b5aa52ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:11.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1191, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "amgn9mnr8d0gpwg27xhvbuvwf.png", + "AvatarItemDesc": "5f1bc3c2-dfbb-4258-9b10-608535e34db2,", + "FriendlyName": "Zombie Hands", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f1bc3c2-dfbb-4258-9b10-608535e34db2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:17.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2643, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cdu27a9jf28i0va22wre1e08h.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,", + "FriendlyName": "NFL Arizona Cardinals Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:03.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2644, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0znrqqn0ggnpfhcs942kyvrtp.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,VxmHqppE6UWiXVBXL2QvTA", + "FriendlyName": "NFL Atlanta Falcons Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:03.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2645, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eexlvsjg8kezkcwttljr1690x.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,kjDg5GM1b0iepa52BX5wbg", + "FriendlyName": "NFL Baltimore Ravens Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:04.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2646, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "707jfnnrkawaq9bdwniic0pye.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,WlfHWDcnO0SBT5wTjEPSmw", + "FriendlyName": "NFL Buffalo Bills Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:04.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2647, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5lqm8xisnbpdwi6my605vy6uj.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,_egTT9EA-UK0aLkn_QnWFA", + "FriendlyName": "NFL Carolina Panthers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:06.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2648, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1lkfrltzaelfhihaleygfgtoz.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,10nSNj7eRE6XwS9y2OvC2g", + "FriendlyName": "NFL Chicago Bears Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:06.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2649, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "axl8sqe1o3pyczpmutxk61ltf.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XPBWTcpQ9kaS0mabpdvYhw", + "FriendlyName": "NFL Cincinnati Bengals Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:07.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2650, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bya9qdvmk13buz121v7p3omb0.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FokpgM8R5EuXZwe89T78yA", + "FriendlyName": "NFL Cleveland Browns Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:08.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2651, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xj6xkknhas1ka2boco8j8tkf.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,v0nZrzbIMEi3AaGuNjmwDw", + "FriendlyName": "NFL Dallas Cowboys Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:08.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2652, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwh5sx4vh6v0gdfi3kkalxutn.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,TtthSwdIq0CuSNOKDLsldg", + "FriendlyName": "NFL Denver Broncos Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:09.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2653, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7q30iarhlropnxgad6s0kjjdc.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,5fjPcf4xQkSEYn3T-f8xLw", + "FriendlyName": "NFL Detroit Lions Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:10.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2654, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3vg7up51ivibzltebmdutv694.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,sjY_XUbXAk6h2TGnjBGDeQ", + "FriendlyName": "NFL Green Bay Packers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:10.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2655, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9kjwm7fopyhvlu9oq60p8kqh1.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,1nFX70Rqo0-qQo2M_Z1KCg", + "FriendlyName": "NFL Houston Texans Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:11.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2656, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "621yuib76ktgb5dus5p65ciue.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,cOyJcjGyp0miaMLAOpz0FA", + "FriendlyName": "NFL Indianapolis Colts Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:12.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2657, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2w56qylw8p57q7ik25gtk5usj.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,nIUjGqY8L0u0ZbX45b4PUQ", + "FriendlyName": "NFL Jacksonville Jaguars Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:12.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2658, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "85q8el387hlfi6qwrheeqg2g2.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6JY7l1gtpE631NTmT6xv3A", + "FriendlyName": "NFL Kansas City Chiefs Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:13.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2659, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2hbcygm3cqto3mirz4d7og1d7.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,27Fg--SzD0ertvGEv1Jgsw", + "FriendlyName": "NFL Las Vegas Raiders Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:13.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2660, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3inbs0h93lddkdaggk3h26yae.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6EjRVha2i0mCUVLaL_em_w", + "FriendlyName": "NFL Los Angeles Chargers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:19.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "di6np6u43ih96rrsdk18jvwox.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,EtgbhY9a_EqO4j86Al9Kpw", + "FriendlyName": "NFL Los Angeles Rams Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:20.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2662, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5h0qrqjtunac31f0srzvmmttz.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,oyvOdKXeGU65V6sKhhb0qg", + "FriendlyName": "NFL Miami Dolphins Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:20.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2663, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "en0jav3kz8odonu5j54j9ckk3.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,-0DfEFcbNEaNI2uvfHF9qA", + "FriendlyName": "NFL Minnesota Vikings Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:21.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2664, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "32qnngk3wsvsjk35n4vcfap7z.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,DVnSVZcj8kyLr1DXRu7kzg", + "FriendlyName": "NFL New England Patriots Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:22.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2665, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70ki9yd0las1hfb2tvielgtqm.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QgECbFDwyk24sI-e_fXy5w", + "FriendlyName": "NFL New Orleans Saints Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:22.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2674, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tah1l71br2opihsogi3xwk34.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,zMWcGYnGdk6lVqVNuFwJHw", + "FriendlyName": "NFL New York Giants Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:28.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2666, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b7zfebrr9rbe6rsoq8y16fx7w.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,jYJehFukzUCHKzWQAaM3uw", + "FriendlyName": "NFL New York Jets Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:23.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2667, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9j2025b6j83s88cooyi6ui7g8.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,4viU0bMT1UuasZHD8euFKg", + "FriendlyName": "NFL Philadelphia Eagles Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:24.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2668, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2aupjxiovt27lh2rkk8a4q4h8.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,YePtDvVnOk6cZkDBVIv-PA", + "FriendlyName": "NFL Pittsburgh Steelers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:24.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2669, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4pa7azn3fyrni6imh3ip7dpg.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,q-Gjg0zzI0mTHxjn8wi5HQ", + "FriendlyName": "NFL San Francisco 49ers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:25.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2670, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4lmbw9jnhj1x5cb2dj6y7wdx.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QD-SRdKijU2RVu60TcU_jw", + "FriendlyName": "NFL Seattle Seahawks Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:25.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2671, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4c9qxd5hhuxj27pqdxfocik1g.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,3m5Q5crrHUGZJ9_GuxubQw", + "FriendlyName": "NFL Tampa Bay Buccaneers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:26.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2672, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9u34u3inzwswkkhcwc7snict0.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XNTUcdaSVEuS3rbzwIwYNg", + "FriendlyName": "NFL Tennessee Titans Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:27.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2673, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ynu85lp1m1sqc2r1upivy5ox.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FnezvbcGD0-oyFOTkpUo7g", + "FriendlyName": "NFL Washington Commanders Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:27.877Z", + "AvatarItemIdSource": "Existing catalog" + } +] diff --git a/Server/RadicadeServer-Config/BugReports/20260925-044419-game_screenshot.png b/Server/RadicadeServer-Config/BugReports/20260925-044419-game_screenshot.png new file mode 100644 index 0000000..e40e14b Binary files /dev/null and b/Server/RadicadeServer-Config/BugReports/20260925-044419-game_screenshot.png differ diff --git a/Server/RadicadeServer-Config/BugReports/20261005-011905-game_screenshot.png b/Server/RadicadeServer-Config/BugReports/20261005-011905-game_screenshot.png new file mode 100644 index 0000000..2426caf Binary files /dev/null and b/Server/RadicadeServer-Config/BugReports/20261005-011905-game_screenshot.png differ diff --git a/Server/RadicadeServer-Config/avatar-item-lookup.json b/Server/RadicadeServer-Config/avatar-item-lookup.json new file mode 100644 index 0000000..f61ef7d --- /dev/null +++ b/Server/RadicadeServer-Config/avatar-item-lookup.json @@ -0,0 +1,2996 @@ +{ + "source": "AvatarItemWardrobeRuntimeConfig — client 2023-09-06T06-53-47Z", + "note": "Extracted variants and special/default selections from the selected client build.", + "descriptions": [ + "b33dbeee-5bdd-443d-aa6a-761248054e08,", + "6d48c545-22bb-46c1-a29d-0a38af387143,", + "24b8b757-4168-4b3b-ba11-46f29d19044d,", + "e41c32ac-61b7-4e59-9d0e-b231ebdc2a68,", + "d84993dd-4e15-4481-9450-f88a34cdbc82,FnhttSsCoUCSAW8j-stC5A", + "c5010738-41fa-4eca-aeac-e24adaa29789,", + "018b39ed-cbef-4fff-862f-f39373b32e2e,", + "1d27b674-f9e2-4ffc-9d8c-a58a1be06457,", + "d84c0ff9-8fbe-4ed8-abf3-7996e81888ab,", + "e5b83dfc-b2e1-4dcb-a4ab-9d3a4c8a34ae,", + "7dd6f7b0-7ba0-429f-a04f-e32d3a79ee61,", + "eb9611c6-bb50-41a2-93e9-7f959815a846,", + "1fd69ef8-0b74-4962-af5a-67f0bf0358f2,", + "a12f724f-4a73-4ab8-aad4-6bfc662b4dd6,", + "0753d7a4-8247-4fca-a6fc-359c26086140,", + "77d3c585-4928-4471-a425-89036efe7299,", + "92302d9d-c527-418c-ac5d-1fa869727505,", + "f9dd08f8-16d3-4c39-af4f-89f7bb6e80d3,", + "b148cb1e-df81-442f-aea6-ab1727aad00e,", + "e36bcd98-7e85-43fa-89f8-57e4ec33823a,", + "880a3cc0-7407-4b61-b759-f9dd890fe9e5,", + "21599b51-c50f-43d8-ac5f-62c30cd02ca5,", + "193a3bf9-abc0-4d78-8d63-92046908b1c5,", + "79b90274-6eec-4664-acfb-4a123334661e,", + "da4e7b34-2095-4a9e-801e-4f409039e0dd,", + "9d9fadb6-97eb-480e-a224-4e0179082071,", + "d8280c0c-d803-4513-be10-a0ba96d8821e,", + "e286863c-2967-4d00-b837-b49487b9484a,", + "2cb4f372-3372-4583-8b57-c4e3988e3c28,", + "06306723-ca20-4aa6-b7b3-917113f41ac3,", + "c70005d5-6276-4a98-acb3-6a77bc19379a,", + "8d10cc78-6b00-45f3-affb-205e9cc5b03f,", + "cc96f8a5-bc5b-4f89-83b7-ecd53905ada7,", + "c6c08eb5-381a-4193-9722-80da95d62abe,", + "4d507dfa-4a99-4ac0-8537-229e9dc0eb4a,", + "d0a9262f-5504-46a7-bb10-7507503db58e,", + "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,", + "8aa79563-ace1-4ba7-ad0c-f3210a78142f,", + "8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c", + "21caa68e-c3fa-474c-af5e-af1e742b7a60,", + "21caa68e-c3fa-474c-af5e-af1e742b7a60,c5deba2a-6e35-4b13-8e94-8ba5457f39df,b75ef67d-00c3-4ac1-9b72-212032460294", + "21caa68e-c3fa-474c-af5e-af1e742b7a60,758752bd-db2f-43d2-b580-55b3e1efffd5,b75ef67d-00c3-4ac1-9b72-212032460294", + "2296ed0d-df56-4d46-b33a-aae9230a47fc,", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,", + "71921831-ba6f-408b-a00e-2fd97663636f,", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,55901f12-d5b5-4fa8-b4c8-e479689ee39d,f600037d-c9c0-43fa-b45b-02f456f9dd5f", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,bf82f2f6-9af8-431e-a296-0890dea48ba7,d015cae7-a905-49e4-8823-6dec069689a6", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,EfdMcnfHt0mr0PQ_maaYOg,DRJcNhkqvkKFEaZpOguR6w", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,6d703981-2734-4c45-8983-cdd5f328902f,a0271cd0-e172-4d3f-aa2f-9806f21a82d2", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,9c03f381-7357-4d0f-8cda-8737d4c43d25", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,48abd952-214f-48b2-a8f1-1146f6f69aa2,b78008e8-abbd-4ece-be34-9a911f721fcc", + "14ef6b00-debf-4a85-9755-b4d37df496d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "14ef6b00-debf-4a85-9755-b4d37df496d3,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,018a5c07-e956-457d-a540-a5e2cd68da09", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,018a5c07-e956-457d-a540-a5e2cd68da09", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,018a5c07-e956-457d-a540-a5e2cd68da09", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,6dd95046-acf8-42fe-ab78-80a334096a9d,56a92c8d-af53-413e-929e-4a9a3cfad780", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "fcfcaf63-deb4-45f7-b711-c051c9ea45cb,", + "de0ac50d-2adb-4114-bd2e-68953b13d706,", + "de0ac50d-2adb-4114-bd2e-68953b13d706,6f2e74bf-1e95-463d-97db-d5d1a53b2c28,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "de0ac50d-2adb-4114-bd2e-68953b13d706,9374bf66-2ee5-493b-8439-efce4b201904,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "de0ac50d-2adb-4114-bd2e-68953b13d706,272fe8eb-5061-4729-a7a8-414ff667a82f,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "de0ac50d-2adb-4114-bd2e-68953b13d706,0ffad843-d6c9-425a-8686-7217009c867e,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,e0397982-c2c2-4733-9a40-46e18675b5af,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,", + "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,49f5864f-9d40-497c-88c8-e87f64d41d74,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,c5deba2a-6e35-4b13-8e94-8ba5457f39df,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "6d815b35-6f68-4ed4-817d-70f141e1a571,f750de46-3758-4f7d-9709-0a84b1027009,2c8924aa-68f8-4912-9759-18992f72f08a", + "6d815b35-6f68-4ed4-817d-70f141e1a571,d66aa400-aa5a-4539-a25d-5f8ce94dc281,2c8924aa-68f8-4912-9759-18992f72f08a", + "6d815b35-6f68-4ed4-817d-70f141e1a571,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,2c8924aa-68f8-4912-9759-18992f72f08a", + "6d815b35-6f68-4ed4-817d-70f141e1a571,", + "241506f6-bf88-4b46-b5fe-513a225421f4,", + "6b9e022c-0b68-48fd-8eca-da8573c18900,d6edbc00-3c1d-4f49-8412-3ef8c7c5f4c2,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "2296ed0d-df56-4d46-b33a-aae9230a47fc,6d703981-2734-4c45-8983-cdd5f328902f,cfabdefe-0890-436e-b2a3-b5c712e22955", + "2296ed0d-df56-4d46-b33a-aae9230a47fc,830be2fa-60a5-48cc-931f-34b670eae4bd,cfabdefe-0890-436e-b2a3-b5c712e22955", + "2296ed0d-df56-4d46-b33a-aae9230a47fc,bbfa08e3-8e6b-4e0f-b264-1b398d7cd44a,cfabdefe-0890-436e-b2a3-b5c712e22955", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "6b9e022c-0b68-48fd-8eca-da8573c18900,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "6b9e022c-0b68-48fd-8eca-da8573c18900,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "24a240f4-1574-420b-b898-a7e91f170759,", + "c45ed7b8-99bd-4a4b-a9ff-e16edf5d7a18,", + "14ef6b00-debf-4a85-9755-b4d37df496d3,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "14ef6b00-debf-4a85-9755-b4d37df496d3,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "14ef6b00-debf-4a85-9755-b4d37df496d3,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "14ef6b00-debf-4a85-9755-b4d37df496d3,", + "14ef6b00-debf-4a85-9755-b4d37df496d3,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,", + "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,4828b50c-95b6-466a-bb25-514891d78202,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,d6823e01-69f0-4f85-b94a-74894356a2cf,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "09177621-9ecd-4f6a-b6a5-64490139141d,", + "95ab7a7c-c35d-4da5-9955-0921064470b6,", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,018a5c07-e956-457d-a540-a5e2cd68da09", + "62ce4109-8dee-4895-bf1b-bfa143db4c7e,", + "62ce4109-8dee-4895-bf1b-bfa143db4c7e,cd5d7285-202d-42d0-b93f-04245875793e,0f36bb97-c61b-4281-929f-ff1d0d11be86", + "62ce4109-8dee-4895-bf1b-bfa143db4c7e,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,0f36bb97-c61b-4281-929f-ff1d0d11be86", + "8b9f1413-e786-4a30-946c-9292f207875a,", + "5cd08cfb-c729-4c30-96d9-6a99bb934d91,", + "1a71064b-794f-40fa-9109-8ad36602b6e1,", + "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,761a3193-60f0-4190-80c7-285b8192e794,91a451c1-b285-4c48-b14d-59ded8cc006f", + "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,a819f49b-6c7a-49d3-9e6a-d9d79ef5019f,91a451c1-b285-4c48-b14d-59ded8cc006f", + "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,64850553-cdfe-455a-ac00-dafbe63d613e,91a451c1-b285-4c48-b14d-59ded8cc006f", + "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,", + "71921831-ba6f-408b-a00e-2fd97663636f,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "71921831-ba6f-408b-a00e-2fd97663636f,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "6b9e022c-0b68-48fd-8eca-da8573c18900,", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,0iSsaY-HgkmLaRHCn5vEdw,PioQ0o3yP0a6szPZ4EKs2A", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,jGj28vhq8EGwP2RuM074aQ,PioQ0o3yP0a6szPZ4EKs2A", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,kmj5zOjcwku_WWKroCeiVQ,PioQ0o3yP0a6szPZ4EKs2A", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,FAviMCQ_EE2Mpt6QPo5OEw,PioQ0o3yP0a6szPZ4EKs2A", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,MFrcSQ1DYUm8imvy4ypgvw,PioQ0o3yP0a6szPZ4EKs2A", + "de0ac50d-2adb-4114-bd2e-68953b13d706,05ac07e1-67f0-486c-abf5-a62866475abb,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "0088603e-ec3b-4478-8694-e6fb1989b3f2,", + "ffea7a65-613f-4835-921e-6dd15f357b7e,", + "45f5e714-8a5f-4385-a97f-675066167011,", + "9bf5d259-7774-4cbe-a90f-7f188cc0dce7,", + "a6cbfe76-534a-4655-a8a8-3fed13d001c7,", + "CTcrvbo3OEepIV4oW8bx4w,", + "-twtjyBdQ02EAdOfBGTiEw,", + "45eaab67-19c2-4601-8f80-3565a4dceba4,", + "c855dcc3-96cb-470d-b159-d37a025a47d1,", + "d7730a9e-78a1-4356-bc09-6b066615850b,", + "8c35c804-e8d5-49d2-8d5a-ea19fb70bfa6,", + "5beeb4c4-f276-4eae-87aa-9302e45b05b7,", + "b6rLwzD4NkKV7xKn9ZYVkA,sxUE0iOSZEmezm54T7xI3Q,tlpa7195x0CkmSjpR1RArQ", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,", + "b6rLwzD4NkKV7xKn9ZYVkA,D_Xmo0rOzkS-kgq1CYXt3g,tnCJp2eDI0SwjVfJMhk3LQ", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,knXPidb-Rkayfc3kSHfZeQ,1yMyo6oTjU-VAygoeWaohQ", + "88b6ddeb-a455-460d-91d9-a4569ef6903c,", + "0abb6b08-20ce-444f-879e-0d1344df096c,", + "9b5bde11-7408-4798-9fcb-c7ec175444df,", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,gsHBUqA1gE2XNTPA3VvBLQ,FwjsTo0nb0uNU6l8XvllNA", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,JUQn0_PLIkytCrkAg0FflQ,FwjsTo0nb0uNU6l8XvllNA", + "12ec292f-0ccd-4c8a-a49c-3598f827659d,", + "12ec292f-0ccd-4c8a-a49c-3598f827659d,dBR1uNgk206eVoWntVzVAg,rL9p5JY83Eyxj00Dieg7oA", + "12ec292f-0ccd-4c8a-a49c-3598f827659d,zPGCmxM_b0yTt7tqMaomcw,rL9p5JY83Eyxj00Dieg7oA", + "b3b59410-d3df-464b-a75b-500a9e5ea154,", + "b3b59410-d3df-464b-a75b-500a9e5ea154,El0s9F6cfkywcKOkqwbJtQ,_o3mI1mjE02vekPVyJg8Aw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,N0fBxjrCekO-HUqmc3MufQ,_o3mI1mjE02vekPVyJg8Aw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,J9cK9BKtHUinG-DwAoPi3g,_o3mI1mjE02vekPVyJg8Aw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,-VF3K4miGE28LxMgqVehRQ,_o3mI1mjE02vekPVyJg8Aw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,pQ_3mZI7yEaxMw9n_Fr4Sg,_o3mI1mjE02vekPVyJg8Aw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,TpFWTdEW2kqtpvc0sPv-dw,_o3mI1mjE02vekPVyJg8Aw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,vxZVeH2b_E6XlqyqCW0hBA,_o3mI1mjE02vekPVyJg8Aw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,oNoEl-nDqk-HLsfz1538oA,_o3mI1mjE02vekPVyJg8Aw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,Aex0Zh1RQ0u9oTDFF1cFIg,_o3mI1mjE02vekPVyJg8Aw", + "b6rLwzD4NkKV7xKn9ZYVkA,DIeasJUIj0KNetAzPzuIrg,o3Q2gADhh06dgaipkkdMsA", + "b6rLwzD4NkKV7xKn9ZYVkA,G6Udm6kMdE28vmRY5KPanA,Fw79pqDPP0C8H07AnTY-Vg", + "b6rLwzD4NkKV7xKn9ZYVkA,3a1oW3_MY0e4lDN2V1X4ig,za8Mz14Ww0KtC6wavUPXdg", + "b6rLwzD4NkKV7xKn9ZYVkA,sf962XJ0tkis0svkMkZCXQ,4bI7QRvdnEansPkwx8Di9A", + "b6rLwzD4NkKV7xKn9ZYVkA,tm7Cnd3LzkqWHA1JMrgoeA,nbLr3c3lOkiWhNg9IYbt6Q", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,t2fOeVWloUOKDlIaf4Sxnw,ynPJe7asTk-x_GtcEBQbgA", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,3JrW3FFUvEe6HZ9VjOVjMw,ynPJe7asTk-x_GtcEBQbgA", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,OT5APcpbpEiMNNah8n4Txw,ynPJe7asTk-x_GtcEBQbgA", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,NjCLn5A5h0-V8jB72etCNQ,ynPJe7asTk-x_GtcEBQbgA", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,jeEWPFvzaEGmXaUrxTFvwg,ynPJe7asTk-x_GtcEBQbgA", + "edc7888c-dc42-48ab-8815-884d227760af,", + "6e48363d-cc62-40a8-87f9-a69fae90c002,", + "0d1ae26e-aaa4-4914-ad70-c9ab8cfabc61,", + "04e348b7-107f-47b4-9331-ae3b9fa184ca,", + "ec474314-fd79-441d-852b-d8999274fe57,", + "36b2ccf4-4f42-4dc4-aeaf-f3d10b2a6d30,", + "37f7b94a-92ba-4f5e-b94c-de00acfa4722,", + "4eaaad39-aa44-4791-8b67-2eafa8305850,", + "9bf75b23-84e9-436b-bbb8-4180ccd86b94,", + "2822aa50-4344-434d-b46f-fd9443f865b9,", + "9c20fdd5-acf1-426c-b85a-2220ceee9472,", + "b6rLwzD4NkKV7xKn9ZYVkA,PIwziHk1BUuYkAf6kuK6wA", + "b6rLwzD4NkKV7xKn9ZYVkA,PRLBrEEZhUumm3VnYVJP6Q", + "b6rLwzD4NkKV7xKn9ZYVkA,Xt3Ndm8mC0qsLClPIbgwUw", + "b6rLwzD4NkKV7xKn9ZYVkA,MfF0wizDjUa7RUE-AVJpvA", + "b6rLwzD4NkKV7xKn9ZYVkA,LovycnPHoUiP_N9w7BzsnA", + "071f179a-dde2-407f-92b2-f0c16fcb4e39,", + "071f179a-dde2-407f-92b2-f0c16fcb4e39,B3jdXiOJs0WO2ytm3-hXPg", + "071f179a-dde2-407f-92b2-f0c16fcb4e39,VudVdOAjZUibHmtFeNkvSw", + "071f179a-dde2-407f-92b2-f0c16fcb4e39,5z3jZhYvq0CGmdJ9IcUUsg", + "071f179a-dde2-407f-92b2-f0c16fcb4e39,_T4zEki_wk-UIPdhSaBr_Q", + "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,", + "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,nPenrNSTE0mDK5cWhxtEug", + "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,a0A1q_e1q06qquiu2KaZhA", + "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,rwESUxIrQEKA2hdQpcaPaA", + "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,M_BhFve7SkC7IJoymLiCLg", + "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,0zldWzi4H0OQwsWPt6gl-w", + "071f179a-dde2-407f-92b2-f0c16fcb4e39,EgEoJzG7A0aJ92qfInbX7g", + "071f179a-dde2-407f-92b2-f0c16fcb4e39,OlfQDuz1NUOr88AL2n0jXg", + "071f179a-dde2-407f-92b2-f0c16fcb4e39,NETGR8iysUyxcZM_oGmrwQ", + "9db35412-7a08-49dd-b80d-59c34a4616ea,", + "9db35412-7a08-49dd-b80d-59c34a4616ea,Rh-RaEHap0e858QeTtCqJg", + "9db35412-7a08-49dd-b80d-59c34a4616ea,hgLq0WyUAUSl2sboUrpTKQ", + "9db35412-7a08-49dd-b80d-59c34a4616ea,M5uFQQpoQE-5CKMJnDv_LA", + "9db35412-7a08-49dd-b80d-59c34a4616ea,JzLPlVNG20G9mxG_4PF8Fg", + "9db35412-7a08-49dd-b80d-59c34a4616ea,VuMDnpnK50mzgUMwJ4F6Jw", + "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,", + "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,gLCis5TFxEyRvvZ0PZq_Jw", + "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,1WBsT99QM0anps6ZoWE59Q", + "c414588b-29b3-4a1c-ae9e-7ad251901429,", + "c414588b-29b3-4a1c-ae9e-7ad251901429,jcG-vMduWEq14vo2sKYdWw", + "c414588b-29b3-4a1c-ae9e-7ad251901429,s-QTVT5eTECKHL9FnHFCAQ", + "c414588b-29b3-4a1c-ae9e-7ad251901429,irw2uj4c2Eil0ndPCTqodQ", + "c414588b-29b3-4a1c-ae9e-7ad251901429,UnHAtXkd2kCdamzw6B3Mvg", + "c414588b-29b3-4a1c-ae9e-7ad251901429,sO8e2epLrUud243Xi9odXg", + "c414588b-29b3-4a1c-ae9e-7ad251901429,pPZ3sDw3B06mKiYsAiAt9w", + "22a857bb-270a-4278-adbd-2df41181b36f,", + "22a857bb-270a-4278-adbd-2df41181b36f,3cPbhpDX-EqDQy6eN1E5Ug", + "22a857bb-270a-4278-adbd-2df41181b36f,iCVglqDJrkKB36za9UiUxg", + "22a857bb-270a-4278-adbd-2df41181b36f,gEuE-X67b0-q5C0Tv11_nA", + "22a857bb-270a-4278-adbd-2df41181b36f,gk_mQ-jsakmxhZYYNlRl_w", + "22a857bb-270a-4278-adbd-2df41181b36f,0p_eXn2gXUKrshSEmrQx6Q", + "22a857bb-270a-4278-adbd-2df41181b36f,fAmEEm6ziUO2ZXTscQWtSg", + "71fd5353-b439-4a23-b471-bfb99b8154d7,NI8a4j9fl0egSrLbrMTn7Q", + "b6rLwzD4NkKV7xKn9ZYVkA,kBaeiw5WG0OHOIsric74Ag", + "b6rLwzD4NkKV7xKn9ZYVkA,EkA09Hj2m0u3c5STynn3Bw", + "ea254491-116c-41c4-a9ca-1fa1fca76374,", + "ea254491-116c-41c4-a9ca-1fa1fca76374,xn8Or1kV-EGhpOqnYj6_0A", + "ea254491-116c-41c4-a9ca-1fa1fca76374,1n-ekJos-EeViiz_zG4jEg", + "2e7cbdb0-cf22-4938-964f-acca57d221b7,", + "2e7cbdb0-cf22-4938-964f-acca57d221b7,aEsXeTI_SEOclbm82n17tg", + "2e7cbdb0-cf22-4938-964f-acca57d221b7,pZDvYABTN06KdJ5Ck60XLA", + "b8e485d4-b5c2-480e-908c-d9b500375759,", + "b8e485d4-b5c2-480e-908c-d9b500375759,7ZRNVpxhWk-j8R3oj0oVXg", + "b8e485d4-b5c2-480e-908c-d9b500375759,8WJI7RAuaEy4KvQiQhoKBw", + "6d55261b-5434-49d4-8057-f4c748f1a238,", + "6f47500a-92d2-42d4-8dfa-42a815c43c02,", + "ff89985c-db80-45f6-9c49-9b7478c38ae0,", + "8c3885a1-b1b5-407e-98e3-4d909724b6c6,", + "2c3773a5-a0b4-41c5-a4df-6572dff516c9,", + "FHKZvPFMLkO8iAIPPQzu1A,", + "FHKZvPFMLkO8iAIPPQzu1A,PEGqjoxP6E2yRx_A-HphrQ,I_OF1c0YWESMwxJr9sXYOA", + "FHKZvPFMLkO8iAIPPQzu1A,K17dIz_TRUq5D50NBfLjyw,3drRcfyKkku0qGwCy16m9Q", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,BLgkgLeS-EyazgPQxGcavg,Rk18_7icF0Srgh7M1_G-hA", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,ZvPidQ57rUS50Qz89ERmLw,Rk18_7icF0Srgh7M1_G-hA", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,eb3SmJTIh0628LrZUkI4KQ,Rk18_7icF0Srgh7M1_G-hA", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,Yr0mF1EWzEaIyRMOWFiY6w,Rk18_7icF0Srgh7M1_G-hA", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,QzjakcO8tUaPeiI5KBLKtg,Rk18_7icF0Srgh7M1_G-hA", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,NpvUosaJTECohTIAR38egg", + "4b51679e-96e4-4a11-96fe-1d2754d12481,", + "383d4bb7-1ffc-4b40-9b7e-cfeaf9ac5fdd,", + "3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686,", + "bu9tb-K7NEiocvZY3o7ukw,", + "bu9tb-K7NEiocvZY3o7ukw,ZpcM5H2Q1UCt0hh82nSu9w,tqyzKPdDwkWq3jfZ8jZdaA", + "bu9tb-K7NEiocvZY3o7ukw,7B7vpnMTfkGLcMdHCZ_kWA,tqyzKPdDwkWq3jfZ8jZdaA", + "bu9tb-K7NEiocvZY3o7ukw,nO-YacKU1kmLjTFAAqNeYQ,tqyzKPdDwkWq3jfZ8jZdaA", + "bu9tb-K7NEiocvZY3o7ukw,76CA_KCr806y1FA35m-YTg,tqyzKPdDwkWq3jfZ8jZdaA", + "bu9tb-K7NEiocvZY3o7ukw,u_H8YUV4MEGNGh5h9ckm_w,a3OlD7iI_EqurYq_RIQCvw", + "bu9tb-K7NEiocvZY3o7ukw,1DQi3Wt8ukK2gWvl0zJnkw", + "17236d76-e3b6-42f6-9179-6fcf0479436f,", + "17236d76-e3b6-42f6-9179-6fcf0479436f,6t2rCb6WWEW6pJbcjsAFCw,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,zu0Jk24WTkan6XWSwGU1Uw,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,i1XbjBqTu0CILIuJClWimA,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,3frH6KxZ9kGUYkjeCDpylQ,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,Lf4FjROSAEqXedFxpXFQfg,B-sIpWNOfEigMeQF-WVR1g", + "AIpF1QUeMUWytpHYBDBAkg,", + "AIpF1QUeMUWytpHYBDBAkg,grutFwJD_0Cb3t6fddNg6w,oSaMpc5rTE-U6yhjtPWQgg", + "AIpF1QUeMUWytpHYBDBAkg,EbVDqEJQOk6j0UvX3t-8rQ,oSaMpc5rTE-U6yhjtPWQgg", + "AIpF1QUeMUWytpHYBDBAkg,C3ll7uPfUkeZ1QLX80_BpQ,oSaMpc5rTE-U6yhjtPWQgg", + "AIpF1QUeMUWytpHYBDBAkg,JS7aa4QSUUOpUYPqRZEb5g,oSaMpc5rTE-U6yhjtPWQgg", + "AIpF1QUeMUWytpHYBDBAkg,rwojzpr0E0ylG6_Vv_MVaQ", + "52VsI_lKlkSUAOJlfDnyrQ,", + "52VsI_lKlkSUAOJlfDnyrQ,HncURj5yi0ykk2cVyCZ4Sw,MV1HW2x43kG4rrhyoC6mmw", + "52VsI_lKlkSUAOJlfDnyrQ,42ZNGHXMvU29yo9s9tVpOQ,MV1HW2x43kG4rrhyoC6mmw", + "52VsI_lKlkSUAOJlfDnyrQ,VDo682NvSUa8xYn8zTX-AQ,MV1HW2x43kG4rrhyoC6mmw", + "52VsI_lKlkSUAOJlfDnyrQ,FvEjo9let0e_Sr-kWq5AvQ,MV1HW2x43kG4rrhyoC6mmw", + "52VsI_lKlkSUAOJlfDnyrQ,6c-AhDM6K0iHbNMf4BNiUg", + "71afc215-3d18-4f36-a114-f11c453e46d2,", + "71afc215-3d18-4f36-a114-f11c453e46d2,O38XfZ3gh0-ofqu36y3mAg", + "71afc215-3d18-4f36-a114-f11c453e46d2,BPp9SkUpsE2PZlStZrIDIg", + "b07a3964-7a53-4467-aa85-ec19cc27a57b,", + "5ed0a62a-19b5-42c0-a255-a2e83f063d04,", + "b841c65f-ed5e-4d2a-803f-48939b909100,", + "8972c6f8-6e11-49dd-92dc-7b12c417fd40,", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,cac27853-e2df-4d77-b3fd-af3bc0813f01,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,97938f4c-52b8-4c85-8575-856654666316,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,ugpS7o-ZxkCv7HAvhewZgw", + "b97fc37e-8f10-48e1-b508-a6a346be896d,", + "b97fc37e-8f10-48e1-b508-a6a346be896d,mWw1qw6jAEuZ9ygGYsHNew", + "b97fc37e-8f10-48e1-b508-a6a346be896d,AmKqwhgKAUSHt9axdxe80g", + "b97fc37e-8f10-48e1-b508-a6a346be896d,-eEKJlUIbkqHtsPJeHz19Q", + "b97fc37e-8f10-48e1-b508-a6a346be896d,LA7Kx2pBIEu07CnM-XRLtw", + "b97fc37e-8f10-48e1-b508-a6a346be896d,MocUXmOH70-h3RxBJNhYYA", + "03f8c394-28fa-4087-978b-8d108f0bd969,", + "03f8c394-28fa-4087-978b-8d108f0bd969,3f6c8bd2-2c1d-4c8c-a9de-16600fee9645,efeb07ab-6253-436d-bb66-991ddad72e58", + "03f8c394-28fa-4087-978b-8d108f0bd969,d7f0ffd3-ccf9-465e-8afe-4d7efb78dd2c,efeb07ab-6253-436d-bb66-991ddad72e58", + "03f8c394-28fa-4087-978b-8d108f0bd969,225209dc-3afc-40cf-858e-89f7afa00c0d,efeb07ab-6253-436d-bb66-991ddad72e58", + "03f8c394-28fa-4087-978b-8d108f0bd969,G7p-E7TJHkSojZtj_5pDXw", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,azbt5pgvrUS4LQwhndFeJg,cHZ4lDnSoUypIXCiRRowzg", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,6dzz5ZcGtE6bEjQHxWmo0A,2cSC290cPkCOrW5vtlVUxw", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,yXVeN9SzQUKE_6u_2KPzgw,sWgRczvpUU6ZHeoMrMD_yw", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,qzvnj7D1tUuf01HvpiIvqw", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,oIVUmOrjjUysnN4XoWvJSA", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,0wRS4Rs4ykqjJE_5Fk-Flg", + "4c032271-a0db-47b0-bfc6-4c330c2c4b21,", + "AbYdU7O_9U-Lwg7zFVZzXQ,", + "aaed6385-0ba5-45f3-b0d2-6b0a9ab79628,", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9b35ae52-febe-486a-8557-ec7190a9756e,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,6e152cc7-e93a-4408-8092-236db420b680,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9ad37173-e640-4b9e-a474-716d482b73d2,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,lgqLGi01o02ZFC9Xs5Ofdg,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,61VZkFjB402brzWm4UsftA,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9b35ae52-febe-486a-8557-ec7190a9756e,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,6e152cc7-e93a-4408-8092-236db420b680,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9ad37173-e640-4b9e-a474-716d482b73d2,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,lgqLGi01o02ZFC9Xs5Ofdg,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,61VZkFjB402brzWm4UsftA,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,47da86b9-4e6e-423c-b8c4-ea6bf8472509,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,d5987fee-e3cc-4336-853a-5df57f73df05,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,0c433086-9501-48c4-8d0e-4474eb420c61,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,2be2f040-68e4-44dd-9099-29f11110f3a4,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,1VGZ1QCvcU6rsKVkUDBNjQ,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,8hUBfmq28EixYjv92Xcrgg,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,d1eb3573-672c-4bb4-9ec5-7c81e6a38d3d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,e548124c-d7f3-45de-aaa0-f66130eca576,6094370d-5e28-473a-b422-f3f1c75d5db8", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,7221132e-053a-4c58-b719-ed3c8558907d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,74b009f4-e128-4c59-8a6a-4c0ba6119bde,6094370d-5e28-473a-b422-f3f1c75d5db8", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,29EZ6r2SsUakWjzuevxeoA,6094370d-5e28-473a-b422-f3f1c75d5db8", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,imDGL6dhrka-KliYlfpdgw,6094370d-5e28-473a-b422-f3f1c75d5db8", + "c65a94e6-7475-4030-8fcd-9ce25852ff7c,", + "02077915-57d3-4e26-b4fe-8e0399cbb293,", + "02077915-57d3-4e26-b4fe-8e0399cbb293,8c94e45c-6605-4b4f-9ba4-2c6c6e557228,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "02077915-57d3-4e26-b4fe-8e0399cbb293,17b16982-871c-476a-860f-f6953c89ee8c,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "17aed0a0-70ed-49da-ad09-5bc4746f7718,", + "17aed0a0-70ed-49da-ad09-5bc4746f7718,14b43bf9-5ba0-484e-ae5d-55cfeb09a70d,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "17aed0a0-70ed-49da-ad09-5bc4746f7718,5c8f36c5-07da-49f4-af02-d23207a43216,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "a95a1b36-7849-4791-89be-c1a56868bfc5,", + "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,", + "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,ffUVovxlWUCgOxJyUJscIA", + "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,", + "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,9GhnX4Pj4E-wxbPlD8GB-g,YSrGwXybGEqaqHNCS64qOA", + "ae7c9d1f-d371-4dfe-a6df-d74662606aae,", + "ae7c9d1f-d371-4dfe-a6df-d74662606aae,i9rUo62Tgk-UaM9BsYRGgQ", + "88739d79-aeb9-471a-b25b-776f46bba9f6,", + "88739d79-aeb9-471a-b25b-776f46bba9f6,y_gTbnuYS0GKJmnCs2retw,hjk9OO8CW0KNGR7A6_p84A", + "88739d79-aeb9-471a-b25b-776f46bba9f6,LqBO6GM7F0mGswRqJqCbtg", + "f218e2de-46fd-45b9-9e0e-670b6bb905e7,", + "f218e2de-46fd-45b9-9e0e-670b6bb905e7,JFalLDUvm0aMm3wh1UVfVg,iQjkS1xYP0S63ltvKpkz5g", + "66bfec71-882b-4981-9d02-9c03d8d79789,", + "eacebd1a-070e-4733-a6b6-65811ef7891a,", + "e149c559-aeb5-4609-9722-52fb6acc70e6,", + "e149c559-aeb5-4609-9722-52fb6acc70e6,QF05cIuqhEeY8oNnO9CocA", + "e149c559-aeb5-4609-9722-52fb6acc70e6,FbedbSonT0izB7Q6cLXCzw", + "e149c559-aeb5-4609-9722-52fb6acc70e6,SoQerU2dOUu9BdRBAsEllA", + "8ae154ba-8e69-48da-a314-507b419d4338,", + "27fa9f11-5368-4f6c-a114-18501478a9fa,", + "546ca714-8355-4455-be44-d7987974f8af,", + "ee835538-058e-43d9-8268-b7d079563799,", + "f10f2d46-ec03-452b-9b94-0e476ccbb9c6,", + "a112b8b5-ea0e-4d23-a7eb-2daa4683a87c,", + "05809f10-98b7-4228-8828-e72cb37fe70f,", + "2036fbec-e562-43a7-bcf1-c12f0307131a,", + "9d630b9b-7f48-4f34-880c-f52bad628b25,", + "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,", + "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,a8de80d3-286a-4fdc-bed2-75d52f8c0964,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,c6cf096c-f0cb-4500-80a0-bc2916b5a426,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,FjnZ-D1hcESLOWL4Ukv7kw,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "42aa7888-f0b9-4584-9cc1-966585c6b36e,", + "42aa7888-f0b9-4584-9cc1-966585c6b36e,da185fbb-ad2b-4f30-87a2-491f24b78ebb,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "42aa7888-f0b9-4584-9cc1-966585c6b36e,bea795cc-aabd-4229-84dd-49ab4f91a445,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "42aa7888-f0b9-4584-9cc1-966585c6b36e,2APfqVnnQUaDYKi-fQg8jg,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "125a3a62-1005-47dc-9fd8-ee09ea803e9f,", + "125a3a62-1005-47dc-9fd8-ee09ea803e9f,97bbb384-b37e-4ea1-902c-88fbd6854ab5,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7fb8496f-0e6c-4e48-b8fa-5709ebbf4820,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7nRz9Jmnn066_PzngkhfFA,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "47629ed1-5378-427c-ad61-4b44d2fe90cc,", + "47629ed1-5378-427c-ad61-4b44d2fe90cc,7b82c3c5-fd1f-45ec-b83f-e32668aa8dce,556f876a-70da-4104-b792-fca8ad67be0c", + "47629ed1-5378-427c-ad61-4b44d2fe90cc,23428ca9-be75-4146-b400-df01d1dadac9,556f876a-70da-4104-b792-fca8ad67be0c", + "47629ed1-5378-427c-ad61-4b44d2fe90cc,qJGX62O_HUiZWN61NAc1Lg,556f876a-70da-4104-b792-fca8ad67be0c", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,d6823e01-69f0-4f85-b94a-74894356a2cf,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c5deba2a-6e35-4b13-8e94-8ba5457f39df,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,e13cf33d-7588-4e2e-955e-f0a29d6380b7,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c9589974-c261-485e-b571-cb2f34fb178f,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,da6d0ced-3d43-4878-a868-925f2ed6fd5b,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "c2d22328-b41a-4e53-9b1e-814e84b86d15,", + "1dbc517c-432b-4c85-8c5e-dfcf22b269cb,", + "23b97189-b49f-477b-8a22-0903b3d236f4,", + "23b97189-b49f-477b-8a22-0903b3d236f4,GfC01_o8QkGTbnGNjv_DSw,7mLb5jMJMUug7Nzwxv6D4A", + "23b97189-b49f-477b-8a22-0903b3d236f4,rFCKF2ku1UWj-TF-VClPRg", + "23b97189-b49f-477b-8a22-0903b3d236f4,zld80uf3y0uu__Yejms-3Q", + "57e05822-ca63-4726-b3a0-260b6ece7bd7,", + "57e05822-ca63-4726-b3a0-260b6ece7bd7,c2e62c10-3120-4991-9aa7-3f3e2cd29054", + "57e05822-ca63-4726-b3a0-260b6ece7bd7,b56bf738-4c41-4169-bee5-b3e36db6d6a9", + "57e05822-ca63-4726-b3a0-260b6ece7bd7,96585250-512f-4559-9526-1ef77356a72e", + "49800740-33d6-4280-aefb-3497597c6366,", + "49800740-33d6-4280-aefb-3497597c6366,0oMo4q5cLEi9LGpkOFIlkA", + "ed4439c6-edad-4239-934f-79f9cc816c0e,", + "ed4439c6-edad-4239-934f-79f9cc816c0e,7ykIpgU8Tkmmq_zzpjTVrg", + "ed4439c6-edad-4239-934f-79f9cc816c0e,iqYc9qtbZEig8jl_ZqKe6w", + "764b4c4e-1009-4772-b01f-7b326dd77e8a,", + "764b4c4e-1009-4772-b01f-7b326dd77e8a,93vga8NJQEe5nWdqrroAaw", + "62e480de-27e9-4719-adac-360698c7a2c1,", + "879c43e8-c877-4c8e-a5c9-100123e7a102,", + "7d6cf363-76ba-45c8-91f1-cf4434c31b36,", + "1a0319e4-24e7-40b3-9976-70bff2c7ff23,", + "07023692-97e1-4a65-a881-64ca6b4ae08e,", + "e48955ae-50e0-4f55-93a1-611150142331,", + "dcf9012d-f9e9-4f15-872e-f130af3b2efa,", + "dcf9012d-f9e9-4f15-872e-f130af3b2efa,631c9015-2a00-47ea-bb35-465a2ddbca5d,83158223-e268-4036-8a6a-218a8430eb2c", + "dcf9012d-f9e9-4f15-872e-f130af3b2efa,7f9cb939-36a4-40da-8337-025e1d7bf8e5,83158223-e268-4036-8a6a-218a8430eb2c", + "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,", + "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,a5925738-a451-4f25-b622-ccbfa12bd88c,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,c64ac7a9-7fd4-4587-8626-0413a190b706,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "ce81210f-8eb8-4cd2-95d5-046d4da229c8,", + "ce81210f-8eb8-4cd2-95d5-046d4da229c8,bb01b51e-a95f-441e-8c52-abc3aeed7145,f7c7d05f-5454-445a-af38-25f110bd204a", + "5687a39e-e5d7-40e4-9b32-190754fc6292,", + "5687a39e-e5d7-40e4-9b32-190754fc6292,km94i3lHJUWfjKgHrV-mHQ", + "7b7574f3-0b0a-4954-a644-6473ddb86424,", + "d85c010f-891e-4ce2-a9b8-ed68874da228,", + "0952eb17-9779-490a-98fd-af734a698c76,", + "0952eb17-9779-490a-98fd-af734a698c76,uQcf_scBfU-XAii9HExRfQ", + "f23e86eb-8b59-4b32-a72a-5d382568e399,", + "f23e86eb-8b59-4b32-a72a-5d382568e399,wuCrGxBRQUa5BLwx1nASPw", + "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26,", + "b17591ba-3c2d-4309-ab01-b6743424e07e,", + "b17591ba-3c2d-4309-ab01-b6743424e07e,l9cF1s3B8EC6BGlQ4Tx1cg", + "8f137b1f-4125-4657-85c4-bcdb74d3b0c2,", + "14ef6b00-debf-4a85-9755-b4d37df496d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "14ef6b00-debf-4a85-9755-b4d37df496d3,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "14ef6b00-debf-4a85-9755-b4d37df496d3,nZor-clxlkm8EO9Gco0Otg,r_GLekt6mkWxXw8zPPR8Mg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,M8IYDK6aTUyn5mRhr51GtA,r_GLekt6mkWxXw8zPPR8Mg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,yxWEn-oaYE2YvvnrTVV5WQ,r_GLekt6mkWxXw8zPPR8Mg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,XLYLo5qgW0eociGDgQ16PA,r_GLekt6mkWxXw8zPPR8Mg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,Vk1n941ZgkKrhrL9mh18oA,lxmw7EbzsUuoy0roRy5McQ", + "14ef6b00-debf-4a85-9755-b4d37df496d3,mdSSJyipq0KvjDNQ2NyUaw,LQheTa-EvUaAonSyoWNmXg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,nzFUAOCAFUCd9S-x4EKRwQ,Qx7nrZH-K06CgIoJYfFCCQ", + "14ef6b00-debf-4a85-9755-b4d37df496d3,xPs8BvGgdUC1i_luWAfSFA", + "14ef6b00-debf-4a85-9755-b4d37df496d3,cfiCxhJZ20u07X1x93tNZQ", + "14ef6b00-debf-4a85-9755-b4d37df496d3,VB_2uEBs70-9MnLn3__upw", + "ee9a7c8a-7383-4a09-b44c-474b66f9008a,", + "ee9a7c8a-7383-4a09-b44c-474b66f9008a,W-xMu8n2CUuHSXfHvEL4yw,-apO3Y7om0GKhC4V_hVYdQ", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,cpTKHXjj0kulivZCKVQqiA,Mrf17aidXEeCF-fFURUUcQ", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,-C1ui-T280GEfW6-vPQ-ig", + "293e7ad3-01c9-43af-96e3-06c34145ff12,", + "293e7ad3-01c9-43af-96e3-06c34145ff12,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,FVdDQFOMQ06teQm2ANDPng", + "293e7ad3-01c9-43af-96e3-06c34145ff12,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,FVdDQFOMQ06teQm2ANDPng", + "2f8518fc-c989-40de-98b4-c6f09b304166,", + "2f8518fc-c989-40de-98b4-c6f09b304166,e5de2e45-207c-46af-9a5e-eb01a0c621fe,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "2f8518fc-c989-40de-98b4-c6f09b304166,de0a5a3a-8aff-4799-8269-9efa3aeb383a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "2f8518fc-c989-40de-98b4-c6f09b304166,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "2f8518fc-c989-40de-98b4-c6f09b304166,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "12ec292f-0ccd-4c8a-a49c-3598f827659d,LIgagNf4gk68EYQdlDruxg,rL9p5JY83Eyxj00Dieg7oA", + "12ec292f-0ccd-4c8a-a49c-3598f827659d,6fmUy748hU2dgwgaijSkUA,rL9p5JY83Eyxj00Dieg7oA", + "12ec292f-0ccd-4c8a-a49c-3598f827659d,buuNFd8sK0eap78TL1gMNQ,-UFHTNvJs0CGDStb0PFkbQ", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,SDbpnjVYmEGQneK5fhZ0Cw,FwjsTo0nb0uNU6l8XvllNA", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,uwoWPY4YjEmZSQcNCrx1Gg,FwjsTo0nb0uNU6l8XvllNA", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,COZbkLDc20mdUlWSkqMX-w,FwjsTo0nb0uNU6l8XvllNA", + "348ac48b-986e-4d42-a0f7-1aea16b88271,", + "348ac48b-986e-4d42-a0f7-1aea16b88271,59a62fa3-9e15-463e-9bd5-3aecdf604c1b,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "348ac48b-986e-4d42-a0f7-1aea16b88271,4effd67c-a52f-4b41-a5b0-04287f91a7bc,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "348ac48b-986e-4d42-a0f7-1aea16b88271,ojTIShRZHEOV1qQRfhwvTA,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "348ac48b-986e-4d42-a0f7-1aea16b88271,se0-2ylLD0u6sqCVPkeP6A,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "348ac48b-986e-4d42-a0f7-1aea16b88271,RU-B71GUF0WPduoSOI1XMg,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "0736e71b-a32b-4485-bedf-6fde8fe3b09f,", + "01d0fc12-3ebe-42e1-8caa-5513a40436c1,", + "a0a38623-2003-469a-b06f-ce451413727b,", + "a0a38623-2003-469a-b06f-ce451413727b,GPX92X5n3UWMbh3C13oB5Q", + "a0a38623-2003-469a-b06f-ce451413727b,dE_ZWKPV90utoyWtRalh8w", + "a0a38623-2003-469a-b06f-ce451413727b,jIAMeDkA80GVHYTi0d9LkQ", + "336aeb24-33a1-4aae-82e6-4fdbd1330452,", + "336aeb24-33a1-4aae-82e6-4fdbd1330452,mnYzlq3Rw0-Q89gmc5f9XA", + "336aeb24-33a1-4aae-82e6-4fdbd1330452,85pvpo4QqUSrdsbofGkoWg", + "336aeb24-33a1-4aae-82e6-4fdbd1330452,f6XXbe_mhEuZJ9in5vDRGw", + "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,", + "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,oO_Ij0Vsvki4Fa6qonrf7A", + "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,IgocvfGIn0u_Jq4GP2_fJg", + "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,Q2BNGA-idUyHDPu7m-ZSnA", + "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,", + "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,PNnfMibAUkqetPThlhtBgw", + "281937b2-f994-45e9-a6d3-03d097247ce6,", + "281937b2-f994-45e9-a6d3-03d097247ce6,Hae8Pfe8mEWeF8mL9hC9dw,Vat8nL4ZBEK9mLABJiXS6A", + "281937b2-f994-45e9-a6d3-03d097247ce6,kE9vr2bKoUSFm7AkcXSmew,5o-249dUdUOhW28lrJY82A", + "281937b2-f994-45e9-a6d3-03d097247ce6,GAzH2pThjECghCmCwKJPrQ,4T-xj-xtSEiv1PDYv30URg", + "281937b2-f994-45e9-a6d3-03d097247ce6,pEWioft3gkOMWS4tZFping,gOw6NOX-QUegt4mxjtfpfA", + "281937b2-f994-45e9-a6d3-03d097247ce6,SN8HDRADZ0GvrFgEHKMhWQ", + "281937b2-f994-45e9-a6d3-03d097247ce6,Im55dSJ4mkmdmHPtX9tYpg", + "281937b2-f994-45e9-a6d3-03d097247ce6,adETWZLIA0O0feRzUjxKQw", + "281937b2-f994-45e9-a6d3-03d097247ce6,z7uu8CimMkCjsCGTDPCZLw", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,-XSM6ZcSjE-vrWL7jyF4fQ,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,RL6HkufaJ0yubTpCGVI9eQ,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,9tTuREkoQkW0ycqOjWXsiA,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,MVkY4sU5BkGC4QhweRiyIA,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,ux9DQVSRyU-_5GA-t197OA,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,t2bByt5oUEG90PMMePCP6A,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,m7KXVnULz0iHvKfCFzFoyw,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,gT0WA7Y1H0my5uMlteIMMg,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,Hd9xNn-UA0uxwv1axFVqGQ", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,C8b4lTZ-TEOlCVv7Gs219g", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,3Xlg70ev6UqdcIAmJ9hmlA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,QCOMyeCUK0Gudx4ljBmAdw", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,58YffnGSUU6umcI1i0RvZw,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,c24f1DNZjEmqkt-FibKUjQ,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,SeVmHtrMS0y7gKCYkHcrOA,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,uDcJZClcFkiNdkEEh9IIyA,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,Iezz6gCFfkKdVcmPXIhunw,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,fLWfBSEO6U6aHrDm3nKFEQ,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,F5vJzMfNB0Ob6Abjadho7Q,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,vbOUaJsGHEaSfQCivNcMWg,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,wbFPXIg5gUK3QK6YycZexg,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,_fBrHCkTKUe82QgxCyI-Kw,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,CJKB3OO2CkeEVsAK0jeeDA", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,k-WZIhNJAkSpIWLxC2D40Q", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,VE_cHMHKBE2Wuugoqj4rZg", + "7553d851-1986-4325-870d-e425dfb7d007,", + "ef7771eb-c5f3-44fd-8c27-f581b035ef10,", + "ef7771eb-c5f3-44fd-8c27-f581b035ef10,OnZujqqU_UqwxmznvBBMfg,_zrnGn1ErEWR8WqrZmKZsA", + "ef7771eb-c5f3-44fd-8c27-f581b035ef10,XKoxnTXYGkSUu52nVS9dnA,TFOAk7kwh0uUgtsyDNxMuQ", + "ef7771eb-c5f3-44fd-8c27-f581b035ef10,9nPWmpM4mUmEpHfthqYeng", + "2786fc22-3d6c-440d-afcc-15890c061577,", + "2786fc22-3d6c-440d-afcc-15890c061577,dXz9B9OH2UuLNV35u8KB-A", + "bb8602d4-f91f-4e35-9c88-1cdf3a04ff0a,", + "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,", + "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,USdUyjIzY0q5af3AeTFCOQ", + "df29d81e-851b-40b8-95ae-8461f702b1e0,", + "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,", + "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,1919d319-6cce-4500-8766-15fed6a13fa8,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,lBe3yxuE40eBmTrnEHBqQQ,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,oZ8k0Qv3SkG-DkY0_dP7UA,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,", + "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,1919d319-6cce-4500-8766-15fed6a13fa8,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,lBe3yxuE40eBmTrnEHBqQQ,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,oZ8k0Qv3SkG-DkY0_dP7UA,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "4025e4c4-abaa-4729-a2a3-a7175313e5ed,", + "4025e4c4-abaa-4729-a2a3-a7175313e5ed,1919d319-6cce-4500-8766-15fed6a13fa8,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "4025e4c4-abaa-4729-a2a3-a7175313e5ed,lBe3yxuE40eBmTrnEHBqQQ,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "4025e4c4-abaa-4729-a2a3-a7175313e5ed,oZ8k0Qv3SkG-DkY0_dP7UA,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,7_n11PF4-0KqTZ9az23hqg,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,TKbjEeTWfUyA1u4laoULMg,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,JShcoWWPz0297HeEUJbDCQ,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,_psc7WCgOk2JzRQwuFAqNw,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,WeP_tPjUNUuGojXrVBOAIA,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,8xqL--ghKUunlS0r4ugCgg", + "71014872-d879-4858-9d68-58da3c673d8b,", + "71014872-d879-4858-9d68-58da3c673d8b,83522c12-9549-4d74-8c87-bae210bcd2bf,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "71014872-d879-4858-9d68-58da3c673d8b,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "71014872-d879-4858-9d68-58da3c673d8b,XNq4Cns4lEGydtktdsXlmQ,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,0XPd5RzNJEepHztqwAznHQ", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,XNq4Cns4lEGydtktdsXlmQ,0XPd5RzNJEepHztqwAznHQ", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,83522c12-9549-4d74-8c87-bae210bcd2bf,0XPd5RzNJEepHztqwAznHQ", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,qspNifi92keI-XssPB5qDA,RVraJOPsOUqZjd8bEiPduQ", + "843e3364-a743-4dfc-a990-0436e47b8c10,", + "843e3364-a743-4dfc-a990-0436e47b8c10,c5884778-a87f-4612-9717-86fbb65d440f,1695608b-e9cc-4a03-b56d-0f09d2804379", + "843e3364-a743-4dfc-a990-0436e47b8c10,83522c12-9549-4d74-8c87-bae210bcd2bf,1695608b-e9cc-4a03-b56d-0f09d2804379", + "843e3364-a743-4dfc-a990-0436e47b8c10,XNq4Cns4lEGydtktdsXlmQ,1695608b-e9cc-4a03-b56d-0f09d2804379", + "n52um7A-b0utGxixd1ESvA,", + "_qXaNbm4yEeyalo372QbvA,", + "VQnoLstCyEGDPy1c38-GvA,", + "dfffe31f-9f82-4622-a920-ea5f5bd26042,", + "308171c3-cfd1-4ff2-8593-856e8de7071a,", + "308171c3-cfd1-4ff2-8593-856e8de7071a,mM7I9kDTx0ut243UkWKXvw,YKaJjmMHo02FWwem8nw1yw", + "308171c3-cfd1-4ff2-8593-856e8de7071a,46f7Ewe99E6tF2qFaMtQ0g,8IK1U23XlUOV-2ibVznkDQ", + "308171c3-cfd1-4ff2-8593-856e8de7071a,R0f3hHGY9UqUEW4aR1fssA,KSqfmVTiGUKOKijwGDSwgQ", + "308171c3-cfd1-4ff2-8593-856e8de7071a,adwzLAQEykK8D5hZGQ6yiA,0Nca5nBUpUquFhExd5dxcQ", + "308171c3-cfd1-4ff2-8593-856e8de7071a,ZsWc8wRafEGl3B7cr6UkzA,ByIfwCtDwU-p9rEfqpjfGg", + "308171c3-cfd1-4ff2-8593-856e8de7071a,kQD41xK4dU6dI_6pRkP0qA,aSeusgYlPE6vOBu9Ua_dWA", + "308171c3-cfd1-4ff2-8593-856e8de7071a,ro4sD9txVE29_eT2jK-wEg,0YF5bACMsk2sN4tixbJbKw", + "308171c3-cfd1-4ff2-8593-856e8de7071a,-YsxhNXLAk2qbeXMqwAqng,aNOhXS4nuUeqs0CVw6K7Hg", + "308171c3-cfd1-4ff2-8593-856e8de7071a,imOiYzABkUm9C5EVzvjXgg,0eZVIsv7wUyZ0ohLmF1jGQ", + "308171c3-cfd1-4ff2-8593-856e8de7071a,FgLAEMh1e0GyHO8J8_xj5Q,iM-04HpG6k2N8n-P3XS22w", + "308171c3-cfd1-4ff2-8593-856e8de7071a,C9CZUwEZeUiuOue4CK4luA,gUxl7WNXlUqFYHZ9eGq1cg", + "b861e5f3-fc6d-43b3-9861-c1b45cb493a8,", + "6930ce13-4be4-4ab9-9817-667bd261ffc3,", + "abc25091-ed5f-4c72-9364-fffeef1bc239,", + "34fa5014-e516-42a1-aaa8-91d2b6f59727,", + "34fa5014-e516-42a1-aaa8-91d2b6f59727,fWZekjhIxUWhxgVGTWrVsg", + "34fa5014-e516-42a1-aaa8-91d2b6f59727,a7477071-b04f-4b9e-9fcd-bd44991e5f0c", + "943d2fc7-a769-4195-b147-a765e6c274c5,", + "943d2fc7-a769-4195-b147-a765e6c274c5,BizZfhG8ikOOEvAxN6w48A", + "943d2fc7-a769-4195-b147-a765e6c274c5,KfjLWNUbpEClo_qafEfUEw", + "943d2fc7-a769-4195-b147-a765e6c274c5,zJKd2ZtIsUGCdHP1M0215Q", + "943d2fc7-a769-4195-b147-a765e6c274c5,Adyl5MkwQEKK_MT5AluYkA", + "943d2fc7-a769-4195-b147-a765e6c274c5,xtgcJ8klHUyAYRntKhwCzw", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,bgWM6wBe60q2mIHN5dPh4Q", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,cqYUoDpfkEi8Qp2DrpQytA", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,lRmNfk8vuEe4Z5ToWvWcTg", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eQy0qwre-Ui7_sCCNuWCSw", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eU2ePFNmbUCPoQUp1WCbzQ", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,csLWYuunH0K9Ij1q1FoBsg", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,Zf0z5Ghkwk-QJJq4u7Fjww", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,sP8lTqgdk0WyuwQczT_ijg", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,dudShaXMYkaIZdZfyleoXw", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,3EOUuzxLSkSopjIohgQ-zQ", + "de0ac50d-2adb-4114-bd2e-68953b13d706,_ycDQ6RcyEGAntVlSDpPeA,6UBZuZQBG0eZtNmty2IGdA", + "de0ac50d-2adb-4114-bd2e-68953b13d706,7WrSbK4Ml0K2z-ig5HIUCw,9skD_xxLq0mpfP13f9n8hQ", + "de0ac50d-2adb-4114-bd2e-68953b13d706,uCA4tnv9Mk2mX-NnFlj2Mg,NvlKJFz4IkKDliVOJKJYhA", + "de0ac50d-2adb-4114-bd2e-68953b13d706,YlgQOY1VJUuGzYxoTYAqGg,jhQ23U8Pmk2K2wfBdrEWNA", + "20b424a9-861a-4d39-a583-22d7b7348c14,", + "20b424a9-861a-4d39-a583-22d7b7348c14,afnIqin4lkKKqWrvBvVagA,bCuW1YadE06p6v1dBHsx4w", + "dedd2e98-2721-4de6-92f2-9d399f34e8fe,", + "dedd2e98-2721-4de6-92f2-9d399f34e8fe,2jj5lxx6Gka5U6j0cWjN5w,HQnd9m0Wi0upaXRNsE5HPQ", + "4e42e02e-33fd-4ff9-b74e-9bde26253c35,", + "5f2992a0-08a2-4638-83a2-11658c51681c,", + "5f2992a0-08a2-4638-83a2-11658c51681c,bpzKQ-B4_0KpMPf4ixTumw", + "5f2992a0-08a2-4638-83a2-11658c51681c,14Gft1zCQECXPxPmiUc5VQ", + "5f2992a0-08a2-4638-83a2-11658c51681c,7ziZgKKqXkuyo9v77chIlA", + "5f2992a0-08a2-4638-83a2-11658c51681c,hKkdXrgEQ0SStVnZsXu0uQ", + "3b30adc7-a1ad-46ff-96ff-08e656396c23,", + "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,", + "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,szDKGCS30UCwXJJcVocFDQ,3v_zUVDTUku_iEf-olDk1w", + "e773fddc-ad9f-408c-82c5-2637f75b3214,", + "e773fddc-ad9f-408c-82c5-2637f75b3214,Q3ctsvvDDEy_S1TDiGdf6w,DobTmYhC7EigHmvYnCTm3w", + "e773fddc-ad9f-408c-82c5-2637f75b3214,2N0meQR4GEGu9Yy61C0Lwg", + "2ddd2c2b-596c-4022-a488-655b0db1c265,", + "2fadddc1-babd-4cb3-8f5a-0e7c889c3785,", + "9ffa6e06-5200-4e53-ac13-f8491aecc864,", + "1e504fee-b593-4037-bd8f-b88025147991,", + "1e504fee-b593-4037-bd8f-b88025147991,c9589974-c261-485e-b571-cb2f34fb178f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "1e504fee-b593-4037-bd8f-b88025147991,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "1e504fee-b593-4037-bd8f-b88025147991,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,", + "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,YBpBFNAFGEunM5GX5lFxHg", + "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,VyqdOGaUnEa8WzccUIn48w", + "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,Dvwz4YRu_ES_rl94y6FNBA", + "CQuKKNpan0O7rhR1fr14-Q,", + "CQuKKNpan0O7rhR1fr14-Q,-k9_-2onrEG407or8t1RFw,b-IM3eYMJUa40395O5XeLg", + "CQuKKNpan0O7rhR1fr14-Q,GDvaUU10-EKh3pKUQ9tnag,b-IM3eYMJUa40395O5XeLg", + "CQuKKNpan0O7rhR1fr14-Q,kLyM-6otSUy2ql5SEmnF9Q,b-IM3eYMJUa40395O5XeLg", + "CQuKKNpan0O7rhR1fr14-Q,8xjU4GQ4OkaqGyad-ypwtw,5RUG6i2Mx0-55IO-IlhUdQ", + "CQuKKNpan0O7rhR1fr14-Q,032pE8s9Ik-2CImFVDrSqg,5RUG6i2Mx0-55IO-IlhUdQ", + "CQuKKNpan0O7rhR1fr14-Q,M4765UHzsUeNTwH-Pcxp1g,5RUG6i2Mx0-55IO-IlhUdQ", + "CQuKKNpan0O7rhR1fr14-Q,FC8ZFBPt4EyTe7IYbv2Tpg,5RUG6i2Mx0-55IO-IlhUdQ", + "CQuKKNpan0O7rhR1fr14-Q,ghM-uFq5-0Ci38AOHa9zZA,IWg9wuda4kaCilJk8JQpTQ", + "CQuKKNpan0O7rhR1fr14-Q,Gtq8yRlt1UeFW8U-sc7FiA,IWg9wuda4kaCilJk8JQpTQ", + "CQuKKNpan0O7rhR1fr14-Q,XrSL-v2MbkK0md-FMOweOA,IWg9wuda4kaCilJk8JQpTQ", + "CQuKKNpan0O7rhR1fr14-Q,KqjQ1xRpokGLKp9GVN1EFA,VO_3cgqQqUOTz30TVUxrFA", + "CQuKKNpan0O7rhR1fr14-Q,yrQCLXiqRUSwnuQRYX2Rsw,VO_3cgqQqUOTz30TVUxrFA", + "CQuKKNpan0O7rhR1fr14-Q,yZR5aX3gQ0WpRUT6cfK1cA,VO_3cgqQqUOTz30TVUxrFA", + "GAWURQSqrEW4G5slrPMgZA,", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e69372eb-2fd8-4e17-a448-ee0be61a2c7a,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e4IDo-fuH0C9YByzcX5k-A,qCIoeLQw-Uq-SKIetIgIpA", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,W4a144so90eRzCojUnDowA,rcuK1wZcXkCe0_LGX7E-Tw", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,wF-WrDaY_kmm1AzA20RsZw", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,Q9TGwLgrgk2t8ba8WlpM1w", + "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,", + "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,Wm30oV5BtEuFQZFk3dPK_w,0R3Mh96tS0631EZYs03dmw", + "86af3a97-18d8-4dda-a2f2-cfd3b4224254,", + "988e4069-4341-428b-bbf5-023309891385,", + "ff15e25d-d467-47a8-9ecc-e415d5acb90c,", + "ff15e25d-d467-47a8-9ecc-e415d5acb90c,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "ff15e25d-d467-47a8-9ecc-e415d5acb90c,rzUd1IMTKUmW0HeMPSIOBQ,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "b904304b-cce0-466f-8569-c621eeadd4f7,", + "b904304b-cce0-466f-8569-c621eeadd4f7,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "b904304b-cce0-466f-8569-c621eeadd4f7,rzUd1IMTKUmW0HeMPSIOBQ,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,", + "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,rzUd1IMTKUmW0HeMPSIOBQ,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "b9987d0c-0cbf-41dc-9a9c-8fdb5165800e,", + "sL7iHCnjyEuQKzrTvciEzQ,", + "7VQvz9sbzUWvGalxd9rISg,", + "7VQvz9sbzUWvGalxd9rISg,YTR8qybyVkOhNh4iqtU_UQ,H8LXGRJj8UOuH5dil8L9qw", + "7VQvz9sbzUWvGalxd9rISg,zNBLaOhCAEKWvgLK4P_X7w", + "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,", + "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,97f3e262-8bac-4238-9d85-3872f293f204", + "aeda4e5d-9cfc-44ec-b325-ab8b2d906a8f,", + "71fd5353-b439-4a23-b471-bfb99b8154d7,", + "71fd5353-b439-4a23-b471-bfb99b8154d7,wZC46s7Id0CUICN_XOK-Rg,knKB2ETYmUupbvBwlleZRw", + "71fd5353-b439-4a23-b471-bfb99b8154d7,yrR3ogjJ50q6ypAnCF-ctQ,knKB2ETYmUupbvBwlleZRw", + "71fd5353-b439-4a23-b471-bfb99b8154d7,ss931x_Hy0-y1au-fT3zTQ,knKB2ETYmUupbvBwlleZRw", + "71fd5353-b439-4a23-b471-bfb99b8154d7,UQfLaKHzo0WqlRul_O61xQ,knKB2ETYmUupbvBwlleZRw", + "c6c08eb5-381a-4193-9722-80da95d62abe,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,0809a5cf-a0f3-4614-9f98-71518524518a,8212812b-e7e7-4f6d-aa27-d1112af45473", + "c6c08eb5-381a-4193-9722-80da95d62abe,31jelNWFyk6FlWt4HyPAlw,9zGwmle_yk25V-KnC_PJEg", + "c6c08eb5-381a-4193-9722-80da95d62abe,W4a144so90eRzCojUnDowA,pHw_MghIYUazSFve6L237w", + "c6c08eb5-381a-4193-9722-80da95d62abe,3svqbYmGt0uyVWnx7vJNmw,iLo1YhFY-06HNWgM6gkB7g", + "fc96ea5d-b49b-479b-a963-92f1eab44b2c,", + "fc96ea5d-b49b-479b-a963-92f1eab44b2c,opDwtb3OdESsA2Zpfx7luQ", + "ec5f5089-7154-425b-8be2-743954ea2d69,", + "c506feae-be73-437b-b18a-1152bb9906e9,", + "c506feae-be73-437b-b18a-1152bb9906e9,PTqpWqqU9k--lNlRrmlVbA,yViVKOLHc0qLiPw9Oy1Obg", + "c506feae-be73-437b-b18a-1152bb9906e9,nODTglESMEC9tBY1k-LrsA,Do__oD53x0-dNtVeHsr4yA", + "c506feae-be73-437b-b18a-1152bb9906e9,1qHXl55Qn0qSb1gAhr8Wjg,GlB_scM-5kuc4A5TmGe6Yw", + "c506feae-be73-437b-b18a-1152bb9906e9,10f0c6ce-319d-4231-ae88-446d33eead54", + "27c2eb7f-9fac-4c42-90c7-44e7a33d5303,", + "620934cd-aa73-42ec-97cd-65dd9eed1226,", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,bIQKJloWV0SEnBEtoxP_Wg", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,kKpiR8YM8UG7xcaIgS9aXA", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,W1LdNEXmX0m72Wr7EjJ_gQ", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,oQspLDUDxEaiudqzXY7gbg", + "264fd0c2-88f9-4780-ad72-52351e31b13f,", + "264fd0c2-88f9-4780-ad72-52351e31b13f,7d025ccf-f3b9-4601-9dba-9f25b135d11e", + "264fd0c2-88f9-4780-ad72-52351e31b13f,2d035c24-8437-4403-a024-4c3bfd0d80a9", + "86694be4-17da-40bc-945b-044d0e735bd8,", + "86694be4-17da-40bc-945b-044d0e735bd8,e5172c0a-3099-4e61-a181-f6b114b0a608", + "86694be4-17da-40bc-945b-044d0e735bd8,bab8193c-f67b-4022-834a-597cbf5dc7dc", + "5929bc1e-7ead-4818-a9ef-8e6d3bc885b3,", + "1fab9cd8-257e-4825-8b7d-bd8bd281d362,", + "1fab9cd8-257e-4825-8b7d-bd8bd281d362,q9hshPgwT0ijshnbKEBKWw", + "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,", + "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,etbo7k9dq0OyjIrNS4sdkQ", + "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,7Ww7mumWXkiyj0R3u_-hrA", + "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,8br3f77_CEiE2Oetiof1Pw", + "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,8606473d-8d58-4e93-8123-3d6cd1d47ad4", + "3d5184e1-0201-4476-bf4b-4eb28190de62,", + "3d5184e1-0201-4476-bf4b-4eb28190de62,37f8378b-b98c-477a-b8c9-9340772800b5,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "3d5184e1-0201-4476-bf4b-4eb28190de62,713781e5-ce47-41c6-a24f-94d42a565e30,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "3d5184e1-0201-4476-bf4b-4eb28190de62,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "3d5184e1-0201-4476-bf4b-4eb28190de62,c1e430c5-fd8d-4032-9e3f-8d87cc8784da,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "7ea796bf-f552-4da8-8f68-a87c4896c8e6,", + "7ea796bf-f552-4da8-8f68-a87c4896c8e6,dr6MrV8O30G0i-c7peOoKQ", + "7ea796bf-f552-4da8-8f68-a87c4896c8e6,UcK6aipko0iE4THJUO1TvA", + "7ea796bf-f552-4da8-8f68-a87c4896c8e6,9yNuA59JO0avaKVA6YxQyA", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,V1M2-FXZSEG782d6D0AhUg", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,wNde5lsob06eQxEH2t-zUw,5KeLrnvaKEWnEiap1xzmjw", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,T-JZqXUd3kC1P8L5Pu4ZmA,ao-eeUnb8ECcW2IFGuk76w", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,NaXiRmxgkkGK1U7IQ4yn8w", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,VeZQlPI72E6e57IQqAOq6w", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,qWS_znZq2km_MUGvv5QDHg", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,jCfhcP5ftk6_ruDnFjIhJA", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,5p1Hwa8IE0KIuucb7hV16w", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,ZqWSyRnr9ECIvoU5yFyqXQ", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,odtEahcf10isGbVQCyc2bg", + "a03e9576-acfc-48d1-82e4-4d320d151a50,", + "b02053b3-013f-42b0-8881-85f5be2b8cda,", + "b02053b3-013f-42b0-8881-85f5be2b8cda,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,faedf215-289b-40f7-90cc-f5b98517d133,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,1773d5d8-4714-4721-b30c-97e25d6f2a5a,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,05K7dmjigkCByO2ufw9jtw,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,yOw1WqEN0EWqR9t53EsQpw,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,qoVZuGa5NkOmyfikHjDhXw,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,yDZFr27Zq0-JH-EsEsxG2g,F9vYkdcuGUOSGEg0wpQt2Q", + "b02053b3-013f-42b0-8881-85f5be2b8cda,PLRhiOhhb0KhatFH7f3pwA,F9vYkdcuGUOSGEg0wpQt2Q", + "b02053b3-013f-42b0-8881-85f5be2b8cda,RwgnKTfMEkWrAp_OMGy__Q,F9vYkdcuGUOSGEg0wpQt2Q", + "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,", + "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,PR5dh2jf2kqGLO0lXgA69A,-lYK7LhYyEy6wqL1tIE9cg", + "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,", + "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,PR5dh2jf2kqGLO0lXgA69A,VDn1hO6ahky0GDJt5dRmyg", + "9f2cf07e-f98f-48bc-9c74-784ba4b94527,", + "c2362f96-0a73-451b-9a85-46410eefd2ce,", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,wl3nqp4FsU2wqEkYFqv3_Q", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,4v1-TIIgnU-sg5ROppnEPQ", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,AvIuVibXj0u7UzWFh90_LA", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,fF9qwnew3Eao2gF0xcOqrw", + "0058a7c6-04ca-4931-b2ff-03c46f99079f,", + "6a51d8e1-8844-4999-ad8b-06f4d414571e,", + "9eb95934-90f3-4f48-a6b4-d87d75d9bdc4,", + "3ad53d67-e89e-47eb-94ee-bbd054652815,", + "3ad53d67-e89e-47eb-94ee-bbd054652815,RvVVu7gWUEmofuJiZqevlg,i0itjI0qz0Cdn-es5NBScw", + "3ad53d67-e89e-47eb-94ee-bbd054652815,g4_xqu9o9US9lIiBsn2BKQ,i0itjI0qz0Cdn-es5NBScw", + "3ad53d67-e89e-47eb-94ee-bbd054652815,09zFwIT2AEaQsijLJR2aSw,i0itjI0qz0Cdn-es5NBScw", + "3ad53d67-e89e-47eb-94ee-bbd054652815,mtIyC_me2EuHSur1ghsKDA,i0itjI0qz0Cdn-es5NBScw", + "b1ca1dc4-6867-48b6-a332-4efff06a5db1,", + "b73ae23b-c911-4b09-a803-40999bdef6b8,", + "1bd891b1-6ec0-4227-9ed8-73c59af4f819,", + "a16d59da-caa5-4388-af67-315bfcb16f64,", + "a16d59da-caa5-4388-af67-315bfcb16f64,w9I-I0GWRE-3RgOUMMlHjg,WWmx0nBo3k2nXUPPRJVJLQ", + "a16d59da-caa5-4388-af67-315bfcb16f64,bmi_S166JEGWhf_7woVL0A,WWmx0nBo3k2nXUPPRJVJLQ", + "a16d59da-caa5-4388-af67-315bfcb16f64,dSEiNUAZnUmErO5Vfv51zg,WWmx0nBo3k2nXUPPRJVJLQ", + "a16d59da-caa5-4388-af67-315bfcb16f64,jTG-jnKn0k-Etg_-4Gb9gw,WWmx0nBo3k2nXUPPRJVJLQ", + "a35b6774-9245-426d-ac9e-70154679faec,", + "a35b6774-9245-426d-ac9e-70154679faec,0EjPIiuYpEGIWup3pKeC8g", + "a35b6774-9245-426d-ac9e-70154679faec,dkJNh15DnEuO1BatzpI_uw", + "a35b6774-9245-426d-ac9e-70154679faec,XMAOwHGXVECoeKoWb_-luw", + "a35b6774-9245-426d-ac9e-70154679faec,GAWBaCDBzUqdyTGA-HPczQ", + "06adb976-ca7f-4b8b-8b72-2e03c8bc10b3,", + "aeadad3b-b83a-442c-829a-3eaccd9726fd,", + "6dcc5a53-621e-4f8c-97ed-e7fe42fc86c2,", + "TcgHS_0aPkehrBg7ytMOMQ,", + "35d67043-ca27-4b8a-a893-2d42674e6e68,", + "35d67043-ca27-4b8a-a893-2d42674e6e68,_3hxdSy9wU6OUPZJA9kGmA", + "35d67043-ca27-4b8a-a893-2d42674e6e68,B5t5RbzKdEObKDlXLcRp7Q", + "35d67043-ca27-4b8a-a893-2d42674e6e68,OaOC278jEE-owjtDP9ZLow", + "35d67043-ca27-4b8a-a893-2d42674e6e68,J36t8RwGPUqtLBsF-_UtEw", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,ojvIFCTpJkeegestF065wA,10fYSIBytk-yUjveDa407w", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,071d1bf3-86dc-4a31-b54a-e6579fab8649,d015cae7-a905-49e4-8823-6dec069689a6", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,48abd952-214f-48b2-a8f1-1146f6f69aa2,c2a91a10-2f5b-4ab0-a79f-51408cf4b047", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,48abd952-214f-48b2-a8f1-1146f6f69aa2,RNN0ejjlm0yqGDcEuUkLjw", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,n4SdiVms4kaqdjXdbjRBbw,YEMLBlOWsEO4tELHseZ7LA", + "07205057-0804-41de-abe4-bd4027fee1df,", + "07205057-0804-41de-abe4-bd4027fee1df,jwXdjDbEmkOFyKZHT_RYOw,YJGsYfwkik2lp7qWqXyXTw", + "07205057-0804-41de-abe4-bd4027fee1df,LdKaVAveokmp3qRwVBxnFA,YJGsYfwkik2lp7qWqXyXTw", + "07205057-0804-41de-abe4-bd4027fee1df,e-oZt-PbKEOfawVammBQTA", + "e60fccda-8711-463d-b8a3-7cd5e76903b2,", + "e60fccda-8711-463d-b8a3-7cd5e76903b2,Chiz8maLoEGDpZOJZEtdQg,cQUquXMk5EGt0VUCuPKBgg", + "e60fccda-8711-463d-b8a3-7cd5e76903b2,2axtF2iOGkez1kfRRGUbYw,cQUquXMk5EGt0VUCuPKBgg", + "5154be09-bb8a-4917-95a0-51c91b6a4e09,", + "8fc7aaef-adec-4534-8f91-445e4e7095d5,", + "8fc7aaef-adec-4534-8f91-445e4e7095d5,5Dbc8qCaY0OXEuwykM4naA,awWjleyNP0akWNYJoehCLA", + "8fc7aaef-adec-4534-8f91-445e4e7095d5,HBt3ToOkUUereoPKwyvzuw,awWjleyNP0akWNYJoehCLA", + "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,", + "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,uym5X1GZgUSzVb8U-pwlIw", + "e9913b06-58cb-4b90-bd08-981a3f19f352,", + "e9913b06-58cb-4b90-bd08-981a3f19f352,QXtBrjnRckKJGiRtsy4AVQ", + "3bb50a88-09ae-48c7-a42c-df5879680eff,", + "3e139dd9-4757-43a8-8546-8da70dc17f0a,", + "f2db0ec1-e536-4218-8385-dc6913941880,", + "3739a9e1-6dfa-45d2-af54-cc83a58d436a,", + "3739a9e1-6dfa-45d2-af54-cc83a58d436a,GpYghupLTUK9cuiL-0uHYg", + "19984948-b6b9-4d3b-8562-d0624930df6e,", + "19984948-b6b9-4d3b-8562-d0624930df6e,Owi_JROR80-apNHw-4KNcg", + "11462b96-dd62-4057-a187-49d8322c009a,", + "QCtqxbvYOkan79YFugH_Cw,", + "QCtqxbvYOkan79YFugH_Cw,i5RKLo-RbEqisnGII1wpaQ,xnxDimLvVUCI9tEJqWDDcA", + "QCtqxbvYOkan79YFugH_Cw,IzkFARlv6EenmrfcNkcOuw,xnxDimLvVUCI9tEJqWDDcA", + "QCtqxbvYOkan79YFugH_Cw,LyVf9lnzdku1F4iPBGqk-g,xnxDimLvVUCI9tEJqWDDcA", + "t9co2516nkuJO4LIJA1bJA,", + "t9co2516nkuJO4LIJA1bJA,i5RKLo-RbEqisnGII1wpaQ,8kdJkmwNM0WQmMVrb8fK8A", + "t9co2516nkuJO4LIJA1bJA,jwEB6LyEaEekqtPNRc6TVA,8kdJkmwNM0WQmMVrb8fK8A", + "t9co2516nkuJO4LIJA1bJA,Wj7I2YiVoE6zp8J_HYs8tw,8kdJkmwNM0WQmMVrb8fK8A", + "ad0bdec5-f767-42e7-99e0-221473464a85,", + "ad0bdec5-f767-42e7-99e0-221473464a85,624c85e5-095a-48b6-85fb-c9bbade19a8d,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,192ba73d-6990-42da-8fe0-914c2a7181eb,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,teVKVbVEsEyqOSuRdqHQzA,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,K5vPqD3BrEuODRDzFdC6Hw,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,_6LmNp0uX0Ky4jp2j62U2w", + "ad0bdec5-f767-42e7-99e0-221473464a85,cNarx7yecE2gk-7mYaoHwA", + "ad0bdec5-f767-42e7-99e0-221473464a85,AEqCasEqOUCgetoTwuMTug", + "ad0bdec5-f767-42e7-99e0-221473464a85,zoUmQIcOrU6jgBu_0urrGA", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,192ba73d-6990-42da-8fe0-914c2a7181eb,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,624c85e5-095a-48b6-85fb-c9bbade19a8d,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,teVKVbVEsEyqOSuRdqHQzA,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,K5vPqD3BrEuODRDzFdC6Hw,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,ODOnI3iYoEKrlQfFabbvoQ", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,BW5oLDA36EK9nLzTm7xViQ", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,17e44296-1cd9-4f8e-9aa7-3837dc69279a,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,c4ebfd1a-fcda-4b98-a151-a941b1801867,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,c0c33a35-494c-4a1a-aa53-d27d9b9ef5a4,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,wat8f6fs80KptZbW39e8tA,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,wkqKPZmnoU-8TXSWzi2-Ow,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,tOlpTL8F1Ui_akCjHq5vRg", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,rfIzQjxeRU60VdRQXG9Ccw", + "vpP2ZvnL0Uy3n-I_Jv3eeQ,", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6o6sNWyPoUCcKJBHW0KC9w", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,X1OFXyDyXES3CgMpIyV3nQ", + "cca7ab3d-5609-4700-92fd-3280fcea9b77,", + "9d2ea3c2-c199-40ed-a6c3-fab5f8dca7d6,", + "2a61c844-81c6-4f82-9c75-66cf308ed4e1,", + "ed695ac2-ff70-4ebe-b1db-18d71a4efdd1,", + "67c0f944-2f70-46d2-8588-2f67028f8bed,", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,B3t4hjeUeEyT17nfbZgxAQ", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,IEnB86oIL0KDa-zzcxhjwg", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,ZzJxs2oz6UuXCX5rGvLHFQ", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,xLcqKXKH-0Gw1lrSSQe-Ag", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,nloukujf9kSwK1T2HiwJwg", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,Grr0Jm6r9kuiZKvnLPz9tQ", + "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f,", + "a2fc5417-07e1-402c-a80e-5111a7f57287,", + "a2fc5417-07e1-402c-a80e-5111a7f57287,McX_jAOowUmjXNPEHbdS7Q", + "a2fc5417-07e1-402c-a80e-5111a7f57287,4YH_0kN1ZUSVEQmCBiTt7Q", + "a2fc5417-07e1-402c-a80e-5111a7f57287,kTyKeivEskaxQNLhfPWo1A", + "6423f23e-dde3-4428-96f5-6091027c92cb,", + "6b337a35-95d9-4b3a-9891-d9fbb06262aa,", + "cf360f73-b349-44e7-b5c1-f175a0f5e70d,", + "cf360f73-b349-44e7-b5c1-f175a0f5e70d,zQgy7h3CO0a02zJfhA9wXA", + "f6eee46c-084b-473e-954b-a0c8aa99a02d,", + "f6eee46c-084b-473e-954b-a0c8aa99a02d,qbEYPChG5EmnFDUfzwAakg", + "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d,", + "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d,cfAfhzaNvkG7wGx0gkaRUg", + "4fcb5b84-13a9-43e6-9680-72b17874ae53,", + "4fcb5b84-13a9-43e6-9680-72b17874ae53,Su7kiW24d0KapNns96JoFQ,a3Er_BCApUiPtBaGkSH9LA", + "4fcb5b84-13a9-43e6-9680-72b17874ae53,CahBzgN3D0y46AQsvGC14A,a3Er_BCApUiPtBaGkSH9LA", + "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,", + "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,Su7kiW24d0KapNns96JoFQ,EiIow5mS80qpoS10r1ZdvQ", + "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,CahBzgN3D0y46AQsvGC14A,EiIow5mS80qpoS10r1ZdvQ", + "8d97303c-5138-4da1-a74d-378da19d11cb,", + "8d97303c-5138-4da1-a74d-378da19d11cb,Su7kiW24d0KapNns96JoFQ,ZpyoWbO6GkGglXlMuYUARw", + "8d97303c-5138-4da1-a74d-378da19d11cb,CahBzgN3D0y46AQsvGC14A,ZpyoWbO6GkGglXlMuYUARw", + "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,", + "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,Su7kiW24d0KapNns96JoFQ,0p5N9U5qokKdQhW07yYLGQ", + "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,CahBzgN3D0y46AQsvGC14A,0p5N9U5qokKdQhW07yYLGQ", + "1a742ff9-c4f7-45d8-8e24-8f24aa021db2,", + "b84f5e9e-11fc-424d-ad39-86901edb9edc,", + "26f5e849-14b3-4cb3-ab92-673c94b37c51,", + "26f5e849-14b3-4cb3-ab92-673c94b37c51,CwObTyhghkWonhm5nTnAaw", + "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae,", + "a5b747b8-c1a5-4bce-999f-e13dbe77bde6,", + "4095a06c-ccdb-41a0-ac95-5a97372c9a02,", + "84992fc3-4801-492b-8a6c-ffa09ae5b593,", + "adbd2eaa-0dd0-4955-be4c-0d5a66d64ed2,", + "5ac25e5a-8c8e-445c-82a4-7272baec1eb5,", + "27e742bf-3081-4274-ba70-301f0edeb4f1,", + "587ca2bb-dd42-42bf-833f-4e2a97349866,", + "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,", + "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,4a85c181-92fe-47a5-925e-05f0ccb25006,944d934d-5e21-42d7-8196-fff9a81ee469", + "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,0ba92db2-690a-44ce-92f4-130e9503b6c1,944d934d-5e21-42d7-8196-fff9a81ee469", + "h_To9WQYdEusYQxlbMmMmQ,", + "uubY_Cn9A0-Ww4lsIRCF6w,", + "05ab6e79-1198-49f2-945b-528523e306bc,", + "05ab6e79-1198-49f2-945b-528523e306bc,aUsPNgV2c0u15VhGBAIyvg", + "84efabec-fe50-4c92-845f-b99b1d84ca82,", + "84efabec-fe50-4c92-845f-b99b1d84ca82,cbk6h_-1Y0exRpGyrQMDMA", + "84efabec-fe50-4c92-845f-b99b1d84ca82,E_pm-PPj_U-nsfMti-AlCA", + "1a028850-64ee-40ab-b547-ebbcfa1b04b3,", + "1a028850-64ee-40ab-b547-ebbcfa1b04b3,5a614f97-ef7f-4b70-afff-9ed837a86407,a8a91216-0fcc-461e-a682-95ee82509cce", + "1a028850-64ee-40ab-b547-ebbcfa1b04b3,b444ed0a-eab5-4bd6-af34-d080de74a149,a8a91216-0fcc-461e-a682-95ee82509cce", + "1a028850-64ee-40ab-b547-ebbcfa1b04b3,8053b5e5-0f67-4329-a45d-1ee9fc830820,a8a91216-0fcc-461e-a682-95ee82509cce", + "a6f1825e-1647-4d06-85d7-ac2d139612c0,", + "a6f1825e-1647-4d06-85d7-ac2d139612c0,fe5288bc-0e02-4ea3-b17d-dee0e576979d,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "a6f1825e-1647-4d06-85d7-ac2d139612c0,82de1ca3-67d6-454d-84fb-0eeece71fcbc,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "a6f1825e-1647-4d06-85d7-ac2d139612c0,54d3c589-30ba-44c1-a77c-8f9e16eb0b20,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "4d533b3a-b5fa-446a-842d-92798ad5b493,", + "4d533b3a-b5fa-446a-842d-92798ad5b493,fe5288bc-0e02-4ea3-b17d-dee0e576979d,6b38ab79-142b-446e-978c-ad829ade8823", + "4d533b3a-b5fa-446a-842d-92798ad5b493,b444ed0a-eab5-4bd6-af34-d080de74a149,6b38ab79-142b-446e-978c-ad829ade8823", + "4d533b3a-b5fa-446a-842d-92798ad5b493,8053b5e5-0f67-4329-a45d-1ee9fc830820,6b38ab79-142b-446e-978c-ad829ade8823", + "162a3442-e6d0-4895-be09-40d78af16dc8,", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,AHL9ydUbqEqMw1tluIvyQA,w-vooMawWU6_4spdgBYufA", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,_SPKrYIcnUWhTiSrTFLwoQ,w-vooMawWU6_4spdgBYufA", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,n2pZHS8A3kSgyFUCI_-8TA,w-vooMawWU6_4spdgBYufA", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,BYbKbXAgUk6vE7H7w4jSMA", + "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb,", + "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,", + "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,M16XTvLPKUWRpiAspgsatQ,tmaPvkx3BUOZ3nsl2E0oRw", + "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,rXhNZtcZCU21dwEHOCgWOA,tmaPvkx3BUOZ3nsl2E0oRw", + "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,OxO9ivoKB0e-fNIEk33Erw,tmaPvkx3BUOZ3nsl2E0oRw", + "2cf6ceee-4c45-4fb4-b9df-cb8437742e85,", + "f8514fc6-93f3-4063-bafc-378ddf454836,", + "f641d4f4-e197-4cf4-b477-89069797967d,", + "2af73868-b619-43f7-9b7a-37a8fe2502bf,", + "6a2fa2f4-d335-4349-88a4-49af251454fa,", + "6a2fa2f4-d335-4349-88a4-49af251454fa,zBfCq7T69kqZPTz3P8KKhg", + "6a2fa2f4-d335-4349-88a4-49af251454fa,liZWUNZih0iakAa7UJkk9A", + "34654303-9760-447f-8614-1c26506db994,", + "3dc003c5-428b-47d6-80fb-6520b63119e8,", + "3dc003c5-428b-47d6-80fb-6520b63119e8,b995b419-1897-4c1d-98be-83d551f7ba71,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "3dc003c5-428b-47d6-80fb-6520b63119e8,67fc9269-8dd8-4d6f-b594-c77e716f2482,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "3dc003c5-428b-47d6-80fb-6520b63119e8,b539777f-48b2-44a2-9d63-17ac703e4d57,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "3dc003c5-428b-47d6-80fb-6520b63119e8,5fb2e235-8c67-4335-b432-edb10db2b139,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "686ac516-630f-4989-9c99-0b8d055a00f9,", + "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,", + "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,Hd5IIMd7VUqndR_og0zxPg", + "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,8k3q_1d_-kSBzNLCl1HQhw", + "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,YwossyXZNEm3OPWwXsyTAg", + "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,i3pwJQoNDkO_68pQ_3Y0tw", + "61969200-2790-41e0-bccb-d9feff8c7b7d,", + "2e50d8d2-b7fc-44e9-a882-a58bd3b6f099,", + "a73e1a56-2710-4448-a594-ecc80571e999,", + "c7ea70bd-7681-4b90-b4e3-10a100e4652e,", + "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,", + "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,z1djgGqy502ha9X_uB3enA", + "485bbda2-37e8-45a7-90db-7c35518dcdd0,", + "485bbda2-37e8-45a7-90db-7c35518dcdd0,M5QwJJRRBUy42HLyp6ghtg", + "a37c3161-1476-44b0-a9d1-d6eb854db2d9,", + "a37c3161-1476-44b0-a9d1-d6eb854db2d9,EmlwkB77pUiiDEmc2pMVaA", + "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,", + "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,MzNNQ6T1HEqbqcLXZIulLw", + "cee80307-68cd-4260-a898-1514dd41dd5d,", + "cee80307-68cd-4260-a898-1514dd41dd5d,u0bLCSxcA0yDHC29XnLZOg", + "cee80307-68cd-4260-a898-1514dd41dd5d,6WJKUeFNn0amYNvKMxijcw", + "cee80307-68cd-4260-a898-1514dd41dd5d,VPvezA1A8UKt2qNoNRW9dg", + "cee80307-68cd-4260-a898-1514dd41dd5d,OIfcKSEil0uHgT5baEFuOw", + "cee80307-68cd-4260-a898-1514dd41dd5d,hNFdYhoJYk24MqU2GhQXlw", + "a628d62c-fa30-4405-bcbc-3e646e3a3434,", + "a628d62c-fa30-4405-bcbc-3e646e3a3434,55901f12-d5b5-4fa8-b4c8-e479689ee39d,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "a628d62c-fa30-4405-bcbc-3e646e3a3434,4828b50c-95b6-466a-bb25-514891d78202,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "a628d62c-fa30-4405-bcbc-3e646e3a3434,d6823e01-69f0-4f85-b94a-74894356a2cf,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "64fddd4e-3d59-43df-bb39-671e18953c0f,", + "64fddd4e-3d59-43df-bb39-671e18953c0f,uDJ5ZqU1f0CiOAH8WY22Yw,KOCzRE4a6EKb3_NVOI_lcg", + "64fddd4e-3d59-43df-bb39-671e18953c0f,j66We9SYr0SNQ2ufZqIqhA,KOCzRE4a6EKb3_NVOI_lcg", + "64fddd4e-3d59-43df-bb39-671e18953c0f,_jkPDSXFUkSU_yOfutQ7BA,KOCzRE4a6EKb3_NVOI_lcg", + "64fddd4e-3d59-43df-bb39-671e18953c0f,MLB_YJtNtUi2Qw9D4a5i2w", + "46cde502-86d6-4061-a80c-67458ffd4d60,", + "46cde502-86d6-4061-a80c-67458ffd4d60,crl8VlZ59kGxG01noGEWmA,EckOEU0mJkGIVDvYoKBN9g", + "46cde502-86d6-4061-a80c-67458ffd4d60,WqX8pAB7LkyPKEzIUCtnNg,8Y2F-cVStkSn9YB47VCoKA", + "46cde502-86d6-4061-a80c-67458ffd4d60,l2BbgbHCe0S_SDUbfmpSeQ,XHJ0lW4zYkGcL5UyI1HfEg", + "46cde502-86d6-4061-a80c-67458ffd4d60,bTAQ-CkyRUOZoXqXuap2Ig", + "oYvOo47DJE-DEPlpn5xNUw,", + "oYvOo47DJE-DEPlpn5xNUw,whs9olNz9Uani7Gou31mvw,y6aASPUwDkas0W5Yl27ydQ", + "oYvOo47DJE-DEPlpn5xNUw,ncERd5bELk23jSRxA7NV9g,N-cAmfOgv0eJZMTo18zJHQ", + "oYvOo47DJE-DEPlpn5xNUw,eaRDMCA4fE6KJHi639QFvQ", + "oYvOo47DJE-DEPlpn5xNUw,zIIQkllHFEyjEvAGYC0cPQ", + "4ee2133d-c419-4fbc-9117-746d756a250d,", + "cea5c141-32d8-4dae-80d9-6fb74834a79c,", + "967187b8-df20-4f4f-b48f-10b9ea75cedf,", + "b6cee099-5840-401a-9d74-0b80ff71ee69,", + "b6cee099-5840-401a-9d74-0b80ff71ee69,an_Z5HMFjkaHyHoZZV4AFA", + "16707919-756d-45ae-95fc-cfb6715b8ae6,", + "7df5697f-2f71-47d1-bc27-cfa43792ffd7,", + "7df5697f-2f71-47d1-bc27-cfa43792ffd7,GCethPzUyUKYxWdfLykbSg", + "BJ2XrGvl40iIl28IbTfbKg,", + "657efe89-620a-46a2-8b58-cafd855c7cd5,", + "657efe89-620a-46a2-8b58-cafd855c7cd5,eWf2c62cqEOkRukLzhl_wQ,KFq8d1VsO0KDqBWUj4qRFg", + "657efe89-620a-46a2-8b58-cafd855c7cd5,a0oe68pikUykAgrRo-axkQ,KFq8d1VsO0KDqBWUj4qRFg", + "657efe89-620a-46a2-8b58-cafd855c7cd5,cFQ1Z7_wp0meS8wdC1hemQ", + "60067e91-18b8-43ab-ae20-a8ea74c757bf,", + "60067e91-18b8-43ab-ae20-a8ea74c757bf,IodiruapIEqjGym-YcXzXA,LjQ1UKN2xkqJkok40Dxaew", + "60067e91-18b8-43ab-ae20-a8ea74c757bf,KUAMuM41hk-YLZoqTiKncA,LjQ1UKN2xkqJkok40Dxaew", + "1c89e162-fbce-45e2-a158-6bf0c4f5e52b,", + "1c89e162-fbce-45e2-a158-6bf0c4f5e52b,1LtCxaCDjUmg62fYCjNUSQ", + "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,", + "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,7b0d6661-e856-4ec1-ab90-d89c28a70826,1fede91a-434e-49a5-882e-e6db107112a8", + "ec836d2a-b1b8-4566-b8eb-123ae2845956,", + "ec836d2a-b1b8-4566-b8eb-123ae2845956,cea53c7f-24a9-4e3b-80b9-fbf95738f973,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "ec836d2a-b1b8-4566-b8eb-123ae2845956,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "ec836d2a-b1b8-4566-b8eb-123ae2845956,8c74ee27-3d56-401b-8a8c-7868a80770e8,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,", + "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,cea53c7f-24a9-4e3b-80b9-fbf95738f973,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,8c74ee27-3d56-401b-8a8c-7868a80770e8,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,", + "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,18b52db5-0261-46a0-9214-ff510d455ed9,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,55b4e661-43e1-4708-82d5-2dd2bebd4f45,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,8c74ee27-3d56-401b-8a8c-7868a80770e8,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "9o0gWLJjREG552gxRreT8A,", + "9o0gWLJjREG552gxRreT8A,dZyVey0_oU-3u6FLbIdGLA", + "9o0gWLJjREG552gxRreT8A,rP0LMoFjqUCU1LWA6_wOnw", + "5PUja-WX00Sme9yBwvDq-g,", + "5PUja-WX00Sme9yBwvDq-g,ngF6e9k3wkme3HdodiB03w", + "5PUja-WX00Sme9yBwvDq-g,FpvZjltPtU6AKTBphz3ZMA", + "5PUja-WX00Sme9yBwvDq-g,z7bASCi-JEKuLiOmFx3EFQ", + "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,", + "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,891d902e-3257-4a8c-9001-858046007997,c8ccb85e-52cd-4b3b-aadf-289819067125", + "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,4c26d1e7-2382-4478-b9c4-a068ed01a607,c8ccb85e-52cd-4b3b-aadf-289819067125", + "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,uahcQNh3IEiooPFq-SQi5w,c8ccb85e-52cd-4b3b-aadf-289819067125", + "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,", + "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,0x7a3hV5XUyzyf8cUuzVtQ,vYAcZMymO0m6bCNQIsI8rQ", + "7ed81aea-0e77-4b38-a87c-729048c6fe41,", + "b734172c-e405-4fc7-88c3-c859309bb4ac,", + "5f600892-aa67-484a-afc7-14c219d89c52,", + "5f600892-aa67-484a-afc7-14c219d89c52,565c17e0-496d-45bd-a2c3-564e118c06cc,a240d44f-963d-4d35-b22f-980938c7788d", + "5f600892-aa67-484a-afc7-14c219d89c52,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,a240d44f-963d-4d35-b22f-980938c7788d", + "473c672d-2b79-48a2-a49d-fd68160c5bde,", + "473c672d-2b79-48a2-a49d-fd68160c5bde,30c61e63-5e09-4a6f-8915-1e1fe4d55cf0,0e42108e-3b08-49b4-86c0-7984719e8380", + "473c672d-2b79-48a2-a49d-fd68160c5bde,2cf2b644-3fa6-449c-9fe8-d0ab917f3106,0e42108e-3b08-49b4-86c0-7984719e8380", + "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,", + "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,565c17e0-496d-45bd-a2c3-564e118c06cc,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,73fc2e2a-a036-410b-bafa-3a07658245b5", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,0ba92db2-690a-44ce-92f4-130e9503b6c1,73fc2e2a-a036-410b-bafa-3a07658245b5", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9aYT6E9sd0OSavlmkhtkGw,73fc2e2a-a036-410b-bafa-3a07658245b5", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,H-ypJNyVSka9as7rDFRZ_A,73fc2e2a-a036-410b-bafa-3a07658245b5", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,-7uPJmO-wUu3cUhqo0caPg,73fc2e2a-a036-410b-bafa-3a07658245b5", + "f567f792-e5bf-41d9-a8a9-e3af7aaddaf7,", + "c7c85554-e118-4433-8d6b-b61a2971e045,", + "c7c85554-e118-4433-8d6b-b61a2971e045,MS0Mglf3qEmzKzbCa6uOAg", + "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,", + "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,c5884778-a87f-4612-9717-86fbb65d440f,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,827a1538-51bb-4fef-99c1-7f1bebd9866c,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,rZOgFhv59UKssAH7k-UDyQ,eVl7mLA1VkmnR81YLF-ZNg", + "f41653f9-ae02-443c-aaf4-1c6f4e49035d,", + "f41653f9-ae02-443c-aaf4-1c6f4e49035d,cf515c87-3c0a-4443-9f63-5ea1bdbaa61a,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "f41653f9-ae02-443c-aaf4-1c6f4e49035d,27680277-aec1-41be-a476-9f51cf775580,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "f41653f9-ae02-443c-aaf4-1c6f4e49035d,5a96ac4d-03c3-4214-9dcb-c2044ff28f2e,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "5a38f864-7d1c-4989-8558-40b9668f78ef,", + "5a38f864-7d1c-4989-8558-40b9668f78ef,d8d490aa-7088-4163-9305-1441367f490c,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,61ff7a02-50ff-49ea-b95e-c31b95a30c8e,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,X24EvVokeUqyDNdqevV2EA,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,UQKS4u4SO0umKOHncLqKpw,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,Q3bhpoWbVkyUBTKUn5A5bg,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,UfhOpLemQ0ypYh7CIC3W1Q,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,_MS-R9DB5kinLBszWle8hg,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "34d45669-b319-4c7d-bd05-078961d70d0b,", + "34d45669-b319-4c7d-bd05-078961d70d0b,32dde6dc-e3dc-43ee-9b7c-987bc6778c17,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,88c3ed05-ce41-4a9d-a4a7-8a428d240b3f,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,vMqhT19x302aYEo6E9HhOA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,G7B-u9qn5EGqSNskBCVsAA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,jV1TGp1dHUWCJzVMbJskyQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,kCqthBeVvUKZA9b76kxuVQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,dzCc_QmTYUGQ67r4yy3XpA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "30380607-8b54-4b63-81d3-cb3f08e452c1,", + "30380607-8b54-4b63-81d3-cb3f08e452c1,rcdwgNvO30Cvx2ycE1GfHw,JmFzNL5v_0q1p0bgoOlBpQ", + "30380607-8b54-4b63-81d3-cb3f08e452c1,ktnYOVEhtEmuxmjM1TjIOQ,EqNosRGf5U-5P1nKIRV3fw", + "30380607-8b54-4b63-81d3-cb3f08e452c1,oM7qz730lke-off_cQw-cA", + "30380607-8b54-4b63-81d3-cb3f08e452c1,HzTJsuYdLkC_NY3ClszRpQ", + "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14,", + "73a119ab-d2c8-4da9-98b2-29edbb38e2c2,", + "73c0e2ba-cba1-482a-89d5-1508997aceeb,", + "80f68c67-2df6-4b58-bf2a-dfd76653a781,", + "d536cf31-5649-4d09-94b0-995c4d6f89df,", + "a98251fe-3eb9-41cb-9982-6ce44e454d85,", + "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,", + "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,A-xoYdSUF0aEr_XZ9Me-3Q", + "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,db-JiDmRhUKX69weDLiHsQ", + "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,UZY81cnsV0meMp2lg3_UaA", + "OTnfT-4NQU6Gd_iVPDx8eg,", + "ZsggwOJ8XUOGfaJltM418g,", + "tkbHKZ9wu0W00VGeUbIaaw,", + "283e98dd-1ba7-4997-858f-96046e0ac431,", + "283e98dd-1ba7-4997-858f-96046e0ac431,J8z15LYBak6jHu3ZaaKAXw", + "283e98dd-1ba7-4997-858f-96046e0ac431,QSXXWZrS6UaoAUn3u7hXFw", + "283e98dd-1ba7-4997-858f-96046e0ac431,AcLApWMhAU6tK10O7KoOog", + "283e98dd-1ba7-4997-858f-96046e0ac431,dhJm0moskU6urIwG1TXs_A", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,MVhTW78G_E6z2mY0Y5ZnAA", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,Adiw7dQqyk6pS0q3T0lcSg", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,_4mk8E4PUEGf_3SPuFUy2g", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,aO5Q4BPpuU-bsNavGii40A", + "405f20aa-f888-4e21-b322-34d11017ff16,", + "405f20aa-f888-4e21-b322-34d11017ff16,f-RfHoCWik6QgNKDhCs7Vw", + "405f20aa-f888-4e21-b322-34d11017ff16,f8100fb5-92fc-4520-9755-2deaa5430ddb", + "2b1f0884-c887-4ec9-9984-426405c50f93,", + "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2,", + "9117bd15-674e-4f22-9ff8-e7365fa43907,", + "9117bd15-674e-4f22-9ff8-e7365fa43907,UdQxfRULDEG6BvJV7HWO_g", + "52ea64e5-f062-4683-a380-4f8a48938686,", + "52ea64e5-f062-4683-a380-4f8a48938686,GCwZMIr720iH3TszOb6SQA", + "52ea64e5-f062-4683-a380-4f8a48938686,LmMg-y8w8kiXdre4vUJrZA", + "52ea64e5-f062-4683-a380-4f8a48938686,ze-eTUX6WUqE-n5nIcH0aA", + "309f3ab1-d2a4-4d63-b248-93d9795a8a23,", + "58d3f945-510f-4f92-87e3-a57e29611e4f,", + "c9badd8f-3f81-4c05-8a31-24abd8839872,", + "788baa71-6ffb-42f5-8dc8-d0f117b04a8b,", + "83ff0105-37a7-4c71-902c-20f473223d1e,", + "ccae1771-434a-48b7-9b4e-1dcf7b70e0da,", + "a3648455-51cb-4ed4-85c5-9a11f96ad3e7,", + "c5760078-15bc-4ea6-acb9-0ea295a1894b,", + "fb64a2ef-ae83-48a9-b021-0b3f0de2d7a5,", + "0d58f3a0-7165-41a4-8d77-f88f393d0003,", + "77c37481-a185-47ff-af55-65f9fdfcf9e5,", + "77c37481-a185-47ff-af55-65f9fdfcf9e5,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "77c37481-a185-47ff-af55-65f9fdfcf9e5,643b5f2d-0666-4869-b699-03742de34d16,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "e042433a-40a2-4758-8074-3b9d407835ec,", + "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,", + "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,1sxxUbsQMki0sTwxEQIbtw", + "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,CEnWMDmcMEyjX5mRdhtXXg", + "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,ySbPHkP8WEee2V_P8lt_RA", + "2e31fd6c-dffa-4571-8e53-fc28fd0c98e0,", + "11cec676-0ec3-4778-abe4-a9d399900b54,", + "ad6e7c98-8056-4e12-92ad-bfa19a00f4af,", + "cf1a6f8f-1649-423b-b513-b8d40d242b1f,", + "cf1a6f8f-1649-423b-b513-b8d40d242b1f,8lwfuCRnU0291wqwgTwgRQ", + "8a26efb4-2116-48ba-84a0-c4fdcf74c191,", + "8a26efb4-2116-48ba-84a0-c4fdcf74c191,tp0jNW7f0Uubr9nTMWuhXw", + "8a26efb4-2116-48ba-84a0-c4fdcf74c191,clKD22mme0y8apzUhHmI_A", + "8a26efb4-2116-48ba-84a0-c4fdcf74c191,9liduuR6IE-6C4YN_FKcZg", + "39dde11e-7aa1-4bd2-8532-62665e8d8dd3,", + "f7764314-c02c-4b46-9c56-8c60351d9b14,", + "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,", + "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,RjX1VTHHoEiNBfXkkniDgw", + "1633766e-fa77-48f3-8c6c-a233e56bf320,", + "594a69fb-9f76-4f83-b2aa-94f0e72e996c,", + "594a69fb-9f76-4f83-b2aa-94f0e72e996c,GL6frGGo2E25VLcw61otXA", + "c70005d5-6276-4a98-acb3-6a77bc19379a,BqNgpjP7a0qmm3I3DNbKLA,l_iJLV14q0eNgOsOI1u0pw", + "c70005d5-6276-4a98-acb3-6a77bc19379a,5H0I1anlkkuUYIM28_ZOlA,l_iJLV14q0eNgOsOI1u0pw", + "c70005d5-6276-4a98-acb3-6a77bc19379a,9tZq7Xvfj0WhhLBAuAacxA,l_iJLV14q0eNgOsOI1u0pw", + "c70005d5-6276-4a98-acb3-6a77bc19379a,dRQI80Qn4E-kzhPJoW66MA", + "c70005d5-6276-4a98-acb3-6a77bc19379a,FuMUQFSnnU6awB-vPaeuZw", + "c70005d5-6276-4a98-acb3-6a77bc19379a,OBcu3bf1NEio_7t9smqGag", + "c70005d5-6276-4a98-acb3-6a77bc19379a,F1IV-zZ22kKtXkr2pHp_YQ", + "c70005d5-6276-4a98-acb3-6a77bc19379a,Sad0HQX_Y0eYTx1qqgxKwQ", + "c70005d5-6276-4a98-acb3-6a77bc19379a,-OgDdlvL9EyzeiA50YY5kw", + "0b38899f-6429-49aa-b626-2696359dc1cf,", + "f90773c6-d0f0-417a-9af5-183d1e5a657f,", + "c51780fd-9eb5-4bc0-9da0-34ab22d800df,", + "b772a5a9-dd79-43e4-8d95-f965d81bed8d,", + "bf13fa1c-f991-4aaa-9402-aac109be9562,", + "bf13fa1c-f991-4aaa-9402-aac109be9562,ERnUPsyT6kuEnIRZF8zPwg,wzt-5C_LjEmIQWUVPcYWhA", + "bf13fa1c-f991-4aaa-9402-aac109be9562,iaDL2im4lkCXUo0NWM-F3g,wzt-5C_LjEmIQWUVPcYWhA", + "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,", + "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,48c18418-01dd-40ff-9dfe-c4cf5f4e1245,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,6584dada-499e-4d7c-958d-466955b20640,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,2Mgx6S9f8k2H1AAYbkzjQg", + "f76b709c-b173-426e-81f2-279a67d2fde1,", + "f76b709c-b173-426e-81f2-279a67d2fde1,4c845dbb-26c3-48a1-b562-e28a7416b154,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "f76b709c-b173-426e-81f2-279a67d2fde1,96e2b29e-e553-40f1-9eb6-8a9f7e64da39,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "eada710d-ea0c-473f-b9c6-23e210607b24,", + "eada710d-ea0c-473f-b9c6-23e210607b24,72a010c0-bddd-4e5d-bca6-9821d82de9a6,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "eada710d-ea0c-473f-b9c6-23e210607b24,32a5e42d-2b60-40b1-a145-ed96fc91af8e,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "KPmC_wyX_ECe7fdKwoLMGg,", + "KPmC_wyX_ECe7fdKwoLMGg,FaWMLkJJgEKFIO493fzGxg,yzL0M_N5Q0eAa8IxACHPAQ", + "KPmC_wyX_ECe7fdKwoLMGg,nCPVMrWXGkeeIN-969xzuQ,yzL0M_N5Q0eAa8IxACHPAQ", + "U_H976qYRUmg9e2clsAflg,", + "U_H976qYRUmg9e2clsAflg,iZy7RmfAz0603oMO-jD-WA,ica0LZyM_kOox8gxW7CaMw", + "U_H976qYRUmg9e2clsAflg,4D4ge1PYokaj_g96k4oasA,ica0LZyM_kOox8gxW7CaMw", + "tTKv7eRODka2KC_hsY2O3Q,", + "tTKv7eRODka2KC_hsY2O3Q,eV3Nv6SWS0yl3f-fgjPb-w,t-ZEWJCgFEyGP2PjNblQMA", + "tTKv7eRODka2KC_hsY2O3Q,Lxx3q6JRnUW_cPG_kGG4ag,t-ZEWJCgFEyGP2PjNblQMA", + "tTKv7eRODka2KC_hsY2O3Q,vek37vRROUKnSJKVP0CH6w,t-ZEWJCgFEyGP2PjNblQMA", + "q7pSsRMNHkqEJmYHuhhG6g,", + "q7pSsRMNHkqEJmYHuhhG6g,90qQe0AR3kOou0MsuoTmPA,ojCeJ1VwtUelyVqFnUwK5A", + "q7pSsRMNHkqEJmYHuhhG6g,hZKPr4sg10SRj9DLuEgOrw,ojCeJ1VwtUelyVqFnUwK5A", + "q7pSsRMNHkqEJmYHuhhG6g,x3Q0vVeG_E6juqGM1o0SOQ,ojCeJ1VwtUelyVqFnUwK5A", + "m5Plrduzr0yj_9mTn1x3Vw,", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,dtuR9xLXVUGzSi2G1DMh7A", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,0FlA_T8GsUe9PdiZamlP_g", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,U8ZZiMEeZ0-qJfcpqBP72A", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,Gct9Vt-L4UiTjAl61hsebQ", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,2r_gYLhQrkS0bjwaxLecfw", + "2d07c0d0-b277-46dd-8980-382bdaa4749e,", + "2d07c0d0-b277-46dd-8980-382bdaa4749e,f6O5cw0vc0ineEptRysocg", + "fe16e054-9cae-4a97-a0c0-256b51cdb0e5,", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,sX0ZZDcCUkyFSJISfvq0vg,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,TnEDe1QoMUCUdqPL_7CxTA,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,AC3zfm38JEyBZWuXDh4Ukg,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,RfZNDPUok0WyQEa3Ko08ow,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,dyEaPloE8kazWC7Zblv8uQ,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,E7zTfb8zbUKuld0rxNt7fg", + "1beaf288-b5a5-475c-8149-c8f3d1146083,", + "1beaf288-b5a5-475c-8149-c8f3d1146083,sX0ZZDcCUkyFSJISfvq0vg,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,TnEDe1QoMUCUdqPL_7CxTA,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,AC3zfm38JEyBZWuXDh4Ukg,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,RfZNDPUok0WyQEa3Ko08ow,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,dyEaPloE8kazWC7Zblv8uQ,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,MvC6bRHxmkaXvWS6Q0cIFg", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Euf1h1qsZ02evDsyoHYbPQ,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,lsyYlxY9o0a3TbUrHkxeyQ,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,JVZHZm6nGEqE2zEEPoHbwA,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,zlqLrGEa8E2WW9ey_szREA,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,L2iFQcL170OajqfW7t6sVw,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Iz6cWpY2B0uOQ9BnY3fSbA", + "8705d28f-49f4-48d9-9946-13f18aabfe4b,", + "JTrfBXhIT02o2-ssP94zew,", + "JTrfBXhIT02o2-ssP94zew,8iZO162KNUiMC8l_z3OC_A,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,f_c50saStkaGJuhLjJ9lfA,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,FUP4iRnoc0ikNmjrFD06lg,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,GmS0wKwzVEqdf3NoTMHG5A,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,ZNKGkou_gU-1cxBFy7pDKw,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,-Iqmonry4EKoqAULCKzRPA,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,D-SngK7hhkOpoZ2aMWv9EA,mkDVc0Mz4kmuLZ7powBHpQ", + "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3,", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,TeSudgm-NEy3DXe3z_VRqg", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,RnrXuIRvMkeW5w1lcNiuaQ", + "J6IQt4G1zUeN8v9Wn50BUw,", + "UcPo9SsF3EGTCBB2J-Efdw,", + "UcPo9SsF3EGTCBB2J-Efdw,viL4CPch2ECwG-RNaWcyZw,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,8ANbOhNnO0yLQOCIBsDwfg,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,a5nUvJZgTEGBUvdHx0Y6Ng,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,JWBK5v78Y0uRVEXlD-Qk6Q,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,thG3Dc1PFEqAvT9xCVshuw,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,hf0gkFVS8ESvceRsC3LAXQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,4scG_UBlzE-5DWRhSrtm6g,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,OqsbZ8Xls0i8uA6ha7H3qQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,nX7krD8e2km_ZlOLWcoxdQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,ns5tIRishUOp-nYqLedcwA,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,k_ZYy0SjtU2TB7IFdq2vDA,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,I1J-xY4YNU2dB_od4P_7Dg,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,2sf-FU4WckGBDWUkVz4z1g,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,3PfP661Fg0CruzRxis9EiA,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,d9mwowRelE2lDKEA0UQMZQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,Uz2hrptCa0erjqyPoiZxew,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,PQTvQiYu-ESRUldnxgm5BQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,oNtLjorrZ06t4yIdWqKCcQ", + "UcPo9SsF3EGTCBB2J-Efdw,pAlEt5sqD0mKgFd_0b7RgA", + "UcPo9SsF3EGTCBB2J-Efdw,PNH0VNS1qk6nWVecYmVDDQ", + "UcPo9SsF3EGTCBB2J-Efdw,sXF1BZr8X06AYnWUrWxy1Q", + "UcPo9SsF3EGTCBB2J-Efdw,e5954339-a379-4510-a54e-18f64e29ffe5", + "akAhnN0cFkecj8q3spixkw,", + "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,", + "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,M9ZKWX59e02037Qy4rgmAA", + "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,u2nh8EAZT02TxPTWVl59UA", + "1db08f02-d559-40ea-9bae-dd11046694a7,", + "fbf74c15-4b42-4cd4-94dd-d0e86f11541b,", + "5944a66e-b386-43d9-a4b6-7b4a8e987fd8,", + "b7ba6920-e000-4081-a025-7da67c803008,", + "b7ba6920-e000-4081-a025-7da67c803008,YfRfaC49J0yW0wnSJf5DwA", + "b7ba6920-e000-4081-a025-7da67c803008,0WW04NMMfU2LLahCq0jQ6Q", + "b7ba6920-e000-4081-a025-7da67c803008,4kYWnAwZo0SDZFmiU8jJwg", + "f09be0ec-19eb-4443-843c-b387b6731e48,", + "28f00eee-d3d1-4cac-ac70-fca064fcbaf0,", + "9ab4ea8a-1b16-4969-9b80-8e36b1f782ca,", + "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,", + "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,ce465d53-3807-423f-baa0-bdba2a75ceb3,65b89eae-347e-4eff-9ff3-7f25cace0125", + "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,596097a8-d545-4256-959d-1d3cd96fc64c,65b89eae-347e-4eff-9ff3-7f25cace0125", + "d15139de-5e57-423b-b8da-335ad7a4df5a,", + "f51abeae-7c5d-4065-aa2a-b2e7025b3fcd,", + "d52ff2bb-d396-478d-8153-2aa845feb306,", + "d52ff2bb-d396-478d-8153-2aa845feb306,N-C_xBTZeUq1YsdELln53Q", + "d52ff2bb-d396-478d-8153-2aa845feb306,G4MuMrpiKEWN0uEBcGlX4w", + "32cdcbc0-6a9e-4245-980f-6888f12fe065,", + "32cdcbc0-6a9e-4245-980f-6888f12fe065,VfSI4zfHbECwcrQmcbelTg", + "32cdcbc0-6a9e-4245-980f-6888f12fe065,qRIbIelHBUOU71eoK2XQeg", + "3916a22b-ef4b-4ecc-9945-fd57867af597,", + "3916a22b-ef4b-4ecc-9945-fd57867af597,IcgjR69rYUuNhsb3s9qjYA", + "3916a22b-ef4b-4ecc-9945-fd57867af597,yMkN0om7l0-_47gDrBWxiw", + "8d703940-224a-4b0c-a04d-f365550071e1,", + "8d703940-224a-4b0c-a04d-f365550071e1,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,7c4d447c-6594-4bd8-9510-12ad683ab116", + "8d703940-224a-4b0c-a04d-f365550071e1,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7c4d447c-6594-4bd8-9510-12ad683ab116", + "8d703940-224a-4b0c-a04d-f365550071e1,qdA34j6p_UqphqChTCHEYQ,7c4d447c-6594-4bd8-9510-12ad683ab116", + "c51877eb-efda-4a04-8394-092637e2a9bf,", + "c51877eb-efda-4a04-8394-092637e2a9bf,Z6o5ySTsmkmwAGslDH7k4w", + "c51877eb-efda-4a04-8394-092637e2a9bf,cLKDeDSwd0OvF27bW_wBWA", + "c51877eb-efda-4a04-8394-092637e2a9bf,m4t_QKkV4EWTVEH8biQuEA", + "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,", + "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,c3Su0qnh-UK-ymcGaUy4Uw", + "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,zhgbWHad00aE2KNFM3FNWQ", + "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,", + "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,fcbdfbfd-254a-4538-b119-43e23404b8b5,e5635772-4e84-4b30-b77f-4a1196cb599b", + "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,b1ea158d-1663-4db1-bb09-9847f6e9d1e6,e5635772-4e84-4b30-b77f-4a1196cb599b", + "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,e5635772-4e84-4b30-b77f-4a1196cb599b", + "b6rLwzD4NkKV7xKn9ZYVkA,", + "b6rLwzD4NkKV7xKn9ZYVkA,mvMeyStxGUGlNLSYNeSubQ,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,u_Gzj9Nro0WS48xKnH9few,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,JTgXKeT7w0K2Mw4Jk2HCqg,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,rNhOQoP3XkOBHV28ze2NpA,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,TEgugZRs80iJbhL87RdYwg,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,ytmSzjhCr0Sm58MSNbwJUA,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,yM5xRrd9OkmDGyQddwOP6g,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,-Nf7WM2TeEKujB637h3tRA,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,LuYelZY8NkqmOp1pnxPXBg,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,Z10TTlOeW0mumezbqXKGzA,Ht0WRvpguUaLZ1tigCzCMA", + "b6rLwzD4NkKV7xKn9ZYVkA,Ist3SXFwKkCCvxirWLLjng", + "b6rLwzD4NkKV7xKn9ZYVkA,9jB8U3yxjE6hanEZgMdvtA", + "b6rLwzD4NkKV7xKn9ZYVkA,Ebi3A88h2U2yiygzWBBL_w", + "b6rLwzD4NkKV7xKn9ZYVkA,gPPg0QrOlEmVmvEc_OheXA", + "b6rLwzD4NkKV7xKn9ZYVkA,d022fcd7-850e-490b-893e-eb713e5848b5", + "5362d685-fc55-4b43-85a5-cc3d6cad3f69,", + "ac957e1e-676b-45c9-a39d-b238d2329a03,", + "9c4976c1-a0eb-4a1e-b5cd-08d44a4b3011,", + "cd77c8ed-ab60-4f42-9bee-0141c7e3e626,", + "b6d10952-d492-45e5-b54c-de3ab138be0b,", + "b6d10952-d492-45e5-b54c-de3ab138be0b,2KCpbel8JkaeAGQRgvpt2w", + "4c52478a-b2c9-4520-8f88-52f2262cc6bf,", + "4c52478a-b2c9-4520-8f88-52f2262cc6bf,YR7P-KQLGUKPF7JbW85K7g", + "b5221aee-22ca-4855-81d9-0b4491229060,", + "2c09d6b4-039e-4475-b013-0630740d820d,", + "W7AJ4UYlVkWF5nLbmpmRBg,", + "W7AJ4UYlVkWF5nLbmpmRBg,5Hk7UR60rkquUNxpQ8O0Gw,zMXDVNsKn0GhVd2Tjho0-g", + "ilDgmey2ZUWlFiuEAN7YoQ,", + "ilDgmey2ZUWlFiuEAN7YoQ,K9wPZ9LHo02GPMtf02rANQ,U7jMLMX4_kKIy1K9IcHOXA", + "3R_zf8xliEiurVU8GwaItg,", + "3R_zf8xliEiurVU8GwaItg,iqSyLD37IUejWYa8ndvqnQ,qrXO8rdEaUqUCXC1QVkhsw", + "3R_zf8xliEiurVU8GwaItg,TRpbSTiI9Ua2E6H7XiT0lQ", + "45e31b92-0cc5-4841-9c3b-74b3f1830435,", + "45e31b92-0cc5-4841-9c3b-74b3f1830435,ENYQugZRP06SDh6d-oJcjQ", + "45e31b92-0cc5-4841-9c3b-74b3f1830435,KdsZQVCA-E2TiEozsSUv7w", + "45e31b92-0cc5-4841-9c3b-74b3f1830435,uQjh0DySZE6M6DzD2VUPgA", + "97253092-90ba-4ff2-b137-6324ab244d11,", + "97253092-90ba-4ff2-b137-6324ab244d11,4oywwjb3-kifr-um9x87Pw,11ZEZTFhy0ONpoXb3kiewQ", + "97253092-90ba-4ff2-b137-6324ab244d11,i4Zu9uZuWkWwm8IVGZwkbA,RmjxL1O9bEGOuYxY9kzHEA", + "97253092-90ba-4ff2-b137-6324ab244d11,HQOKcDCvVUCItkAKo2ygGg,RmjxL1O9bEGOuYxY9kzHEA", + "97253092-90ba-4ff2-b137-6324ab244d11,Rd-iQjCjUUW8_o2kIb1rFw,Ez3OIIGl0Eqys2JGnSnQHw", + "97253092-90ba-4ff2-b137-6324ab244d11,lYsxZfxd0EadrnjliEj4Ug,xnZj7ipl60WO8e_BXK-mlw", + "97253092-90ba-4ff2-b137-6324ab244d11,ymRNNF0h3UOv-yH0ILxhiA", + "5873814e-c9a1-4042-b96d-2aa0e0d52e2a,", + "1014dded-3bbc-479b-a6cb-afa7413de2da,", + "1014dded-3bbc-479b-a6cb-afa7413de2da,DtFYrQwaCky5C1ZycqZbiQ,egpzBs77Q0CpDndZHqXOog", + "1014dded-3bbc-479b-a6cb-afa7413de2da,9SAPupKy_EO6YeG0SMkI5A,egpzBs77Q0CpDndZHqXOog", + "1014dded-3bbc-479b-a6cb-afa7413de2da,hiMYlmk5sEqh-sElOY3A9g,egpzBs77Q0CpDndZHqXOog", + "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,", + "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,RBRFHN8_rkuImbyDAfcHWw,BOB_mAXdA0u2khOH5qjqXQ", + "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,QJSPgq2KXEOWNynY8uqQvA,BOB_mAXdA0u2khOH5qjqXQ", + "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,4RH8_Z1mqU-JIVvx31wOsA,BOB_mAXdA0u2khOH5qjqXQ", + "d12e5738-00c7-47e7-9334-8399eec5aa89,", + "097a3266-4aab-4b23-8f56-373c9b1bc837,", + "097a3266-4aab-4b23-8f56-373c9b1bc837,YB7kXMyLJ0mF_5WidhmH8A,SeA8s77hr0K6DRZyVlRTCg", + "097a3266-4aab-4b23-8f56-373c9b1bc837,bQyNgFWAe0SPttPri6hxyg,SeA8s77hr0K6DRZyVlRTCg", + "097a3266-4aab-4b23-8f56-373c9b1bc837,wRPoHTsXFESK5pjkxVMqGA,SeA8s77hr0K6DRZyVlRTCg", + "6fccd476-c621-4f3f-b2e3-45c95be912b5,", + "6fccd476-c621-4f3f-b2e3-45c95be912b5,v_5qFi-vAUSDd1fLNL7pvA,X0_GvGrZGkSP9Ub81CHgqQ", + "6fccd476-c621-4f3f-b2e3-45c95be912b5,y5O8zRJy4kuhnXzATSWKew,X0_GvGrZGkSP9Ub81CHgqQ", + "6fccd476-c621-4f3f-b2e3-45c95be912b5,tLbmGIoldE2CC5Qi1oyqyA", + "85e03c16-4288-45f4-a579-c911437d27d4,", + "105a5100-cb57-410f-a26b-1ee21002f067,", + "105a5100-cb57-410f-a26b-1ee21002f067,prEZHWvOM0ysaNfAe06ywA,q0nBwoIx_UWPe93wHDWTYA", + "e6216087-fb37-46f1-bda4-a88c4640f339,", + "e6216087-fb37-46f1-bda4-a88c4640f339,KwhdQ1deNE6Y6jzO9vaAnQ,byvLynH8x06OQ7GNCXOVYA", + "fbdca48d-4ae6-41ad-8c33-b70a9e33fd77,", + "022ce375-ed02-479d-b87d-9d72d126bbd4,", + "022ce375-ed02-479d-b87d-9d72d126bbd4,H6fcuLxeBUSY3oERFc-plA,oGrNKZLpMU2bLwNEe4MPnw", + "022ce375-ed02-479d-b87d-9d72d126bbd4,dpfTnOvsrUePTVs0XZD9qQ,gOt7BYXvw0uUS-U53KThAA", + "022ce375-ed02-479d-b87d-9d72d126bbd4,3XVjOeAlF0K23sB1krWo7g", + "49397e16-1027-4286-9bc4-b59eae565ff0,", + "5f492f8f-ab61-484a-9587-747132b54309,", + "5f492f8f-ab61-484a-9587-747132b54309,3b2cfc85-4e2a-4e0d-b738-c296ae8692f8,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "5f492f8f-ab61-484a-9587-747132b54309,78eb0969-f246-4cac-acd6-295df7c092e4,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "5f492f8f-ab61-484a-9587-747132b54309,oF_wUv-ShU6GP34Ch0MnlQ,YTd0lIQLcU2Y2trwuLhJTg", + "5f492f8f-ab61-484a-9587-747132b54309,zwKRET9pP0O5uhPU_yHrlg", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,d2b40639-5d34-45b6-904e-6dd29030ff44,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,724c79a3-822c-422f-9462-a5374ee0211c,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,c72911d0-453f-4732-b1bd-dad520220a06,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,90bc2d31-1715-4d1a-813a-7c57e66cb017,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "1631eb78-6718-478c-9b62-abbb0537e09c,", + "1631eb78-6718-478c-9b62-abbb0537e09c,9Yj55hS4gEOB34bggoRnzw", + "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,", + "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,g_jYTusUbU6W_6jHif-CKw", + "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,", + "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,YzJL2s9S0Umv5XH_g5PG_g", + "96781359-a8e3-4cfb-be5e-79456ead88c6,", + "17e64798-f6ca-47b9-84ed-0c83b8678056,", + "01107a3f-9a4c-4fb9-8918-e6195e346d42,", + "d7410e54-5193-48b9-b5c9-06e4439dd924,", + "d7410e54-5193-48b9-b5c9-06e4439dd924,njOH4bbntk6THrxbd9TDDA", + "d7410e54-5193-48b9-b5c9-06e4439dd924,0aNp_2sWHUS_08TMe-_AzQ", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ed134bee-d199-43eb-98bd-206571603f40,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,59866578-d4e0-417b-9483-233ab108b1ac,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,22dc463a-a89b-46ad-9a0d-557c742b4a80,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ITH2OpzjP022vZ5JgxV_iQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,PMa9370LPEig_7pKwCceEQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ed134bee-d199-43eb-98bd-206571603f40,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,59866578-d4e0-417b-9483-233ab108b1ac,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,22dc463a-a89b-46ad-9a0d-557c742b4a80,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ITH2OpzjP022vZ5JgxV_iQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,PMa9370LPEig_7pKwCceEQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ed134bee-d199-43eb-98bd-206571603f40,fd67ec40-f898-4df6-b846-8bf350f362b8", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,59866578-d4e0-417b-9483-233ab108b1ac,fd67ec40-f898-4df6-b846-8bf350f362b8", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,22dc463a-a89b-46ad-9a0d-557c742b4a80,fd67ec40-f898-4df6-b846-8bf350f362b8", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,fd67ec40-f898-4df6-b846-8bf350f362b8", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ITH2OpzjP022vZ5JgxV_iQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,PMa9370LPEig_7pKwCceEQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ed134bee-d199-43eb-98bd-206571603f40,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,59866578-d4e0-417b-9483-233ab108b1ac,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,22dc463a-a89b-46ad-9a0d-557c742b4a80,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ITH2OpzjP022vZ5JgxV_iQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,PMa9370LPEig_7pKwCceEQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "9176086b-a0b4-4298-a7fb-eb91ea421e89,", + "39278302-b52a-48a8-a566-ed756a5fa1a9,", + "39278302-b52a-48a8-a566-ed756a5fa1a9,e3454bc7-c88d-4958-8b81-db3fe5472a1e,da36bbcc-2a9a-4ba2-9e4a-6f52c9b1ef74", + "7134c60b-0640-463a-8d97-caa9184bef93,", + "7134c60b-0640-463a-8d97-caa9184bef93,e3454bc7-c88d-4958-8b81-db3fe5472a1e,7e688b30-ed13-4790-9ad8-c49885d1b7e8", + "c859a371-5d34-4e53-a96f-a6f7f3377aba,", + "c859a371-5d34-4e53-a96f-a6f7f3377aba,aMOwLebcBU6SuPigtovUYg", + "123de433-a005-4994-8a11-d2d84ad66b11,", + "123de433-a005-4994-8a11-d2d84ad66b11,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,4e378419-d043-4e86-ad0e-66288ac21cf4", + "123de433-a005-4994-8a11-d2d84ad66b11,oT1vLrHW6UC2hWLIT3f_SQ,4e378419-d043-4e86-ad0e-66288ac21cf4", + "123de433-a005-4994-8a11-d2d84ad66b11,-e6bmXnwZU2EVwZ8RupKbg,4e378419-d043-4e86-ad0e-66288ac21cf4", + "24d94673-f394-42e6-a3c8-276f8b8aa722,", + "24d94673-f394-42e6-a3c8-276f8b8aa722,08b7c12d-3317-4d27-9b40-34737a709454,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "24d94673-f394-42e6-a3c8-276f8b8aa722,jkuw9EhCpEKn-2lKJXWd3Q,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "24d94673-f394-42e6-a3c8-276f8b8aa722,2Z_FxSPiyUODtlFZWF9DFQ,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "3044adce-90c0-4f66-81f5-39c4fca86fdf,", + "3044adce-90c0-4f66-81f5-39c4fca86fdf,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,3b825377-d2eb-41e4-a736-b107f88f62d6", + "3044adce-90c0-4f66-81f5-39c4fca86fdf,EC0ttvk6eU2HEMacLAx2Iw,3b825377-d2eb-41e4-a736-b107f88f62d6", + "3044adce-90c0-4f66-81f5-39c4fca86fdf,mhsglVEtREmElF5wuLw3NQ,3b825377-d2eb-41e4-a736-b107f88f62d6", + "665d4fe9-88dd-4fae-97ad-201884a6ca02,", + "mfFLpKQRE0anhrmeUvW0bA,", + "mfFLpKQRE0anhrmeUvW0bA,p5TStfnDXk-EY7k6UdHxnA", + "37b68987-73d9-4aae-8e3a-edd24ae890a9,", + "37b68987-73d9-4aae-8e3a-edd24ae890a9,UCxz68a5E0WaZkocDfEf5A", + "32b7a724-1112-476d-9ddc-2d04655c9df0,", + "C4uuztTiYUe20PACMARksA,", + "C4uuztTiYUe20PACMARksA,apSQiQ1eO0mEzRsbugeu9w,fz_0gZ6RXEC78NpYsx_eqA", + "C4uuztTiYUe20PACMARksA,VpqHNpPiY0CfRJEjRRM-mw,fz_0gZ6RXEC78NpYsx_eqA", + "C4uuztTiYUe20PACMARksA,Z1F9rjKqCE-Hd2-Ip5g3gA,fz_0gZ6RXEC78NpYsx_eqA", + "C4uuztTiYUe20PACMARksA,iufpnteVCEiiZu_m-yPovQ,fz_0gZ6RXEC78NpYsx_eqA", + "2dFMy4zc7ky_Fd5fzfgInw,", + "2dFMy4zc7ky_Fd5fzfgInw,UUyjVU4mvECPhI9xTMYR1Q,4EHIyZkOOEqnM-PuD_a5xw", + "2dFMy4zc7ky_Fd5fzfgInw,HWc25-d8dUmNN2F3n8mV5A,4EHIyZkOOEqnM-PuD_a5xw", + "2dFMy4zc7ky_Fd5fzfgInw,Y0e7o0_BKUm7XJSLediN5w,4EHIyZkOOEqnM-PuD_a5xw", + "2dFMy4zc7ky_Fd5fzfgInw,MS7EPNLJEEyMLC6_yHQYtA,4EHIyZkOOEqnM-PuD_a5xw", + "exQz9Zr3HEyOR9nDDrimlQ,", + "exQz9Zr3HEyOR9nDDrimlQ,z-yaXP9ImkysLOGuCQiblw,S21TW8NwMEWLV9zqHxWfFw", + "exQz9Zr3HEyOR9nDDrimlQ,SmRcrycMZ0qYNsHuQQOORA,S21TW8NwMEWLV9zqHxWfFw", + "exQz9Zr3HEyOR9nDDrimlQ,0svDrGn2NkmhUjC1VJUgRg,S21TW8NwMEWLV9zqHxWfFw", + "exQz9Zr3HEyOR9nDDrimlQ,Aq4fjEW5gEyzetXMoG2tdQ,S21TW8NwMEWLV9zqHxWfFw", + "affdc9ad-8312-4421-8f56-e35dfdaeb63a,", + "e97f05da-57d3-4927-aa22-c124a94d8432,", + "6b9e022c-0b68-48fd-8eca-da8573c18900,WWVPdeMMi0S4_Dmn85yEqQ,FRUp0aYZTUmAXsybauOs7g", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,ZXeI9jooq0-dQaX-FlkNuQ,CAqT_iLfQ0qQ-GbIdAsZow", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,IzdUC8x0F02i3BhPfEUOYw,CAqT_iLfQ0qQ-GbIdAsZow", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,nPUQvBsxu0mzhQg9YnWEgw,CAqT_iLfQ0qQ-GbIdAsZow", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,wyKAZSZwT0u9cuSaQ_-IHg,vUgpsaXrO0ak7huhkHqlUA", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,_OM7UNZENEejgO5h7H1ztg,vUgpsaXrO0ak7huhkHqlUA", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,D9U3j_XgSUesfHtSNoAdLw", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,I8SCbauJX0OLAKWOjy78yQ", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,SiraUm3ZaUOCeXCekunJuA", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,9Z95fSKS00GscDxK8-ARlA", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,wLpOk1g2rUyDeCKb5uOeSg", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,RKftA1Fw30-hriuZNMGsoQ", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,P6w6X7-Dak2G6O8HDRwlVg", + "3eb14fcf-5d20-47d3-b991-97808798a225,", + "3eb14fcf-5d20-47d3-b991-97808798a225,fV4H_1ZXx0-OVtlasI-xGQ,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,B9DJPqTeYUuPnPHRbKzEJA,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,yXZO-donI0SHB9-TT0jUdw,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,J5VsmblfWU2MeUjNo0fz1Q,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,Asfe0ZRi50eryD5wlJzgzA,5ekjxLQZwUm7-iXJ0bYSiQ", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,916384ca-e209-42a0-bd64-c64f5a40b56f,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,TLrw3oncCE2MeuVifXSWTg,-_ViYCHHRUip7oN_AnH0Og", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,7Jx8mrxoi0aQtZEb03CdRA,-IgxYllxkEyr7V1gosWXUA", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,_VfdBTi7PU276qzsYKayow,qSsEcadcm0-twR1TvVcgYQ", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Dtis2HpVzkelZb6M9mW5TQ,iqGv6iRJ-UGbofv8-aHKVA", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,xNjX_6-aREm0cYZObVLiAg,OE-PtL0-0kWcJPwn1wvuVg", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Kcyp8sv0202Puym1Wf86xg,OE-PtL0-0kWcJPwn1wvuVg", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,r_QZLd-Aw0SsdQb_uYroog,eYGWiwqSGUG7Hx1R3I-11A", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,916384ca-e209-42a0-bd64-c64f5a40b56f,96b36c98-cba1-4337-91dd-c74c00316f97", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,96b36c98-cba1-4337-91dd-c74c00316f97", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,RA7cizxxYE6J8yV_0XwPxA,VSLcLM0TJU2kJU5gS4KZGw", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,5_UnQ7vZ9E2T-EhJ7M-mQw,jL-LAsd03EeN1J-Zp8opew", + "b537c30c-faef-4649-9a1f-be3985936f93,", + "b537c30c-faef-4649-9a1f-be3985936f93,7amHTgEv-UOJSGl0ofo4mw,lb5tuJOqnUKp9prjjlgHmA", + "b537c30c-faef-4649-9a1f-be3985936f93,cKHAetNcgUOxFfDkAPPF9w,lb5tuJOqnUKp9prjjlgHmA", + "350c5c1e-318b-4233-a7e5-1f49909d8e65,", + "350c5c1e-318b-4233-a7e5-1f49909d8e65,7amHTgEv-UOJSGl0ofo4mw,CUCKsNVnIkavHjcspWAv_A", + "350c5c1e-318b-4233-a7e5-1f49909d8e65,cKHAetNcgUOxFfDkAPPF9w,CUCKsNVnIkavHjcspWAv_A", + "61ba3c90-c81e-4deb-bc79-50c0f1fe3e83,", + "hT7nSD7Ts06F_1SNlOMLwg,", + "hT7nSD7Ts06F_1SNlOMLwg,lhfOKGDAgEKWucerCmH7Fg,mG60EPIQIkaJNgOkonVmZg", + "hT7nSD7Ts06F_1SNlOMLwg,kLv_wsbbGUmlfIUO1QLYLw,mG60EPIQIkaJNgOkonVmZg", + "hT7nSD7Ts06F_1SNlOMLwg,f0pjsV0XMUWoE_z2kqOlSA,mG60EPIQIkaJNgOkonVmZg", + "hT7nSD7Ts06F_1SNlOMLwg,EDLxuGjEVEGdOPXDqyN-xA,mG60EPIQIkaJNgOkonVmZg", + "WuShlrJnHUKuscn8e6Rt2g,", + "WuShlrJnHUKuscn8e6Rt2g,Ot9EG-FfKEauT0u6pSZuWg,M5z2wk-GDkOAjvZzqsfcNQ", + "WuShlrJnHUKuscn8e6Rt2g,i3rD_hnQG0a-5f6Ve4AfWg,M5z2wk-GDkOAjvZzqsfcNQ", + "WuShlrJnHUKuscn8e6Rt2g,S9uymuvraUCOM8gmB7bnEA,M5z2wk-GDkOAjvZzqsfcNQ", + "WuShlrJnHUKuscn8e6Rt2g,_uh3kj6hKUyBKOSR3R8dog,M5z2wk-GDkOAjvZzqsfcNQ", + "WuShlrJnHUKuscn8e6Rt2g,kaRBW1u3dkODGjPMePW6Uw,M5z2wk-GDkOAjvZzqsfcNQ", + "MJkDYDH510izAab13ZfXQw,", + "MJkDYDH510izAab13ZfXQw,VQjkyb5aoEKHKo_eZsq9ug,2UNd6eViFUO359xjSiuu2A", + "MJkDYDH510izAab13ZfXQw,kTP3xKhzBkuQF3SqvmYFKA,2UNd6eViFUO359xjSiuu2A", + "MJkDYDH510izAab13ZfXQw,hYu9itGe1Ee5Neanhj9VtQ,2UNd6eViFUO359xjSiuu2A", + "MJkDYDH510izAab13ZfXQw,FE7_gQNBt0CXzMN6di5HWA,2UNd6eViFUO359xjSiuu2A", + "MJkDYDH510izAab13ZfXQw,dYPaOkwy3EGAfvl8RyN4pw,2UNd6eViFUO359xjSiuu2A", + "eS_or4AnMka9M_bD7YN28g,", + "eS_or4AnMka9M_bD7YN28g,zaSTWOUy_k6cnoVVYzypWg,ZtCqMnFTQUWv2h0MfZ4m6A", + "eS_or4AnMka9M_bD7YN28g,1FcyrVGyGEC9dmOq_51nFQ,ZtCqMnFTQUWv2h0MfZ4m6A", + "eS_or4AnMka9M_bD7YN28g,xpkEQ6yjhU-WfFPSjvPdHg,ZtCqMnFTQUWv2h0MfZ4m6A", + "eS_or4AnMka9M_bD7YN28g,N9CBuYaMtUOrCks5UcOycg,ZtCqMnFTQUWv2h0MfZ4m6A", + "eS_or4AnMka9M_bD7YN28g,LDjFJtM-WkeYhtwA7cyDOg,ZtCqMnFTQUWv2h0MfZ4m6A", + "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,", + "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,6secgNPvmUyFnI3NC5dIYw,5V_1JEDrRUa1UPKhtrSB2w", + "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,X5SuBHIGBE6wy8N8KDop8Q,5V_1JEDrRUa1UPKhtrSB2w", + "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,bc-aVFn__E61pyA4SvUoGA", + "5cd08cfb-c729-4c30-96d9-6a99bb934d91,dDSR-6ck8UGOeCSXR8Xnug,OFIqUvE4r0yMLHSMnlWMhw", + "5cd08cfb-c729-4c30-96d9-6a99bb934d91,EaJMu_M0OUyUZ_3x-X-d7Q,OFIqUvE4r0yMLHSMnlWMhw", + "b3949221-9416-4afe-8777-440f01ec4c1c,", + "b3949221-9416-4afe-8777-440f01ec4c1c,oMz5rMzCxkiw9690ZkvlGw,gW2zFv0sFEmiodfw8p9P-Q", + "b3949221-9416-4afe-8777-440f01ec4c1c,zlBPAWMbOE2RuKoIy1Ps8Q,UwQiLub0-kqf4dNvQc-R0A", + "d0a9262f-5504-46a7-bb10-7507503db58e,5864d23d-1a86-40a6-a52c-46ee670e4508,2ee3944d-e9fb-4959-973e-570a7fc6cc57,167ba0d4-4fac-4143-b5ae-1dbd8e02dc76", + "d0a9262f-5504-46a7-bb10-7507503db58e,14afd295-bcd0-4595-80dc-d477618b9ab9,2ee3944d-e9fb-4959-973e-570a7fc6cc57,a74e6c0d-1300-4928-9141-977a711b71c4", + "d0a9262f-5504-46a7-bb10-7507503db58e,f8635fe2-90bc-42c8-9bbe-3387b5157e18,2ee3944d-e9fb-4959-973e-570a7fc6cc57,49934059-cc45-46ce-8277-f96959e1f6ba", + "d0a9262f-5504-46a7-bb10-7507503db58e,922f229c-124f-4f07-b4e3-19138420ca19,2ee3944d-e9fb-4959-973e-570a7fc6cc57,ceb07e4f-50a9-4ed0-b50b-c59d92ba66b5", + "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,0c496f32-0011-4c4d-9778-59446f70c032", + "d0a9262f-5504-46a7-bb10-7507503db58e,c0ff7719-c56a-4934-831b-506ad0f51cca,3259238f-4a9e-4e06-99de-6e7324fa0894,e38294cf-6279-4863-9aea-1a19aeaf8476", + "d0a9262f-5504-46a7-bb10-7507503db58e,381a9f13-5b44-48e6-8a49-221422a963f5,0440f08f-ef1d-49d8-942b-523056e8bb45,9e782751-71c6-4208-87da-3a3cffa6a4f0", + "d0a9262f-5504-46a7-bb10-7507503db58e,befa2f11-b9fd-457c-b1d5-9d82e90b70ce,0440f08f-ef1d-49d8-942b-523056e8bb45,770e7674-6f9b-41f8-b023-ae79738db0d3", + "d0a9262f-5504-46a7-bb10-7507503db58e,bde08851-a9e4-4faa-8f04-a0a661672472,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "d0a9262f-5504-46a7-bb10-7507503db58e,5e7a48ed-8f08-4990-a3ae-05722c9f59a8,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "d0a9262f-5504-46a7-bb10-7507503db58e,4d52bbd6-ef74-45ae-a6c2-bde10ab8eb16,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "d0a9262f-5504-46a7-bb10-7507503db58e,bda7a57e-5227-49f4-a119-e3f166604738,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,6c395d04-2aa6-4a3e-8c2c-695932bc9736", + "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,703ff56b-560d-4ff4-8c63-d195e879a328", + "d0a9262f-5504-46a7-bb10-7507503db58e,fe304452-b57b-45bc-9c1c-b85b70bcec12,0440f08f-ef1d-49d8-942b-523056e8bb45,6a007c73-d930-408a-93df-041a715e0dd4", + "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,8f19d7f9-4f28-48ab-a1c5-d8f4f788514c", + "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,75162f12-44e7-499f-bc6c-9b0612e9164b", + "d0a9262f-5504-46a7-bb10-7507503db58e,ba6b6e1a-a09a-4ba0-9523-552869f03336,0440f08f-ef1d-49d8-942b-523056e8bb45,d461ca71-45c9-415e-8e09-ba93e8d73450", + "d0a9262f-5504-46a7-bb10-7507503db58e,rnM1qDlYBU6_Dl1bjg0J1Q,0440f08f-ef1d-49d8-942b-523056e8bb45,OZb8bb5FNUuFYOkwANvxAA", + "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,0qh4mcxJH0Wsrb3nz8UeGw", + "d0a9262f-5504-46a7-bb10-7507503db58e,Ib_iMwvUm0CJ-t_Thwt77Q,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "d0a9262f-5504-46a7-bb10-7507503db58e,yaQ7AlUc3kGq8_RW3eTSmQ,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "d0a9262f-5504-46a7-bb10-7507503db58e,dDLQbD9jtkOmw3PR5QJdfg,0440f08f-ef1d-49d8-942b-523056e8bb45,2tFUNn-4kkK6ICo_Zp065g", + "d0a9262f-5504-46a7-bb10-7507503db58e,g4diCmk0wEmIKUVozk9tDA,0440f08f-ef1d-49d8-942b-523056e8bb45,nHg4Uifww0GEX15L27WonA", + "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,MPfe2wahQECCDGfhZE-drA", + "d0a9262f-5504-46a7-bb10-7507503db58e,nLRxEIBhUkyQsfrk3nzsDA,0440f08f-ef1d-49d8-942b-523056e8bb45,n4hqSCv9OUewI30KnbPhFw", + "d0a9262f-5504-46a7-bb10-7507503db58e,M1JVd5ur7U2ZtpvhEhPNhw,0440f08f-ef1d-49d8-942b-523056e8bb45,tzMFIJyxZE6cZNuLdaKs7w", + "d0a9262f-5504-46a7-bb10-7507503db58e,6kPcLZtEqU2Hs4yBFvc_sA,0440f08f-ef1d-49d8-942b-523056e8bb45,NxMm0sSBdUatynU16o0_Zw", + "d0a9262f-5504-46a7-bb10-7507503db58e,TtncjvYcK0qHqWN-IMMoqg,0440f08f-ef1d-49d8-942b-523056e8bb45,hHhjZeLcaUGN1OyFoYHznw", + "d0a9262f-5504-46a7-bb10-7507503db58e,fwIgUAM_C0WhKds0O6DyPg,0440f08f-ef1d-49d8-942b-523056e8bb45,fJVxsLWuTESd-2xTCJPZ2w", + "d0a9262f-5504-46a7-bb10-7507503db58e,ZNPzHmrU6kGVQW7Q5ei03w,0440f08f-ef1d-49d8-942b-523056e8bb45,fJVxsLWuTESd-2xTCJPZ2w", + "d0a9262f-5504-46a7-bb10-7507503db58e,d3-sxPTJOk2rKo-Jgy3wvw,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "d0a9262f-5504-46a7-bb10-7507503db58e,v12eJ8louE-cW7-pCGzVLQ,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "d0a9262f-5504-46a7-bb10-7507503db58e,6kPcLZtEqU2Hs4yBFvc_sA,0440f08f-ef1d-49d8-942b-523056e8bb45,br9hzhDoZUmIevgl7SJkmg", + "d0a9262f-5504-46a7-bb10-7507503db58e,TuAjeDmPuUmFr_ESx4q6WA,0440f08f-ef1d-49d8-942b-523056e8bb45,AXtRUZcYWU6g1ZtM-L4vfQ", + "d0a9262f-5504-46a7-bb10-7507503db58e,LrQ1z-mTHE2vIePBwm8l_Q,0440f08f-ef1d-49d8-942b-523056e8bb45,omS7gF10C0ec0ozEoJGJXw", + "d0a9262f-5504-46a7-bb10-7507503db58e,owoIOIEM70e6WxnYdlKYWQ,0440f08f-ef1d-49d8-942b-523056e8bb45,t2do8WSejE-Y0aiEirKw9g", + "d0a9262f-5504-46a7-bb10-7507503db58e,eb1vYTP_KEGQH-f33_whQg,0440f08f-ef1d-49d8-942b-523056e8bb45,t2do8WSejE-Y0aiEirKw9g", + "d0a9262f-5504-46a7-bb10-7507503db58e,evgH6VWUgkKJNews32hUxQ,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "d0a9262f-5504-46a7-bb10-7507503db58e,UUlB4S3GVUGyil6ZK-EbWQ,0440f08f-ef1d-49d8-942b-523056e8bb45,omS7gF10C0ec0ozEoJGJXw", + "c8a97aeb-e8dc-40ba-b6bc-a7c7be907381,", + "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,", + "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,mkkk2N_2-EKX3pVfk7U5aA,UQe6v62i6ESImdpc7gB_8w", + "43354e59-4938-44bc-81e0-4fba47d0ac12,", + "43354e59-4938-44bc-81e0-4fba47d0ac12,4xp-xZyNWEi_jJCIhAPI3g,Ulie-RkcJEmblLyoucx-JA", + "74c65554-0d15-409b-b9a8-0b2620a7d7e2,", + "74c65554-0d15-409b-b9a8-0b2620a7d7e2,kkvNbVRjcEKtChpy3W1bqQ,nbhzQy_Hdkeohh4S1IezwA", + "74c65554-0d15-409b-b9a8-0b2620a7d7e2,GVoh3mdWf0WNA9lfqL1GEw,w9qG5gHlAUqh8f9ZgnezsQ", + "74c65554-0d15-409b-b9a8-0b2620a7d7e2,x4PPvuhg6USE6GSFgZZnCg,w9qG5gHlAUqh8f9ZgnezsQ", + "d4f86b2b-0fa3-4264-9eac-cf15b62463bd,", + "c4622dca-80dc-423b-ae22-2065174e817b,", + "c4622dca-80dc-423b-ae22-2065174e817b,pCEBoSP2pESLl9ih-J-55g,8SzXFoWD8k20cekFe6cLKg", + "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,", + "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,GhulL0k9q0euEooMTUKTHQ", + "40258a3d-0d5b-4b67-bb99-4bafc6410311,", + "b74ef8c3-405a-4d68-a2bf-01579a9cc397,", + "08dfa3f9-d545-4707-be94-6342db90c17c,", + "08dfa3f9-d545-4707-be94-6342db90c17c,eG1Lf1vG60qNQiCBRsAhqA", + "08dfa3f9-d545-4707-be94-6342db90c17c,a9oUAXZpHUaW1_ncqhkYZg", + "08dfa3f9-d545-4707-be94-6342db90c17c,CeebPv2TZUixq8cmVD5lUA", + "a936c982-28db-4e7e-bc79-ec5eca975beb,", + "a936c982-28db-4e7e-bc79-ec5eca975beb,cS-oBNlLE061SIB40QjiDg", + "a936c982-28db-4e7e-bc79-ec5eca975beb,rFcMAp5UvkuGalprkr_ARQ", + "a936c982-28db-4e7e-bc79-ec5eca975beb,oSoW8v6emEW4g1IduUNzOA", + "16359b51-96c4-4be0-b598-319c7549e18a,", + "16359b51-96c4-4be0-b598-319c7549e18a,3ya4VDfxLU-krfBh_H0VAA", + "fd7774bb-b29c-40b5-84b1-5c2bbe574255,", + "fd7774bb-b29c-40b5-84b1-5c2bbe574255,499irXZltkSvv9pWsdBSyw", + "c8c00394-4cd9-4bad-9774-a825ded78f80,", + "c8c00394-4cd9-4bad-9774-a825ded78f80,Qevoxmtsa0eNzGss_aG2gw", + "51b85276-3a40-4c40-93a1-9df307c3e78e,", + "745fd5ef-705b-40f0-bfd7-dbf4af5a82e8,", + "7652db86-bc67-4665-8937-f5199d27b5c7,", + "7a5cb589-5eab-4d95-9040-e9bfdc9590fc,", + "dece6d88-b6b1-4e01-b341-8ca4171353cc,", + "f0abcb90-d8e7-414a-b81b-a2aafacd4dac,", + "e441a297-22ba-4a43-92ef-3549c6cc45c1,", + "fe85784d-070d-4b33-82a2-e0875da053d6,", + "19d83cda-692f-4ec1-8da0-15211f3a0c34,", + "a28d3819-e5e0-457f-8363-09aaed6a1382,", + "696f46ae-eaf3-43b5-9635-70a78972826e,", + "26b8c05a-e960-4039-9e50-4e27befaa44c,", + "9815999a-c4d3-4741-a34f-2545b0cb8e11,", + "34023e02-f59f-4e90-b245-e298cbc1a6d8,", + "ad21788e-1995-4c2b-9c7f-610bbc1019c4,", + "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,", + "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,f841cdfa-45ba-4f09-a117-503eef34cb4b,9c55b609-b7dc-4070-a3ad-6351a0054a06", + "a4c26527-8f75-44da-b150-7aaf8ef739c1,", + "d57cf97e-b5d2-4e47-8231-a226a161f6e3,", + "87792b6e-1468-45e1-b28c-83f27ada7299,", + "973dc863-b9a5-40d8-8f15-4096e75bd2cb,", + "e2620c2c-c28e-45ce-a83c-0588329b9c81,", + "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5,", + "76369aef-aeda-46d2-996a-cd00594d0543,", + "76369aef-aeda-46d2-996a-cd00594d0543,40283127-2ffc-4989-82f6-29c3381bf9e2,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "76369aef-aeda-46d2-996a-cd00594d0543,1421caa9-9a0d-42c2-b847-1500ba2e483a,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "76369aef-aeda-46d2-996a-cd00594d0543,390a476a-5545-48eb-955c-1bf1a16012bf,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "76369aef-aeda-46d2-996a-cd00594d0543,46970903-9799-4d25-b90a-7162c1f5dbef,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "76369aef-aeda-46d2-996a-cd00594d0543,PZx7Iae0pkiDaxT9t3-vgg,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "76369aef-aeda-46d2-996a-cd00594d0543,8XajS2adOEOqzqnLy6-zcQ,P12CUkIg8EuFihGQsvPlxg", + "76369aef-aeda-46d2-996a-cd00594d0543,_Fl6LKUUykSBrdEgaN-GFw", + "4308da04-af84-40bb-845a-7a9e1a2726ec,", + "4308da04-af84-40bb-845a-7a9e1a2726ec,531b4297-b38b-4362-9854-4f2139ea43be,33ee376a-586e-4aff-a7e4-4213445a3e92", + "4308da04-af84-40bb-845a-7a9e1a2726ec,46970903-9799-4d25-b90a-7162c1f5dbef,c2052d8c-fbcd-4462-8f08-bcf5b3dfde2f", + "4308da04-af84-40bb-845a-7a9e1a2726ec,4VkPVbBVLE-eJHilwGMAbQ,HTjlzAlWZ0ezY6w3ngSQSg", + "4308da04-af84-40bb-845a-7a9e1a2726ec,newGzX_zA0eGUX3Iayq5Ag,6Jv4AJGBYUGP_n4sDcz2Kw", + "4308da04-af84-40bb-845a-7a9e1a2726ec,352obeXyMkyLIv62h2g-Ng", + "08d56627-4500-4724-8692-a23c96ddb3ec,", + "08d56627-4500-4724-8692-a23c96ddb3ec,531b4297-b38b-4362-9854-4f2139ea43be,c2a9c007-2748-4fa2-ab9b-0081eef792f1", + "08d56627-4500-4724-8692-a23c96ddb3ec,46970903-9799-4d25-b90a-7162c1f5dbef,05cff8b8-d437-485a-b4b8-41d35becd022", + "08d56627-4500-4724-8692-a23c96ddb3ec,newGzX_zA0eGUX3Iayq5Ag,QBzMp7MdAUOLIofvddui1A", + "08d56627-4500-4724-8692-a23c96ddb3ec,6ItbfT4igECHl2eLuhGLIA", + "08d56627-4500-4724-8692-a23c96ddb3ec,326QgcQbnUGJBs2xzFKVOA", + "e49d5d78-157e-4bc8-b878-ff7c4963fdc4,", + "e21b9dd6-c3d1-42cb-a90d-1b60ffb826fd,", + "5f6a7dee-0b44-4138-b3a7-a079dbc63e83,", + "3bceb51c-ef29-4942-8b8b-a15ed4a41451,", + "3bceb51c-ef29-4942-8b8b-a15ed4a41451,4lwF00Rvb0Kr3FJz07HlOQ", + "936d7126-a3b7-48a2-bceb-6cad9221b267,", + "936d7126-a3b7-48a2-bceb-6cad9221b267,eutz2irupECSYGMtru3NMQ,Gruqp7EGM0WJbAM_F8hWbQ", + "936d7126-a3b7-48a2-bceb-6cad9221b267,4PI6XssggEKRxkGlWeo8KQ", + "936d7126-a3b7-48a2-bceb-6cad9221b267,Mp_TzaiSTUaULweziW9_AA", + "eb91dbb0-0dc1-497e-b6d0-b7cd4482bacf,", + "ab446412-84cd-490d-84e0-2e666633b72f,", + "3028ba83-0c85-4e27-a47a-56609fa58b33,", + "40c1f20f-182c-4751-afd1-b572b0fd061e,", + "2b7f4dae-51da-4af0-b009-12d6005c85c6,", + "Lli8-aqt1EmCmyXwBRQaYA,", + "oRUh266ohkWQil14uxcEdQ,", + "zpjkkb1HtUORoPaODFuuxg,", + "25e76b00-4c56-4259-968f-046c14fb36fd,", + "60cc933c-5bc8-49c1-aa6b-9c2adfef088e,", + "f4f372b8-33a1-462e-a9f7-8a0e85b2c0d7,", + "b79a6f2c-d960-4d8a-b05f-24e818ec5f60,", + "fe2721b5-16c8-4d9d-bca5-0233ab008fbb,", + "5e9bbda6-d681-42b1-9cc4-5758acf34bc0,", + "b95678aa-351a-4929-8a0e-0274a183eb18,", + "8df990d9-141a-4757-b4f2-138d5e68ab74,", + "8df990d9-141a-4757-b4f2-138d5e68ab74,AfqXBWLBQEiiag4cEtoEiA,MrrsmY0J0EKduH-f5wUj6g", + "8df990d9-141a-4757-b4f2-138d5e68ab74,RROvzwtshUGyRudVIBn8bw,MrrsmY0J0EKduH-f5wUj6g", + "8df990d9-141a-4757-b4f2-138d5e68ab74,69vv21jfTUu1E_LjK5bytg,MrrsmY0J0EKduH-f5wUj6g", + "8df990d9-141a-4757-b4f2-138d5e68ab74,IUDG__cu9EG9wK57ajRTHQ", + "86dd9ff9-643f-4f11-8243-93ad948db4cc,", + "745b4725-5bf2-4b78-9847-076f02eab97e,", + "745b4725-5bf2-4b78-9847-076f02eab97e,G9cFrBRvw0S-nDp-Fn494g", + "745b4725-5bf2-4b78-9847-076f02eab97e,KcsFLjkhCUmg8HbamtChbg", + "745b4725-5bf2-4b78-9847-076f02eab97e,ye7GYcd6PkObqAsla6BJaA", + "745b4725-5bf2-4b78-9847-076f02eab97e,xVPRKfkYG0-u4MFhJYGe9g", + "745b4725-5bf2-4b78-9847-076f02eab97e,84OVMNAiv0mQwX2bpkw2yg", + "745b4725-5bf2-4b78-9847-076f02eab97e,ishXZTP3bkeVNk68PBZAiA", + "745b4725-5bf2-4b78-9847-076f02eab97e,cLswsDJEmkCqYDGI8DJmPQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,fdmR0dt0hEakaNp8mpjGmg", + "745b4725-5bf2-4b78-9847-076f02eab97e,5mFA4uHLmku8oCc1rDJmXw", + "745b4725-5bf2-4b78-9847-076f02eab97e,ry0fWzcysk2fsr-zIvzjUg", + "745b4725-5bf2-4b78-9847-076f02eab97e,S9YQnGF2WkaFSG07d-5TUw", + "745b4725-5bf2-4b78-9847-076f02eab97e,pcEwV_ZPxkSKP39UgPh6fw", + "745b4725-5bf2-4b78-9847-076f02eab97e,nHa0WjWfl0-g-sknorb10g", + "745b4725-5bf2-4b78-9847-076f02eab97e,w_8Bff5J_Uej5iCAz2WTBw", + "745b4725-5bf2-4b78-9847-076f02eab97e,kdExSysXREOAYqobK_Wzpw", + "745b4725-5bf2-4b78-9847-076f02eab97e,XlkkIjP4EEKq0ObjXYGtBA", + "745b4725-5bf2-4b78-9847-076f02eab97e,Rlxn533MdUqMefvUSoZrrQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,_BV42pO3n0e0V-eOf9CyGg", + "745b4725-5bf2-4b78-9847-076f02eab97e,g6PcjAOH0UinJtrGmxVuyw", + "745b4725-5bf2-4b78-9847-076f02eab97e,i1e3GavcdEu7EJFzwpBXvg", + "745b4725-5bf2-4b78-9847-076f02eab97e,ebjoqQ_pnU2qdFVR1yDIzA", + "745b4725-5bf2-4b78-9847-076f02eab97e,Fr--3nBIUUuD6EYmj1iklA", + "745b4725-5bf2-4b78-9847-076f02eab97e,hrTGnJLb4U21P9PCi7u3xw", + "745b4725-5bf2-4b78-9847-076f02eab97e,coRfJXTxhUKsFs1tzSBgUQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,Y75a0D5Wj0qmpfxG18LvWQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,VExSaZXZ9kan-0qjP6YPzQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,Yec2FLXhpEmrocbVUOkhhQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,EziQI39wZUWqIy8zs5I7LA", + "745b4725-5bf2-4b78-9847-076f02eab97e,PWMkuXHWFEKGKfSiT8rWNg", + "745b4725-5bf2-4b78-9847-076f02eab97e,xdO8lwFuG0e1v2FRmza4dQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,vtQ3bx6n7kSFdES121M5IQ", + "250bbb53-1fae-440d-b25a-46a16c271367,", + "250bbb53-1fae-440d-b25a-46a16c271367,2vawoQvJPUegnClmr2RNAQ,RR69IGV4j0Sd2xtmyN5wFg", + "250bbb53-1fae-440d-b25a-46a16c271367,kIcd-yEnsEqxoeogquvmRw,RR69IGV4j0Sd2xtmyN5wFg", + "250bbb53-1fae-440d-b25a-46a16c271367,qHyKfCsrbUqSWomjLXf7Hw,RR69IGV4j0Sd2xtmyN5wFg", + "250bbb53-1fae-440d-b25a-46a16c271367,ZvUwd0aVfk6SjA7oyouXUQ", + "ca218759-7eed-440e-b5dc-dba15f81e598,", + "ca218759-7eed-440e-b5dc-dba15f81e598,ulOBBvFEJ0ioFA1qmL390A,u9yHtj_tfkuowGp0h_YpFg", + "ca218759-7eed-440e-b5dc-dba15f81e598,V4CVhCB8eU-nrfYgticVOA,u9yHtj_tfkuowGp0h_YpFg", + "ca218759-7eed-440e-b5dc-dba15f81e598,6ZtRAbD-xUufWNLCrlrrCg,u9yHtj_tfkuowGp0h_YpFg", + "a600fa80-ed4e-4acb-8d6d-72fa87d2bef3,", + "beee4e09-9d93-4023-bc78-c5dc551a0ab8,", + "9b636782-47f0-4218-9a1d-be65e550dc44,", + "96ca8d50-0721-47a3-a988-018a1c157677,", + "c8d2503e-5d43-4828-8d1d-40c64a577753,", + "caf1e229-9660-4abf-8074-f706709f4097,", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,sCj73U9p5E6DQreOsbutqw", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,HDpCyFn40kOxJ0P_xJr4-A", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,OZ1v-s_XFUyfMA5mfd47FA", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,e8aNxGFlBUWy59Bfrvvv7w", + "a9404818-d7d0-4061-8eb8-90f23e82115c,", + "a9404818-d7d0-4061-8eb8-90f23e82115c,08593d9f-aaed-4662-973d-0f30e77f1955", + "a9404818-d7d0-4061-8eb8-90f23e82115c,e002caa2-3447-4102-8d9f-570d1942101b", + "eda29bce-5a07-4439-a2a7-cafa11e2ed62,", + "34144f4c-f6ef-49e5-a286-1781e82e6782,", + "34144f4c-f6ef-49e5-a286-1781e82e6782,kS7jek5tPkSkIgjIF369Vg,_Jx5VqGbVUCPwl_SaUCTRA", + "34144f4c-f6ef-49e5-a286-1781e82e6782,3HAMDzfFO06jtYNXm2Bsjg,_Jx5VqGbVUCPwl_SaUCTRA", + "64ad8789-1645-45f7-819a-a413ff7c9622,", + "64ad8789-1645-45f7-819a-a413ff7c9622,-Moek0heVkWtKpTw9oZ0EA,FsirX90YM0y5lk7mnqDnEQ", + "64ad8789-1645-45f7-819a-a413ff7c9622,D-XJyw1BPE-m0gXpvcXrQw,FsirX90YM0y5lk7mnqDnEQ", + "0c537a84-9095-406f-8c87-1464f3a774ba,", + "0c537a84-9095-406f-8c87-1464f3a774ba,C9cPjOoiCUCHrCBi809CMQ,3pdsIrhM9EW-y-LNnjbFxg", + "0c537a84-9095-406f-8c87-1464f3a774ba,24zVmiGiaESrM5qcRfF6gA,3pdsIrhM9EW-y-LNnjbFxg", + "0c537a84-9095-406f-8c87-1464f3a774ba,LOeHr4xoik6VOv1fOeImjA,3pdsIrhM9EW-y-LNnjbFxg", + "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,", + "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "07116e9b-b182-40b4-90c2-99738e7aedd6,", + "74e6878f-94e0-41f2-93ea-de6412238385,", + "56da4739-f628-4894-95cf-e21a600f2124,", + "56da4739-f628-4894-95cf-e21a600f2124,gYXppJSpDUG9MB0xej4-kQ", + "56da4739-f628-4894-95cf-e21a600f2124,VF7QrfNdC0ex5p62bkdt7w", + "56da4739-f628-4894-95cf-e21a600f2124,eEsEJ78Me02dXnp6gTuZRw", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,fvoaMN4M10WS6jiA6XlOJQ,AZnPWCN41kmNwFwDqWGFWQ", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,PwsLxCnzOkSW-RGz3tNb1w,AZnPWCN41kmNwFwDqWGFWQ", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,uiBklotTUEqRI2F67vyJyA,AZnPWCN41kmNwFwDqWGFWQ", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,jXBi28cRV0CEQgKIrpD0_A,AZnPWCN41kmNwFwDqWGFWQ", + "5db418c3-1aae-42d3-af59-4ad03a9a3bf4,", + "e457f946-b985-417c-9dec-0d8d3a7da71a,", + "7be2eafc-21ea-4266-ae64-8888c06798d6,", + "7be2eafc-21ea-4266-ae64-8888c06798d6,sqPMH0LhLkeG-4XkxpU-sw,URWHffwjGUKgXeV01upePA", + "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,", + "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,RRmRsm6R6kOyn5HfbXNJWw", + "b66cb611-c079-42f0-ae04-6f51384dbe90,", + "3cd52c5d-0c46-41c0-8254-c4542805da31,", + "ad55bb62-671a-4a29-aec8-36a1e3104f05,", + "bb6ee7fa-7a31-4a16-a97e-be6eac42c595,", + "38b38e24-d636-41c6-b80e-a285ae3106dc,", + "38b38e24-d636-41c6-b80e-a285ae3106dc,0f71b9a2-dd88-435c-a571-4503d12dcf2d,9b2102f0-e16b-464f-9f1c-f068de52bb89", + "38b38e24-d636-41c6-b80e-a285ae3106dc,f4ad4b9e-dd42-4711-9437-5beae51966f6,9b2102f0-e16b-464f-9f1c-f068de52bb89", + "3df394de-9c3e-4141-aba4-12eac4742102,", + "3df394de-9c3e-4141-aba4-12eac4742102,17040a88-87f1-4613-b439-3316e33dbd48,71fd06e3-cac8-4923-a09c-ef91183712d2", + "3df394de-9c3e-4141-aba4-12eac4742102,f9aaa7c5-59bc-486c-9a6e-27a8ac618f84,4cc0ea9f-65af-486f-8319-02b85ab5197f", + "3df394de-9c3e-4141-aba4-12eac4742102,jQwNBXyoU0Gk4FyNw6jFxg,4cc0ea9f-65af-486f-8319-02b85ab5197f", + "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,", + "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,556141f7-6fbc-4065-9faa-8d8240a93d7f,00ac4e37-6eac-4ed5-b46c-2c075038bc3f", + "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,d81d0b62-e052-46c2-9252-4bc5fb219f53,00ac4e37-6eac-4ed5-b46c-2c075038bc3f", + "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,xGmnUGF-Ck-jP43r8tVVjg,52053afd-9f12-4fec-a358-7eb3fe8c19bb", + "eceb7d49-be41-4228-86a9-2973d62e7135,", + "eceb7d49-be41-4228-86a9-2973d62e7135,556141f7-6fbc-4065-9faa-8d8240a93d7f,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "eceb7d49-be41-4228-86a9-2973d62e7135,d81d0b62-e052-46c2-9252-4bc5fb219f53,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "eceb7d49-be41-4228-86a9-2973d62e7135,xGmnUGF-Ck-jP43r8tVVjg,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,", + "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,clFrpYWFQEmDkE14f3o_Ig,DE1C-RJkuEWIvpR0q1i42g", + "11b3413f-2c18-41cb-8ef2-55fb825ed476,", + "11b3413f-2c18-41cb-8ef2-55fb825ed476,DNGqrCGe8kKrNmJaGzjjew", + "b3b59410-d3df-464b-a75b-500a9e5ea154,Ris_kNVcgU-EK4SoMckC2Q,74J7skzs9EaIq09bZArjiQ", + "b3b59410-d3df-464b-a75b-500a9e5ea154,PkMkvHzXek-yKyqJESZ3tw,nbgwLqKBnUyvieWX0i47dg", + "b3b59410-d3df-464b-a75b-500a9e5ea154,P49CJ5imfEuxH1rryrfFRA,nbgwLqKBnUyvieWX0i47dg", + "b3b59410-d3df-464b-a75b-500a9e5ea154,logtSrZSTUqwEmBw9i0qjA", + "06306723-ca20-4aa6-b7b3-917113f41ac3,tMsCUhDslkmdNBfogeSz7w,PIjEKhXiCUqtA8FBhLBcmA", + "06306723-ca20-4aa6-b7b3-917113f41ac3,cypMpMaVeUuRjm4FLH4czQ,PIjEKhXiCUqtA8FBhLBcmA", + "06306723-ca20-4aa6-b7b3-917113f41ac3,whWKAUSGp0qprsJomq9Nwg,PIjEKhXiCUqtA8FBhLBcmA", + "06306723-ca20-4aa6-b7b3-917113f41ac3,LTXy6g4xe0abuFzOLpVDUg,PIjEKhXiCUqtA8FBhLBcmA", + "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,", + "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,tPAJqFHiX0yUQpDOE0MJ6A", + "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,zhLs4Hgw1UCrcNekAWd4RQ", + "75-HXZBOAEiDBqclnOn8YQ,", + "75-HXZBOAEiDBqclnOn8YQ,LRv1X0ZrjkqO7azGpZ7uMg,sx10-Hsfl0OS62pIFvHH8A", + "75-HXZBOAEiDBqclnOn8YQ,NMmOn0zQZ0mJLxCMnsbIVw,v1ZOE9G9gEWAqNJX1A9f2A", + "75-HXZBOAEiDBqclnOn8YQ,1w_u4d4AjESxw5dxRkKHrw,CylZksC7KUGXF2VeJhlLSw", + "75-HXZBOAEiDBqclnOn8YQ,6wCu0bPODE6tayAY_aicRQ", + "75-HXZBOAEiDBqclnOn8YQ,7BMBl3iA6USKijNCKfCXqA", + "75-HXZBOAEiDBqclnOn8YQ,akqD9mYQQ0qks9SJe80MbA", + "75-HXZBOAEiDBqclnOn8YQ,RCit-BlLyE6cvzpbfCVShA", + "75-HXZBOAEiDBqclnOn8YQ,1Z24Obg1MEm6dC_blOy4IA", + "75-HXZBOAEiDBqclnOn8YQ,KrNRgSR3qEq9f6hfcsTFQQ", + "awOFE0_GSECr8GBNBo6a_w,", + "EJFGAW-TnUCaeD59parEHg,", + "EJFGAW-TnUCaeD59parEHg,k3dbqP5z30KU37MVPpsEPQ,GtoHkTpyA0iGIHsxphwivg", + "EJFGAW-TnUCaeD59parEHg,hM0OmxjWLESbsJ-M0Z3QvQ,qZzkrGXpPkGq_nnGLZorwA", + "EJFGAW-TnUCaeD59parEHg,9WpjdWJbhkCoEgDljqZkdw,Uyn3QOl1zkGR2_OUOhNWjQ", + "EJFGAW-TnUCaeD59parEHg,zxDWWqb0S0uDJgYeVQQ3oA", + "2565d597-9d17-4c47-a66f-b683583f0cf8,", + "efac28ae-ff44-4322-9bc0-61eaaef95399,", + "84574356-98ec-415f-8668-82cb52016bd3,", + "84574356-98ec-415f-8668-82cb52016bd3,p379ObEL9Eea0DX9Ld4qnw", + "84574356-98ec-415f-8668-82cb52016bd3,ljbO0JAKVky8M97MsjKnPg", + "187569d3-23b4-4118-862a-ace7f50fec45,", + "7a98503f-0460-4d25-a0d5-72a32db8f4c2,", + "7a98503f-0460-4d25-a0d5-72a32db8f4c2,A8ht7G1iqEeLvP0SW6SwyQ,QppxPdf550-jT7dK7XrTzw", + "UGkQa4atNUe1cE5TN79s4Q,", + "e633d56d-5121-4492-b606-40759108a00e,", + "e633d56d-5121-4492-b606-40759108a00e,6CFwXqWT50mH1v6WueFsjw", + "7a97838f-24ac-41a0-8390-0c9308644eb1,", + "76a73866-5b60-424d-b751-a105e030f46e,", + "5b3cae00-88bd-4179-b75a-9444c5a52fa5,", + "5b3cae00-88bd-4179-b75a-9444c5a52fa5,ZM6W70pGt0aAPLYf4DykeA,icHpJch4uUeGdhoLkawyyg", + "06475461-f3ab-4e92-ad92-243c00b19ff0,", + "076e98b6-dc1b-48f8-bb02-aa65d26b33d1,", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,DOY6LpHq8kui8yDWGsNgZw,48yVsRt8sU6jf3P4wlPA8Q", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,HUxqkzeE-Umq96TgSn0Hbg,48yVsRt8sU6jf3P4wlPA8Q", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,QMScBauef0uOE48-YcDcCg", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,25X6s0sTZkyzUOKiIt4cGQ", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,tkuP0sNwjEO3XH5Lf_KqkA", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,G_QPd8YBe0-n8ywAGLMKBg", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,Kuh-HrhzTUmuGaw7cpLUMg", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,SJ7DV0meUU6QjksIcAAeRw", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,bS-St6XLHkSwKt41J7BmKw", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,jir883uk5k-cEgyAPDtQww", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,a2mUuKOr-UOM5nPpR8dSMw", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,501dae9f-2fc4-45fe-a276-f606b93f434f", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,b8cfa1ab-13bb-4e71-afe0-490820aef449", + "9ad40310-7cb9-473d-920c-5c18d7972030,", + "9ad40310-7cb9-473d-920c-5c18d7972030,mQTpoW0-AEufdFmApqtRXg", + "84600de6-93fc-4a89-961e-1f18f4b80643,", + "84600de6-93fc-4a89-961e-1f18f4b80643,Be2v1bpBSEu4lwKJePscPA", + "84600de6-93fc-4a89-961e-1f18f4b80643,p4X1ilkxU0OjEWXdmQrVwQ", + "7e6e24b6-6f9c-4315-b457-73eba52d824c,", + "7e6e24b6-6f9c-4315-b457-73eba52d824c,Mnpd7r3eu0CNmZYSPTMASg", + "07bebdb7-bbe1-49ea-a746-c1d00e11f174,", + "07bebdb7-bbe1-49ea-a746-c1d00e11f174,ciwVtCaRHEO72BFPe4Ujmg", + "cfce35b4-d182-4505-a631-5a8e5011f550,", + "98e4c868-df73-4755-84b6-c4d12723b811,", + "cb061cf3-05fb-4494-be5e-091ddb080271,", + "ce250e13-8137-4f62-9e52-2b1cebe7d0ad,", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,4169bfed-8403-4590-a29d-4941275ac0b6,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,0724b166-9ace-46ff-a191-b13f135b00a8,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,MFsWxydybE-dw3KO2hkuZA", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,9bb0x4qgokyQLbt1jA25Wg", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,YceWQmfB7U-R7OeBIr2Ogw", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,G2ncsS9U8E6fyjvliFqdOw", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,AdJ3ExsUq0ixzaKDHK5RUQ", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,TC79rbThdkCZHGWaxk-qYA", + "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,", + "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,a6796264-9c7e-4605-830c-06db8d0dc780,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,f3b2330a-b981-4c7e-902f-1f57382f2388,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,baffdefe-de21-4551-9330-b50873965bdd,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "745c5110-104e-4b37-aabf-7c690fd54811,", + "745c5110-104e-4b37-aabf-7c690fd54811,b42d903b-b393-4f49-af5e-38d0fb6c42b0,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "745c5110-104e-4b37-aabf-7c690fd54811,8cab8cc6-9a2d-434c-8b72-e66edbdd223c,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "745c5110-104e-4b37-aabf-7c690fd54811,657f28d5-672a-4f27-86c6-3cb00f25bb84,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "745c5110-104e-4b37-aabf-7c690fd54811,w_aK2K9bHEeU6oSQ7BK0qg,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "e2ObiQFqCUOdKniKL9a1EA,", + "BqpRbr_T1kO_YJqB1787kA,", + "PryEX4MEvkGKwST6dqJHjA,", + "691b2198-2ee7-425b-b59f-bbadda1253e1,", + "691b2198-2ee7-425b-b59f-bbadda1253e1,pqtwCB_c20yRmOEs6jlH3g", + "691b2198-2ee7-425b-b59f-bbadda1253e1,SSstW2ZPk0SB47qYlgbZzg", + "691b2198-2ee7-425b-b59f-bbadda1253e1,p5zYbKLGU0m0ZutOicfjsw", + "691b2198-2ee7-425b-b59f-bbadda1253e1,wgIwYsXqiUekieE6pSt1Ng", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zljmtj5WqUaRvB3XQTgJXg,XFuSiGupZ02Fut1Dxum_Iw", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,rmq1AOQvtUGOHvbPUximwg,XFuSiGupZ02Fut1Dxum_Iw", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,rxMCIkLb2UO0Z3tsqtYuSw,x8nPf0xEwUiRdBQU00Bu1g", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zu3hTm6JyEa0sE72KtVGXg", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,QBglgj80zkuvUkk55Y5Feg,ACrBLiAIgEur9V6l_B0CvA", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,-HYrY2A5Tk2UWMi4_hmYbg,ACrBLiAIgEur9V6l_B0CvA", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,IjRMopsYlUqSE6327I9TIQ,ACrBLiAIgEur9V6l_B0CvA", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,zTsJHduQ5kirH9-6mewV4g,ACrBLiAIgEur9V6l_B0CvA", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,ons8s4ILoUmkdtliouAscw,ACrBLiAIgEur9V6l_B0CvA", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,j3I0vVz0yUOYLX9wCOMyyA", + "be75b46c-ce22-4758-8810-c409c5aa1c50,", + "be75b46c-ce22-4758-8810-c409c5aa1c50,ZLaDizWS7kOmnHVGlLLJ-g,TUZ21v0C9UK9GPsfbRwpsw", + "45695fba-5abc-4c5b-b9eb-949d5174d1f5,", + "45695fba-5abc-4c5b-b9eb-949d5174d1f5,oFjt2tSsa0CJ7j0s3P6b4Q,XD9dH8LF_ECmL0TwXJW1zg", + "9fb62d0c-67ff-4675-85d3-a2869c2330aa,", + "9fb62d0c-67ff-4675-85d3-a2869c2330aa,ijCbiOeGD0C4uznegq9CXw,VzyoPsQyCUaW6RSusgVUGQ", + "7adae61e-cd62-46c3-ae42-83ef0bced5c7,", + "7adae61e-cd62-46c3-ae42-83ef0bced5c7,lhqUg1VMNkC8nrsGZF4edQ,uFERkSfSl0i9BPFBVOICeg", + "7adae61e-cd62-46c3-ae42-83ef0bced5c7,yO5pgI2quE2i3tNBlvJPGA", + "7adae61e-cd62-46c3-ae42-83ef0bced5c7,jGNpSO25HkO7KbR3v05nkA", + "e4888429-6f5b-4f57-9b9b-2559d3a54856,", + "24781418-38ad-411f-925e-2321fe616b51,", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,GH9q8_0O9EqjGdz7lGWD2g,T7zwcIIc8Ee3ud8a2JCuwQ", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,M-N9vQt0CEefqgmeLp3l4g,T7zwcIIc8Ee3ud8a2JCuwQ", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,q_HcScqHgEG-x70g1wtfLA,T7zwcIIc8Ee3ud8a2JCuwQ", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,gDP3y0J2EUayzVvBT9-92w,dMDMDExSmEKOF_kQIHaowg", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,kBmmFoqo-0eLSSAhen-VLw", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,tVbu4hUFHkmEKEz38GG2Nw", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,m8x6fiLEw0m4pJywntEpzg", + "2bd53190-46f8-4956-9388-87c5a04efc4f,", + "2bd53190-46f8-4956-9388-87c5a04efc4f,v3af60p1Q02NpdCqIRO9lw,1hVOI7IuY0mKvtr2PZXLww", + "2bd53190-46f8-4956-9388-87c5a04efc4f,kX7ocqn8X0-4M1nw-KrwFQ,qC66Hu6yKEu2Res2o2z1Kw", + "f277af6a-0807-4716-8948-1bee236ffb74,", + "f277af6a-0807-4716-8948-1bee236ffb74,d0bb468c-b78f-4e80-916d-65fec3ec0010,0272eb19-fc0e-4ad6-8db9-cc0cda2528b0", + "fb907eb2-91c8-45ff-84a5-462bb86ea7de,", + "fb907eb2-91c8-45ff-84a5-462bb86ea7de,b415fe60-3e9d-4f4d-a53a-ca15eac6af7d,d21e1b6f-9c9e-42b4-bad5-201ec43161e9", + "5d4b097b-2850-4a13-b26b-280be7ad3f57,", + "5d4b097b-2850-4a13-b26b-280be7ad3f57,RTrcYpLtukm83lwAfWi5WA", + "5d4b097b-2850-4a13-b26b-280be7ad3f57,3oar1K99PU2OTGaT0i7Hag", + "5d4b097b-2850-4a13-b26b-280be7ad3f57,T5z4W4H8sU-2NxpepVH0Qg", + "ab1d252a-c15c-4a97-8e48-da0813d33b00,", + "ab1d252a-c15c-4a97-8e48-da0813d33b00,cxwdcbGTfUS_LYQoXpcQng", + "ab1d252a-c15c-4a97-8e48-da0813d33b00,b6b5aad9-1211-48ce-ac2e-8053a879ccbd", + "9fd1be12-1577-4800-b79f-6da29f299826,", + "9fd1be12-1577-4800-b79f-6da29f299826,zNLzEvpTpkOIi0J3fIvh1A,OqC4ny2rk0yJJOHemspV7A", + "9fd1be12-1577-4800-b79f-6da29f299826,mVj9t_XVCEqdx89v2zdjHQ,kJ_pNxSWKE6HZX4Sc7Dv0g", + "9656a307-0c79-4b8f-910f-e67c454faefb,", + "9656a307-0c79-4b8f-910f-e67c454faefb,mG0HW8fJkUOE5u7Whg4kag,3CLmVNB_p0Gd5N8dN74TBw", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,WkI_v-Sua0WlhmMf8R4aGA,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,3bqY_T4_HEy406HcXrfLuA,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,I4lNZy7lR0uyUN5Vk3fabA,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,SMeMLRUF-UWrOcXYJzgScw,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,aaaG7Fg8fky2dkrwVsA30g,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,GSO7t0cNs0SIbU-jQjnX5g", + "ef937765-d827-45c1-babe-5bcf6d68c08f,", + "ef937765-d827-45c1-babe-5bcf6d68c08f,WkI_v-Sua0WlhmMf8R4aGA,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,3bqY_T4_HEy406HcXrfLuA,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,I4lNZy7lR0uyUN5Vk3fabA,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,SMeMLRUF-UWrOcXYJzgScw,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,aaaG7Fg8fky2dkrwVsA30g,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,d0D2htQZvEKajd0oAesNow", + "ef937765-d827-45c1-babe-5bcf6d68c08f,W7E9KfRTLEK-1EZTNKM3Yw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,WkI_v-Sua0WlhmMf8R4aGA,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,3bqY_T4_HEy406HcXrfLuA,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,I4lNZy7lR0uyUN5Vk3fabA,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,SMeMLRUF-UWrOcXYJzgScw,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,aaaG7Fg8fky2dkrwVsA30g,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,9ir4Es5d0Ue-8bn2EUcCZQ", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,XaNgcjkwP0uxS5_S1JaQrA", + "f51a7ed5-52ea-485f-a430-303697f6fec9,", + "f51a7ed5-52ea-485f-a430-303697f6fec9,5b58d879-2bc4-47ed-a996-1af2ace16425,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,d320ad9b-ba5c-45ea-b2b8-0d3e409a9db1,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,d61711fd-589a-4669-b991-9408a58fffd9,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,oJFfs62tR0WVRry2Smt8yA,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,MIfCkcrFOEasJqfDmxThdw,3d971439-41d1-43a2-8434-704f9ec8d906", + "515dfc36-7f7b-47bc-abee-380d68f41be6,", + "515dfc36-7f7b-47bc-abee-380d68f41be6,1acb1f56-4509-449f-9c6a-065bdeaa9ee3,2d7361d2-31a2-404b-90da-10e89114e1f2", + "515dfc36-7f7b-47bc-abee-380d68f41be6,e98c3f98-7844-4d6e-a21c-05027033c18c,2d7361d2-31a2-404b-90da-10e89114e1f2", + "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4,", + "51e5467f-d310-44f8-80c3-17d7e27450bf,", + "51e5467f-d310-44f8-80c3-17d7e27450bf,pue_FwobQEiaVubgc5eMUg", + "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,", + "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,ZYjUekG8JkC9uPkm4xH8ww", + "352b49fc-99a2-4f05-a688-866ade1cd394,", + "352b49fc-99a2-4f05-a688-866ade1cd394,PabSuDB_GEejMahhLvLkJg", + "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,", + "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,EM1x0p66L0Gf6BNBP8-1kA", + "4885683e-a87e-4d27-990a-c16f1df7a28f,", + "4885683e-a87e-4d27-990a-c16f1df7a28f,hKBSHovOeUy84yCbvutkOw", + "e036f042-92c1-4c35-ab71-e9c7d5fbf301,", + "73994140-58f1-40b8-a7a4-baca020ab2fc,", + "53a73a1a-70d1-447b-a594-3c96890f52ab,", + "1fed823a-22bb-4225-bdc4-491da6eba875,", + "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a,", + "3e307eca-0850-4d85-8cc1-f5c176bfa7e9,", + "23944856-b567-4075-815f-261cb4655cff,", + "23767c66-31de-4e6f-81f5-b17769a0fd5b,", + "7f4317a3-0f47-4fcd-bfeb-ff62e33d37c9,", + "d33c2f3a-6831-4deb-82b2-062d1c4224e4,", + "d33c2f3a-6831-4deb-82b2-062d1c4224e4,gIDTSqjSvECgxxd4YtOZIA", + "d33c2f3a-6831-4deb-82b2-062d1c4224e4,BoaDgMfzEEycbxVmWneHNA", + "63b5196e-881a-4b96-82c8-9df54de4a506,", + "63b5196e-881a-4b96-82c8-9df54de4a506,C64nh9MHv0mlUEP9-s7sJw", + "63b5196e-881a-4b96-82c8-9df54de4a506,ay9ktLnHfUKdj0-q52Gvyg", + "63b5196e-881a-4b96-82c8-9df54de4a506,WJ27X_UnfUmFKpY6mRk_Rw", + "63b5196e-881a-4b96-82c8-9df54de4a506,YVkcCfJZekq7GvdMUy2IWg", + "4b72736e-6fca-4961-98f2-1d4e199eb1e0,", + "155c13b9-3c72-4314-85ac-6fbac19cdc76,", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,P8w3-vcH5kaHaMEsFsKDjw", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,KUSVg33DSUK8aYXxJzOTdA", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,g8h43tuiN0CCifW9XbnFhQ", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,T0PMM2iQqkCCMZoznrdkug", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,sA0KRBVMikOo6BBfk0dC1g", + "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,", + "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,4ve7AWD5pkGjvmtC6JRdiw", + "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,10nOM4HBRU-e7Bb05SrtLw", + "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,F3m52YaKyEqwbgUyPVfFxQ", + "bc7299ac-be2b-404f-b8e6-402e2c0660f0,", + "bc7299ac-be2b-404f-b8e6-402e2c0660f0,2pnfLqkYZ0-GbpeE8g0vVA,hGtyDIIWckGoRgCaSG6GBw", + "bc7299ac-be2b-404f-b8e6-402e2c0660f0,vw2iLomK9keTfEWsqJrmhA,hGtyDIIWckGoRgCaSG6GBw", + "bc7299ac-be2b-404f-b8e6-402e2c0660f0,t_y2xmFYLU2v6Jb6PX6v_g,4J9GPTKjjUOjyLVPmJyxWg", + "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,", + "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,G_fqC7KPaUWFM4Dk3stJYw", + "a44766dc-19ad-4476-849c-5113a5faabd0,", + "a44766dc-19ad-4476-849c-5113a5faabd0,5_-QLURc_0W8qeeN3Q3iGQ,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,mqdncbYcuUSGFvFqEU0z4A,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,MbusQ9bte0i9zirlxtNkSQ,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,_9QlnTPXuEO5OQ3H0CwFkA,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,cKLBS6pbf0SFuavWyeZ7GA,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,DZyW-gOG9U-QLGOMFBhOoA,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,pEiU6yZm2U-X9ip_CM1SAw,PIV6RtD7Pk-UTcUp2wF1ww", + "40734c4d-e1fe-426c-8921-930f7463d8e8,", + "40734c4d-e1fe-426c-8921-930f7463d8e8,E1GfXDLUaUKaG9hTLAfFPA,hCeaYHy1TE2s6rTPxAQP9A", + "40734c4d-e1fe-426c-8921-930f7463d8e8,wCe2yC2U1kyYvPd8Z1rX2w,hCeaYHy1TE2s6rTPxAQP9A", + "40734c4d-e1fe-426c-8921-930f7463d8e8,QkfA71-1z0qOiZAVrbzhtA,hCeaYHy1TE2s6rTPxAQP9A", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,0ps19D7b_U2iejdKQFORyA,PoMG28QAskagun1LSd4C_A", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,t1MeoroFOUioYmPfT9mCeA,PoMG28QAskagun1LSd4C_A", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,pjkTGVHkIE-BzCVi1KAesQ,PoMG28QAskagun1LSd4C_A", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,QP38EMygY0S-KIvdGatARA,m5Vw_MMCWUiSOOCL94QO0w", + "e13f6558-d49c-4314-820c-962ef48eb0c6,", + "e13f6558-d49c-4314-820c-962ef48eb0c6,5TJPDmvEBEOkP82ZWLFqtg,50NUFrDxbUKmXbyAHBenvQ", + "e13f6558-d49c-4314-820c-962ef48eb0c6,rCeRk8Pdl0W5M47JAQPhpw,50NUFrDxbUKmXbyAHBenvQ", + "e13f6558-d49c-4314-820c-962ef48eb0c6,8zXWUxwZHU6tThCV4p5iGg,50NUFrDxbUKmXbyAHBenvQ", + "e13f6558-d49c-4314-820c-962ef48eb0c6,s2kCppLsN0uY7Hy6qjQc-g,50NUFrDxbUKmXbyAHBenvQ", + "418ed21e-766f-4c69-9d57-6f866dcc7feb,", + "418ed21e-766f-4c69-9d57-6f866dcc7feb,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,0dabda0d-842c-4d39-a6b9-a8ca3ec20825", + "1041d745-c5e9-4218-a3e1-198f5c9c418c,", + "1041d745-c5e9-4218-a3e1-198f5c9c418c,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,84c016ad-133f-45e3-a412-cac35612540b", + "f47f81e7-fcdd-4687-9b99-013147bef30f,", + "74365234-cb72-409d-8cf5-9a7209f707e1,", + "74365234-cb72-409d-8cf5-9a7209f707e1,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,fa9a3d8e-16c9-4a7e-9166-3226e0d13c39", + "02a5b48c-abba-4385-967b-ba900424829e,", + "2c679f89-c76e-4cfb-94e9-448c8fd44d55,", + "50c9c6f8-2963-4ef3-95d5-e999a898269f,", + "274cb9b2-2f59-47ea-9a8d-a5b656d148c6,", + "76f8f9b8-0793-4d98-962c-9e6a71979540,", + "76f8f9b8-0793-4d98-962c-9e6a71979540,XIluUn9vQEabkEpqyYBG0g,9sSCSLRkYUCs4CCHK8FfAA", + "76f8f9b8-0793-4d98-962c-9e6a71979540,d6525663-1028-463a-be52-7b8ca5de99ba", + "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,", + "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,tdECtZp1UUuRRKHV60coyA,PIg3ywG2J0CiGUPpm1A6Kw", + "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,689e15e9-7e7e-4fd5-927d-c015d69c1178", + "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,", + "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,c7wiHNZ-X0KazEW4cY9PeA,5fVeR6S6R0yo_NcT-TB1lQ", + "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,5d21a059-fdc1-4edc-898a-5fef98eed137", + "dfcc5a47-5f73-496c-84b4-d44702231f7e,", + "dfcc5a47-5f73-496c-84b4-d44702231f7e,h-uMOz2Hz06ymEjtz4wuzQ,4Ooi0h3SrkObClvS2cGk9Q", + "dfcc5a47-5f73-496c-84b4-d44702231f7e,ec8b5518-1fda-450c-ae57-d0cf06700532", + "f0fd87b4-4891-40a0-b463-7ce1da2c8929,", + "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c,", + "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c,veF50UiWeUS8ugHSm6g_3g,7KDavHUPDUKTJI0fFzJHpQ", + "3af4c506-d49c-4126-9437-1d3bb1658863,", + "3af4c506-d49c-4126-9437-1d3bb1658863,53dd9e44-7096-438e-a7c3-11d7c307426f,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "3af4c506-d49c-4126-9437-1d3bb1658863,c3c9f89c-580c-43c4-b38b-07394044e0e1,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,", + "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,53dd9e44-7096-438e-a7c3-11d7c307426f,a4903da3-249d-4f0a-a746-b547fda371ff", + "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,c3c9f89c-580c-43c4-b38b-07394044e0e1,a4903da3-249d-4f0a-a746-b547fda371ff", + "664485e9-a159-4fcb-b32c-2b114b4a1218,", + "664485e9-a159-4fcb-b32c-2b114b4a1218,85eb7f47-bb31-4c66-a8b3-562d35028e6c,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf", + "664485e9-a159-4fcb-b32c-2b114b4a1218,39f04eee-3a58-4e53-9f5e-7e995d46ed71,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "664485e9-a159-4fcb-b32c-2b114b4a1218,6y_fzLhG7E65Px3-CYiyXQ,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf", + "b9e0f03b-2609-4911-82e9-1e324dbbe8be,", + "b9e0f03b-2609-4911-82e9-1e324dbbe8be,cdca593a-8840-4140-9145-fe962732040c,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "b9e0f03b-2609-4911-82e9-1e324dbbe8be,62bd5dda-08da-4528-83c1-f0ca3812dc6e,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "b9e0f03b-2609-4911-82e9-1e324dbbe8be,X3MkhL55Rk-0UK6h4dw0Eg,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "d960b47d-f8bd-4c2e-813c-a6a09876ea18,", + "d960b47d-f8bd-4c2e-813c-a6a09876ea18,TZ3oQBp_3kWY4Vzscgv5Wg,xCpdHx1iM06Gg0tYoGv0DA", + "1fcaf4fd-9012-407e-b691-fbc3ccf22421,", + "1fcaf4fd-9012-407e-b691-fbc3ccf22421,S0AKRB_m2k-I7snHnUigtw,uTkbswCedUGyuZcaX0W6Ww", + "3c9acb9e-3353-4359-aa5a-ae9512b086b4,", + "3c9acb9e-3353-4359-aa5a-ae9512b086b4,dro7VLV8BkGjqpPAbJheYA,bNA44H2wzUy-FkWgyOq1Kw", + "f17a2f9e-1eab-44e8-b10a-1f53c5d0778c,", + "fe34adaf-47d3-435b-b2fd-b2fee05fceae,", + "fe34adaf-47d3-435b-b2fd-b2fee05fceae,KTidYfH0ckWxJGs5qzUBTQ,w07bkcUoQEiNJTVDeSywzQ", + "fe34adaf-47d3-435b-b2fd-b2fee05fceae,X0nzvhAtgEKqzjQPb9d3qQ", + "fe34adaf-47d3-435b-b2fd-b2fee05fceae,8OdkUQPXzkGqXFa3LNXupw", + "11e508a7-fe7d-470e-b270-2dba33e2f7a1,", + "11e508a7-fe7d-470e-b270-2dba33e2f7a1,chxai_T-BEOc0-uZv6OAQg", + "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,", + "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,KTidYfH0ckWxJGs5qzUBTQ,VSZX99mSt0e7gPRM2_XzMw", + "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,mRNobpJiWUCpih1zETyaZA", + "380a5e35-b4d0-4119-9721-8ce4c0840fbd,", + "b89be768-a169-4b24-8022-751e7b817865,", + "b89be768-a169-4b24-8022-751e7b817865,SXG9rdEkxkOR31Nk0aTOww,uB4gFxfrh0W5OtkmZdFN1Q", + "b89be768-a169-4b24-8022-751e7b817865,FFIpxInE-EKbk2GY-pIkyg,uB4gFxfrh0W5OtkmZdFN1Q", + "8691fa78-3069-41e6-8431-01c294e8d56b,", + "KNZ1DvCLC0WHyH4opYAXtw,", + "KNZ1DvCLC0WHyH4opYAXtw,FwiizGjYnUaM1VJ53R1cCQ,uB4gFxfrh0W5OtkmZdFN1Q", + "54ab4eb0-e28a-438f-9977-1ae8d642d136,", + "1736a60b-6fb6-49bd-948a-fd3f3fc1be50,", + "66dcdd7c-7dee-490f-b2d6-1113daa782a7,", + "50721e23-67e6-4f16-8e70-b0eccc2d9c3d,", + "f60e093f-33be-4527-b827-060adc77b1f4,", + "f60e093f-33be-4527-b827-060adc77b1f4,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,464fcf20-361f-4dc9-b074-150c3bc27523", + "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,", + "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,5814a331-6155-4df4-b921-3ffc6059e44c", + "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,OX1ituawfUqYC-caC1WcCg", + "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,5owofMF_Y02WX2vwVL2aew", + "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,b3LMvviE_0-ObF1x5s1dtw", + "7c1552a6-bc36-46e6-884c-1de6b8111a92,", + "7c1552a6-bc36-46e6-884c-1de6b8111a92,926ae943-76cb-46dd-9c66-a6ee44ea9194,f1cd10fb-3d0b-436f-a173-65bc0d88d8bf", + "bf9dc196-5727-4752-8126-7ad5fcd96d55,", + "bf9dc196-5727-4752-8126-7ad5fcd96d55,67490e3b-0f9d-4bf8-84fb-54957b981c8c,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "bf9dc196-5727-4752-8126-7ad5fcd96d55,b520f3bc-599a-498c-8a4f-97cc25aa741f,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "bf9dc196-5727-4752-8126-7ad5fcd96d55,639f5e2c-6fbd-4ab4-aa10-7772e0932798,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,", + "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,2bb65868-ee12-4871-b50d-64bbc9066f0b,e65530f7-543c-43e9-bf12-b6cda2bc755c", + "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,XxVqGAlweUKta__sKdMP3Q", + "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,VL_OnsbYNkGgGAV16x5tQA", + "9c28e450-8e12-455c-a4c8-c0a2160d4190,", + "9c28e450-8e12-455c-a4c8-c0a2160d4190,DRS8xu3AqUOy2L0RKF3f7g,A7Y52vGIN0ijEKsWR_GQLg", + "9c28e450-8e12-455c-a4c8-c0a2160d4190,ZSOuWuKVCkqaIKc5d23G8Q,A7Y52vGIN0ijEKsWR_GQLg", + "57552652-d902-43b7-a68e-5c7da87582bc,", + "57552652-d902-43b7-a68e-5c7da87582bc,VqtOYBiKRkaSW4xAlBxQgg,xTqtLPQVyEKjAQZYl6HGow", + "57552652-d902-43b7-a68e-5c7da87582bc,J-K34UX_B0yObjQI9tn0vQ,m0R4Jtf4HES8krxMnwIWFg", + "57552652-d902-43b7-a68e-5c7da87582bc,ib2g1jTBgEeuj_s_uD1LKA", + "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,", + "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,V_duN2oWaUmmao1xHhzCJA,Uvarm_CwTUWWQYZvzI8OaQ", + "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,GWi1rEebqkCV_TS2WzQhcA,4zPm9miF5UOouEWFpOxYZg", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,f5403c89-9769-479b-8a05-c71607f9b189,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,36e63797-5a42-4779-a5cc-ef5c2e7d17d4,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,70112887-dc8d-49d5-96ed-0845204e0b58,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,2738ab5f-8c94-46b3-bac6-4a8111c41c21,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,hhs9GY3XZkWb-8TXODvo6A,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b9f51861-ad10-4878-a9b1-d849bccf532a,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b5d62931-e756-4a8a-8f3b-760beba49319,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,6145292a-9002-41c6-8cd5-43679b6f542f,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,7c1aaa7e-503b-46ef-b1c5-0c61a75b916e,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,27af51bc-8226-4911-8d47-22d28ea8e378,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,857fdcd8-704f-43d0-92b9-bca26619b153,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,3df5d13b-ba77-4b51-90b2-7a946e4d0be7,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "d86e2e65-5d87-466d-b885-8c52ac2eeeb0,", + "c161dc73-6ce3-49c4-8d15-45e6911f84e4,", + "9b62cbca-694f-4a32-b910-14da66bee9bb,", + "8cc01536-2f43-4de4-ac63-7304d8beb73c,", + "e27d33ca-8a81-4c16-a3c1-ffdbc9104d7f,", + "13e70c80-69a4-4ad5-90e5-4dd5e03da677,", + "13e70c80-69a4-4ad5-90e5-4dd5e03da677,ln5aTG458UOVh4sR820FJQ", + "157d609c-3827-45e3-ace1-be7cc250d132,", + "157d609c-3827-45e3-ace1-be7cc250d132,ppkByMiIi0inxbXKX4fu4A", + "157d609c-3827-45e3-ace1-be7cc250d132,KaTmQ95fV0KAUM-nvOdHwg", + "157d609c-3827-45e3-ace1-be7cc250d132,1GKZY3pku0Wuq4_vdiO21Q", + "45e9bd7b-b1df-4751-b3ac-994652a5745a,", + "c0e3d978-c4c8-4606-a1af-b37de8995dbc,", + "5bde836f-9343-4934-b01a-606f707020ef,", + "5bde836f-9343-4934-b01a-606f707020ef,fEPwvuJCWkGBe4IekXpmLw,axPBGoqorEe77Oqqjfzb5w", + "5bde836f-9343-4934-b01a-606f707020ef,MQOxmUk-mUG-Ee9m9nX31Q,axPBGoqorEe77Oqqjfzb5w", + "033e328f-d9d2-4e53-af20-44f18e3ea208,", + "033e328f-d9d2-4e53-af20-44f18e3ea208,Z1Ib9r_fkUqgoZk0uid_Xw,9ZP617DQk0mZ10J3d5q9oA", + "033e328f-d9d2-4e53-af20-44f18e3ea208,msOFHoFN9UORkeXXtdqBsw,9ZP617DQk0mZ10J3d5q9oA", + "pNOfTDOgJUGQvGAlTjBoCg,", + "159aa0f1-38b7-4a31-b1c3-111bcde687c1,", + "159aa0f1-38b7-4a31-b1c3-111bcde687c1,RDkLPQ8OukGIuLgTwnyqfw", + "159aa0f1-38b7-4a31-b1c3-111bcde687c1,7WzGZTf51EWClLTLNhhGVg", + "159aa0f1-38b7-4a31-b1c3-111bcde687c1,655a48e6-ab7c-4c77-9c52-262f5bb8cbc2", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,y6NrbfS070WvASkKmPK-9Q", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,MVXeErsEE06KenrYaEm7Kg", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,7IYi1twr6kO3xJiQo5CB4w", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,qwQ8OV3E60WG_VEl9ECHoA", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,s6DQxJ3gY0CG8L7JmcmRTQ,DfyODsHfIESXxVjcfZmBOA", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,DJnKyNXtuEW1tbKBFscIng,DfyODsHfIESXxVjcfZmBOA", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,ZY0rVNemjEuEMGMjuPNZyA,DfyODsHfIESXxVjcfZmBOA", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,jhPUIkf5bkinMSsqPTK3fg,DfyODsHfIESXxVjcfZmBOA", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,FO32QjZ17E66PiSEDyf0xA", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,rv8cpfd8kUCK5NbwjNmExg", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,kHPSqeM7JUqzMDtVxBGyBg", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,7BrllPDzNEyD9UZgdgM1Dw", + "28e5dd74-7963-42cf-aa68-f8005aaf312e,Ftonil9tzEWQnmRQuTxwxQ", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,s6DQxJ3gY0CG8L7JmcmRTQ,GXS9LQgPU0mJIEuXo_Oocw", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,DJnKyNXtuEW1tbKBFscIng,GXS9LQgPU0mJIEuXo_Oocw", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,ZY0rVNemjEuEMGMjuPNZyA,GXS9LQgPU0mJIEuXo_Oocw", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,jhPUIkf5bkinMSsqPTK3fg,GXS9LQgPU0mJIEuXo_Oocw", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,GYeYHzKcekODxx_zbtk8Qg", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,lwyGrpMk-kGZwIkioHr8Sg", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,KWYMlbpZiEWRUb0F7CYDjA", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,CsPK8sqkHUSV1u9d4LijHw", + "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,dPM-UHMtSEKC3rN8l4JsUQ", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,s6DQxJ3gY0CG8L7JmcmRTQ,eZGiYEx6sk2PsN39RzQwlQ", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,DJnKyNXtuEW1tbKBFscIng,eZGiYEx6sk2PsN39RzQwlQ", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,ZY0rVNemjEuEMGMjuPNZyA,eZGiYEx6sk2PsN39RzQwlQ", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,jhPUIkf5bkinMSsqPTK3fg,eZGiYEx6sk2PsN39RzQwlQ", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,x_UvGO3yQEmprFEXhPExDg", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,WRi0rK1O60qEpDPgOH5P6g", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,NvnHqZ-nb068DOSrxZPYIQ", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,2mwfYNx7iUGbSf6u8rRXwg", + "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,xtjgHdnYREm_gx_tLsTpvg", + "6936d8e0-9d4a-4e22-99b6-04e5182c1109,", + "ba96458f-84c3-42b7-acc0-e4d96f31ec84,", + "7ee02f33-1135-4adf-a626-44a1b6d1b0cb,", + "5fwjFQoC_Uu9ChvF6uNGZw,", + "5fwjFQoC_Uu9ChvF6uNGZw,fXNkMWFMV0aXalK_y_8SHw,SSY8ZYMgaE2Bl4oFmMjKQA", + "5fwjFQoC_Uu9ChvF6uNGZw,n741vKj02kyt8B5fT6s2og", + "5fwjFQoC_Uu9ChvF6uNGZw,iDnij3VaEkSAnpt5QRevig", + "5fwjFQoC_Uu9ChvF6uNGZw,oCB1ZeT0hU6uQGhHNsEvkg", + "5fwjFQoC_Uu9ChvF6uNGZw,IQMO6E5ofkyC3S-IgrKyIw", + "6QT99hx6DE6t9cKAs61i3w,", + "6QT99hx6DE6t9cKAs61i3w,KwCEye0nsESFs-yj4h9sjQ,c_t9XDbn-Eqml1O-wC5zJQ", + "6QT99hx6DE6t9cKAs61i3w,m2znGrdH-EOhrbLbFsmyOg", + "6QT99hx6DE6t9cKAs61i3w,ojrXnWkUkEOGM3_oT6V29w", + "6QT99hx6DE6t9cKAs61i3w,sipxCGA1uUu72KV07vAWFA", + "6QT99hx6DE6t9cKAs61i3w,1_vRe9nJMEWcNX8kAwgh9g", + "6RhwWExXekaYpK0NGHSXGQ,", + "6RhwWExXekaYpK0NGHSXGQ,fXNkMWFMV0aXalK_y_8SHw,HQQWccdyakaEmntPhvC6CQ", + "6RhwWExXekaYpK0NGHSXGQ,1BQdC-mTdUqfm4vUf0lx-g", + "6RhwWExXekaYpK0NGHSXGQ,S7C2A8nga0SlIeOEkeYaUw", + "6RhwWExXekaYpK0NGHSXGQ,vpxBkYv6LUSVV4NhYAS1Fw", + "6RhwWExXekaYpK0NGHSXGQ,0mwtvRpkmECaT5_4aqCttQ", + "2f8ba691-4508-4254-adb1-c2b554db4925,", + "2f8ba691-4508-4254-adb1-c2b554db4925,sNtmG8EODkOoiAdOi5Hetg", + "2f8ba691-4508-4254-adb1-c2b554db4925,HQ1M8kINaEu-0HJtz7f8Gg", + "2f8ba691-4508-4254-adb1-c2b554db4925,hPVbRnQq-EW3g0q17N6G5w", + "2f8ba691-4508-4254-adb1-c2b554db4925,OScwXIzJHUKzb-NC6gUnxw", + "2f8ba691-4508-4254-adb1-c2b554db4925,QVsGyxlefEeomtJO-dFAOw", + "3e3f5ed5-d810-4054-b648-d93167b90033,", + "3e3f5ed5-d810-4054-b648-d93167b90033,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "3e3f5ed5-d810-4054-b648-d93167b90033,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "3e3f5ed5-d810-4054-b648-d93167b90033,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "3e3f5ed5-d810-4054-b648-d93167b90033,4Eck_dNL1UKlIZWsA1YppQ,WeyKw_WS70Cx3NZdOaxGeA", + "3e3f5ed5-d810-4054-b648-d93167b90033,wMo3Bq1aXkCMFSRJft81BQ", + "a79e855a-c4fc-4ffa-be2f-9f4fef0ab1d5,", + "cc30f7f7-6c12-4a96-acfd-11aec984c2e4,", + "86e0a4cf-b112-40d2-94b9-7ef35abe5a61,", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,67fc9269-8dd8-4d6f-b594-c77e716f2482,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,d6823e01-69f0-4f85-b94a-74894356a2cf,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,08r7prRREUi-Hvp1Xze-wQ", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,7qVilU94X0Kldpvku2mcng", + "25d4c652-1730-4925-925e-31a290a2272a,", + "25d4c652-1730-4925-925e-31a290a2272a,e821aa74-2d47-4388-bcfb-1893b8a83a52,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "25d4c652-1730-4925-925e-31a290a2272a,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "25d4c652-1730-4925-925e-31a290a2272a,90869e86-cbc5-4024-9d08-fcdc41c64eb5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "25d4c652-1730-4925-925e-31a290a2272a,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "25d4c652-1730-4925-925e-31a290a2272a,002b07bb-4256-44ce-8ca4-25510e70b6b5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "25d4c652-1730-4925-925e-31a290a2272a,bzYdPb8GWUS-CgT4wrziIA,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "3dec4865-cf23-4c19-a460-dd2db80ee851,", + "3dec4865-cf23-4c19-a460-dd2db80ee851,e821aa74-2d47-4388-bcfb-1893b8a83a52,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "3dec4865-cf23-4c19-a460-dd2db80ee851,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "3dec4865-cf23-4c19-a460-dd2db80ee851,90869e86-cbc5-4024-9d08-fcdc41c64eb5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "3dec4865-cf23-4c19-a460-dd2db80ee851,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "3dec4865-cf23-4c19-a460-dd2db80ee851,002b07bb-4256-44ce-8ca4-25510e70b6b5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "3dec4865-cf23-4c19-a460-dd2db80ee851,bzYdPb8GWUS-CgT4wrziIA,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,e821aa74-2d47-4388-bcfb-1893b8a83a52,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,90869e86-cbc5-4024-9d08-fcdc41c64eb5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,002b07bb-4256-44ce-8ca4-25510e70b6b5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,bzYdPb8GWUS-CgT4wrziIA,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "rJ9zPPGdlk2i24sxLfkqng,", + "22pyI3Kz-UullhcebC6DLQ,", + "75e51889-defa-4689-b3fb-32882e08e5d1,", + "75e51889-defa-4689-b3fb-32882e08e5d1,PI3_sfTcNEW5TcAmHbsUOw", + "75e51889-defa-4689-b3fb-32882e08e5d1,K1-5YGg2rEiJFRWUKEfEMA", + "75e51889-defa-4689-b3fb-32882e08e5d1,DSZKCNhXAEiAxMM1Gqfhsw", + "75e51889-defa-4689-b3fb-32882e08e5d1,E6md4xTNYkS9geB8zkNRmA", + "75e51889-defa-4689-b3fb-32882e08e5d1,2dGY5MHfxEunzr7jtSFRVw", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,W83EASFUS06FBVMfL80ruw,Ydd6tdb9dUGooM6WAczNjQ", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,8M72ydR_uUSsli43PH2Eyw,wQxO4xmXCUGeC9ZHfDNGTw", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,F0o3eeWbCUaUAGRUdI0z7A", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,NnqAnqqLmUypWnNR2s-_sg", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,qvK8xgbyuE-dW3--Ku-mvw", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,hRl4UTGoBUi5dmu0f4s76A", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,07qFFGmwm0qmIikemncBkQ", + "771863b6-70d1-4f48-9c64-c621ce0fea46,", + "771863b6-70d1-4f48-9c64-c621ce0fea46,I_3bHRSG_EmC6novYyy2dA,Cq8BOWcHb0GTDLvXl1iIcg", + "771863b6-70d1-4f48-9c64-c621ce0fea46,HWAWBHI3zkGsh06he9CoPg,UAeSz-XeZUaZsk9FNlPX5w", + "b6610b7b-3978-46fd-9af6-41b4568c9b4d,", + "b6610b7b-3978-46fd-9af6-41b4568c9b4d,OnQeVxdstkS2zGxLBAuM3Q,i8bvbGAKREmAEbjKO-geOQ", + "b6610b7b-3978-46fd-9af6-41b4568c9b4d,-v3ddzUMF0aKxJR82SDevw", + "b6610b7b-3978-46fd-9af6-41b4568c9b4d,mtR99SoHQ0aJLJfXPsKW1w", + "94eb7ef3-528f-4f59-92a0-48b0ec287527,", + "94eb7ef3-528f-4f59-92a0-48b0ec287527,whEHo6Rhr0CxsDBsBD32WA,av4PwLz6YkmpBRImQZgn4Q", + "94eb7ef3-528f-4f59-92a0-48b0ec287527,3by5X5XZ4EW7eaERsaBJKg", + "94eb7ef3-528f-4f59-92a0-48b0ec287527,l2g5qO9dokiNepH92oPmug", + "Wua43ED71UqL71ATtyS0iQ,", + "Wua43ED71UqL71ATtyS0iQ,XFCpIdeDXUqeYCIgh4fGtg,sV_ByCug30G1Gw-77KUe1Q", + "Wua43ED71UqL71ATtyS0iQ,oKxmH-1s2EyIDkZoOQSQQw,sV_ByCug30G1Gw-77KUe1Q", + "Wua43ED71UqL71ATtyS0iQ,MxnAq_XOw0uTzQ8IIxqb_Q,sV_ByCug30G1Gw-77KUe1Q", + "iai6P7dDTUq8S4hqQmY1YA,", + "iai6P7dDTUq8S4hqQmY1YA,XFCpIdeDXUqeYCIgh4fGtg,MBuxBAIkpkWI26d-arAKQg", + "iai6P7dDTUq8S4hqQmY1YA,oKxmH-1s2EyIDkZoOQSQQw,MBuxBAIkpkWI26d-arAKQg", + "iai6P7dDTUq8S4hqQmY1YA,MxnAq_XOw0uTzQ8IIxqb_Q,MBuxBAIkpkWI26d-arAKQg", + "341dcb4a-818c-4d7c-9ae3-1dabbb79519a,", + "4ce7a70f-15a0-4cad-98ee-5cbb40827338,", + "4ce7a70f-15a0-4cad-98ee-5cbb40827338,c8cc9589-4831-4e73-a865-ef8c139a2ba7,7ac81d60-05b8-4a85-982c-f69213a93818", + "4ce7a70f-15a0-4cad-98ee-5cbb40827338,a089bcc6-798d-4777-98e2-7afd290dc6b6,7ac81d60-05b8-4a85-982c-f69213a93818", + "4ce7a70f-15a0-4cad-98ee-5cbb40827338,a36d252b-16ac-4a82-bece-de145147e7d9,7ac81d60-05b8-4a85-982c-f69213a93818", + "4ce7a70f-15a0-4cad-98ee-5cbb40827338,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,7ac81d60-05b8-4a85-982c-f69213a93818", + "4ce7a70f-15a0-4cad-98ee-5cbb40827338,68273c9e-96d6-4ddb-aaab-d99229e864d5,7ac81d60-05b8-4a85-982c-f69213a93818", + "4ce7a70f-15a0-4cad-98ee-5cbb40827338,4ElTihrMA0ipcgeb9UG01Q,7ac81d60-05b8-4a85-982c-f69213a93818", + "4ce7a70f-15a0-4cad-98ee-5cbb40827338,1pJN1AfxgUmUN_bTaH3rng", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,c8cc9589-4831-4e73-a865-ef8c139a2ba7,7634b7ef-9607-497a-b41f-c0191dc75998", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,a36d252b-16ac-4a82-bece-de145147e7d9,7634b7ef-9607-497a-b41f-c0191dc75998", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,a089bcc6-798d-4777-98e2-7afd290dc6b6,7634b7ef-9607-497a-b41f-c0191dc75998", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,7634b7ef-9607-497a-b41f-c0191dc75998", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,68273c9e-96d6-4ddb-aaab-d99229e864d5,7634b7ef-9607-497a-b41f-c0191dc75998", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,4ElTihrMA0ipcgeb9UG01Q,7634b7ef-9607-497a-b41f-c0191dc75998", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,LVnNy7-nFkyvi19nNcdFgQ", + "4763436c-2dcc-482a-bf37-a6b16d7aae6e,uWe5nxA4qkaicmXm3tMw9g", + "59d9560d-e5af-4205-b860-3afea1cc31a9,", + "59d9560d-e5af-4205-b860-3afea1cc31a9,c8cc9589-4831-4e73-a865-ef8c139a2ba7,b94411f6-5b71-48d0-ace1-ac6f63167065", + "59d9560d-e5af-4205-b860-3afea1cc31a9,a36d252b-16ac-4a82-bece-de145147e7d9,b94411f6-5b71-48d0-ace1-ac6f63167065", + "59d9560d-e5af-4205-b860-3afea1cc31a9,a089bcc6-798d-4777-98e2-7afd290dc6b6,b94411f6-5b71-48d0-ace1-ac6f63167065", + "59d9560d-e5af-4205-b860-3afea1cc31a9,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,b94411f6-5b71-48d0-ace1-ac6f63167065", + "59d9560d-e5af-4205-b860-3afea1cc31a9,68273c9e-96d6-4ddb-aaab-d99229e864d5,b94411f6-5b71-48d0-ace1-ac6f63167065", + "59d9560d-e5af-4205-b860-3afea1cc31a9,4ElTihrMA0ipcgeb9UG01Q,b94411f6-5b71-48d0-ace1-ac6f63167065", + "bc0a1c68-2eb5-4dde-a242-c4e19940699c,", + "b37582a1-7904-4288-a47a-7a63a3cb28ae,", + "88ae6d12-6d39-4770-ac2a-f8c4602a10a5,", + "a471995f-1fe7-4b77-a05f-8eea4e0970ab,", + "a471995f-1fe7-4b77-a05f-8eea4e0970ab,jXRXO3CVE0-H_iaxbMhFhA,VZyHWGKNJEGSxDT2GO3ebA", + "a471995f-1fe7-4b77-a05f-8eea4e0970ab,YSnoHFmJDUa_XCK6JMMh2g,VZyHWGKNJEGSxDT2GO3ebA", + "a471995f-1fe7-4b77-a05f-8eea4e0970ab,JilOXG1I_Ei65mw6TzFWgg", + "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,", + "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,1-_ssucgeEalgr6EDL3S9g,Ru4HN3stVUutQ9rUh7ro1g", + "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,cbn6vE0gDUOuklVhJv1vNA,Ru4HN3stVUutQ9rUh7ro1g", + "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,2eTBbHTkf0uxy9JqA507Hw", + "62a28dea-adcc-40b3-9045-917ae4f38c63,", + "62a28dea-adcc-40b3-9045-917ae4f38c63,Gep43Ix44EWybJ80XAyDbg,kxkqq5UA_kCM6lFsFe3dvw", + "62a28dea-adcc-40b3-9045-917ae4f38c63,EmTrJKm8cUy8_BHAvP8Meg,kxkqq5UA_kCM6lFsFe3dvw", + "62a28dea-adcc-40b3-9045-917ae4f38c63,VnRSTH4HqEqADLtryqSQNQ,kxkqq5UA_kCM6lFsFe3dvw", + "62a28dea-adcc-40b3-9045-917ae4f38c63,T670e7nOakqKl3L6LTlTtA", + "e29d044e-79fb-4280-bca9-aa1e1257e968,", + "2b325613-84c1-46d8-86b4-a167d79661da,", + "2b325613-84c1-46d8-86b4-a167d79661da,ggpNxFtZMEaHlvWXsDl9Tw", + "2b325613-84c1-46d8-86b4-a167d79661da,Jd_ahlwRskehLggn1T-GdA", + "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,", + "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,MtbewwIGSEu-F9Ks_Ibb4g", + "c2df65e0-2e85-4666-bea1-e8efa654b155,", + "d1de5e76-0367-42f2-be88-6f28a140e5f6,", + "4fb6e083-ae10-4c71-898a-6a55138a68a3,", + "f527ffaf-da03-4519-82ed-46a9cb981dc3,", + "f527ffaf-da03-4519-82ed-46a9cb981dc3,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "f527ffaf-da03-4519-82ed-46a9cb981dc3,c5884778-a87f-4612-9717-86fbb65d440f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "kghEf_iMKUSxPO-h5iRhSQ,", + "899f1d17-eee1-4b7d-a112-fbbba597e738,", + "899f1d17-eee1-4b7d-a112-fbbba597e738,t8kKXvBU-kSM_MHaqZYpkQ", + "34f45c5b-fea7-40b4-add7-bc7a37000ab5,", + "3ce86805-987e-4958-bcc9-57d89c4a5b1a,", + "3ce86805-987e-4958-bcc9-57d89c4a5b1a,PGZihr096kC4XZvHfmVMzA", + "b4c93141-f387-4e42-9b04-12b7a97c9416,", + "gwLiwmqBIk6glFxvaTzxeA,", + "gwLiwmqBIk6glFxvaTzxeA,AAbGHOOitUu9enweOXbrkw,fXnd_COSzUeznwe7P4ZYRg", + "gwLiwmqBIk6glFxvaTzxeA,Jy2TN1KBTEeoVehy_x32Ng,fXnd_COSzUeznwe7P4ZYRg", + "E9VTiIVFPEa-OuQRto0e_Q,", + "E9VTiIVFPEa-OuQRto0e_Q,fD5vtKxBWECH54Jj4FbnUg,C8gCmBZ8RUG83vvDw_UYew", + "E9VTiIVFPEa-OuQRto0e_Q,gy-xeYKKREaBlx9fUmuW2w,C8gCmBZ8RUG83vvDw_UYew", + "sOREgAGs-06CeGGsG-WIkw,", + "sOREgAGs-06CeGGsG-WIkw,rSzG7s1ocUmx9_VrJLOcHQ,ISycihytUUyoypVb-kXN_Q", + "qxovV6Vym0ufL2cTCyKjOQ,", + "qxovV6Vym0ufL2cTCyKjOQ,64Hvmzrb3Uu9X_dCA3M3Jw,rPpYm5fIzky4vqVB26guAw", + "JWanxvOHyESg9F_HlmTOfA,", + "JWanxvOHyESg9F_HlmTOfA,vuSwk04zykCe3ANKN838OA,Uhw5NBQy-EOIiCWiZFiL7g", + "JWanxvOHyESg9F_HlmTOfA,ZUMIXcyOpEGiHgslbHQbwA,Uhw5NBQy-EOIiCWiZFiL7g", + "LK8jt9zCuUGwLRccgOOvjA,", + "LK8jt9zCuUGwLRccgOOvjA,A6VnGtpY9Ey0atf3zaLo3g,GJRG8sCM7kSGfUd9gLwpgQ", + "LK8jt9zCuUGwLRccgOOvjA,rsUfUnTwbU6zrjrHWps1yA,GJRG8sCM7kSGfUd9gLwpgQ", + "CdeM6RJmPUumXtdmdM3RXA,", + "CdeM6RJmPUumXtdmdM3RXA,mZ2ASMrs80axStLp9g1enA,dGzmSibucEe7i1HD7lPcNQ", + "iBQ-v6xaqkipUlfzkSHEeQ,", + "iBQ-v6xaqkipUlfzkSHEeQ,bH3dgwnwt0KEw8oCDVIcEQ,erPtGSgq_kmqfeSlZWw4VA", + "42f1a780-1fe1-402e-bf11-176b768e33b9,", + "42f1a780-1fe1-402e-bf11-176b768e33b9,jzGTu3tSB0WM430AiMGAAw", + "25769d1f-4ecb-4d92-b692-322b38c013b5,", + "b795c631-f075-4d32-ac87-c732b36cb932,", + "703f1987-2b31-4b6f-bab7-1939b1105ac0,", + "703f1987-2b31-4b6f-bab7-1939b1105ac0,YS-fXtVLTkG-wl40HPBLOg", + "703f1987-2b31-4b6f-bab7-1939b1105ac0,dzH_40F-MUCy_7gATqL4PA", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,OJYpuFw810Sg4bTw0XQf-g,NqU5Mw75BE6kPaD8t11Qow", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,t4gMAKp4L0WBaTGf_rtqFw,NqU5Mw75BE6kPaD8t11Qow", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rGMFdPKmK02_OvIIFYlAxA,NqU5Mw75BE6kPaD8t11Qow", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,wxvSDMommkenIl5hTY2fdQ,thwvF0DFRk-mV99ruqYFoQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,-sqfCQpzpUyjEShEXH9JYQ,thwvF0DFRk-mV99ruqYFoQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,as9fjqDWiUi1w7yYavKZXA,thwvF0DFRk-mV99ruqYFoQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,sz-J7RjesE-ke3bKwumzZQ,mkNzDvyGfEijbdzAxKYrPQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,QlZt51mdn0WuiqKoJaIRyA,mkNzDvyGfEijbdzAxKYrPQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,6PyJEQxyd0K9TPRnvt_86Q,mkNzDvyGfEijbdzAxKYrPQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rsh2uKli90OoEnJZRFc2Ww", + "e0c4c93d-ea3e-420f-9e8f-7890b5bd4b6f,", + "6427bd57-fcb2-420f-8b3e-d1da43470b84,", + "6427bd57-fcb2-420f-8b3e-d1da43470b84,dZM1bs6_MU2F6Vz1mH0QKg,KfM7veOVlE-dNZmsbDNZBA", + "6427bd57-fcb2-420f-8b3e-d1da43470b84,0Ytm7G4_lkmZWPA_YfaIeQ,KfM7veOVlE-dNZmsbDNZBA", + "1988692c-aae3-450b-acd6-e9015b831f5c,", + "1988692c-aae3-450b-acd6-e9015b831f5c,zw-evwOmg0q14P040EYYiA,a3I7EyRNVUilLfE6hriYtw", + "1988692c-aae3-450b-acd6-e9015b831f5c,bhYZTPD1nEaEercLIZRucw", + "1988692c-aae3-450b-acd6-e9015b831f5c,_C4RxcDBQE-mHh2oyL9vGw", + "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,", + "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,-K940hWJXkee1HDrlurepg,ItJCR1utoU6i-syjJpxQqw", + "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,SdSjmCkq-ECoboR2PwxJfQ", + "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,iGhiA5wU8kG3NH6P9NZvGg", + "ffad1160-8383-4b4a-a5b9-223476b49b92,", + "ffad1160-8383-4b4a-a5b9-223476b49b92,JY4zXsTbX0W1sU6g1o4kuw,Y-XQzHOjIkC_-it1j9m9jA", + "ffad1160-8383-4b4a-a5b9-223476b49b92,W_AVltwf7kSzmftSsGdijw", + "ffad1160-8383-4b4a-a5b9-223476b49b92,trFPTiehdUm2Q3bdvBQr7Q", + "fa0e701c-5e80-46f4-8817-6e2dbf904734,", + "fa0e701c-5e80-46f4-8817-6e2dbf904734,oCeStQfVkE2ZwCcQjtV1bg,r94RX2106E2TRK2wQ-u8-w", + "fa0e701c-5e80-46f4-8817-6e2dbf904734,nNJ8Tkg57k6pTS_0HSLkUQ", + "fa0e701c-5e80-46f4-8817-6e2dbf904734,VmHQJIt7HkO4INJEXCBbNA", + "ffddbd79-0158-4f93-b8ec-dd5573c035a7,", + "5b2d0a6c-238d-43a7-a20b-bddcf14315f1,", + "pQqNvrhDJ0uuipKHA2qUOA,", + "pQqNvrhDJ0uuipKHA2qUOA,lq59KZwElEStrviDEQLLcA,olyS074RHU-ojE0GzBBYJw", + "pQqNvrhDJ0uuipKHA2qUOA,PSpysyn5gEKXCEi7f6gDoQ", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,LVuUiGBtn0mp4Wfs-2F5EQ", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,R8VYB2EGPkmQX4dbmGLuRA", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,wCCq14_wc06hkmjg6TTFxw", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,0JVWqxXYoUqgI-5ORIr0bA", + "97c8e2f3-e786-42e5-8852-4abec5dc645d,", + "97c8e2f3-e786-42e5-8852-4abec5dc645d,_8OmgC7c80CsvQN41U6XyA", + "39854685-4793-439b-9c64-c98825456a03,", + "39854685-4793-439b-9c64-c98825456a03,qVeFYveZQEmobBTEMg1RTw", + "a928c6dc-442f-4cbb-b705-cc5a3969b34a,", + "f159e878-7563-46db-b59d-c838612a0a78,", + "38e7e182-5166-42d9-9ed3-b525b98579c1,", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,304e5cf1-5c94-481f-8e49-e6a943e6b300,90e0da8b-1941-4ca8-b7e5-b4e31921873c", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,NpMkoIK62k63QoLVEXId6g,iT7UGMahoEu7BZZqeKXKww", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,PLRhiOhhb0KhatFH7f3pwA,iT7UGMahoEu7BZZqeKXKww", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,RwgnKTfMEkWrAp_OMGy__Q,iT7UGMahoEu7BZZqeKXKww", + "hTHM3Kjqy0ykqBD8UCRQJw,", + "hTHM3Kjqy0ykqBD8UCRQJw,7-lAnMa7M0u0n0nmeIINgw,6GfSjqIb4ECCvIPC8vO8_A", + "hTHM3Kjqy0ykqBD8UCRQJw,8HR0DwDl2ECtHf5aEkqSHQ", + "BR0mZKsKaE-t4R8CzEwuyg,", + "BR0mZKsKaE-t4R8CzEwuyg,Q1knTl-n-EeAUwotFspNew,FHB5XHwNWka7aUt5km3LNg", + "BR0mZKsKaE-t4R8CzEwuyg,B1IotYmwbkiUAfdZiqzq8w", + "7b568bd0-ce59-456f-af69-537364a52f69,", + "7b568bd0-ce59-456f-af69-537364a52f69,_fabGqrxrkW2mfx0bejHsA", + "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,", + "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,46WaeyRZnUCuLkEpxECRMg", + "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,", + "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,8NY4MevLp0C2lP178fIZ9w", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "05ec4e87-088f-4281-a118-2e63c2585200,", + "05ec4e87-088f-4281-a118-2e63c2585200,Wm30oV5BtEuFQZFk3dPK_w,tfiSfpRez0y_R4QjAhO0bQ", + "0d4e7ebe-efd9-4e68-9900-5645916e7596,", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,AtF5be-huUeADedbRurRcQ", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,T9XdU5xAFEqX3QV__SjXYA", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,cxWuO4qBtUOKYmCXI0D5Jg", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,FIZdMEUgykOHb7fVlOfa5g", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,5GEhQr7Iy0-kB0yCA3uJ6g", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,wrobkXV-10CD1JJtgyy6Fw", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,8e5280a8-7771-4278-b985-c325f2490f9d", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,545ed5e0-beab-46d8-9d14-7de0420ff90d", + "00f0b914-aa22-4d14-9af1-996e1af61f4d,", + "591af332-a01c-454d-89dd-5ed6ae3ed09c,", + "2f77696c-618c-4fb8-9220-33839cd3bfcd,", + "0bd750d9-82ff-4d77-9f2e-7e2651316f51,", + "74902dee-08a5-4a8b-a113-80f450a0de0a,", + "b0a2e988-9942-4c52-a714-f1bc12fa6290,", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,bd4a84e2-b67a-4269-a26a-17fb23ddb09e,ccf1ccc1-e229-4157-bb74-f2cdef01e547", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,DzgN67HoikexE2rTlZLdYQ,vm1ESK9ls0SaxSwHplNALQ", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,xeK_acj2hE-sYGhsMRwjhw,HwhHShudGkGjXagJxCN2uA", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,Em__GeYWaU2pAEqWYCBKpA,DG83ruIdI0Wu-GSRg4icYQ", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,n8t0gIYFbUaPMhyQeWkPSQ", + "7dc6c916-b887-4929-93b5-dcea3022e6a9,", + "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03,", + "0QOlT9c5xUuinpfZaoEe8Q,", + "0QOlT9c5xUuinpfZaoEe8Q,Mt2a9ygWPkS-DyXj1q-Kvw,znNiHfNdTE-szOIAnIWFQg", + "0QOlT9c5xUuinpfZaoEe8Q,LEUQnDz-k0m-hY-AX3Z9Sg,znNiHfNdTE-szOIAnIWFQg", + "0QOlT9c5xUuinpfZaoEe8Q,cSFt9MWLj06CDzIzATqrTg,znNiHfNdTE-szOIAnIWFQg", + "0QOlT9c5xUuinpfZaoEe8Q,B6qNcZQ770eJel13RskuIg,sIP83lqv3kG31TH5Rw4gqg", + "0QOlT9c5xUuinpfZaoEe8Q,p3g8YAOhTUKAiE-Wh-X-aA,sIP83lqv3kG31TH5Rw4gqg", + "0QOlT9c5xUuinpfZaoEe8Q,SqHAYcqBXU2PtF2urWGruQ,sIP83lqv3kG31TH5Rw4gqg", + "0QOlT9c5xUuinpfZaoEe8Q,Pu_1I6Zq-0G1HBMHa5MrKg,Cjy-bE0PikWLCRrL4U2NGQ", + "0QOlT9c5xUuinpfZaoEe8Q,7Rw_gt9IUEOYsvVcOTbVBw,Cjy-bE0PikWLCRrL4U2NGQ", + "0QOlT9c5xUuinpfZaoEe8Q,aiBfwCNwpUyJ0rYKCMa7qw,Cjy-bE0PikWLCRrL4U2NGQ", + "vDSlMnayzEqWXwT-lkcKSA,", + "vDSlMnayzEqWXwT-lkcKSA,otQaNOsODUSTfHlCX0IVOA,0WOBGm34H0ySBC10VToa9g", + "vDSlMnayzEqWXwT-lkcKSA,NKf5-r2S_0iP0SaO9bjvaw,0WOBGm34H0ySBC10VToa9g", + "vDSlMnayzEqWXwT-lkcKSA,adJVA26PyUSVvmZgxkCAmQ,0WOBGm34H0ySBC10VToa9g", + "vDSlMnayzEqWXwT-lkcKSA,4ijeE3LqUUuSplKrMto6Kg,ytfIq0TiYU6EUdd0ruaGAQ", + "vDSlMnayzEqWXwT-lkcKSA,d27e5m1BLUWI42Pxnj6F4A,ytfIq0TiYU6EUdd0ruaGAQ", + "vDSlMnayzEqWXwT-lkcKSA,RZFWcTzxPkOfUuTIQ1mzkg,ytfIq0TiYU6EUdd0ruaGAQ", + "vDSlMnayzEqWXwT-lkcKSA,XT_txjZfIUOT400UPHLaRA,M-mi8QUHwEeIewXE8uAlrQ", + "vDSlMnayzEqWXwT-lkcKSA,iEvS6I9LjkSaGAmjcxz8uQ,M-mi8QUHwEeIewXE8uAlrQ", + "vDSlMnayzEqWXwT-lkcKSA,gwdtq3NiEEmMKYtn1VPoiw,M-mi8QUHwEeIewXE8uAlrQ", + "vDSlMnayzEqWXwT-lkcKSA,6taO0KRM_kuKoaTnOIsHRg", + "vDSlMnayzEqWXwT-lkcKSA,7WLoulmRgEKX3Eshtn_cxw", + "vDSlMnayzEqWXwT-lkcKSA,BzzPuw8i0km4VQLd77mXEg", + "vDSlMnayzEqWXwT-lkcKSA,er8BswhSkUqh5TVV5fDAng", + "vDSlMnayzEqWXwT-lkcKSA,zUZHddmmhE2rXfCVuz_GGg", + "cbN5e-t_jEKa4hzir5JAxQ,", + "59bbb9d4-4fdb-4400-90d5-244643dca4ce,", + "d834be2b-b107-49aa-b0ec-b54bdb4c61aa,", + "2b7a32da-eda1-45e1-b28a-f9f882cda7fa,", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,026ca50e-3a81-43cf-87f5-950687b7bbdd,adae82eb-a4b9-4541-acf2-76cea839a593", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,6584dada-499e-4d7c-958d-466955b20640,adae82eb-a4b9-4541-acf2-76cea839a593", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,uUOO-k5KS0mU7B5BLaOpnA,adae82eb-a4b9-4541-acf2-76cea839a593", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,Ijf-j7EgEkGYOKo9SQNLRw,adae82eb-a4b9-4541-acf2-76cea839a593", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,J7l1YwWjqkutybwykwih7w", + "-SQLtL4rdEWTJdw2IroQCQ,", + "-SQLtL4rdEWTJdw2IroQCQ,CoSVCyrI-0qg-EkqB2R0hg,JO9lJOQSREWbPyDBZH_cyg", + "QK6lBlX_wUCiSbJ9RkIwdA,", + "QK6lBlX_wUCiSbJ9RkIwdA,H0sqS2_VPEieVrHSyMNo7g,9HVoTIf90Ea8_czICVg9AA", + "QK6lBlX_wUCiSbJ9RkIwdA,TjeZwaUp10uhM72q8j4R0A,9HVoTIf90Ea8_czICVg9AA", + "QK6lBlX_wUCiSbJ9RkIwdA,ZlO_DqkSKUStV4-gGPvQOg,9HVoTIf90Ea8_czICVg9AA", + "t_66r_j5oUSh9awIPmdUKg,", + "t_66r_j5oUSh9awIPmdUKg,YTlnBuEjhUG1hSfAs8qwuw,IcR9Z_MxKkS9uh4uFYuFzQ", + "ImwG0snB9ECc_gqRRBoz6A,", + "ImwG0snB9ECc_gqRRBoz6A,lAjUeUtTLU2-3bG2OXBukg,LFB_YqTym0-tQmUIj-YcYA", + "TjrFi6TFhUC5tQXWkZiK9A,", + "TjrFi6TFhUC5tQXWkZiK9A,3qu7mxSNpkKRrYMhsWHVGg,K5NXVecrqk6Nb6SkBu08ww", + "TjrFi6TFhUC5tQXWkZiK9A,Ij8MXrl1Okip4M7y-gF23Q,K5NXVecrqk6Nb6SkBu08ww", + "TjrFi6TFhUC5tQXWkZiK9A,qEojxjSXm06CX52hRDJAew,K5NXVecrqk6Nb6SkBu08ww", + "ffdda4d3-a768-4335-a883-aa3f9dc897bf,", + "ReqOfohe6UamzaiHG1x3Ow,", + "ReqOfohe6UamzaiHG1x3Ow,lDXambcP60CeY2C8qeGDFw,uiAwVt3HK0mRVtWp9B-gTg", + "8b065e2a-b106-4f11-b415-4c47ce87993e,", + "8b065e2a-b106-4f11-b415-4c47ce87993e,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,o2WfRyjJy0CKoUg8YIxNjQ,sxWOOK1RwUOp5YqZ7a36xg", + "8b065e2a-b106-4f11-b415-4c47ce87993e,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "8b065e2a-b106-4f11-b415-4c47ce87993e,sscD2O3LEUOAKkqjGpGe8g,uZjxYOMa10GzJ9wQkRge2w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,hRxyLsKfsESFAsXAyZQjoA,uZjxYOMa10GzJ9wQkRge2w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,kgCKjtSjFEmFq9ez2llxKQ,uZjxYOMa10GzJ9wQkRge2w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,KaZqNx5VKkarRrOJcTuD9A,uZjxYOMa10GzJ9wQkRge2w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,eOJkmpSL4EypK-AQz7j8gQ,uZjxYOMa10GzJ9wQkRge2w", + "973d84ee-5b88-475c-b579-a76c34a7c533,", + "973d84ee-5b88-475c-b579-a76c34a7c533,yA0KhVg19kWqG1UkyxQogQ,yBNsYcZXzEiuR0u3tWnnWQ", + "973d84ee-5b88-475c-b579-a76c34a7c533,P9bBSL0YLEyjN8PxMfgbKQ,yBNsYcZXzEiuR0u3tWnnWQ", + "973d84ee-5b88-475c-b579-a76c34a7c533,o2WfRyjJy0CKoUg8YIxNjQ,sHyNs-dpc0-_Ah-HJVczcw", + "973d84ee-5b88-475c-b579-a76c34a7c533,bgRKH1wfqkWVHcuSnFYnlQ,sHyNs-dpc0-_Ah-HJVczcw", + "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,", + "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,iokdlBfjuEuAwqIMOQwxdA,H3OQDBef1UaacSD52p8Hdw", + "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,nKauYAyWhkm2AMHFyC-fLg,H3OQDBef1UaacSD52p8Hdw", + "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,EiGgN7uVukqoRE7UJcMLTw,H3OQDBef1UaacSD52p8Hdw", + "618b979e-c6e6-434e-98e5-9b51e117fb69,", + "618b979e-c6e6-434e-98e5-9b51e117fb69,iIjl6PamKECL_JWvrZNHfg,2IFVtBYV8EqQXs8XHqeqdg", + "618b979e-c6e6-434e-98e5-9b51e117fb69,vtXUyu-xmkaYtOCL9r3PsA,VtepaI2fvUWhcvNmpQ8r6Q", + "618b979e-c6e6-434e-98e5-9b51e117fb69,r4O0JuB0dke1IoOEqW4nkg", + "618b979e-c6e6-434e-98e5-9b51e117fb69,UJRSCFfPq0K0yV0ETS3uSQ", + "618b979e-c6e6-434e-98e5-9b51e117fb69,3bWtQstEmUeHJXngec632A", + "618b979e-c6e6-434e-98e5-9b51e117fb69,NEU8mYk_TkqS78l0zY9GAQ", + "618b979e-c6e6-434e-98e5-9b51e117fb69,K4_AIRWX-U-f-nXsBOc82g", + "618b979e-c6e6-434e-98e5-9b51e117fb69,9lPZEIVbbUK1hdR1aVPLwA", + "618b979e-c6e6-434e-98e5-9b51e117fb69,W_RwnySBMk2GlrknM7NDrg", + "618b979e-c6e6-434e-98e5-9b51e117fb69,LXR5H_-7JUO7F3JkJh3ouQ", + "618b979e-c6e6-434e-98e5-9b51e117fb69,mKWQMgBuTEq9EwXBTusuJw", + "618b979e-c6e6-434e-98e5-9b51e117fb69,N2ofIXeSBECHTBAoP2yQ6w", + "618b979e-c6e6-434e-98e5-9b51e117fb69,rg6sMjE1XkijY4Pq73pjcg", + "618b979e-c6e6-434e-98e5-9b51e117fb69,t9-7AjmLgEWawcyWCD4xVw", + "618b979e-c6e6-434e-98e5-9b51e117fb69,5Q9E76b580CnMWIg6zbPwA", + "9b5406c4-8589-47e4-9f69-4fa4733615e7,", + "9b5406c4-8589-47e4-9f69-4fa4733615e7,yXDNXmcJvUC4eCx9oGItcA", + "5da97d5c-0d1b-4b0d-a2be-d255871d833d,", + "002a0f2f-1a24-4439-b578-470818ef8325,", + "002a0f2f-1a24-4439-b578-470818ef8325,d-scjeOeCESC0rQMKsozsA", + "b5a83be1-fd78-4797-b3e4-e5fa9730fab8,", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,9b9t8_aKH0-gQXMhZycaOA,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Zz4XatKfUEapReRAQHxKKA,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Jt0QqNrAp0qzUR_RQoivVg,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,lBePHNL9LkyaAjEFyFE8Sw,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G4HIPUjekUigiU0-Kr9mdA,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,5mcp7-2ZEE6n3-zN3_CoWw,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,R0UCacpdhUS906Xf7l142A,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G-VKgepBkkeATcwCaS6Rwg,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,1LonkCbt4kykeAScYKpwyg,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,IXWd396grEOWHt32L7fCpQ,rzoH8P44a0esN-HnYd3aQg", + "M9weaekvIUimiJSVJqZl-w,", + "M9weaekvIUimiJSVJqZl-w,gDH1WmW5rk2RdY4wJYYZow,GV8z2dlqGEqIAYaGrF1ZYQ", + "M9weaekvIUimiJSVJqZl-w,wHJnbDOo8ESmUbQDr0k31w,GV8z2dlqGEqIAYaGrF1ZYQ", + "M9weaekvIUimiJSVJqZl-w,GK3yolCNlEm3B_5DtHkE8w,GV8z2dlqGEqIAYaGrF1ZYQ", + "M9weaekvIUimiJSVJqZl-w,U2ZbHn8WJEySUXVpzAY6ig,GV8z2dlqGEqIAYaGrF1ZYQ", + "M9weaekvIUimiJSVJqZl-w,L333w3dfIEWDbtt1Yn1gBg,GV8z2dlqGEqIAYaGrF1ZYQ", + "ASHnWS0h9kOJhbOUwxWKFQ,", + "ASHnWS0h9kOJhbOUwxWKFQ,U2ZbHn8WJEySUXVpzAY6ig,6nmHu9zj8k-tEz0tBHblnQ", + "MJQiQ3LATEOymxCC8wfSjw,", + "MJQiQ3LATEOymxCC8wfSjw,L333w3dfIEWDbtt1Yn1gBg,lSDrKR69rkSFULy9GA__1w", + "0yFzgVESokC3gFd3hrS8bg,", + "0yFzgVESokC3gFd3hrS8bg,GK3yolCNlEm3B_5DtHkE8w,yKb2A8M_m0GQDhFWCRUHTQ", + "vP9EKTMXP0yni8U7uCz2-g,", + "vP9EKTMXP0yni8U7uCz2-g,FsATqzRVekO9hQ3u_q7keg,Q7hqvEo7DUyD9AqIOunWWA", + "O8VqC11xNkSKpDh6Omp_0w,", + "O8VqC11xNkSKpDh6Omp_0w,mvj5RDTuh0qoql0OyBFI9g,YQQamy-tQkCkpZPXKepUrQ", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,UjSxxUK7z0GGdjvI_NsJ3Q", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,lBkOSQNDZUOvoknntnEMew", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,sq5Qv4nVAUqzhMdB7U2MVQ", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,6fp7nDdMvU6Ig-EmvjMpIA", + "jUpFzO5MPEGTbS-E0a99Nw,", + "a994fc19-974e-444e-ae9e-c2f6c32432bb,", + "246e6caa-c94c-420d-9805-9f0282c6d052,", + "95a519de-f2cb-429c-b014-508477f20d42,", + "c5d70cb4-71dd-4fe4-b719-34fe2073c611,", + "c6ae0a86-e590-4097-b77e-ba6fce83a826,", + "c6ae0a86-e590-4097-b77e-ba6fce83a826,Ii2WNQxqtUeoIfGItUkVbQ", + "c6ae0a86-e590-4097-b77e-ba6fce83a826,U-KT3nJlW0-JT8GSQuQXhg", + "c6ae0a86-e590-4097-b77e-ba6fce83a826,3L-AGgM50kCCz2V3UJtErA", + "2caHvVQ1vECrPv-YcdxdyA,", + "2caHvVQ1vECrPv-YcdxdyA,FNe9LkWJMUeC3frYJMyEaQ,nW_fBsuZA0OfIjTMXm6ZCA", + "2caHvVQ1vECrPv-YcdxdyA,mudt-Z5-5USdnIq-xaDgOw,nW_fBsuZA0OfIjTMXm6ZCA", + "1940c740-695d-4481-a802-bb11b6561635,", + "1940c740-695d-4481-a802-bb11b6561635,vjO5ctMe0UC8GN6p61IC4A", + "1940c740-695d-4481-a802-bb11b6561635,hbF6ChMyEk-L8O4f6o_l6A", + "1940c740-695d-4481-a802-bb11b6561635,rQaQALql2E-zdM8072gfCg", + "1940c740-695d-4481-a802-bb11b6561635,y78S5o6VXUGva1Wpd6sp3A", + "1940c740-695d-4481-a802-bb11b6561635,S_dLd3dzvk2hM6ZA2DKTWg", + "1940c740-695d-4481-a802-bb11b6561635,GU267AQ5T0m1jco3mNw_uw", + "1940c740-695d-4481-a802-bb11b6561635,SV2UmeQDjEaMK_A2Q4tt3w", + "1940c740-695d-4481-a802-bb11b6561635,V-vCW0ICl0Ke3QPAGo9GSg", + "1940c740-695d-4481-a802-bb11b6561635,g_f9bH8tykGq3yRwWuQPOA", + "1940c740-695d-4481-a802-bb11b6561635,Eh1jgOjxpE-AEModcPAFMg", + "1940c740-695d-4481-a802-bb11b6561635,_aQEMQzp20eMT_Izfc64wA", + "1940c740-695d-4481-a802-bb11b6561635,ppH8miE39kGaSuACQv34WA", + "1940c740-695d-4481-a802-bb11b6561635,hZvsroTOnU6ew9HBWaqxHg", + "1940c740-695d-4481-a802-bb11b6561635,QQVh8FdTakO26iOFdzGoBQ", + "1940c740-695d-4481-a802-bb11b6561635,mzu-a8fcsUCW591ENABAgg", + "1940c740-695d-4481-a802-bb11b6561635,JpaGA1tG6U-VLsHH_vklzA", + "1940c740-695d-4481-a802-bb11b6561635,42YaouiGVEe3zSjTsvyfoA", + "1940c740-695d-4481-a802-bb11b6561635,fzQzxcpDNEW8Rx122zmjAw", + "1940c740-695d-4481-a802-bb11b6561635,b11GS6Jd4E-UhO8wxhfQXw", + "1940c740-695d-4481-a802-bb11b6561635,cd9d37fb-51a9-45dd-8e87-2756eb62f48b", + "jiBzrzDPkkuL6DqIFKpcCw,", + "jiBzrzDPkkuL6DqIFKpcCw,lvDIOeSI10SNxnCO_M_9zA,StxMBEprVkqMa_IoDG-Q9A", + "jiBzrzDPkkuL6DqIFKpcCw,0S7Nqm2kv0qMDTh-5Wy0MQ,StxMBEprVkqMa_IoDG-Q9A", + "jiBzrzDPkkuL6DqIFKpcCw,poqpmOloPUKotHH9p0YAyw,StxMBEprVkqMa_IoDG-Q9A", + "jiBzrzDPkkuL6DqIFKpcCw,F7luQ18ZXEaITFiCqqwkpg,StxMBEprVkqMa_IoDG-Q9A", + "jiBzrzDPkkuL6DqIFKpcCw,okpg4ftsak-wc8Zu83uytw,StxMBEprVkqMa_IoDG-Q9A", + "jiBzrzDPkkuL6DqIFKpcCw,OfenZaa1-UGdQ8YVOrXjzw,StxMBEprVkqMa_IoDG-Q9A", + "jiBzrzDPkkuL6DqIFKpcCw,WOxMXWKGrUWhnNInttZ-Zw,StxMBEprVkqMa_IoDG-Q9A", + "JWfs9IiQs0Weu8MmyGpqLQ,", + "JWfs9IiQs0Weu8MmyGpqLQ,Z8Bfk0MTb0evZehvvgzuBA,x10FeRBsQU2DIIpG8XIlwg", + "JWfs9IiQs0Weu8MmyGpqLQ,9QG6qp5FBU-u0-JK-yMMqA,x10FeRBsQU2DIIpG8XIlwg", + "JWfs9IiQs0Weu8MmyGpqLQ,CavRDqIlmUeTZBtCLn1hZQ,x10FeRBsQU2DIIpG8XIlwg", + "_OWVy3z6iU-M3-zbQgSLig,", + "_OWVy3z6iU-M3-zbQgSLig,D0tP58wC30mJ00vNxw8hqA,N5wN1l0tYEau5np-9PoIiA", + "_OWVy3z6iU-M3-zbQgSLig,Vxq6v55a_kGPODzwB5MC1g,N5wN1l0tYEau5np-9PoIiA", + "_OWVy3z6iU-M3-zbQgSLig,iJW8XGJfU0G12iXarjxhdQ,N5wN1l0tYEau5np-9PoIiA", + "3ec19cb7-d0a3-4c35-a7e0-dc1f8609d373,", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,ffHKr5EcIU24PbAtWSnalw,bVrkIyADCUiGNYWdmP_HPw", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,m4W5D0Oa9kaWa-8I96YYzA,WF_fg80f30qvHit0vGBwCA", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,9RPbhHJWBkqwpDbMg-QumA,Qzklnrvw0EOiZ1mrj_nAYw", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,bJXcVtY7qUK101QUAgr05A", + "b92f9873-5fbd-4529-803e-80151f5685c2,", + "b92f9873-5fbd-4529-803e-80151f5685c2,mO5wjf7_cEWNCHFR6ymGxA", + "b92f9873-5fbd-4529-803e-80151f5685c2,EjNmQJBRnUet_ovEQ3HnDA", + "b92f9873-5fbd-4529-803e-80151f5685c2,oy67Rd9BskuB8yloqSv0Jw", + "b96426ec-093d-4bfe-95e1-b88489479198,", + "b96426ec-093d-4bfe-95e1-b88489479198,eBm7gRXomkivUz4Qg_ab8w", + "b96426ec-093d-4bfe-95e1-b88489479198,agkPLQG_Mkmqf3WkDaFaWA", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,l1cBXE49rkKTDEl1NvmWog", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,MYCVPJDuz0Od_aZJekuWGA", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,kfZalp0wCkypX-QDhF0YoA", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,2U3ZEvBzoESCb7kk4zSokQ", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,S5IPVluRPkWzapjf7Bdtxg", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,oDkSnfL7Vk64ZdxkuenEmA", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,8kAqQHAgnUypHdxnwVSQwQ", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,pUHlnnttXUmqP-9Rab8YaA", + "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,", + "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,rixBVVHS1E2Q4Y43ix4ySA", + "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,zvs7t0P2Q02wIELJzMa2TQ", + "3b792b59-50c0-4c28-b367-fc694b1d65d1,", + "3b792b59-50c0-4c28-b367-fc694b1d65d1,0Ua_WtxcNEWc_KoRuuHhtg", + "3b792b59-50c0-4c28-b367-fc694b1d65d1,PXssHhcJsUmUKGA8T4u85A", + "c87239ca-a255-4b13-bbc5-1b38236fb4cc,", + "8c51fc1a-b1ad-4002-a13c-4313abd92c7f,", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,nu_UGtkJOUK-5MWaW83MUg,pgBFpOtl00K2zsTJcfrefQ", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,sJioq5ARDUyMHFji_6OxFw,bjXO36Nu6EWD2dU1UC4Uwg", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,56qchovVVEaVwL9S52kneA,BAjMoTHbD0mSCjoG9jmzjA", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,zJMD-_HR80WCAUqYGe7_NA,rAzhaLh1nEWMd9v1D-HWcQ", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,9P8ARRUYdk6hCoGMKhix4w,o__JORNtYEiJEyyoBIimPA", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,Pg2cVQonrkmbXZdsNcn-KA", + "767684e7-0eec-4f6e-ad58-3d5ae715206e,", + "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,", + "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,HdwZhjrqxE6wuf68MnDhDw,f5z_jhdcqEGjo-1WUmldaw", + "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,b7oobDcDtE-69fhM-5g5AQ,f5z_jhdcqEGjo-1WUmldaw", + "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,Blev_VFjRUysY_nymGWAVg,f5z_jhdcqEGjo-1WUmldaw", + "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,", + "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,GQSpXmD0B02_gwzpq6StFA,ORmS9HkfoUeFuO4RhqcW7g", + "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,YvPeCbQS2UOcLjPfoIML_g,ORmS9HkfoUeFuO4RhqcW7g", + "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,zGOD45zNAk2i6P_zPsNPfg,ORmS9HkfoUeFuO4RhqcW7g", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,nuR8wvvCXU-EMXjBZuTXJA", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,TqhSo1hbPE6wdB_6lvLhFw", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,SExtD3FBk0GwseMZDdGJlw", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,96556f7f-a6e8-48f4-a4e4-3fad9be5df4b", + "05b7af56-71f2-45ba-a377-c105bf6a6f7a,", + "833e8ab3-9b54-4e15-8f4d-ee1bac5823c9,", + "16f4dc71-d35a-4269-b925-1a0cd85a43dd,", + "67d8d599-4611-428f-98cd-98d8a527ace5,", + "67d8d599-4611-428f-98cd-98d8a527ace5,4qSWIO-cy0q5JWHp7tagHA", + "6d3be838-85cf-46db-b33d-0985aec9609f,", + "6d3be838-85cf-46db-b33d-0985aec9609f,Vn2gH3yjiE6bwBRgh5ntZQ", + "730453b8-a352-4e6b-9573-54294690c2ad,", + "730453b8-a352-4e6b-9573-54294690c2ad,4yMvQ3-hnU2zZQf_XT9r-A", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,g14i4cTzkUu6RCOELBZEZA", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,JUOrYcAd7U-ChkFZRiBeKA", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,yxBqgXjlVEeRUOC_6iv77g", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,6JihVI8pm0eJ-wDWuXm0sg", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,wwT8NdC-bk-aAWLoJvef5A", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,9Zm7zOFBXkGRHrtp0JnaeA,oEsjaKTnKUq-Zgg4vm-EfA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,0jnL66i2_kGjfzDBc8ZNSQ,Dac47T16Wk6LmmDd2cs1LA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,pY3RdcA2lU6H5rVXp8csfg,Dac47T16Wk6LmmDd2cs1LA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,1Cbw9819NE-Fmv1NQIYKow,Dac47T16Wk6LmmDd2cs1LA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,5JQPQ2JBUkSFyXeYWvr4tA,Dac47T16Wk6LmmDd2cs1LA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,omnSzDLD5ESCU6ndropzPQ,Dac47T16Wk6LmmDd2cs1LA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,chqfCVxsEUuuUfp6qIadrg", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,VtsT6sKDYE24bAU-PKkIDQ", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,tXhgU1o_wkaNQ-7P3KXidQ,njEWOjNEQEWCEqmytodpow", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,nn86ATWxekWdIGlPePPgGQ,njEWOjNEQEWCEqmytodpow", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,Rf7CDh2bg0-HdG5n7phGaw,njEWOjNEQEWCEqmytodpow", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,oxi26T5a4U6L73pF-Pgrhg,njEWOjNEQEWCEqmytodpow", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,6547pqf6vUm2L89AG6D91A,njEWOjNEQEWCEqmytodpow", + "24c4404a-839f-46bd-98b6-6848d747a98b,", + "24c4404a-839f-46bd-98b6-6848d747a98b,6vjA_vHq70Gclypll_GS_A,nalaCGqtL0SFA4Itwqmp8g", + "24c4404a-839f-46bd-98b6-6848d747a98b,3UxfTkPsEkezOXO__wz_mg,nalaCGqtL0SFA4Itwqmp8g", + "24c4404a-839f-46bd-98b6-6848d747a98b,xVwzV9-m6E-8PVpkg4D-AQ,NH0L99bd00OVKMyltuSnHQ", + "24c4404a-839f-46bd-98b6-6848d747a98b,mraJrTA4V0eARb_Kq7yYVw,NH0L99bd00OVKMyltuSnHQ", + "24c4404a-839f-46bd-98b6-6848d747a98b,09Cg6dOq2kC2-tI5yyGSYg,YFZimVHKuk--lXS8RCrZTA", + "24c4404a-839f-46bd-98b6-6848d747a98b,u1JNHHTxSU-N53Bs9pRCoQ,0zHD3DaMAUOogqOqyd59SQ", + "24c4404a-839f-46bd-98b6-6848d747a98b,w7ybaJQVo0WCZFDPCJAtWw", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,2lD-mjl3pk2wWv24RY2tbA,dUu7b-JFOky9c2lIVCNmcg", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,OrqqGMhK1UKGowFxpJi2mw,6WVXb20giEWyZbW2gWGz_w", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,-tDd0gdxQkuvL2id10AweQ,6WVXb20giEWyZbW2gWGz_w", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,TDcZZ2ynEU-Mgmju_qIVIg,6WVXb20giEWyZbW2gWGz_w", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,09Cg6dOq2kC2-tI5yyGSYg,eUktNKKK3kqUR8-gA3rNxA", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,cj9OYinX5kyzlhBUQwZKog,AKWH3_SDqkG8SRm5sauROQ", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,NJcJw51-xkq0phT-nlJw2A", + "3757149b-f478-4975-81e5-d808a1bed8d5,", + "3757149b-f478-4975-81e5-d808a1bed8d5,LDqcPkvNXE2gFXxbpNkfhA,Too5jpb1w0iQenkjRRS-QA", + "3757149b-f478-4975-81e5-d808a1bed8d5,XOBIPrKCr02xa4tKj8wN9g,Yn1uv3Y8JE6VSxsf0LOBvw", + "3757149b-f478-4975-81e5-d808a1bed8d5,i33RmJJCwUem-ADkOwMNQQ,Yn1uv3Y8JE6VSxsf0LOBvw", + "3757149b-f478-4975-81e5-d808a1bed8d5,fq0xC9BjLECbsQIZfAvK9A,Yn1uv3Y8JE6VSxsf0LOBvw", + "3757149b-f478-4975-81e5-d808a1bed8d5,09Cg6dOq2kC2-tI5yyGSYg,3fQj_7meDEeQqm_7RfEfxQ", + "3757149b-f478-4975-81e5-d808a1bed8d5,fTWOc1eEl0aVKXwTYYjA9Q,31nK13dsTkaIf3tGFrZlJQ", + "3757149b-f478-4975-81e5-d808a1bed8d5,PG6H6px5HkO4X_xPRd1idA", + "3a790be3-2937-44d4-be01-b5d65353bd3d,", + "3a790be3-2937-44d4-be01-b5d65353bd3d,SWvJWvMDpE-it5PFoPgERg,jQjfiz2i6kaPfrkjnBGxBA", + "3a790be3-2937-44d4-be01-b5d65353bd3d,83jS7d-cUUC_GNDcGpiDxw,jQjfiz2i6kaPfrkjnBGxBA", + "3a790be3-2937-44d4-be01-b5d65353bd3d,afWY5VGiF0isGn6qpo4KeQ,_yvss6L59US4-8QYZgcCdA", + "3a790be3-2937-44d4-be01-b5d65353bd3d,tyzdtSkR9k-YCnjNvPAT6w,_yvss6L59US4-8QYZgcCdA", + "3a790be3-2937-44d4-be01-b5d65353bd3d,09Cg6dOq2kC2-tI5yyGSYg,xq7bizx6UEGaMTHB9zI3pw", + "3a790be3-2937-44d4-be01-b5d65353bd3d,MmGNESXgOkiZ0xCG41E9ig,Nc6jwNeK_kyajH5xKzCNVw", + "3a790be3-2937-44d4-be01-b5d65353bd3d,e6hTzFc-fEiJe0L4jDVxRQ", + "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,", + "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,pXrZhtzjV02_qvIQoWEFWA,Hj8WnsdidE6rJeNyOmWUfg", + "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,VK4YBLXO10G2_aEl3ZQUGA,Hj8WnsdidE6rJeNyOmWUfg", + "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,A7l6Q2clsk2XyKf62D3dHA,Hj8WnsdidE6rJeNyOmWUfg", + "s6mUIqiPCk6e57UYCx3SZA,", + "s6mUIqiPCk6e57UYCx3SZA,PcUZrdKXhk2XHa5Gy0YnnA,dpBx3DxB7kKJphxCvfmKKw", + "s6mUIqiPCk6e57UYCx3SZA,0cP9EcZRFEuMOvmZ_Kx9dA,smmfK-N2R0uoDiuIHrhImg", + "7LSm-xCYakmgcAr--Fn-JA,", + "7LSm-xCYakmgcAr--Fn-JA,uTHwo8qd6Ei3Oy4gz_j9Ow,AK4CoAB2IUGkwqECNaO75g", + "7LSm-xCYakmgcAr--Fn-JA,rZ45-lecBESqHaLX0c7NAw,iDxHI0T4Gkifs3_GY5gdjQ", + "wy2cfgb8mUqjMrIOwHH2ew,", + "wy2cfgb8mUqjMrIOwHH2ew,Hk6svQ5Sr0GDKLxOyktk9w,IQvHmZ8ei02hVuXZiXb6TA", + "wy2cfgb8mUqjMrIOwHH2ew,0ZKR_qxrWkiWkjpot8s6Cg,5aX524Lfok-0dC3UFv-OkQ", + "6d36091e-7083-4e3d-8729-94612602c8cb,", + "6d36091e-7083-4e3d-8729-94612602c8cb,64545dcc-a8eb-4c69-a539-78ee73a2d327,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "6d36091e-7083-4e3d-8729-94612602c8cb,e02838f8-4029-455a-b35c-dd40f972b05b,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "6d36091e-7083-4e3d-8729-94612602c8cb,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "6d36091e-7083-4e3d-8729-94612602c8cb,909ed249-ce09-4304-8b02-56794fa7567d,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "6d36091e-7083-4e3d-8729-94612602c8cb,Tbrc2MlSlE6mKua8qSoluQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "6d36091e-7083-4e3d-8729-94612602c8cb,aBQ3R9boek-mYeSYSNvDbQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "418db0e0-c7af-40c2-9cac-d461ce41e210,", + "418db0e0-c7af-40c2-9cac-d461ce41e210,64545dcc-a8eb-4c69-a539-78ee73a2d327,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "418db0e0-c7af-40c2-9cac-d461ce41e210,e02838f8-4029-455a-b35c-dd40f972b05b,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "418db0e0-c7af-40c2-9cac-d461ce41e210,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "418db0e0-c7af-40c2-9cac-d461ce41e210,909ed249-ce09-4304-8b02-56794fa7567d,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "418db0e0-c7af-40c2-9cac-d461ce41e210,Tbrc2MlSlE6mKua8qSoluQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "418db0e0-c7af-40c2-9cac-d461ce41e210,aBQ3R9boek-mYeSYSNvDbQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,64545dcc-a8eb-4c69-a539-78ee73a2d327,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,e02838f8-4029-455a-b35c-dd40f972b05b,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,909ed249-ce09-4304-8b02-56794fa7567d,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,Tbrc2MlSlE6mKua8qSoluQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,aBQ3R9boek-mYeSYSNvDbQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "01689baf-405e-4b38-9f09-b624e03865b8,", + "01689baf-405e-4b38-9f09-b624e03865b8,357eb160-360b-4a63-840d-65d11cc8ffc2,abdf525b-b871-41be-a1c8-810f49740ad4", + "01689baf-405e-4b38-9f09-b624e03865b8,a38254c0-00e3-429b-9d5d-b9e6a9812e69,abdf525b-b871-41be-a1c8-810f49740ad4", + "01689baf-405e-4b38-9f09-b624e03865b8,d3a9b576-5433-4305-bd96-b08820c09212,abdf525b-b871-41be-a1c8-810f49740ad4", + "01689baf-405e-4b38-9f09-b624e03865b8,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,abdf525b-b871-41be-a1c8-810f49740ad4", + "01689baf-405e-4b38-9f09-b624e03865b8,RKmRHk2N_U2gd91nybo6vg,abdf525b-b871-41be-a1c8-810f49740ad4", + "01689baf-405e-4b38-9f09-b624e03865b8,d3KmqVuMQkCYK6sVTMOTOA,abdf525b-b871-41be-a1c8-810f49740ad4", + "97eae086-cfea-45ff-9b77-1d184256d493,", + "97eae086-cfea-45ff-9b77-1d184256d493,357eb160-360b-4a63-840d-65d11cc8ffc2,8768aa33-b014-4062-b323-b146dd7d78d2", + "97eae086-cfea-45ff-9b77-1d184256d493,a38254c0-00e3-429b-9d5d-b9e6a9812e69,8768aa33-b014-4062-b323-b146dd7d78d2", + "97eae086-cfea-45ff-9b77-1d184256d493,d3a9b576-5433-4305-bd96-b08820c09212,8768aa33-b014-4062-b323-b146dd7d78d2", + "97eae086-cfea-45ff-9b77-1d184256d493,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,8768aa33-b014-4062-b323-b146dd7d78d2", + "97eae086-cfea-45ff-9b77-1d184256d493,RKmRHk2N_U2gd91nybo6vg,8768aa33-b014-4062-b323-b146dd7d78d2", + "97eae086-cfea-45ff-9b77-1d184256d493,d3KmqVuMQkCYK6sVTMOTOA,8768aa33-b014-4062-b323-b146dd7d78d2", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,357eb160-360b-4a63-840d-65d11cc8ffc2,47514b9b-0d17-4727-97ed-dab67e7fe031", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,a38254c0-00e3-429b-9d5d-b9e6a9812e69,47514b9b-0d17-4727-97ed-dab67e7fe031", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3a9b576-5433-4305-bd96-b08820c09212,47514b9b-0d17-4727-97ed-dab67e7fe031", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,47514b9b-0d17-4727-97ed-dab67e7fe031", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,RKmRHk2N_U2gd91nybo6vg,47514b9b-0d17-4727-97ed-dab67e7fe031", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3KmqVuMQkCYK6sVTMOTOA,47514b9b-0d17-4727-97ed-dab67e7fe031", + "27cdecd2-88f1-40a8-80a9-87abd827c3c5,", + "fb2a0e86-3363-4579-9f18-f41138dc85ba,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,dpxEW-2Un0eeVIsg5_o6Xw", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,4UTuNYISo0OP70FuGzmOsg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,yVNoqFjSeU6mPJxEJnzRLg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hE5scNMqJ0igY_jhufvNmg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ibmxOUIFs06gOpRYLe58xg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,LpUOG6fHz0uQPtmOLtUlKw", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,sVeZH49mP0yBRKrbVr-0LA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eAq9xK51mE-Nu4RQY0ds4Q", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eCEF5P5N7EC_cEjM9-5iUA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,OpWc8pumTk6M6x1WSw3a6Q", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ow_9g2xNj0G33-zWIhjW-Q", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,2MUexARJAU-Vc-Qebqqepg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,9bQaIrqI00qtjdtkIkj_3g", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,pvnZdyL9NEaYYX8T12AgVA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,clStIXndXk2ayewq-1LKng", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,oflbr1qvHUyvwUxdFZMcmA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,j3S5Gk92mEOKlD424seciA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,JBFu1kZ8C0ya5JZLDh0ynw", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,a4r3MYNjT0Ok0uQX9BYH6w", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ZDsylFpkEUi5PijTLSfYNg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,B3wjSG3Ui0ipGgN3h7BFiw", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,H_lV3FQG0EC4ebW5vfnRIA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hU2rt41rvUqulLM1H5NX4g", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,49mpP0duKEylQ2v5ZU9SYg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hoXpo41nnEOPGq20Tq-t9A", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,RddbstjtZ0ClxgY-s66tTw", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,kk271nI1MUu-gZjec9djrQ", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,tkHJl8TQHEiSeD-seDV4jg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,M3vXSuDRw0GLNh9A4dF4vA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,nEBw9Nh1-kWcOynsm7P8Pw", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,B5WV1io6fkaAJdm4GbzM_Q", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,bNWm_2_VE0-4fdcF6S0aIA", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,q9Nganq9nUaV3zuvx9s3nQ", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,y8yTimlV2EWhJMU-Ay9e8A", + "9231a9c6-7638-4fe9-bd2f-eac6c943009b,", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,x3kGXCD0UEWkjfXqu_kuxg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,fWxn_AarpESryNA9UN8wvA,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,sEKIlMfbJ0eED6VtcXcQrw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Dg3DxHr8GU2e190TGveXqw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,5rLtRfNWO0qT7X-0sJkUXg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,DiJGFIO-5Ue8rkEhvF5-Mg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Sg5yKkqJEEulQeUvRhFgJg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,M0nOQl44DkG_GjTXzAEQCQ,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Ai85jH88H06ze74VdWolfw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,NRjJuinZ4ki8qaEPvP84tw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Z2L6AqDdcUqxiN1mVw-X9g,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,j367nSYyg06rOMZ1GmfmFA,FS7kgc4DpkKuar24J9dW8g", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,2qYs9IkE90eppx1DggNlsA,6Y43EyTpiEejGp0ODWTSjA", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,JV60jZGerUWuzQ2rRnwquQ,gSb8iLTEZ0mi3g5-jZXifQ", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,gOP1m4pVHkmlR2k8RrcdmQ", + "d9c025e1-561b-4997-9f69-c96eb3c00786,", + "a302741d-313b-4682-82e5-ac76764e662c,", + "a302741d-313b-4682-82e5-ac76764e662c,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "a302741d-313b-4682-82e5-ac76764e662c,64850553-cdfe-455a-ac00-dafbe63d613e,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "a302741d-313b-4682-82e5-ac76764e662c,31ac9d4d-3d6b-48f2-85d5-dc4b8165a17a,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "459ff0a0-ad68-4337-9399-57e3b03e4bf6,", + "7524b3f3-130b-41b1-83f6-9c241bd92bb4,", + "184368a6-17fa-4cfd-bb57-a2862f6e17bd,", + "f5b01307-e495-469c-afac-c10f375cffb0,", + "f5b01307-e495-469c-afac-c10f375cffb0,lIURXMnmn0GcYFUVSXHzWA", + "9168b3f9-c7e5-40b6-bb2c-9e708596e675,", + "de19b239-3ed0-4f03-acb6-da69b5aa52ea,", + "5f1bc3c2-dfbb-4258-9b10-608535e34db2,", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,VxmHqppE6UWiXVBXL2QvTA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,kjDg5GM1b0iepa52BX5wbg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,WlfHWDcnO0SBT5wTjEPSmw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,_egTT9EA-UK0aLkn_QnWFA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,10nSNj7eRE6XwS9y2OvC2g", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XPBWTcpQ9kaS0mabpdvYhw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FokpgM8R5EuXZwe89T78yA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,v0nZrzbIMEi3AaGuNjmwDw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,TtthSwdIq0CuSNOKDLsldg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,5fjPcf4xQkSEYn3T-f8xLw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,sjY_XUbXAk6h2TGnjBGDeQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,1nFX70Rqo0-qQo2M_Z1KCg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,cOyJcjGyp0miaMLAOpz0FA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,nIUjGqY8L0u0ZbX45b4PUQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6JY7l1gtpE631NTmT6xv3A", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,27Fg--SzD0ertvGEv1Jgsw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6EjRVha2i0mCUVLaL_em_w", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,EtgbhY9a_EqO4j86Al9Kpw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,oyvOdKXeGU65V6sKhhb0qg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,-0DfEFcbNEaNI2uvfHF9qA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,DVnSVZcj8kyLr1DXRu7kzg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QgECbFDwyk24sI-e_fXy5w", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,zMWcGYnGdk6lVqVNuFwJHw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,jYJehFukzUCHKzWQAaM3uw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,4viU0bMT1UuasZHD8euFKg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,YePtDvVnOk6cZkDBVIv-PA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,q-Gjg0zzI0mTHxjn8wi5HQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QD-SRdKijU2RVu60TcU_jw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,3m5Q5crrHUGZJ9_GuxubQw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XNTUcdaSVEuS3rbzwIwYNg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FnezvbcGD0-oyFOTkpUo7g" + ] +} diff --git a/Server/RadicadeServer-Config/avatar.json b/Server/RadicadeServer-Config/avatar.json new file mode 100644 index 0000000..02c9ade --- /dev/null +++ b/Server/RadicadeServer-Config/avatar.json @@ -0,0 +1,44364 @@ +[ + { + "AvatarItemDesc": "b33dbeee-5bdd-443d-aa6a-761248054e08,", + "FriendlyName": "Team Jersey", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b33dbeee-5bdd-443d-aa6a-761248054e08", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1213992309 + }, + { + "AvatarItemDesc": "6d48c545-22bb-46c1-a29d-0a38af387143,", + "FriendlyName": "Team Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d48c545-22bb-46c1-a29d-0a38af387143", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1037333475 + }, + { + "AvatarItemDesc": "24b8b757-4168-4b3b-ba11-46f29d19044d,", + "FriendlyName": "Glowing Team Jersey", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24b8b757-4168-4b3b-ba11-46f29d19044d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1816296005 + }, + { + "AvatarItemDesc": "e41c32ac-61b7-4e59-9d0e-b231ebdc2a68,", + "FriendlyName": "Glowing Team Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e41c32ac-61b7-4e59-9d0e-b231ebdc2a68", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1140800281 + }, + { + "AvatarItemDesc": "d84993dd-4e15-4481-9450-f88a34cdbc82,FnhttSsCoUCSAW8j-stC5A", + "FriendlyName": "Default Placeholder Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d84993dd-4e15-4481-9450-f88a34cdbc82", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1234016191 + }, + { + "AvatarItemId": 1185405589, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "c5010738-41fa-4eca-aeac-e24adaa29789,", + "FriendlyName": "Helmet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5010738-41fa-4eca-aeac-e24adaa29789", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.489778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1842916691, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "018b39ed-cbef-4fff-862f-f39373b32e2e,", + "FriendlyName": "Curly Helmet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "018b39ed-cbef-4fff-862f-f39373b32e2e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.489778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 986, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "47u2if3fx2rt8p4wj90qo988g.png", + "AvatarItemDesc": "1d27b674-f9e2-4ffc-9d8c-a58a1be06457,", + "FriendlyName": "Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1d27b674-f9e2-4ffc-9d8c-a58a1be06457", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:49.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 556, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9qr2ce4r54kz6w69sxrp0cm74.png", + "AvatarItemDesc": "d84c0ff9-8fbe-4ed8-abf3-7996e81888ab,", + "FriendlyName": "Large Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d84c0ff9-8fbe-4ed8-abf3-7996e81888ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-10-26T01:09:06.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 584, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ctj8umc3ax8se6s476eqty6l0.png", + "AvatarItemDesc": "e5b83dfc-b2e1-4dcb-a4ab-9d3a4c8a34ae,", + "FriendlyName": "Long Wavy Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e5b83dfc-b2e1-4dcb-a4ab-9d3a4c8a34ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:12.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 585, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "85pkqd12j64n3n0av6xlffsxm.png", + "AvatarItemDesc": "7dd6f7b0-7ba0-429f-a04f-e32d3a79ee61,", + "FriendlyName": "Short Wavy Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7dd6f7b0-7ba0-429f-a04f-e32d3a79ee61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T02:21:32.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 587, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eds0e7isjgdyo1d98d51ibdiw.png", + "AvatarItemDesc": "eb9611c6-bb50-41a2-93e9-7f959815a846,", + "FriendlyName": "Dreads Long Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb9611c6-bb50-41a2-93e9-7f959815a846", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T01:07:52.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 998, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8k7za5lr83ybmyz1z2kx03poc.png", + "AvatarItemDesc": "1fd69ef8-0b74-4962-af5a-67f0bf0358f2,", + "FriendlyName": "Ponytail Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fd69ef8-0b74-4962-af5a-67f0bf0358f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:18.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1002, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cy0bdcxljjgonlcc039y6o326.png", + "AvatarItemDesc": "a12f724f-4a73-4ab8-aad4-6bfc662b4dd6,", + "FriendlyName": "Undercut Long Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a12f724f-4a73-4ab8-aad4-6bfc662b4dd6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:24.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 993, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5h63afsihyt4jqw4p15rg5bnh.png", + "AvatarItemDesc": "0753d7a4-8247-4fca-a6fc-359c26086140,", + "FriendlyName": "Fonzie Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0753d7a4-8247-4fca-a6fc-359c26086140", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:12.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1001, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "95n2l3ib6p6i60mz8mpfd18uj.png", + "AvatarItemDesc": "77d3c585-4928-4471-a425-89036efe7299,", + "FriendlyName": "Spiky Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77d3c585-4928-4471-a425-89036efe7299", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:22.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 996, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "epvz80ugc50j724bv8du1ivqo.png", + "AvatarItemDesc": "92302d9d-c527-418c-ac5d-1fa869727505,", + "FriendlyName": "Part Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "92302d9d-c527-418c-ac5d-1fa869727505", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:16.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1003, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4wmgnksu1wj9yabh6v9uxjmtp.png", + "AvatarItemDesc": "f9dd08f8-16d3-4c39-af4f-89f7bb6e80d3,", + "FriendlyName": "Undercut Short Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f9dd08f8-16d3-4c39-af4f-89f7bb6e80d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:26.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 990, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3mpvrp2oqpvbugfnbb57ooj1m.png", + "AvatarItemDesc": "b148cb1e-df81-442f-aea6-ab1727aad00e,", + "FriendlyName": "Chunky Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b148cb1e-df81-442f-aea6-ab1727aad00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:04.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 988, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ddt2ggc02cukv16x74awz2f5s.png", + "AvatarItemDesc": "e36bcd98-7e85-43fa-89f8-57e4ec33823a,", + "FriendlyName": "Bob with Bangs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e36bcd98-7e85-43fa-89f8-57e4ec33823a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:58.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 987, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aj4ljso81bddjrnou7k4r9jvc.png", + "AvatarItemDesc": "880a3cc0-7407-4b61-b759-f9dd890fe9e5,", + "FriendlyName": "Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "880a3cc0-7407-4b61-b759-f9dd890fe9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:55.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 994, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dmg8ywtfbzy57cutb6j4h3vjh.png", + "AvatarItemDesc": "21599b51-c50f-43d8-ac5f-62c30cd02ca5,", + "FriendlyName": "Lori Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21599b51-c50f-43d8-ac5f-62c30cd02ca5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:13.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 991, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "60usmea9f77xjypmnvoptbc9g.png", + "AvatarItemDesc": "193a3bf9-abc0-4d78-8d63-92046908b1c5,", + "FriendlyName": "Emo Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "193a3bf9-abc0-4d78-8d63-92046908b1c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:06.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 997, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9gvfh0l74sodnh53dk03ioc7k.png", + "AvatarItemDesc": "79b90274-6eec-4664-acfb-4a123334661e,", + "FriendlyName": "Pig Tails Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "79b90274-6eec-4664-acfb-4a123334661e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:17.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 989, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5yfm6hc4pnazd6475re6lhwdc.png", + "AvatarItemDesc": "da4e7b34-2095-4a9e-801e-4f409039e0dd,", + "FriendlyName": "Buzz Cut Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da4e7b34-2095-4a9e-801e-4f409039e0dd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:01.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 995, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2k57qatxawouqn71m6owgx9bc.png", + "AvatarItemDesc": "9d9fadb6-97eb-480e-a224-4e0179082071,", + "FriendlyName": "Meatball Buns Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d9fadb6-97eb-480e-a224-4e0179082071", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:15.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1000, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5en879epjtfbbwep4k3qz00wa.png", + "AvatarItemDesc": "d8280c0c-d803-4513-be10-a0ba96d8821e,", + "FriendlyName": "Flowhawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8280c0c-d803-4513-be10-a0ba96d8821e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:21.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 992, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2a7algukgsrdsvd69r2r6nqlw.png", + "AvatarItemDesc": "e286863c-2967-4d00-b837-b49487b9484a,", + "FriendlyName": "Fauxhawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e286863c-2967-4d00-b837-b49487b9484a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:10.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 999, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0v8t308zwge73zqhs45g27w4f.png", + "AvatarItemDesc": "2cb4f372-3372-4583-8b57-c4e3988e3c28,", + "FriendlyName": "Punky Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2cb4f372-3372-4583-8b57-c4e3988e3c28", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:19.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1004, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "814p75pufultq4i0yd07s46mi.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,", + "FriendlyName": "Cat-Eye Glasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:28.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1005, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "56tk3ruae6anx7arnsnyk6k03.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,", + "FriendlyName": "Glasses (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:29.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1006, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "f0wdmxgtyo7xkofqt6gy6s0hn.png", + "AvatarItemDesc": "8d10cc78-6b00-45f3-affb-205e9cc5b03f,", + "FriendlyName": "Beard (Close)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d10cc78-6b00-45f3-affb-205e9cc5b03f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:30.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1007, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "amvkakyjtnfjh0sip52ftw6uk.png", + "AvatarItemDesc": "cc96f8a5-bc5b-4f89-83b7-ecd53905ada7,", + "FriendlyName": "Beard (Thick)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc96f8a5-bc5b-4f89-83b7-ecd53905ada7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:32.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1008, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dfc0t1w1r9z7p33f1562b1u2u.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,", + "FriendlyName": "Business Tie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:33.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1017, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3rr58syq4bpp9v0ot7isaecb3.png", + "AvatarItemDesc": "4d507dfa-4a99-4ac0-8537-229e9dc0eb4a,", + "FriendlyName": "Rec Room Tank Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d507dfa-4a99-4ac0-8537-229e9dc0eb4a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:44.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1019, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2theh2wmoj85n9yoigkwvalhv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,", + "FriendlyName": "Rec Room Shirt (Crew Neck, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:48.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1749, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "67l6cwkqcs2tczds20bgj5rxy.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45", + "FriendlyName": "Rec Room T-Shirt (Crew Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-30T01:35:18.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1009, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aouoxffd4nu47wnb2ngptwmhv.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,", + "FriendlyName": "Collared Shirt (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:34.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1020, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8h9x3f8d3cbsn1mixmjnnxakw.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,", + "FriendlyName": "T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:49.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1018, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9b4c3le7lmjt85hxgjr9xtaz9.png", + "AvatarItemDesc": "8aa79563-ace1-4ba7-ad0c-f3210a78142f,", + "FriendlyName": "Rec Room Shirt (V-Neck, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8aa79563-ace1-4ba7-ad0c-f3210a78142f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:46.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 675, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4n6q6y6u8lbqej4m7zacg6pgv.png", + "AvatarItemDesc": "8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c", + "FriendlyName": "Rec Room T-Shirt (V-Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8aa79563-ace1-4ba7-ad0c-f3210a78142f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:40.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1021, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4hlua7fbqw8nlu7f4buxxbjgy.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,", + "FriendlyName": "Tennis Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:51.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 681, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aqlcnxazbwcmfgx7cp4mpgqpy.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,c5deba2a-6e35-4b13-8e94-8ba5457f39df,b75ef67d-00c3-4ac1-9b72-212032460294", + "FriendlyName": "Tennis Skirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:48.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 682, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bjhjw79pvyn169f35rgbnyhqa.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,758752bd-db2f-43d2-b580-55b3e1efffd5,b75ef67d-00c3-4ac1-9b72-212032460294", + "FriendlyName": "Tennis Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:50.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1022, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c45s37acw47y54ey191vc8g64.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,", + "FriendlyName": "Zipper Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:52.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1023, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9a4gj5yigfksglnec4np95a2h.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,", + "FriendlyName": "Wristbands (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:53.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1024, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b0nybni7980f1n0fvwjlcpwba.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,", + "FriendlyName": "Wrist Tape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:54.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 674, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0bb1u3you3uny9smblxzcmwyf.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,55901f12-d5b5-4fa8-b4c8-e479689ee39d,f600037d-c9c0-43fa-b45b-02f456f9dd5f", + "FriendlyName": "Collared Shirt (Denim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:38.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 672, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "077ospe8q8kka3lqwcvd2df6w.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,bf82f2f6-9af8-431e-a296-0890dea48ba7,d015cae7-a905-49e4-8823-6dec069689a6", + "FriendlyName": "Collared Shirt (Argyle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:36.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "130079y56zsb2j2jv5bahhp77.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,EfdMcnfHt0mr0PQ_maaYOg,DRJcNhkqvkKFEaZpOguR6w", + "FriendlyName": "Collared Shirt (Flowers, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:43.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 676, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "apumxi9psa3dgu88d82sv7a82.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,6d703981-2734-4c45-8983-cdd5f328902f,a0271cd0-e172-4d3f-aa2f-9806f21a82d2", + "FriendlyName": "Tank Top (Camo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:41.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 679, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cyeqj6jrqz7lap5blce53jyxc.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,9c03f381-7357-4d0f-8cda-8737d4c43d25", + "FriendlyName": "Tank Top (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:45.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 680, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b5581rifqvl56c14u3pjcb613.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:47.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 678, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5pbiviztmh19hus4bp2sifgsl.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tank Top (Dots)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:44.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 677, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4iwbk5q2yccf8huy5ut62aqjm.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,48abd952-214f-48b2-a8f1-1146f6f69aa2,b78008e8-abbd-4ece-be34-9a911f721fcc", + "FriendlyName": "Tank Top (Zebra)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:42.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 669, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3eon8zbs1np0ijovp5ri8gb2h.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:29.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 670, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "drn8bzb22wxe4zkanykjdw7il.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:32.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 664, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6xdunx482y5cr10gt2joszy8u.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:22.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 663, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9x7n4sd9ecxc01cpbo417a6h9.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:16.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 667, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8a4fwzyn75woyrsc9rz1hucpr.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Orange, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:26.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 666, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d1xnxod7dtl3g6htemy60mxxj.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Pink, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:25.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 665, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bt1et6gecchq8vwnxdydsijvs.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:23.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 668, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5j9o29zmheavd8i64j65gc7d5.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,6dd95046-acf8-42fe-ab78-80a334096a9d,56a92c8d-af53-413e-929e-4a9a3cfad780", + "FriendlyName": "Headband (Red, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:27.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 686, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6pf2ftyxdwcm1n73wbvsvqgww.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:56.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 684, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7etdcocmjntbessmyb4q1yrql.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:53.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 685, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dkn9mf26l54mq2qdaq2wxjmmb.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:55.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 687, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8ymxu6vs24zw45qrtx7r3wiir.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:57.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 683, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cbxl5wtnmplfz5vth23g97xve.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:52.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 390, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5n8kq96onh4dhg7u9k46jmucw.png", + "AvatarItemDesc": "fcfcaf63-deb4-45f7-b711-c051c9ea45cb,", + "FriendlyName": "Top Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fcfcaf63-deb4-45f7-b711-c051c9ea45cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:28.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 143, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ap1up0rb8i4a1km9mn9e44dsm.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,", + "FriendlyName": "Blazer (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 147, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00wlb607co30g3d5s8a51rat5.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,6f2e74bf-1e95-463d-97db-d5d1a53b2c28,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Black, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 146, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "crmfr0dn581ysgtbeh5d3m2mr.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,9374bf66-2ee5-493b-8439-efce4b201904,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Grey, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 145, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cugl1bt43o2xnok3ehawcralp.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,272fe8eb-5061-4729-a7a8-414ff667a82f,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Grey, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 148, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0z5rg3veja6t3bw5vpad3759e.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,0ffad843-d6c9-425a-8686-7217009c867e,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Green, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 152, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "28ly2m56wth4ldtr1r5anpadx.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,e0397982-c2c2-4733-9a40-46e18675b5af,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 150, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dw4gnw7y1oclj75acv9nvncv6.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,", + "FriendlyName": "Button Top (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 151, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e1qpklmk4sy92lxlwf1zus9lh.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,49f5864f-9d40-497c-88c8-e87f64d41d74,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 153, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "08g89jon46625gx6apyzgaq7p.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,c5deba2a-6e35-4b13-8e94-8ba5457f39df,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 194, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9lqg5xqiktt0cxifvy1kkwpa3.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,f750de46-3758-4f7d-9709-0a84b1027009,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 195, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3nhman1tbwhikxyha0tj215xw.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,d66aa400-aa5a-4539-a25d-5f8ce94dc281,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 196, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7nlns9i6osonn2bq8867ceizn.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 193, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "02aazblyiy14oivk4h218q67i.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,", + "FriendlyName": "Collared Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 417, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3w6yoi9bpa2pl8w7wab8zwodb.png", + "AvatarItemDesc": "241506f6-bf88-4b46-b5fe-513a225421f4,", + "FriendlyName": "Half Up Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "241506f6-bf88-4b46-b5fe-513a225421f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:30:04.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 459, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "08e7y3dct2h7xht6m1vfda9yg.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,d6edbc00-3c1d-4f49-8412-3ef8c7c5f4c2,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:24.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 255, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8aayhgqe518c46fqbtd1x55jk.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,6d703981-2734-4c45-8983-cdd5f328902f,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 254, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6ih1uyez4f5apbi0u4n5szfns.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,830be2fa-60a5-48cc-931f-34b670eae4bd,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 253, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b119rnoqjo60vhf5wcwc9my42.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,bbfa08e3-8e6b-4e0f-b264-1b398d7cd44a,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 289, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0r354na62acon4jev863fj1vh.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 294, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1eoaif6mmgo9dolltq50ob9o6.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 287, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "16b0f84wdbvxcp649og13dk8v.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 230, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b1hdewklda06oi0oveqpmi9wc.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 229, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5k1gsvgmixc21tqvaomwh0yft.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 227, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "csvjo09nx4adboqztoamuuq7p.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 228, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7wrlcy8lbaezttb5m6olhzmu2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 461, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "58gu9l73pm4ez45p70yrkyplt.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:26.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 460, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3akcjd4a37lw3qz88ajis1kcz.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:25.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 15, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5kh0jn19pdtrpbc2g180026s5.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 10, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4nxr6u9ei794sh6gu0ucu4ddi.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 7, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0sr1gkccjdo3hx6ruc2ss61p8.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 6, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9tssl8yucapudl7wbupmjwwfw.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,", + "FriendlyName": "Headband (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 16, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5uw2gb9nq7yeqmoplvw1pcsqf.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 582, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7a6q8lttqfwygyoo0xob5drc6.png", + "AvatarItemDesc": "24a240f4-1574-420b-b898-a7e91f170759,", + "FriendlyName": "Back Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24a240f4-1574-420b-b898-a7e91f170759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:09.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 583, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b8pcnjd03lqqw0jzj8ueug6d4.png", + "AvatarItemDesc": "c45ed7b8-99bd-4a4b-a9ff-e16edf5d7a18,", + "FriendlyName": "High Pony Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c45ed7b8-99bd-4a4b-a9ff-e16edf5d7a18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:11.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 29, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "35b472lvh2ejmoj4ppvh8nqvy.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 983, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6u0dzdrw11sr5n7ix18tfuhu1.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:26.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 31, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aghnwd2jnaxd0vtt7oq9ks3pp.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 25, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d7lvri8z5oviwaw0fgktu2pqv.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,", + "FriendlyName": "Baseball Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 30, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9si5yzip1v33nbt5clp7t208l.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 172, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d004gewj67bopyl1n223awz19.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,", + "FriendlyName": "Equestrian Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 173, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9sz04th4t4o0izh811x1j6i3j.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 174, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0ssz8kkzkquzlbsny70teq07u.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,4828b50c-95b6-466a-bb25-514891d78202,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 175, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1ww790yppnpxni1c0om3j7p3e.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,d6823e01-69f0-4f85-b94a-74894356a2cf,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 81, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4q0530imylzalquyt6zrs4br4.png", + "AvatarItemDesc": "09177621-9ecd-4f6a-b6a5-64490139141d,", + "FriendlyName": "Flat Top Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "09177621-9ecd-4f6a-b6a5-64490139141d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 83, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "63iadlt4uzd2bynj3varsfesy.png", + "AvatarItemDesc": "95ab7a7c-c35d-4da5-9955-0921064470b6,", + "FriendlyName": "Gekko Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "95ab7a7c-c35d-4da5-9955-0921064470b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 11, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cwxdnevuziov7r3meknz7vmkr.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 12, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c4pq7xaobf8onr988cn3t6lzb.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 13, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "338vomh9pt5m5bf07wih64yjo.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 14, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6onf6k881dne4cqk3bid15ryf.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 20, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1sdg06t7cb2h44qy4zmiv7uz8.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Red, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 149, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "22i8ypgdvikndkalc1fn6g0be.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,", + "FriendlyName": "Slim Blazer (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 469, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0no2vbmhmulieokvn9ikhxy23.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,cd5d7285-202d-42d0-b93f-04245875793e,0f36bb97-c61b-4281-929f-ff1d0d11be86", + "FriendlyName": "Slim Blazer (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:37.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 468, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "27ztuh73mvhqa7kbriw9y9jjt.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,0f36bb97-c61b-4281-929f-ff1d0d11be86", + "FriendlyName": "Slim Blazer (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:36.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 91, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "16t3t321doi4ngo5nknn2kxkt.png", + "AvatarItemDesc": "8b9f1413-e786-4a30-946c-9292f207875a,", + "FriendlyName": "Pulp Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b9f1413-e786-4a30-946c-9292f207875a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 124, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "79i106lkaeghior2bmvlgc8kb.png", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,", + "FriendlyName": "Rec Room Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 95, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7taocf1uwgbysywpo7p7fmcsm.png", + "AvatarItemDesc": "1a71064b-794f-40fa-9109-8ad36602b6e1,", + "FriendlyName": "Shagg Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a71064b-794f-40fa-9109-8ad36602b6e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 200, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "enwc99fhlpla2ltqpv7ug4r1z.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,761a3193-60f0-4190-80c7-285b8192e794,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 201, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "de87szlv72hk7tfhds4c7qowj.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,a819f49b-6c7a-49d3-9e6a-d9d79ef5019f,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 199, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dvdbsnev23t30jm3v75yrn25a.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,64850553-cdfe-455a-ac00-dafbe63d613e,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 198, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dqkxn82hm6chx9ysdnrcfvj60.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,", + "FriendlyName": "Stoll Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 297, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9x6rq5lgtswz7z6pcm6ccptiy.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wrist Tape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 296, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6w33yvf6g7oo4h3fayjgbs5gj.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wrist Tape (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 288, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "beff1xr2vo6gnh2vgobzx0hrw.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 292, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "el05p2p32cqdh78wlg2aq7vhl.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 131, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "74jrfu3nny1joqt8ee5vlxpok.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,", + "FriendlyName": "Long Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1014, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cpcymryww9ol7mfcdpyp63o47.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,0iSsaY-HgkmLaRHCn5vEdw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:41.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1012, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "60nyc11l4xym89g52ynudx3bm.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,jGj28vhq8EGwP2RuM074aQ,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:38.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1011, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0f3qdah3r9o4qv7cqk39g3l7s.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,kmj5zOjcwku_WWKroCeiVQ,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:37.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1015, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4pvbqpmw9ghysmvyr445pupr1.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,FAviMCQ_EE2Mpt6QPo5OEw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:42.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1013, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "13r4rms2vr6r67kp1gm17g388.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,MFrcSQ1DYUm8imvy4ypgvw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:40.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 144, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7si3m7no7ulqg9he671mhowgx.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,05ac07e1-67f0-486c-abf5-a62866475abb,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Black, Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 620, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "am6f16pg9zk7wcjm1ks7meivl.png", + "AvatarItemDesc": "0088603e-ec3b-4478-8694-e6fb1989b3f2,", + "FriendlyName": "Angled Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0088603e-ec3b-4478-8694-e6fb1989b3f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-22T19:07:25.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 621, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "91fafagogc6ouzeyn500okchs.png", + "AvatarItemDesc": "ffea7a65-613f-4835-921e-6dd15f357b7e,", + "FriendlyName": "Long Bangs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffea7a65-613f-4835-921e-6dd15f357b7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-22T19:07:26.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 112, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "acy5d2ixrgg8d4qlb09csvhm4.png", + "AvatarItemDesc": "45f5e714-8a5f-4385-a97f-675066167011,", + "FriendlyName": "Seventies Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45f5e714-8a5f-4385-a97f-675066167011", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 361, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1eb87x8cqfkan1tpf4fnxe6e5.png", + "AvatarItemDesc": "9bf5d259-7774-4cbe-a90f-7f188cc0dce7,", + "FriendlyName": "Thick Goatee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf5d259-7774-4cbe-a90f-7f188cc0dce7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:01.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 726, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3pvimigcs9imx8k1sd5juxlf5.png", + "AvatarItemDesc": "a6cbfe76-534a-4655-a8a8-3fed13d001c7,", + "FriendlyName": "Bald Top Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6cbfe76-534a-4655-a8a8-3fed13d001c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T01:29:49.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 727, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6eylc335jw4t1874pjzcwpudy.png", + "AvatarItemDesc": "CTcrvbo3OEepIV4oW8bx4w,", + "FriendlyName": "Receding Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd2b3709-37ba-4738-a921-5e285bc6f1e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T19:25:02.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 728, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bvkq9o8mojgbtpcokr90wuxiv.png", + "AvatarItemDesc": "-twtjyBdQ02EAdOfBGTiEw,", + "FriendlyName": "Van Dyke Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8f2ddcfa-5d20-4d43-8401-d39f0464e213", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T22:56:15.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1199, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6xn7zsaqaimo5w5vryw7cyaxt.png", + "AvatarItemDesc": "45eaab67-19c2-4601-8f80-3565a4dceba4,", + "FriendlyName": "Pompadour Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45eaab67-19c2-4601-8f80-3565a4dceba4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:36.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1200, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dpd9st8mzzq7fb7gyrsby9mnh.png", + "AvatarItemDesc": "c855dcc3-96cb-470d-b159-d37a025a47d1,", + "FriendlyName": "Dutch Braid Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c855dcc3-96cb-470d-b159-d37a025a47d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:41.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1477, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3ybnk0r2sa9x5nfulb4n5hfua.png", + "AvatarItemDesc": "d7730a9e-78a1-4356-bc09-6b066615850b,", + "FriendlyName": "Afro Updo Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7730a9e-78a1-4356-bc09-6b066615850b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:09.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1478, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "256bejo1cjwehqhza4ysng9ho.png", + "AvatarItemDesc": "8c35c804-e8d5-49d2-8d5a-ea19fb70bfa6,", + "FriendlyName": "Pencil Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c35c804-e8d5-49d2-8d5a-ea19fb70bfa6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:13.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1480, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eezddupufyjfaxdd5lstfz5gn.png", + "AvatarItemDesc": "5beeb4c4-f276-4eae-87aa-9302e45b05b7,", + "FriendlyName": "Cornrows Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5beeb4c4-f276-4eae-87aa-9302e45b05b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:21.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1457, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dhqnvyh6gae90iy240h1b07t5.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,sxUE0iOSZEmezm54T7xI3Q,tlpa7195x0CkmSjpR1RArQ", + "FriendlyName": "Rec Room Hoodie - Pride (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:18.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1459, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0c78877wqm87gczt3yy70z08e.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,", + "FriendlyName": "Winged Hat - Pride (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:42.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1458, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9lq1a36dbi2qvm0v5g0dci5ne.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,D_Xmo0rOzkS-kgq1CYXt3g,tnCJp2eDI0SwjVfJMhk3LQ", + "FriendlyName": "Rec Room Hoodie - Pride (Trans Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:24.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1460, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7ulpsllq59oaufnjyuw78wn2q.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,knXPidb-Rkayfc3kSHfZeQ,1yMyo6oTjU-VAygoeWaohQ", + "FriendlyName": "Winged Hat - Pride (Trans Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:45.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1621, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2h1h9p8vlyjw7l757o4oin73b.png", + "AvatarItemDesc": "88b6ddeb-a455-460d-91d9-a4569ef6903c,", + "FriendlyName": "Square Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88b6ddeb-a455-460d-91d9-a4569ef6903c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:15.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1620, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3xh3iq78mo5epeepsiol6ghz1.png", + "AvatarItemDesc": "0abb6b08-20ce-444f-879e-0d1344df096c,", + "FriendlyName": "Round Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0abb6b08-20ce-444f-879e-0d1344df096c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:12.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1619, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1jnggaqfe4t82pv8okgvje9e7.png", + "AvatarItemDesc": "9b5bde11-7408-4798-9fcb-c7ec175444df,", + "FriendlyName": "Hoop Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5bde11-7408-4798-9fcb-c7ec175444df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:09.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1742, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4wuzoct4aayihilzg800scbtb.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,", + "FriendlyName": "Leather Necklace (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:33.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1745, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ctkuncvuk0nur9iwwe48eo2ed.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,gsHBUqA1gE2XNTPA3VvBLQ,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:41.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1744, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8umo8jp69be1v9log6wxdhkct.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,JUQn0_PLIkytCrkAg0FflQ,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:38.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1740, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "byk3riscj527wx5q6aq2u1h1y.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,", + "FriendlyName": "Basic Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:20.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1738, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3bofvoeb5yy5y86dljqn9nqyw.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,dBR1uNgk206eVoWntVzVAg,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:14.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1737, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3g3ewvd49267e67sqmpy9df0v.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,zPGCmxM_b0yTt7tqMaomcw,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:10.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1772, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2jsxx5bos2r2jual185q0pgcu.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,", + "FriendlyName": "Pocket Tee (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:07.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1779, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9d8ocr6t26ofb3frswgln8ghp.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,El0s9F6cfkywcKOkqwbJtQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:31.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1775, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9r42wuf4n5jgtidnopmbsho11.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,N0fBxjrCekO-HUqmc3MufQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:24.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1774, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "97wov16x2whj90451paeq5wr9.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,J9cK9BKtHUinG-DwAoPi3g,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Dreen)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:22.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1773, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aj0epqqoal6o3lrbfmlvdjvrw.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,-VF3K4miGE28LxMgqVehRQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:21.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1778, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1hb9mgxu2zrau3k0gdj4psvsv.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,pQ_3mZI7yEaxMw9n_Fr4Sg,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:30.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1777, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "avz1q0x4dp2ujj3k9yzfvcufz.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,TpFWTdEW2kqtpvc0sPv-dw,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:28.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1780, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2q0u4aonxjqnyx4hulnj3xvcj.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,vxZVeH2b_E6XlqyqCW0hBA,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:33.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1776, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0u6tuseoo4aelelolic3o693u.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,oNoEl-nDqk-HLsfz1538oA,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:26.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1781, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9yt8aczo24n68e4zolsg3k0cs.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,Aex0Zh1RQ0u9oTDFF1cFIg,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:35.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1757, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8z6u9864bufzxe9myyoisnl28.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,DIeasJUIj0KNetAzPzuIrg,o3Q2gADhh06dgaipkkdMsA", + "FriendlyName": "Rec Room Hoodie - Pride (Bi Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:15.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1756, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aa9c59vzcixu27kaoazqzpbsd.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,G6Udm6kMdE28vmRY5KPanA,Fw79pqDPP0C8H07AnTY-Vg", + "FriendlyName": "Rec Room Hoodie - Pride (Lesbian Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:11.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1771, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7px3ud7g3dce8g0qtfmbzy9ar.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,3a1oW3_MY0e4lDN2V1X4ig,za8Mz14Ww0KtC6wavUPXdg", + "FriendlyName": "Rec Room Hoodie - Pride (Nonbinary Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:59.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1791, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "arzqgzmg36st1ri623d76xhhx.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,sf962XJ0tkis0svkMkZCXQ,4bI7QRvdnEansPkwx8Di9A", + "FriendlyName": "Rec Room Hoodie - Pride (Pan Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-08T01:17:55.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1770, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5xxg31ft29j91d3n4nxdkw270.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,tm7Cnd3LzkqWHA1JMrgoeA,nbLr3c3lOkiWhNg9IYbt6Q", + "FriendlyName": "Rec Room Hoodie - Pride (Ace Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:50.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1970, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3jkufkhi2w8i2n3lsvlvf65c1.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,t2fOeVWloUOKDlIaf4Sxnw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:21.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1971, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2izsye42u9sx691sx6b1bpdil.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,3JrW3FFUvEe6HZ9VjOVjMw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:24.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1972, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "955r71z2z3dwqtuuo4h7wtm00.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,OT5APcpbpEiMNNah8n4Txw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:26.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1973, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bm3lgqlrymyw0u7z8sipii563.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,NjCLn5A5h0-V8jB72etCNQ,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:29.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1974, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bbonh2ov1akhkcgu49ws63rse.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,jeEWPFvzaEGmXaUrxTFvwg,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:35.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1984, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "79dqcbiedmmieho50ebbbddj9.png", + "AvatarItemDesc": "edc7888c-dc42-48ab-8815-884d227760af,", + "FriendlyName": "Short Locs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edc7888c-dc42-48ab-8815-884d227760af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:28.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1978, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2qqensahxq2o8r4avbpt9zoo7.png", + "AvatarItemDesc": "6e48363d-cc62-40a8-87f9-a69fae90c002,", + "FriendlyName": "Box Braid Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6e48363d-cc62-40a8-87f9-a69fae90c002", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:09.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1979, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d45c0mzfbqha85oe0312hkatw.png", + "AvatarItemDesc": "0d1ae26e-aaa4-4914-ad70-c9ab8cfabc61,", + "FriendlyName": "Box Braids with Cuff Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d1ae26e-aaa4-4914-ad70-c9ab8cfabc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:10.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1980, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6vl2aov7qwvmkpf2wux0rctq6.png", + "AvatarItemDesc": "04e348b7-107f-47b4-9331-ae3b9fa184ca,", + "FriendlyName": "Braided Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "04e348b7-107f-47b4-9331-ae3b9fa184ca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:18.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1986, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bathop4rdgdsysv3jg6wi2uua.png", + "AvatarItemDesc": "ec474314-fd79-441d-852b-d8999274fe57,", + "FriendlyName": "Wavy Middle Part Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec474314-fd79-441d-852b-d8999274fe57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:43.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1982, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "abs1vh2rd21swanjh7vd6hi3h.png", + "AvatarItemDesc": "36b2ccf4-4f42-4dc4-aeaf-f3d10b2a6d30,", + "FriendlyName": "Etched Fade Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "36b2ccf4-4f42-4dc4-aeaf-f3d10b2a6d30", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:23.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1983, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "252m5s6mamg4ajchjb98l8me6.png", + "AvatarItemDesc": "37f7b94a-92ba-4f5e-b94c-de00acfa4722,", + "FriendlyName": "Etched Flattop Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37f7b94a-92ba-4f5e-b94c-de00acfa4722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:26.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1977, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1qr2ehkeevot7hvkonb7ygtv6.png", + "AvatarItemDesc": "4eaaad39-aa44-4791-8b67-2eafa8305850,", + "FriendlyName": "Afro Puffs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4eaaad39-aa44-4791-8b67-2eafa8305850", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:05.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1985, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dzytj60lnm9n8bstqbvcrikss.png", + "AvatarItemDesc": "9bf75b23-84e9-436b-bbb8-4180ccd86b94,", + "FriendlyName": "Wolf Cut Hair)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf75b23-84e9-436b-bbb8-4180ccd86b94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:40.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1981, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e9zj12umhuny6tnh1cdk7tu47.png", + "AvatarItemDesc": "2822aa50-4344-434d-b46f-fd9443f865b9,", + "FriendlyName": "Curly Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2822aa50-4344-434d-b46f-fd9443f865b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:20.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1987, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2fujgcfkoqu654pjz73tner1m.png", + "AvatarItemDesc": "9c20fdd5-acf1-426c-b85a-2220ceee9472,", + "FriendlyName": "Short Curly Twists Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c20fdd5-acf1-426c-b85a-2220ceee9472", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:47.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2059, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mNgon98gpkamzdOKrEwBhQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,PIwziHk1BUuYkAf6kuK6wA", + "FriendlyName": "Rec Room Hoodie - BHM (Black History Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:52.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2192, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3qt9ioRkZ0ClPXKVL2YMvw.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,PRLBrEEZhUumm3VnYVJP6Q", + "FriendlyName": "Rec Room Hoodie - Pride (Aromantic Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:26.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2193, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "pJQjmPlV80Kjht3dBWJjEw.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Xt3Ndm8mC0qsLClPIbgwUw", + "FriendlyName": "Rec Room Hoodie - Pride (Genderfluid Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:29.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2194, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "PO7jrliur0CVizm0SH3TbA.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,MfF0wizDjUa7RUE-AVJpvA", + "FriendlyName": "Rec Room Hoodie - Pride (Intersex Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:33.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2232, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7Z-IZ-6LvUypmMCX8IwHSQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,LovycnPHoUiP_N9w7BzsnA", + "FriendlyName": "Rec Room Hoodie (Juneteenth)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:08.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2337, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "Exs987LYAUOtBgHMuw7Rqw.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,", + "FriendlyName": "Head Scarf (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:08.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2338, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5L_MOzszIUOYUpjCKDoaog.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,B3jdXiOJs0WO2ytm3-hXPg", + "FriendlyName": "Head Scarf (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:11.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2339, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bymnCdtwu0SDAsat4NNB4g.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,VudVdOAjZUibHmtFeNkvSw", + "FriendlyName": "Head Scarf (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:13.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2340, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "lEfY-3T1Lk6G8rEL0ZkSOg.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,5z3jZhYvq0CGmdJ9IcUUsg", + "FriendlyName": "Head Scarf (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:15.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2341, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "J-3USpg66EG6TP6A0OF22g.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,_T4zEki_wk-UIPdhSaBr_Q", + "FriendlyName": "Head Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:17.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2359, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "O9Rxhi7Xp0uMsUE3NL64uQ.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,", + "FriendlyName": "Head Wrap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:55.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2360, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yEzjZ8Usm0289gIRWJ6TqA.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,nPenrNSTE0mDK5cWhxtEug", + "FriendlyName": "Head Wrap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:57.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2361, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mcJSwkCUbUSoHOpUPPSxcw.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,a0A1q_e1q06qquiu2KaZhA", + "FriendlyName": "Head Wrap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:59.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2362, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "TAGSVr0Y6kmZcsvKeNOdxw.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,rwESUxIrQEKA2hdQpcaPaA", + "FriendlyName": "Head Wrap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:01.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2363, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "vlAp24nn10uyws8TDY5Rug.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,M_BhFve7SkC7IJoymLiCLg", + "FriendlyName": "Head Wrap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:04.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2364, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ZBnmV3ahdUa6yCVkJ16fGQ.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,0zldWzi4H0OQwsWPt6gl-w", + "FriendlyName": "Head Wrap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:06.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2342, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "OfSv7-HEQEudAjfpEbowJA.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,EgEoJzG7A0aJ92qfInbX7g", + "FriendlyName": "Head Scarf (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:20.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2343, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "L9nYH9XNvkGX3ZD8c3UfLw.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,OlfQDuz1NUOr88AL2n0jXg", + "FriendlyName": "Head Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:23.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2344, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dBD0grhWnUavKcZsyyx1FQ.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,NETGR8iysUyxcZM_oGmrwQ", + "FriendlyName": "Head Scarf (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:25.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2328, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "EcBeEDeJN0a4mHWBsi4Npg.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,", + "FriendlyName": "Brimless Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:49.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2329, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Eu1IKmX_QkyptaNvKNG26w.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,Rh-RaEHap0e858QeTtCqJg", + "FriendlyName": "Brimless Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:51.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2330, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rUQ5YQ1ymk6xtBaPHA9obw.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,hgLq0WyUAUSl2sboUrpTKQ", + "FriendlyName": "Brimless Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:53.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2331, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "BqkhCnPdjkqPD8KI_tvDBg.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,M5uFQQpoQE-5CKMJnDv_LA", + "FriendlyName": "Brimless Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:55.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2332, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mWIwHLWbnE2ZVzfBK3Y4CQ.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,JzLPlVNG20G9mxG_4PF8Fg", + "FriendlyName": "Brimless Cap (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:57.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2333, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "jfoJFkvJhEaInI7ytPqonA.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,VuMDnpnK50mzgUMwJ4F6Jw", + "FriendlyName": "Brimless Cap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:59.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2334, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3PQQmjlqZE2pSQnjsADLfw.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,", + "FriendlyName": "Headdress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:00.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2335, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hgq25v2vbk6EY3TPtz8O8A.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,gLCis5TFxEyRvvZ0PZq_Jw", + "FriendlyName": "Headdress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:02.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2336, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ad1rqlw7oUiEHiwbXRJHuQ.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,1WBsT99QM0anps6ZoWE59Q", + "FriendlyName": "Headdress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:05.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2352, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "taLvR9OtMkuwOHCj9sa8Yw.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,", + "FriendlyName": "Head Scarf Twist (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:39.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2353, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "aTq6lg1RF0KsrotD8g8PVA.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,jcG-vMduWEq14vo2sKYdWw", + "FriendlyName": "Head Scarf Twist (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:42.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2354, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "vkUv2dsvVEqj7J_oudnCnQ.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,s-QTVT5eTECKHL9FnHFCAQ", + "FriendlyName": "Head Scarf Twist (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:44.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2355, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "n71xleEsHEKKy_YJgkOBbQ.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,irw2uj4c2Eil0ndPCTqodQ", + "FriendlyName": "Head Scarf Twist (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:46.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2356, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9EfM-NBnuUud7oDeXW_t_w.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,UnHAtXkd2kCdamzw6B3Mvg", + "FriendlyName": "Head Scarf Twist (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:48.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2357, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8mn7hHezlUePzyx-HN48rg.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,sO8e2epLrUud243Xi9odXg", + "FriendlyName": "Head Scarf Twist (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:50.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2358, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "DSuLZADyxUa5EzpsYky2cg.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,pPZ3sDw3B06mKiYsAiAt9w", + "FriendlyName": "Head Scarf Twist (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:53.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2345, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "j8rw1jLEy0qFDD5N24UOaw.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,", + "FriendlyName": "Head Scarf Halo (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:27.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2346, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e0Y4CS-9IU2km0B3jwPCtg.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,3cPbhpDX-EqDQy6eN1E5Ug", + "FriendlyName": "Head Scarf Halo (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:29.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2347, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "VH7wAcdrW0aDxhWBl3qs3Q.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,iCVglqDJrkKB36za9UiUxg", + "FriendlyName": "Head Scarf Halo (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:31.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2348, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9ftPEgJCtkmrO_fAsxmXxw.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,gEuE-X67b0-q5C0Tv11_nA", + "FriendlyName": "Head Scarf Halo (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:32.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2349, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "UD9QOouEvE2Msgsa8r4Qng.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,gk_mQ-jsakmxhZYYNlRl_w", + "FriendlyName": "Head Scarf Halo (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:34.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2350, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "kHKrIJ-sKU-U7fZBCFu4rg.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,0p_eXn2gXUKrshSEmrQx6Q", + "FriendlyName": "Head Scarf Halo (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:36.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2351, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rcFCLdci0EKcU2Y-zY7EuQ.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,fAmEEm6ziUO2ZXTscQWtSg", + "FriendlyName": "Head Scarf Halo (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:38.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2732, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "7Seg-2HnmEuiyQanSBov5A.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,NI8a4j9fl0egSrLbrMTn7Q", + "FriendlyName": "Rabbit Ears (Lunar New Year)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:24:58.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2824, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "iegw0q7VnkCblb2xmJZiRQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,kBaeiw5WG0OHOIsric74Ag", + "FriendlyName": "Rec Room Hoodie - WHM (Women's History Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:58.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262140308, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "k4xHpFAnkEm0feaYPcjMzQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,EkA09Hj2m0u3c5STynn3Bw", + "FriendlyName": "Rec Room Hoodie - ANAM (Autism & Neurodiversity Awareness Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.490779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2875, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4lvbe8qhhnk5qhf9gs7iglqbj.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,", + "FriendlyName": "Hearing Aids (BTE - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:42.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2877, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8v81gej0s8m49iztzzyb4eicc.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,xn8Or1kV-EGhpOqnYj6_0A", + "FriendlyName": "Hearing Aids (BTE - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:46.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2876, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b84s2wgn3n5fz4wcz7jmslcez.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,1n-ekJos-EeViiz_zG4jEg", + "FriendlyName": "Hearing Aids (BTE - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:44.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2878, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4hydnfkwj5yum1oq7pdpajh6t.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,", + "FriendlyName": "Hearing Aids (BTE Earmold - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:48.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2880, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9ejntda7ab26w71takrqy1wuc.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,aEsXeTI_SEOclbm82n17tg", + "FriendlyName": "Hearing Aids (BTE Earmold - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:52.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2879, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5kr6spnm1wjdvlki1808o40y6.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,pZDvYABTN06KdJ5Ck60XLA", + "FriendlyName": "Hearing Aids (BTE Earmold - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:50.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2881, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6g30dvwaavnpkdqlzdb6czfja.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,", + "FriendlyName": "Hearing Aids (Cochlear Implant - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:54.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2883, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bmepd5uq48h51oc7xxq9kepf2.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,7ZRNVpxhWk-j8R3oj0oVXg", + "FriendlyName": "Hearing Aids (Cochlear Implant - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:58.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2882, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3vu86qw9p3ikkjm0hx774naxc.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,8WJI7RAuaEy4KvQiQhoKBw", + "FriendlyName": "Hearing Aids (Cochlear Implant - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:56.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "6d55261b-5434-49d4-8057-f4c748f1a238,", + "FriendlyName": "Full Body Clothes Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d55261b-5434-49d4-8057-f4c748f1a238", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1968384766 + }, + { + "AvatarItemDesc": "6f47500a-92d2-42d4-8dfa-42a815c43c02,", + "FriendlyName": "Full Body Clothes Sneakers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f47500a-92d2-42d4-8dfa-42a815c43c02", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1551964329 + }, + { + "AvatarItemDesc": "ff89985c-db80-45f6-9c49-9b7478c38ae0,", + "FriendlyName": "Full Body Clothes Long Pants", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff89985c-db80-45f6-9c49-9b7478c38ae0", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1647504812 + }, + { + "AvatarItemDesc": "8c3885a1-b1b5-407e-98e3-4d909724b6c6,", + "FriendlyName": "Full Body Lumberjack Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c3885a1-b1b5-407e-98e3-4d909724b6c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1416203662 + }, + { + "AvatarItemId": 1275, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eauoml21rxj3iqwoccpn04y5p.png", + "AvatarItemDesc": "2c3773a5-a0b4-41c5-a4df-6572dff516c9,", + "FriendlyName": "2020 Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c3773a5-a0b4-41c5-a4df-6572dff516c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:18.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 967, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0mvz1timijk659hshhzec8jtt.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,", + "FriendlyName": "Swing Dress (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:13:02.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1255, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "holidays", + "ThumbnailImage": "3rls67cl0dxudh9kj7rqgqzlp.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,PEGqjoxP6E2yRx_A-HphrQ,I_OF1c0YWESMwxJr9sXYOA", + "FriendlyName": "Swing Dress (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:48.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1353, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6zl0lixajd47uo5t8r26lce00.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,K17dIz_TRUq5D50NBfLjyw,3drRcfyKkku0qGwCy16m9Q", + "FriendlyName": "Swing Dress (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:35.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 757, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1ofk56iwklqug6u3ihb27rfex.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,", + "FriendlyName": "Swing Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:22.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 807, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aje5q2u76zjjzgeehfysg6dik.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,BLgkgLeS-EyazgPQxGcavg,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-10T19:02:35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 790, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "clsb49us68oqy8d5uyq5q31r0.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,ZvPidQ57rUS50Qz89ERmLw,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Halloween)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:31.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 797, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7wagu7eu1akvlpi9036qjkgkx.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,eb3SmJTIh0628LrZUkI4KQ,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 789, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3rko5wknqf4g6gbl016rfci55.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,Yr0mF1EWzEaIyRMOWFiY6w,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:27.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 788, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6sddopu5d9s5sghb2jjycwa4u.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,QzjakcO8tUaPeiI5KBLKtg,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2777, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MGp8JbVCX0OTgJwakQwccA.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,NpvUosaJTECohTIAR38egg", + "FriendlyName": "Ladybug Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:36.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TzqM7z7igEq2-tMG1UDu0Q.png", + "AvatarItemDesc": "4b51679e-96e4-4a11-96fe-1d2754d12481,", + "FriendlyName": "Clock Chain (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b51679e-96e4-4a11-96fe-1d2754d12481", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:03.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BktGoS3OckmmBQXz83eY4g.png", + "AvatarItemDesc": "383d4bb7-1ffc-4b40-9b7e-cfeaf9ac5fdd,", + "FriendlyName": "Thick Shades (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "383d4bb7-1ffc-4b40-9b7e-cfeaf9ac5fdd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:05.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2768, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "q3OFxpl3vkyZ2Y6zGTfobg.png", + "AvatarItemDesc": "3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686,", + "FriendlyName": "Retro Layered Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:07.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 755, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8p01g7rfkflo0g5se24b17jny.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,", + "FriendlyName": "Shutter Shades (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:19.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 913, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1md3c8azai7phcdzdwuu83inx.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,ZpcM5H2Q1UCt0hh82nSu9w,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:47.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bo52eq6h1gjw33zb6kj234swy.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,7B7vpnMTfkGLcMdHCZ_kWA,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:46.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ajtgswd058zwm4k13wu9gtw62.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,nO-YacKU1kmLjTFAAqNeYQ,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:45.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c9ilqoetseg0h6s3twpchmkfp.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,76CA_KCr806y1FA35m-YTg,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:43.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1811, + "AvatarItemType": 0, + "Tooltip": "For earning $1000", + "Rarity": 50, + "ThumbnailImage": "3bgeqvxk1ut8bz0ipoy471x1b.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,u_H8YUV4MEGNGh5h9ckm_w,a3OlD7iI_EqurYq_RIQCvw", + "FriendlyName": "Shutter Shades (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:55.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2074, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DePlu75lck-gLWofC8uKmg.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,1DQi3Wt8ukK2gWvl0zJnkw", + "FriendlyName": "Shutter Shades (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:01:38.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1159, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "daw2bnn661kjxraiwzbj4o13s.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,", + "FriendlyName": "Stunt Singlet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:50.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1158, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0q2tmuvx7l3fxuhgiq8vaftru.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,6t2rCb6WWEW6pJbcjsAFCw,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:46.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1157, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4u9h5rvuhapi02hegh14s9zet.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,zu0Jk24WTkan6XWSwGU1Uw,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:43.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1156, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9jul59tnxdwa6w6bjib681az8.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,i1XbjBqTu0CILIuJClWimA,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:40.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1155, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tpxkoq49mfgt3w5y2vsjnqdm.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,3frH6KxZ9kGUYkjeCDpylQ,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:36.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1154, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "92q8bqs03d33p72ljcvn79lpe.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,Lf4FjROSAEqXedFxpXFQfg,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:32.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 758, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6qo0q0lqcx2crqugl2xmhnz4d.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,", + "FriendlyName": "Windbreaker (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:23.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 896, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5rgodebtv8essp0sxgveqp7fd.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,grutFwJD_0Cb3t6fddNg6w,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:25.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 895, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "37ag45u7zn3cqqya03wz5nmgw.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,EbVDqEJQOk6j0UvX3t-8rQ,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:23.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 894, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0kifl6w66mqa2wbhliszoe52l.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,C3ll7uPfUkeZ1QLX80_BpQ,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:22.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 893, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "au0r6ubbl0fyr6xi7sqch0b59.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,JS7aa4QSUUOpUYPqRZEb5g,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:20.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2065, + "AvatarItemType": 0, + "Tooltip": "Breakdance inspired colorway", + "Rarity": 10, + "ThumbnailImage": "5aqX6fFXKkulI-FwAVm9Kw.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,rwojzpr0E0ylG6_Vv_MVaQ", + "FriendlyName": "Windbreaker (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:18.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 761, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8v6i3ja1btg3mkpog657iixa1.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,", + "FriendlyName": "Windbreaker Cuffs (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:26.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 862, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0xnunm0kmgc2va3zevhooe9ya.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,HncURj5yi0ykk2cVyCZ4Sw,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:15.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 861, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9qf35u5xai7bpf8k7d518b9gz.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,42ZNGHXMvU29yo9s9tVpOQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:13.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 860, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8mq00l9l162o9unwk33egvmsd.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,VDo682NvSUa8xYn8zTX-AQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:02.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 859, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7ndz1tjworge5u01d8cvf815f.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,FvEjo9let0e_Sr-kWq5AvQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:00.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2071, + "AvatarItemType": 0, + "Tooltip": "Breakdance inspired colorway", + "Rarity": 10, + "ThumbnailImage": "HQaEvdJ2yEKBpIlsj1ChiA.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,6c-AhDM6K0iHbNMf4BNiUg", + "FriendlyName": "Windbreaker Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:31.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2217, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "sVLupklIaUKmgYNatMdcSg.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,", + "FriendlyName": "Acoustic Guitar (Wood)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:02.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2243, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "ElDAaYT06kqpFifv_brl_Q.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,O38XfZ3gh0-ofqu36y3mAg", + "FriendlyName": "Acoustic Guitar (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:54.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2242, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "vpkdcMOxAUylrfe2PrxZfg.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,BPp9SkUpsE2PZlStZrIDIg", + "FriendlyName": "Acoustic Guitar (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:51.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2385, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0n2uuqp3f7gj3z8o0ynlzl3et.png", + "AvatarItemDesc": "b07a3964-7a53-4467-aa85-ec19cc27a57b,", + "FriendlyName": "Alien Head Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b07a3964-7a53-4467-aa85-ec19cc27a57b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:26.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5ed0a62a-19b5-42c0-a255-a2e83f063d04,", + "FriendlyName": "Alien Hunter Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5ed0a62a-19b5-42c0-a255-a2e83f063d04", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1733459499 + }, + { + "AvatarItemDesc": "b841c65f-ed5e-4d2a-803f-48939b909100,", + "FriendlyName": "Alien Hunter Glasses Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b841c65f-ed5e-4d2a-803f-48939b909100", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1626620437 + }, + { + "AvatarItemDesc": "8972c6f8-6e11-49dd-92dc-7b12c417fd40,", + "FriendlyName": "Alien Hunter Tie Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8972c6f8-6e11-49dd-92dc-7b12c417fd40", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1145186950 + }, + { + "AvatarItemId": 363, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6i5j2nqmud9i7j69mikjo4wjl.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,", + "FriendlyName": "Angler Shirt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:03.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 403, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7q4pokzc6588ccgqdjft4wov4.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,cac27853-e2df-4d77-b3fd-af3bc0813f01,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:28.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 404, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8gpdgnc9rmf5fzvsp4sio5zla.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:29.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 405, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4qj5sy46am7e176dh6xnr2ejv.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,97938f4c-52b8-4c85-8575-856654666316,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:30.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2520, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "Joz7a1NRQES_i5VXRjxiKg.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,ugpS7o-ZxkCv7HAvhewZgw", + "FriendlyName": "Angler Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:31.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1804724359 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,mWw1qw6jAEuZ9ygGYsHNew", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1020115116 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,AmKqwhgKAUSHt9axdxe80g", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1611305998 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,-eEKJlUIbkqHtsPJeHz19Q", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1014853776 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,LA7Kx2pBIEu07CnM-XRLtw", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1426268107 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,MocUXmOH70-h3RxBJNhYYA", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1429871593 + }, + { + "AvatarItemId": 357, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2taoaui5i9giqj24levmdbst6.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,", + "FriendlyName": "Angler Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:54.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 396, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2i2e0tjhgvflcauuva6tqucvx.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,3f6c8bd2-2c1d-4c8c-a9de-16600fee9645,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:17:57.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 397, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6e94bkakcfscxp997xunr45se.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,d7f0ffd3-ccf9-465e-8afe-4d7efb78dd2c,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:20.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 398, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a791dc940zynp12ezpj4sdavg.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,225209dc-3afc-40cf-858e-89f7afa00c0d,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:22.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2515, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "lRJW1xa-LU2CmI1Uvlzzsw.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,G7p-E7TJHkSojZtj_5pDXw", + "FriendlyName": "Angler Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:19.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1658, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2i9990m2ddrxscxf3csgnpkm5.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,", + "FriendlyName": "Animal Sweater (Ferret)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:38.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1659, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4say0eirgh8p9gunfvf23skx6.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,azbt5pgvrUS4LQwhndFeJg,cHZ4lDnSoUypIXCiRRowzg", + "FriendlyName": "Animal Sweater (Axolotl)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:46.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1795, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "diqbk46zibrfi814wmivw7o97.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,6dzz5ZcGtE6bEjQHxWmo0A,2cSC290cPkCOrW5vtlVUxw", + "FriendlyName": "Animal Sweater (Penguin)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:19.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1796, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1viyefngw9h64p6bq76mhiaym.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,yXVeN9SzQUKE_6u_2KPzgw,sWgRczvpUU6ZHeoMrMD_yw", + "FriendlyName": "Animal Sweater (Toucan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:21.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2402, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "92nW2XOGOE2VbuJ1_fV_2A.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,qzvnj7D1tUuf01HvpiIvqw", + "FriendlyName": "Sweater (Cow Face)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:43.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2748, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wI9h7dAeEUOvsa1ogCNjiw.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,oIVUmOrjjUysnN4XoWvJSA", + "FriendlyName": "Friendship Sweater (Olivia Otter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:14.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2749, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Q5OR2v7ST0mft6WFh5KyAg.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,0wRS4Rs4ykqjJE_5Fk-Flg", + "FriendlyName": "Friendship Sweater (Oliver Otter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:18.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1937, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "emruqyjzh6ox3jrozmu8mooyp.png", + "AvatarItemDesc": "4c032271-a0db-47b0-bfc6-4c330c2c4b21,", + "FriendlyName": "Alpaca Lights Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c032271-a0db-47b0-bfc6-4c330c2c4b21", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:36:34.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 960, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "68fe918b1l9gkogciqeizqf0o.png", + "AvatarItemDesc": "AbYdU7O_9U-Lwg7zFVZzXQ,", + "FriendlyName": "Reindeer Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "531db601-bfb3-4ff5-8bc2-0ef31556735d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T03:04:20.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2085, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X0LbhAoDYk-wQI2tMehxJQ.png", + "AvatarItemDesc": "aaed6385-0ba5-45f3-b0d2-6b0a9ab79628,", + "FriendlyName": "Disguise Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aaed6385-0ba5-45f3-b0d2-6b0a9ab79628", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:03.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 33, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d0ct7ztnb49pz8t2575vjicnw.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,", + "FriendlyName": "Archer Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 34, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "665whx3sukltm50bvhdjb35kb.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9b35ae52-febe-486a-8557-ec7190a9756e,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 35, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "85o1rs3o13bqcy2gu8qjvzgij.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 36, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6neueh6asjwdnysb7fxmpi9ep.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,6e152cc7-e93a-4408-8092-236db420b680,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 37, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ilinnf748ep3x9ag9hht0gds.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9ad37173-e640-4b9e-a474-716d482b73d2,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1123, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6jvwgyrx37n8e3913svxb1s0i.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,lgqLGi01o02ZFC9Xs5Ofdg,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:11.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1301, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wxsli15xh05ark51tui5k2b0.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,61VZkFjB402brzWm4UsftA,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:06.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 216, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vybukcx6j4f40n6enwd6d9mh.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,", + "FriendlyName": "Archer Tunic (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 217, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "azaisqtidm1kgo72k6ahsn245.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9b35ae52-febe-486a-8557-ec7190a9756e,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 218, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5ifgufbqvr5jnazfx4cruhixk.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,6e152cc7-e93a-4408-8092-236db420b680,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 219, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "05hq8v4ju1q79njz45lwlti5v.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dkhkp10elyre43sbqzangzn0e.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9ad37173-e640-4b9e-a474-716d482b73d2,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1115, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "71zptzc6ptipah0zjqi4oj5ks.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,lgqLGi01o02ZFC9Xs5Ofdg,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:23.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1311, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c2wrex5zlcm4q7bek6fuavelp.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,61VZkFjB402brzWm4UsftA,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:02.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bstnvjdujs0tgbj3uwztvija9.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,", + "FriendlyName": "Archer Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 262, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "btgunfy4izmpoj8xglwadngpl.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,47da86b9-4e6e-423c-b8c4-ea6bf8472509,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 263, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "883f69uc5v73zvor8ltld7zdx.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,d5987fee-e3cc-4336-853a-5df57f73df05,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 264, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5a2i66dhqnlvimed4a3fjhz2j.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,0c433086-9501-48c4-8d0e-4474eb420c61,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 265, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78f5tat5pkmywligx33rcvv4k.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,2be2f040-68e4-44dd-9099-29f11110f3a4,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1102, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dclo54kfdapgc9n3yba702l93.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,1VGZ1QCvcU6rsKVkUDBNjQ,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:36.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1317, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1v4tdvaof4avz08bp7oapq1nq.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,8hUBfmq28EixYjv92Xcrgg,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:42.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 125, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "32boa62st14xrnw6alwpdqer3.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,", + "FriendlyName": "Archer Quiver (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 126, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1d7kcdxyqd6vbcvooyqhqiqsv.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,d1eb3573-672c-4bb4-9ec5-7c81e6a38d3d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 127, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "65ob47rf57lzttas5qiw96qkt.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,e548124c-d7f3-45de-aaa0-f66130eca576,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 128, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dakgj68r389mpgtl45ly2nsvo.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,7221132e-053a-4c58-b719-ed3c8558907d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "46p3j6coqok4330sha7pm7whe.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,74b009f4-e128-4c59-8a6a-4c0ba6119bde,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1116, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "88qgugcpk7v2ra2fi4pz1ehjy.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,29EZ6r2SsUakWjzuevxeoA,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:32.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1307, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a87ioalwyvn2xvaj4bhy5mu0n.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,imDGL6dhrka-KliYlfpdgw,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:42.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "xIvzfECzA02itIo1Gx1FnQ.png", + "AvatarItemDesc": "c65a94e6-7475-4030-8fcd-9ce25852ff7c,", + "FriendlyName": "Arrow Through Head Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c65a94e6-7475-4030-8fcd-9ce25852ff7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:44.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 364, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "49e7ejka5p8qmdd0wplyjic3s.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,", + "FriendlyName": "Artist Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:04.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 406, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eiioy82z0xbvbvebsm9vamslv.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,8c94e45c-6605-4b4f-9ba4-2c6c6e557228,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "FriendlyName": "Artist Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:32.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 407, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7e9wiybe217zugyt61b0i6ten.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,17b16982-871c-476a-860f-f6953c89ee8c,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "FriendlyName": "Artist Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:33.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 358, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "buiszvrl96xas7c9f7b7i84ia.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,", + "FriendlyName": "Artist Beret (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:56.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 428, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c8eo8kzis4cfksqzadwx60q7a.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,14b43bf9-5ba0-484e-ae5d-55cfeb09a70d,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "FriendlyName": "Artist Beret (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:55.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 429, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "er774snrrar0qxts7cl1d2zb0.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,5c8f36c5-07da-49f4-af02-d23207a43216,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "FriendlyName": "Artist Beret (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:56.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a95a1b36-7849-4791-89be-c1a56868bfc5,", + "FriendlyName": "Artsy Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a95a1b36-7849-4791-89be-c1a56868bfc5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1538583493 + }, + { + "AvatarItemId": 1849, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "emuh8dfzl0l4dji5yae6lwwde.png", + "AvatarItemDesc": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,", + "FriendlyName": "Ascot", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:14.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,ffUVovxlWUCgOxJyUJscIA", + "FriendlyName": "Ascot Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1091914702 + }, + { + "AvatarItemId": 134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "60g0w2ayv9lwn8fokpqhccd0m.png", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,", + "FriendlyName": "Astronaut Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4pm9zzbb13116s8257iggug13.png", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,9GhnX4Pj4E-wxbPlD8GB-g,YSrGwXybGEqaqHNCS64qOA", + "FriendlyName": "Astronaut Suit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:30.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1938, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6z6iwsstjeii7vhx905bsn4tj.png", + "AvatarItemDesc": "ae7c9d1f-d371-4dfe-a6df-d74662606aae,", + "FriendlyName": "Stranded Astronaut Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae7c9d1f-d371-4dfe-a6df-d74662606aae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:36:40.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ae7c9d1f-d371-4dfe-a6df-d74662606aae,i9rUo62Tgk-UaM9BsYRGgQ", + "FriendlyName": "Astronaut Stranded Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae7c9d1f-d371-4dfe-a6df-d74662606aae", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1162904659 + }, + { + "AvatarItemId": 266, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h8xjhjeecs5u49k0pqh1txaz.png", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,", + "FriendlyName": "Astronaut Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 801, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ayc9cl76c3xmxyeui2n4vtju.png", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,y_gTbnuYS0GKJmnCs2retw,hjk9OO8CW0KNGR7A6_p84A", + "FriendlyName": "Astronaut Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:43.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,LqBO6GM7F0mGswRqJqCbtg", + "FriendlyName": "Astronaut Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1310840186 + }, + { + "AvatarItemId": 38, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3xwp4xtloierefgfubh97k778.png", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,", + "FriendlyName": "Astronaut Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f218e2de-46fd-45b9-9e0e-670b6bb905e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ovfqg649aj942uo3a5rqkbni.png", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,JFalLDUvm0aMm3wh1UVfVg,iQjkS1xYP0S63ltvKpkz5g", + "FriendlyName": "Astronaut Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f218e2de-46fd-45b9-9e0e-670b6bb905e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:47.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "66bfec71-882b-4981-9d02-9c03d8d79789,", + "FriendlyName": "Astronaut Helmet Hat Sglitch", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66bfec71-882b-4981-9d02-9c03d8d79789", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1672693583 + }, + { + "AvatarItemId": 1933, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0w9bwz7zburoq47xylvojlem9.png", + "AvatarItemDesc": "eacebd1a-070e-4733-a6b6-65811ef7891a,", + "FriendlyName": "Stranded Astronaut Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eacebd1a-070e-4733-a6b6-65811ef7891a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:34:19.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2063, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "hItB5Kz2jkyqLdN93apHFQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,", + "FriendlyName": "Women's Athleisure Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:13.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2769, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wKJw4wkhlE2Io6Asemls-Q.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,QF05cIuqhEeY8oNnO9CocA", + "FriendlyName": "Floral Athleisure Crop Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:10.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2770, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Lt9m7wE1U0-91--MzopOoQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,FbedbSonT0izB7Q6cLXCzw", + "FriendlyName": "Orange Floral Athleisure Crop Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:17.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2771, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XNVa9yB1X0SSTUTytHozCQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,SoQerU2dOUu9BdRBAsEllA", + "FriendlyName": "Floral Athleisure Crop Top (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:21.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "8ae154ba-8e69-48da-a314-507b419d4338,", + "FriendlyName": "Atlantis Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ae154ba-8e69-48da-a314-507b419d4338", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1123636091 + }, + { + "AvatarItemDesc": "27fa9f11-5368-4f6c-a114-18501478a9fa,", + "FriendlyName": "Atlantis Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27fa9f11-5368-4f6c-a114-18501478a9fa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1068640728 + }, + { + "AvatarItemDesc": "546ca714-8355-4455-be44-d7987974f8af,", + "FriendlyName": "Atlantis Trident Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "546ca714-8355-4455-be44-d7987974f8af", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1015465370 + }, + { + "AvatarItemDesc": "ee835538-058e-43d9-8268-b7d079563799,", + "FriendlyName": "Atlantis Warrior Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee835538-058e-43d9-8268-b7d079563799", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1924464343 + }, + { + "AvatarItemDesc": "f10f2d46-ec03-452b-9b94-0e476ccbb9c6,", + "FriendlyName": "Atlantis Warrior Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f10f2d46-ec03-452b-9b94-0e476ccbb9c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1699140506 + }, + { + "AvatarItemDesc": "a112b8b5-ea0e-4d23-a7eb-2daa4683a87c,", + "FriendlyName": "Atlantis Warrior Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a112b8b5-ea0e-4d23-a7eb-2daa4683a87c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1473012152 + }, + { + "AvatarItemDesc": "05809f10-98b7-4228-8828-e72cb37fe70f,", + "FriendlyName": "Autumn Colors Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05809f10-98b7-4228-8828-e72cb37fe70f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1575197462 + }, + { + "AvatarItemDesc": "2036fbec-e562-43a7-bcf1-c12f0307131a,", + "FriendlyName": "Autumn Colors Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2036fbec-e562-43a7-bcf1-c12f0307131a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1242079628 + }, + { + "AvatarItemDesc": "9d630b9b-7f48-4f34-880c-f52bad628b25,", + "FriendlyName": "Autumn Colors Cuff Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d630b9b-7f48-4f34-880c-f52bad628b25", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1240388911 + }, + { + "AvatarItemId": 379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzqcpgtdl5etxzj1p61vv6r5r.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,", + "FriendlyName": "Aviator Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:17.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 399, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cq2ejdon4vbkzqptgu6lhugwi.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,a8de80d3-286a-4fdc-bed2-75d52f8c0964,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:23.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 400, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ewo7xaztrkgxbdwc8nnspdmtl.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,c6cf096c-f0cb-4500-80a0-bc2916b5a426,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:24.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1606, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vm41bj3a6vlqwhygscxqlcy8.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,FjnZ-D1hcESLOWL4Ukv7kw,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:36.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 383, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bqf55zk90gccgpaf57wtsf14z.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,", + "FriendlyName": "Aviator Coat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:23.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 408, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "66bl5lmlw2ayp67bzz7mlitzm.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,da185fbb-ad2b-4f30-87a2-491f24b78ebb,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:34.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 409, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c32bq37y8bertjy6ivb388xbe.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,bea795cc-aabd-4229-84dd-49ab4f91a445,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:35.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1611, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2tmvikftvfs2sv1ll3oaqfq0w.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,2APfqVnnQUaDYKi-fQg8jg,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:20.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 384, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dik18b9lkprego4zu9vx7mv75.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,", + "FriendlyName": "Aviator Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:24.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 410, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cfko6vz2smqr4u7q3b5jh5jvh.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,97bbb384-b37e-4ea1-902c-88fbd6854ab5,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:36.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 411, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c6s4wxr4cgcoqmqlgwz47lrnh.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7fb8496f-0e6c-4e48-b8fa-5709ebbf4820,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:37.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1613, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "68cuiknni4ah1844qwlndids2.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7nRz9Jmnn066_PzngkhfFA,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:35.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 381, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xipn9pseh8wykz3t6hpadi3l.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,", + "FriendlyName": "Aviator Goggles (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:21.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 401, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dzmu6jtu8g9jr5k1y002d2m6v.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,7b82c3c5-fd1f-45ec-b83f-e32668aa8dce,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:25.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 402, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "edzgkcu9p46xum15ttgjb0r3p.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,23428ca9-be75-4146-b400-df01d1dadac9,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:27.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1629, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bx1ejsvvshgpo945a7kst7w1x.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,qJGX62O_HUiZWN61NAc1Lg,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:54.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 135, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6sqmpmy9umgo11ba977kv392b.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,", + "FriendlyName": "Babydoll Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 136, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9qamxgi6spdwdg7gwy4xnedzt.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,d6823e01-69f0-4f85-b94a-74894356a2cf,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 137, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c24u162frxoa2ztfi76luw9ji.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c5deba2a-6e35-4b13-8e94-8ba5457f39df,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 138, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9zwdqgsqcsxt5ced3hkhejxc2.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,e13cf33d-7588-4e2e-955e-f0a29d6380b7,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 514, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6ag9z8xgaq11l42y9relrmux1.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c9589974-c261-485e-b571-cb2f34fb178f,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-20T20:52:13.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 660, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7f7hajiw2luckub1h51u4co33.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,da6d0ced-3d43-4878-a868-925f2ed6fd5b,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:26.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2893, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HsPbJKuTm0WZI6Of5AnXQA.png", + "AvatarItemDesc": "c2d22328-b41a-4e53-9b1e-814e84b86d15,", + "FriendlyName": "Back Kite", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2d22328-b41a-4e53-9b1e-814e84b86d15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-24T17:08:22.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1dbc517c-432b-4c85-8c5e-dfcf22b269cb,", + "FriendlyName": "Back Scythe Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1dbc517c-432b-4c85-8c5e-dfcf22b269cb", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1359892989 + }, + { + "AvatarItemId": 1762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58mihxk3jk5j3t6ag197iz7x8.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,", + "FriendlyName": "Backsword (Leathered Iron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-12T21:10:22.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1886, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8him9qwzellzbybe456axlevp.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,GfC01_o8QkGTbnGNjv_DSw,7mLb5jMJMUug7Nzwxv6D4A", + "FriendlyName": "Backsword (Wolf Steel)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:16.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2114, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "u0lK1AO41UOMSqQHLjVEEw.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,rFCKF2ku1UWj-TF-VClPRg", + "FriendlyName": "Backsword (Cricket)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:23.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2441, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "45ZUSM5vSES1q6qkk-rkFQ.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,zld80uf3y0uu__Yejms-3Q", + "FriendlyName": "Backsword (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:15.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1570227863 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,c2e62c10-3120-4991-9aa7-3f3e2cd29054", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1413041026 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,b56bf738-4c41-4169-bee5-b3e36db6d6a9", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1874084400 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,96585250-512f-4559-9526-1ef77356a72e", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1481339357 + }, + { + "AvatarItemId": 1455, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d93bbdei1z4uj950nvf3c8ai6.png", + "AvatarItemDesc": "49800740-33d6-4280-aefb-3497597c6366,", + "FriendlyName": "Folded Bandana (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49800740-33d6-4280-aefb-3497597c6366", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:22.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2075, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "QBEmq47zQ0ipqcJfw_re5Q.png", + "AvatarItemDesc": "49800740-33d6-4280-aefb-3497597c6366,0oMo4q5cLEi9LGpkOFIlkA", + "FriendlyName": "Riveter Bandana", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49800740-33d6-4280-aefb-3497597c6366", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:10.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2365, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "mrQZPItuWEiqGyXjajov1Q.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,", + "FriendlyName": "Banjo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:05.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2366, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "6Y0DhxPd1UaxX3rZPdXFYQ.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,7ykIpgU8Tkmmq_zzpjTVrg", + "FriendlyName": "Banjo (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:08.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2367, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "I6hIZL0Kp0CyPWnLkxSSMg.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,iqYc9qtbZEig8jl_ZqKe6w", + "FriendlyName": "Banjo (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:10.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2258, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SBxpBk0gcUG34NYQ8jtNvw.png", + "AvatarItemDesc": "764b4c4e-1009-4772-b01f-7b326dd77e8a,", + "FriendlyName": "Banker Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "764b4c4e-1009-4772-b01f-7b326dd77e8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:00.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2772, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BRPCDGHJ1USSO-gAt69-HQ.png", + "AvatarItemDesc": "764b4c4e-1009-4772-b01f-7b326dd77e8a,93vga8NJQEe5nWdqrroAaw", + "FriendlyName": "Banker Vest (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "764b4c4e-1009-4772-b01f-7b326dd77e8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:24.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2259, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "n1YJ_xf3VE6EzTlwUlW_9g.png", + "AvatarItemDesc": "62e480de-27e9-4719-adac-360698c7a2c1,", + "FriendlyName": "Banker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62e480de-27e9-4719-adac-360698c7a2c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:04.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2815, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "oea3WEkmiU6otRXRPGNm3w.png", + "AvatarItemDesc": "879c43e8-c877-4c8e-a5c9-100123e7a102,", + "FriendlyName": "Banker Hat (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "879c43e8-c877-4c8e-a5c9-100123e7a102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:35.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2260, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9Wd8fqP_uUygfK_VJAt3UA.png", + "AvatarItemDesc": "7d6cf363-76ba-45c8-91f1-cf4434c31b36,", + "FriendlyName": "Banker Monocle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7d6cf363-76ba-45c8-91f1-cf4434c31b36", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:05.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "amQEmFoB-EK_ExgRPUaVoQ.png", + "AvatarItemDesc": "1a0319e4-24e7-40b3-9976-70bff2c7ff23,", + "FriendlyName": "Banker Neck Tie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a0319e4-24e7-40b3-9976-70bff2c7ff23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:07.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1677, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0j5eiu15ludyj18o9vlghi84f.png", + "AvatarItemDesc": "07023692-97e1-4a65-a881-64ca6b4ae08e,", + "FriendlyName": "Barbarian Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07023692-97e1-4a65-a881-64ca6b4ae08e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:28.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1676, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7m1uy36svx5mpua6eq7senkzm.png", + "AvatarItemDesc": "e48955ae-50e0-4f55-93a1-611150142331,", + "FriendlyName": "Barbarian Tunic", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e48955ae-50e0-4f55-93a1-611150142331", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:24.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chch6uszf5qnxlwl6jtw9bftb.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,", + "FriendlyName": "Barbershop Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:28:57.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 412, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d67bfbfq6h2c493xoldetiunp.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,631c9015-2a00-47ea-bb35-465a2ddbca5d,83158223-e268-4036-8a6a-218a8430eb2c", + "FriendlyName": "Barbershop Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:19.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 413, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4d6mxhehvygwsuycquilcwmtt.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,7f9cb939-36a4-40da-8337-025e1d7bf8e5,83158223-e268-4036-8a6a-218a8430eb2c", + "FriendlyName": "Barbershop Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:20.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 387, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "224pkbmy07eojqcchbf4djy4z.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,", + "FriendlyName": "Barbershop Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:28:59.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 414, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "93dtf3uavctg3si13zdcnelxe.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,a5925738-a451-4f25-b622-ccbfa12bd88c,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "FriendlyName": "Barbershop Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:22.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 415, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cw4kg51jcq1ugso9kcmngkt04.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,c64ac7a9-7fd4-4587-8626-0413a190b706,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "FriendlyName": "Barbershop Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:23.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 388, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "24zf9tyok63h84td17n6zo3j5.png", + "AvatarItemDesc": "ce81210f-8eb8-4cd2-95d5-046d4da229c8,", + "FriendlyName": "Barbershop Wrist (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce81210f-8eb8-4cd2-95d5-046d4da229c8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:29:00.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 416, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "06z3ng7dlwz8wcsxzadsfgslx.png", + "AvatarItemDesc": "ce81210f-8eb8-4cd2-95d5-046d4da229c8,bb01b51e-a95f-441e-8c52-abc3aeed7145,f7c7d05f-5454-445a-af38-25f110bd204a", + "FriendlyName": "Barbershop Wrist (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce81210f-8eb8-4cd2-95d5-046d4da229c8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:24.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5687a39e-e5d7-40e4-9b32-190754fc6292,", + "FriendlyName": "Barbie Western Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5687a39e-e5d7-40e4-9b32-190754fc6292", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1670250213 + }, + { + "AvatarItemDesc": "5687a39e-e5d7-40e4-9b32-190754fc6292,km94i3lHJUWfjKgHrV-mHQ", + "FriendlyName": "Barbie Western Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5687a39e-e5d7-40e4-9b32-190754fc6292", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1077124656 + }, + { + "AvatarItemDesc": "7b7574f3-0b0a-4954-a644-6473ddb86424,", + "FriendlyName": "Barbie Western Ken Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b7574f3-0b0a-4954-a644-6473ddb86424", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1076331846 + }, + { + "AvatarItemDesc": "d85c010f-891e-4ce2-a9b8-ed68874da228,", + "FriendlyName": "Barbie Western Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d85c010f-891e-4ce2-a9b8-ed68874da228", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1008995980 + }, + { + "AvatarItemDesc": "0952eb17-9779-490a-98fd-af734a698c76,", + "FriendlyName": "Barbie Western Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0952eb17-9779-490a-98fd-af734a698c76", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1951232994 + }, + { + "AvatarItemDesc": "0952eb17-9779-490a-98fd-af734a698c76,uQcf_scBfU-XAii9HExRfQ", + "FriendlyName": "Barbie Western Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0952eb17-9779-490a-98fd-af734a698c76", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1279100943 + }, + { + "AvatarItemId": 1887, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25kjk74xrqchihponvtrfcese.png", + "AvatarItemDesc": "f23e86eb-8b59-4b32-a72a-5d382568e399,", + "FriendlyName": "Bard's Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f23e86eb-8b59-4b32-a72a-5d382568e399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:20.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2503, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VHA9zLkhZ0uAaJkYQaimwQ.png", + "AvatarItemDesc": "f23e86eb-8b59-4b32-a72a-5d382568e399,wuCrGxBRQUa5BLwx1nASPw", + "FriendlyName": "Bard's Vest (Solstice Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f23e86eb-8b59-4b32-a72a-5d382568e399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:50.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1888, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dub0ri6t7szkccf54a8e3p9ef.png", + "AvatarItemDesc": "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26,", + "FriendlyName": "Bard's Cuffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:24.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1885, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7ixk1u407fxtupt3m3sjujxlx.png", + "AvatarItemDesc": "b17591ba-3c2d-4309-ab01-b6743424e07e,", + "FriendlyName": "Bard's Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b17591ba-3c2d-4309-ab01-b6743424e07e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:05:57.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2794, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "l1uILA2f4EKvZEBtGvSETQ.png", + "AvatarItemDesc": "b17591ba-3c2d-4309-ab01-b6743424e07e,l9cF1s3B8EC6BGlQ4Tx1cg", + "FriendlyName": "Bard's Hat (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b17591ba-3c2d-4309-ab01-b6743424e07e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:13.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2268, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aSaV2nvIFkOp_qOuz0uFBA.png", + "AvatarItemDesc": "8f137b1f-4125-4657-85c4-bcdb74d3b0c2,", + "FriendlyName": "Barrel Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8f137b1f-4125-4657-85c4-bcdb74d3b0c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:08.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 26, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4rmm71n7z0wmlx9lj3u30z4u0.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (White))", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1217, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "593u0j7z80kmuoamluu1rn0dg.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "FriendlyName": "Graphic Cap (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:03.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1630, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3lp8gu18idyrp1ify9n7g7h8m.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "FriendlyName": "Graphic Cap (Buckaneer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:59.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1216, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "d45mukwdi8abvszf0t0hbk5hr.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,nZor-clxlkm8EO9Gco0Otg,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Red, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:59.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1633, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9x0xbf4dhn0sn8fttdfpdd2k2.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,M8IYDK6aTUyn5mRhr51GtA,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Orange, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:18.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1632, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ajy5peougbbudsm27960iaohq.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,yxWEn-oaYE2YvvnrTVV5WQ,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Turquoise, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:16.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1631, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dospnezxsaqyxcdej4ac9k08b.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,XLYLo5qgW0eociGDgQ16PA,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Yellow, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:14.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1707, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "59nj2htuon9c6gj41ptaat7jk.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,Vk1n941ZgkKrhrL9mh18oA,lxmw7EbzsUuoy0roRy5McQ", + "FriendlyName": "Graphic Cap (Minitron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:17.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2g7h0rh45zr980awo5cvp4si2.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,mdSSJyipq0KvjDNQ2NyUaw,LQheTa-EvUaAonSyoWNmXg", + "FriendlyName": "Graphic Cap (Holographic)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T23:41:16.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2003, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8jcewsfim13hla1ex6nlwucmp.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,nzFUAOCAFUCd9S-x4EKRwQ,Qx7nrZH-K06CgIoJYfFCCQ", + "FriendlyName": "Baseball Cap (Heart)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:53.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2172, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YHvgN-qla0-yF9DyWIkm1g.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,xPs8BvGgdUC1i_luWAfSFA", + "FriendlyName": "NBA 75th Anniversary Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:21.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2773, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-lxSQ88W0UOAP-8tne9rSw.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,cfiCxhJZ20u07X1x93tNZQ", + "FriendlyName": "Baseball Hat (QuinnXCII)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:27.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1186165494, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BJUjvFUapUqyNI-sHp5Plw.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,VB_2uEBs70-9MnLn3__upw", + "FriendlyName": "Skater Hat (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.492778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1712, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0jjswupors8a1sy83fp1oeipi.png", + "AvatarItemDesc": "ee9a7c8a-7383-4a09-b44c-474b66f9008a,", + "FriendlyName": "Robo Logo Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee9a7c8a-7383-4a09-b44c-474b66f9008a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:08.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1713, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "26f4i1hdx3pb8xotdo0vkhqr9.png", + "AvatarItemDesc": "ee9a7c8a-7383-4a09-b44c-474b66f9008a,W-xMu8n2CUuHSXfHvEL4yw,-apO3Y7om0GKhC4V_hVYdQ", + "FriendlyName": "Robo Logo Cap (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee9a7c8a-7383-4a09-b44c-474b66f9008a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1218, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9gmjkk497nr0pq75gtfcilfjl.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,", + "FriendlyName": "Graphic Cap (High Fashion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:10.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1219, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bfdnrz82ejhtasrdzh7azcyxx.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "FriendlyName": "Graphic Cap (Sticker, Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:15.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1721, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cllwjm7phajd4j2pjnk1lq87l.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "FriendlyName": "Graphic Cap (Buckaneer, Collector's Edition)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:31.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3cnf1qorsglce2vtvvj1nf4cz.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,cpTKHXjj0kulivZCKVQqiA,Mrf17aidXEeCF-fFURUUcQ", + "FriendlyName": "Graphic Cap (Hyper Ramen)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:20.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2221, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IDVmIRptBkSiYWCmC3zeVA.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,-C1ui-T280GEfW6-vPQ-ig", + "FriendlyName": "Flat Brim Hat (Basketball)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-24T19:42:09.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 39, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e9shr5go0vsw8gb8dght6710s.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,", + "FriendlyName": "Baseball Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 984, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6at7h1u119kqoj0j8z06fflxl.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,FVdDQFOMQ06teQm2ANDPng", + "FriendlyName": "Baseball Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:44.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 985, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ampb4pvmmtdhc5913do72skwc.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,FVdDQFOMQ06teQm2ANDPng", + "FriendlyName": "Baseball Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:46.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 139, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "703se3u45kueznyorg28dx2d4.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,", + "FriendlyName": "Baseball Jersey (White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 492, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b124kxp8adqvgs4bryq7tcrra.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,e5de2e45-207c-46af-9a5e-eb01a0c621fe,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (White, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:15.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 493, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "coknzs3avuwcbyl6z2drrxtd1.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,de0a5a3a-8aff-4799-8269-9efa3aeb383a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:16.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 478, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4ur6eolr90837mh7229goao2z.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-10T16:50:09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 477, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5iy6yfd9qvpfzdo6symnk32xf.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-10T16:50:08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1736, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e9d1k2tttj2ohpp10c8fp9uu1.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,LIgagNf4gk68EYQdlDruxg,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:07.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1739, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a6xlnnqmochthrsk0xmuemrpf.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,6fmUy748hU2dgwgaijSkUA,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:17.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1741, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "48ag7b3c5m2vy68rclwrb8km0.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,buuNFd8sK0eap78TL1gMNQ,-UFHTNvJs0CGDStb0PFkbQ", + "FriendlyName": "Basic Belt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:23.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1743, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "05r4o0sr1cfy4hmhwsw9aaygk.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,SDbpnjVYmEGQneK5fhZ0Cw,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:36.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1746, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1z2bq777lr7jukwio4h4ahhcr.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,uwoWPY4YjEmZSQcNCrx1Gg,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:42.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1747, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b5f3a10evhggitywma8wlxt69.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,COZbkLDc20mdUlWSkqMX-w,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:44.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 140, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dyz7t3e9p724lfy2lqohcvl0m.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,", + "FriendlyName": "Basketball Jersey (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 650, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2z5f34xrfn5i5rko8oh08suh0.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,59a62fa3-9e15-463e-9bd5-3aecdf604c1b,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:35.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 651, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5csh82iknt6zuuty86qfpx32k.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,4effd67c-a52f-4b41-a5b0-04287f91a7bc,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:37.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1728, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4nvt3kwtn2k367794r49ve87e.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,ojTIShRZHEOV1qQRfhwvTA,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:41.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1727, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "24n1sqxebe37pk9ina0smo1nn.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,se0-2ylLD0u6sqCVPkeP6A,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:35.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1726, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "86hv8nip4c5kphcs19ulpi7xv.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,RU-B71GUF0WPduoSOI1XMg,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:25.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1909, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "2nbi9m6b47r4wal4q24bnxfms.png", + "AvatarItemDesc": "0736e71b-a32b-4485-bedf-6fde8fe3b09f,", + "FriendlyName": "Bat Hair Bow", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0736e71b-a32b-4485-bedf-6fde8fe3b09f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:03:59.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1908, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "1snahzhx4gvw2hmv9vx8ves5r.png", + "AvatarItemDesc": "01d0fc12-3ebe-42e1-8caa-5513a40436c1,", + "FriendlyName": "Bat Bowtie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01d0fc12-3ebe-42e1-8caa-5513a40436c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:03:50.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1927, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7s85boxheb6528qy06bkgcocn.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,", + "FriendlyName": "Bat Costume Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:56:23.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2521, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "PImwtIHMIkmPg7xHGM7PXQ.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,GPX92X5n3UWMbh3C13oB5Q", + "FriendlyName": "Bat Costume Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:35.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2522, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6wBcnTssRk-eCxqSkfa3Mw.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,dE_ZWKPV90utoyWtRalh8w", + "FriendlyName": "Green Bat Costume Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:38.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2523, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "ERAg0XXt4EicefSqiFHYcQ.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,jIAMeDkA80GVHYTi0d9LkQ", + "FriendlyName": "White Bat Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:41.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1926, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "cheqz81i6ydobk59q4ybrjfks.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,", + "FriendlyName": "Bat Costume Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:55:55.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2516, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "lQoHL1kpXEKcBNWXhquQXw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,mnYzlq3Rw0-Q89gmc5f9XA", + "FriendlyName": "Bat Costume Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:21.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2517, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "kWsDoa_x1UmDw33BY2rMCw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,85pvpo4QqUSrdsbofGkoWg", + "FriendlyName": "Green Bat Costume Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:23.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2518, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "TP2iURkjWE2-TVyQJGWkVw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,f6XXbe_mhEuZJ9in5vDRGw", + "FriendlyName": "Bat Costume Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:26.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1928, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "5ox8ama5khsmyb0okqnvem5p2.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,", + "FriendlyName": "Bat Costume Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:56:39.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2528, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "R-4c5-nUAkebQ9WzWNLZgQ.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,oO_Ij0Vsvki4Fa6qonrf7A", + "FriendlyName": "Bat Costume Wings (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:59.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2529, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "-KAlFceGmEy5iT39k9-pNQ.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,IgocvfGIn0u_Jq4GP2_fJg", + "FriendlyName": "Green Bat Costume Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:00.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2530, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "DdcTk6htq0eP97IwF_w0mA.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,Q2BNGA-idUyHDPu7m-ZSnA", + "FriendlyName": "White Bat Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:02.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1819, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f4jxrcx7v17e4u9uskgyxjvje.png", + "AvatarItemDesc": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,", + "FriendlyName": "Sarong Swimsuit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:16.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2238, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "CbY0IoFre0CFmDX2p82E4g.png", + "AvatarItemDesc": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,PNnfMibAUkqetPThlhtBgw", + "FriendlyName": "Sarong Swimsuit (Cactus)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:43.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9cqrtp4vgci8mnuccke5s2itk.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,", + "FriendlyName": "Towel Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:29.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2016, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HTVSEzDsKUSnkvdGvplfvg.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,Hae8Pfe8mEWeF8mL9hC9dw,Vat8nL4ZBEK9mLABJiXS6A", + "FriendlyName": "The Amazings Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:40.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2015, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "S0ChF4sn40ewzPHkvLcNGA.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,kE9vr2bKoUSFm7AkcXSmew,5o-249dUdUOhW28lrJY82A", + "FriendlyName": "Valor Society Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:37.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2014, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2IWuPA8HGU6KrdC4ohARXQ.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,GAzH2pThjECghCmCwKJPrQ,4T-xj-xtSEiv1PDYv30URg", + "FriendlyName": "The Defenders Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:36.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2013, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "avkdszecpez6tb8kq3563bp59.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,pEWioft3gkOMWS4tZFping,gOw6NOX-QUegt4mxjtfpfA", + "FriendlyName": "Masterminds Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:34.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2181, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pyxx1jg0F0qzsZMmUcX81w.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,SN8HDRADZ0GvrFgEHKMhWQ", + "FriendlyName": "Towel Cape (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:39.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,Im55dSJ4mkmdmHPtX9tYpg", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1727884092 + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,adETWZLIA0O0feRzUjxKQw", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1454267065 + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,z7uu8CimMkCjsCGTDPCZLw", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1018984627 + }, + { + "AvatarItemId": 1400, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2jzqx1rojlzf9q9pado9u0t1o.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,", + "FriendlyName": "Brimmed Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:48.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1401, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "110fo1snzkucd8yr0l83dt96u.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,-XSM6ZcSjE-vrWL7jyF4fQ,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:50.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1402, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5tpxbss93q52nayhe1d5fpao8.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,RL6HkufaJ0yubTpCGVI9eQ,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:51.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1403, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7k8ttzh9wehwhm80g5p4gopa0.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,9tTuREkoQkW0ycqOjWXsiA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:52.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1404, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cijefzd028p1ktkkbndlk0iio.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,MVkY4sU5BkGC4QhweRiyIA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:54.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1405, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9bijtfphf1w416loccnpc2v4m.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,ux9DQVSRyU-_5GA-t197OA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:55.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1406, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bl1xc0woq0y8j4zowmiyc2wbz.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,t2bByt5oUEG90PMMePCP6A,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:57.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1407, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "340d5n3lde4t69b8l5kc2jupr.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,m7KXVnULz0iHvKfCFzFoyw,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:58.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1408, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7bp85mlgk50a43bna39drnbwj.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,gT0WA7Y1H0my5uMlteIMMg,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:59.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2093, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "CCiQ0TXblUORW6PKW2gHkg.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,Hd9xNn-UA0uxwv1axFVqGQ", + "FriendlyName": "Brimmed Beanie (Pastel Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:22.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2094, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "nJX4hvcYAEaOsI07CK_m0w.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,C8b4lTZ-TEOlCVv7Gs219g", + "FriendlyName": "Beanie with Brim Holiday Market", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:28.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2095, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8i3Shpwj70mbaFZ0_YazXg.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,3Xlg70ev6UqdcIAmJ9hmlA", + "FriendlyName": "Brimmed Beanie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:30.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2774, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ODGgLfHg60m9YanIDHhtGA.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,QCOMyeCUK0Gudx4ljBmAdw", + "FriendlyName": "Beanie (Musical Notes)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:29.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1389, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3kk89nprwfzgkc47ddr46nppo.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,", + "FriendlyName": "Beanie (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:43:56.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1390, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0si8omfozqsiv0jjns5uz7x3v.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,58YffnGSUU6umcI1i0RvZw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:43:59.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1391, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6go7wf3t618gcga8rjoywcwo1.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,c24f1DNZjEmqkt-FibKUjQ,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:02.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1392, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "clgnc91prro3ozkp7tnx5ld66.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,SeVmHtrMS0y7gKCYkHcrOA,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:04.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1393, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "59xbft3ixffciz9wybly1ufd4.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,uDcJZClcFkiNdkEEh9IIyA,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:06.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1394, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2rnnyghpth3r3etbu364c02mc.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,Iezz6gCFfkKdVcmPXIhunw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:07.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1395, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e4rjntdbfbgxnzzds63z8k3i7.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,fLWfBSEO6U6aHrDm3nKFEQ,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:09.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1396, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2ejkmb3z1znhlpdst7q3jztb2.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,F5vJzMfNB0Ob6Abjadho7Q,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:10.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1397, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7gi71ssytha4oltxzdg3vekb0.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,vbOUaJsGHEaSfQCivNcMWg,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:12.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1398, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5p5se3p4opi1nd646csh6pvsd.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,wbFPXIg5gUK3QK6YycZexg,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:14.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1399, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6w4cwzijj4bww69aju5fi9pzj.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,_fBrHCkTKUe82QgxCyI-Kw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:15.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2219, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zsqqOW8x1EqW9fjjiRFwTw.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,CJKB3OO2CkeEVsAK0jeeDA", + "FriendlyName": "Beanie (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:06.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2438, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7kcx60xKr0C5aWqnubk3Og.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,k-WZIhNJAkSpIWLxC2D40Q", + "FriendlyName": "Pumpkin Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:06.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2439, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "j73o6WaQmEubCEKCMVcsLQ.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,VE_cHMHKBE2Wuugoqj4rZg", + "FriendlyName": "Pumpkin Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:10.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 369, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1jj51yv6mj3i8duvbctn67g5j.png", + "AvatarItemDesc": "7553d851-1986-4325-870d-e425dfb7d007,", + "FriendlyName": "Farmer Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7553d851-1986-4325-870d-e425dfb7d007", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:09.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1281, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e8i3fa3rlphdsfofxs7sewwio.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,", + "FriendlyName": "Bear Hug Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:23.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1556, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6rqv598elyp668kqusa9g2dxe.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,OnZujqqU_UqwxmznvBBMfg,_zrnGn1ErEWR8WqrZmKZsA", + "FriendlyName": "Bear Hug Hat (Holiday Panda)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:31.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1557, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ujfxsx62pzx2i549y1zix2uy.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,XKoxnTXYGkSUu52nVS9dnA,TFOAk7kwh0uUgtsyDNxMuQ", + "FriendlyName": "Bear Hug Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:33.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2102, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CneUY-gB-U2whOHJXkONBw.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,9nPWmpM4mUmEpHfthqYeng", + "FriendlyName": "Honeybear Hug Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:13.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1187, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ubi3qjigszacxmdn65q4d2nj.png", + "AvatarItemDesc": "2786fc22-3d6c-440d-afcc-15890c061577,", + "FriendlyName": "Bee Antenna", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2786fc22-3d6c-440d-afcc-15890c061577", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:52.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2775, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TgcLGc1A7UK1C6_N3Au8WA.png", + "AvatarItemDesc": "2786fc22-3d6c-440d-afcc-15890c061577,dXz9B9OH2UuLNV35u8KB-A", + "FriendlyName": "Ladybug Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2786fc22-3d6c-440d-afcc-15890c061577", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:31.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1186, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3w6s9ko5eqhqffltdu2xx937j.png", + "AvatarItemDesc": "bb8602d4-f91f-4e35-9c88-1cdf3a04ff0a,", + "FriendlyName": "Bee Body", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb8602d4-f91f-4e35-9c88-1cdf3a04ff0a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:48.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1188, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ex0h92b7vqpk3ao6jhdf8acdf.png", + "AvatarItemDesc": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,", + "FriendlyName": "Bee Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:58.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2776, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NvZjNy5k-k6-IOXr5Mm2TA.png", + "AvatarItemDesc": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,USdUyjIzY0q5af3AeTFCOQ", + "FriendlyName": "Ladybug Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:33.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2816, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GTnJ7Ox8b0SRwSSDw8LESQ.png", + "AvatarItemDesc": "df29d81e-851b-40b8-95ae-8461f702b1e0,", + "FriendlyName": "Beehive Beanie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df29d81e-851b-40b8-95ae-8461f702b1e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:37.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d0s9wj7bjb9h01c8w7xwma53o.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,", + "FriendlyName": "Beekeeper Veil (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:48.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 696, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zg7qszxejhn7ysl2ofksqed8.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,1919d319-6cce-4500-8766-15fed6a13fa8,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:45.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 926, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2zodiht9cu9f4628u37rle1f0.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,lBe3yxuE40eBmTrnEHBqQQ,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:00.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 925, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "28ohmms47updexq8mqh9ey2ih.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,oZ8k0Qv3SkG-DkY0_dP7UA,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:59.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "67ho8qppd1uvf14zlw9l7bdmg.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,", + "FriendlyName": "Beekeeper Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:50.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2k4lcmck9k4oohw7zr54f80ke.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,1919d319-6cce-4500-8766-15fed6a13fa8,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:50.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4if4tnkvzu35yc8tyran086ks.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,lBe3yxuE40eBmTrnEHBqQQ,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:19.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 891, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bbjryyglvkoz6f7rpa627np9b.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,oZ8k0Qv3SkG-DkY0_dP7UA,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:17.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 710, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "36kccfcoypwfay2raguim9kb9.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,", + "FriendlyName": "Beekeeper Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:52.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 703, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dqln9eqsu4l7dk5wpe8ad2i8c.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,1919d319-6cce-4500-8766-15fed6a13fa8,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:56.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 927, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "82jnl4qpioioo5e84i408z96v.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,lBe3yxuE40eBmTrnEHBqQQ,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:03.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 928, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "egoh9oarmlgl1e2wh5az6t7vu.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,oZ8k0Qv3SkG-DkY0_dP7UA,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:04.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 40, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8zfbgqf2y36bwmt9v0iq15tuj.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,", + "FriendlyName": "Beret (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1601, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cpgewpuv8enuu5r81mj51rdpb.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,7_n11PF4-0KqTZ9az23hqg,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:21.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1602, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5sln9xpcozzr4gxw3l8dxk5cs.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,TKbjEeTWfUyA1u4laoULMg,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:23.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1603, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "61ormf3tp9m7rfze5jen89ywn.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,JShcoWWPz0297HeEUJbDCQ,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:27.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1604, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "16amrlrlhliiat2ld7vswaurs.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,_psc7WCgOk2JzRQwuFAqNw,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:30.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1605, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4b809ml4l1gchsyq9ftgm08bj.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,WeP_tPjUNUuGojXrVBOAIA,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:32.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2519, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r4KMHNh_x0-32HKgY1U3sQ.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,8xqL--ghKUunlS0r4ugCgg", + "FriendlyName": "Mushroom Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:29.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 41, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6bwvu5xxyxigc6heyrh7sw2bi.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,", + "FriendlyName": "Bike Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 483, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2mdqjp0pan01n0jy9wmab9gka.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,83522c12-9549-4d74-8c87-bae210bcd2bf,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:02.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 484, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bfdtco5996aiaxyo9h38yw0b7.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:03.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 850, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d8wwf7fmiptl4zd7nj52mqemx.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,XNq4Cns4lEGydtktdsXlmQ,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-05T20:03:02.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 267, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "28jeraj9pdlhoxmjt0mhkqvj7.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,", + "FriendlyName": "Bike Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 929, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "30u7o3e8jtfekidt2dw5uia5v.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:05.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 930, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "53455700060fde41p5z5thud2.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,XNq4Cns4lEGydtktdsXlmQ,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:06.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 931, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5tnpmoxpb4gujzn1apm2ywi3z.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,83522c12-9549-4d74-8c87-bae210bcd2bf,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:07.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2024, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X4W5cZITW06S0a_Bere_PQ.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,qspNifi92keI-XssPB5qDA,RVraJOPsOUqZjd8bEiPduQ", + "FriendlyName": "Bike Gloves (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:11.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 141, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "do070rgyuzlvn3h08jelsszce.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,", + "FriendlyName": "Bike Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 494, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1yuq0u2hdapelj37471lkep1i.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,c5884778-a87f-4612-9717-86fbb65d440f,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:17.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 495, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4guodx87w200pz0ev441o50w0.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,83522c12-9549-4d74-8c87-bae210bcd2bf,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:18.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 799, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "13zz3iuzmt1r8kfhefj6kqj9h.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,XNq4Cns4lEGydtktdsXlmQ,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:38.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 751, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3m8jywcsatm5rq2um5dbimatv.png", + "AvatarItemDesc": "n52um7A-b0utGxixd1ESvA,", + "FriendlyName": "Biker Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bae9d9f-3eb0-4b6f-ad1b-18b1775112bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:22.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 749, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dyz4zg15sdjewdf5zjojjk36p.png", + "AvatarItemDesc": "_qXaNbm4yEeyalo372QbvA,", + "FriendlyName": "Biker Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35daa5fe-b8b9-47c8-b26a-5a37ef641bbc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:19.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 746, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1shya0fxztg1hrivkr6kz5t5s.png", + "AvatarItemDesc": "VQnoLstCyEGDPy1c38-GvA,", + "FriendlyName": "Biker Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ee80955-42cb-41c8-833f-2d5cdfcf86bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:07.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dfffe31f-9f82-4622-a920-ea5f5bd26042,", + "FriendlyName": "Binoculars Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfffe31f-9f82-4622-a920-ea5f5bd26042", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1059720796 + }, + { + "AvatarItemId": 1682, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9gv2f7uy1uo763i2urf7qdtys.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,", + "FriendlyName": "Birthstone Earrings (June Pearl)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:24.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1683, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "awvrj98971hzwbeeg8i9zxo3l.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,mM7I9kDTx0ut243UkWKXvw,YKaJjmMHo02FWwem8nw1yw", + "FriendlyName": "Birthstone Earrings (March Aquamarine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:25.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1684, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ekdcofkpc4pqcwpk0rwxxdu21.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,46f7Ewe99E6tF2qFaMtQ0g,8IK1U23XlUOV-2ibVznkDQ", + "FriendlyName": "Birthstone Earrings (April Diamond)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:27.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1685, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4xaaxp8lgcjr1lrwq0jnqoxbt.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,R0f3hHGY9UqUEW4aR1fssA,KSqfmVTiGUKOKijwGDSwgQ", + "FriendlyName": "Birthstone Earrings (May Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:28.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1686, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2qnbcqo4a9vldg7eer5ofwli9.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,adwzLAQEykK8D5hZGQ6yiA,0Nca5nBUpUquFhExd5dxcQ", + "FriendlyName": "Birthstone Earrings (July Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:33.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1687, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3ap2lnn1cayd34hkw01ody958.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,ZsWc8wRafEGl3B7cr6UkzA,ByIfwCtDwU-p9rEfqpjfGg", + "FriendlyName": "Birthstone Earrings (August Peridot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:38.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1688, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4z8ff179kc1znimkl5hhclu3c.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,kQD41xK4dU6dI_6pRkP0qA,aSeusgYlPE6vOBu9Ua_dWA", + "FriendlyName": "Birthstone Earrings (September Sapphire)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:40.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1689, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "62x43h2lnvgu2pa5ubjuz4yuk.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,ro4sD9txVE29_eT2jK-wEg,0YF5bACMsk2sN4tixbJbKw", + "FriendlyName": "Birthstone Earrings (October Opal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:45.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1690, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "17kngr9n3ywkzhzibzp9gckn0.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,-YsxhNXLAk2qbeXMqwAqng,aNOhXS4nuUeqs0CVw6K7Hg", + "FriendlyName": "Birthstone Earrings (November Citrine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:51.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1691, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1o20o1igl0kxcykasd3bf7drr.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,imOiYzABkUm9C5EVzvjXgg,0eZVIsv7wUyZ0ohLmF1jGQ", + "FriendlyName": "Birthstone Earrings (December Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:19.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1692, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b4372447x75hrt7zmp36xexi3.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,FgLAEMh1e0GyHO8J8_xj5Q,iM-04HpG6k2N8n-P3XS22w", + "FriendlyName": "Birthstone Earrings (January Garnet)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:22.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1693, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "51ybcms7jhdsyx03odmfrhned.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,C9CZUwEZeUiuOue4CK4luA,gUxl7WNXlUqFYHZ9eGq1cg", + "FriendlyName": "Birthstone Earrings (February Amethyst)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:26.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 74, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "a1hubxyh2x781ngkvrnl6q4r2.png", + "AvatarItemDesc": "b861e5f3-fc6d-43b3-9861-c1b45cb493a8,", + "FriendlyName": "awesome sauce", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b861e5f3-fc6d-43b3-9861-c1b45cb493a8", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 221, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "c7r9nusyu8a3jy7ydrv4shwx2.png", + "AvatarItemDesc": "6930ce13-4be4-4ab9-9817-667bd261ffc3,", + "FriendlyName": "aaaaa", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6930ce13-4be4-4ab9-9817-667bd261ffc3", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 268, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "d5ga9td4yt4xjgrj7q9m03toz.png", + "AvatarItemDesc": "abc25091-ed5f-4c72-9364-fffeef1bc239,", + "FriendlyName": "STOP", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "abc25091-ed5f-4c72-9364-fffeef1bc239", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RWXXh3Ixg0KSe82fQIeKiw.png", + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,", + "FriendlyName": "Bladed Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:39.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2552, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "te1b6n09g0WjqI3yH5n3mg.png", + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,fWZekjhIxUWhxgVGTWrVsg", + "FriendlyName": "Midnight Gala Bladed Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:38.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,a7477071-b04f-4b9e-9fcd-bd44991e5f0c", + "FriendlyName": "Bladed Wings Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1516632647 + }, + { + "AvatarItemId": 2447, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uF47LVvccUucyESgIuygpw.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,", + "FriendlyName": "Blaster Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:09.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2471, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "oLjvlMCC2kuFc3_lVEfvEA.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,BizZfhG8ikOOEvAxN6w48A", + "FriendlyName": "Blaster Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:07.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2450, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6JCWD1J9U-qP46fezaoXQ.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,KfjLWNUbpEClo_qafEfUEw", + "FriendlyName": "Blaster Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:23.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2448, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KX8r_kEiAU2h5vaiH70O2g.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,zJKd2ZtIsUGCdHP1M0215Q", + "FriendlyName": "Blaster Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:16.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2451, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "P9xsKr4uU0mPWfnYZEGItQ.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,Adyl5MkwQEKK_MT5AluYkA", + "FriendlyName": "Blaster Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:27.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2449, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "JYKGsy8zbE-uVRh5F0aELw.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,xtgcJ8klHUyAYRntKhwCzw", + "FriendlyName": "Blaster Suit Hat) Blaster Suit Hat Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:21:20.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2452, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZQvc0FZMVUKVsARripsYXA.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,", + "FriendlyName": "Blaster Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:31.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2454, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HE3yVvkZXE294hudT6aXLQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,bgWM6wBe60q2mIHN5dPh4Q", + "FriendlyName": "Blaster Suit (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:39.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2425, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QVu4sV34w0mJ62ZfIhLifQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,cqYUoDpfkEi8Qp2DrpQytA", + "FriendlyName": "Blaster Suit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:21.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2453, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CY_yVMmPd0SsiIdZ5xLZfw.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,lRmNfk8vuEe4Z5ToWvWcTg", + "FriendlyName": "Blaster Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:34.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2455, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wa-FDCaLx0er4dEZFzygbw.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eQy0qwre-Ui7_sCCNuWCSw", + "FriendlyName": "Blaster Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:42.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2472, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yD1_cAEdh0a6Uf6vIWKVyQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eU2ePFNmbUCPoQUp1WCbzQ", + "FriendlyName": "Blaster Suit Shirt) Blaster Suit Shirt Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:23:11.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2456, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "zmUtvtALhEyonXMMbmhMLQ.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,", + "FriendlyName": "Blaster Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:46.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2458, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OlimAxzdbEWXp8C2t0UdRw.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,csLWYuunH0K9Ij1q1FoBsg", + "FriendlyName": "Blaster Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:51.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2460, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "C3a5a-EHuUeFBNVj8c4TcA.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,Zf0z5Ghkwk-QJJq4u7Fjww", + "FriendlyName": "Blaster Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:56.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2457, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a8n5oQkGBU6RKd7IECdwjA.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,sP8lTqgdk0WyuwQczT_ijg", + "FriendlyName": "Blaster Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:49.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2461, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0aPPOEDh1EqRvNey2G6Buw.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,dudShaXMYkaIZdZfyleoXw", + "FriendlyName": "Blaster Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:58.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2459, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "swrVud60vU-52WTaynUCqQ.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,3EOUuzxLSkSopjIohgQ-zQ", + "FriendlyName": "Blaster Suit Wrist) Blaster Suit Wrist Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:21:53.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1254, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "holidays", + "ThumbnailImage": "d57lxs90awzdwpbei5f4j6nz8.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,_ycDQ6RcyEGAntVlSDpPeA,6UBZuZQBG0eZtNmty2IGdA", + "FriendlyName": "Blazer (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:42.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1352, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "10hg6w8e7ui5q5yod1bb5dhab.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,7WrSbK4Ml0K2z-ig5HIUCw,9skD_xxLq0mpfP13f9n8hQ", + "FriendlyName": "Blazer (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:21.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1918, + "AvatarItemType": 0, + "Tooltip": "Available until 10/25", + "Rarity": 50, + "ThumbnailImage": "dwz0pxz74xy4zmr653hor487a.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,uCA4tnv9Mk2mX-NnFlj2Mg,NvlKJFz4IkKDliVOJKJYhA", + "FriendlyName": "Hallow's Blazer (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:20:50.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1929, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dswjk6ox1i8gqtdq0td0c43qy.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,YlgQOY1VJUuGzYxoTYAqGg,jhQ23U8Pmk2K2wfBdrEWNA", + "FriendlyName": "Snowflake Blazer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:59:25.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1338, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5g3kf0itlpuocmwiedgwzxbrn.png", + "AvatarItemDesc": "20b424a9-861a-4d39-a583-22d7b7348c14,", + "FriendlyName": "Bog Monster Hood", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20b424a9-861a-4d39-a583-22d7b7348c14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:59.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1654, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9uu4lgs1wp05xxg31w3ixcj1t.png", + "AvatarItemDesc": "20b424a9-861a-4d39-a583-22d7b7348c14,afnIqin4lkKKqWrvBvVagA,bCuW1YadE06p6v1dBHsx4w", + "FriendlyName": "Bog Monster Hood (Movie Magic Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20b424a9-861a-4d39-a583-22d7b7348c14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:50.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1336, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cdcrail4xl68zz5i57rfmgvcm.png", + "AvatarItemDesc": "dedd2e98-2721-4de6-92f2-9d399f34e8fe,", + "FriendlyName": "Bog Monster Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dedd2e98-2721-4de6-92f2-9d399f34e8fe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:46.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1653, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7jabhuhr3ufptld365gkfrn58.png", + "AvatarItemDesc": "dedd2e98-2721-4de6-92f2-9d399f34e8fe,2jj5lxx6Gka5U6j0cWjN5w,HQnd9m0Wi0upaXRNsE5HPQ", + "FriendlyName": "Bog Monster Suit (Movie Magic Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dedd2e98-2721-4de6-92f2-9d399f34e8fe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:45.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1337, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "15ql8y6860hwqib0t4k041550.png", + "AvatarItemDesc": "4e42e02e-33fd-4ff9-b74e-9bde26253c35,", + "FriendlyName": "Bog Monster Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4e42e02e-33fd-4ff9-b74e-9bde26253c35", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:52.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2462, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "QQLvWQYc7Uuml1bMBoD0UQ.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,", + "FriendlyName": "Book of Elseware Vol I* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:00.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2463, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "UV1LMH8or0KUAZAidj0x-Q.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,bpzKQ-B4_0KpMPf4ixTumw", + "FriendlyName": "Book of Elseware Vol III (Cursed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:03.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2466, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "vHLg9EajA02BxJcYdCE1Iw.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,14Gft1zCQECXPxPmiUc5VQ", + "FriendlyName": "Cosmic Cultist Book", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:11.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2464, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "kXsMoNdwF0e5eg7JqgZnMA.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,7ziZgKKqXkuyo9v77chIlA", + "FriendlyName": "Cipher of Elseware (Uncursed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:05.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2465, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "DFFMheRvREahgpIp_KezuA.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,hKkdXrgEQ0SStVnZsXu0uQ", + "FriendlyName": "Book of Elseware Vol II* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:08.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2734, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "BwziYgoOMkSILcJf4wBllg.png", + "AvatarItemDesc": "3b30adc7-a1ad-46ff-96ff-08e656396c23,", + "FriendlyName": "Boombox*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b30adc7-a1ad-46ff-96ff-08e656396c23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:35.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1549, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0rwlmd8pei5yizua0rge6be77.png", + "AvatarItemDesc": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,", + "FriendlyName": "Bounty Hunter Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:00.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1932, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7hb7tjten2kt2wn72jc6b3djj.png", + "AvatarItemDesc": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,szDKGCS30UCwXJJcVocFDQ,3v_zUVDTUku_iEf-olDk1w", + "FriendlyName": "Bounty Hunter Belt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-01T21:38:20.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1550, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wi7442sqsy6n251dpxoai2q1.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,", + "FriendlyName": "Bounty Hunter Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:03.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1976, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e82j0afggi0d70h9zv4jq8z5q.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,Q3ctsvvDDEy_S1TDiGdf6w,DobTmYhC7EigHmvYnCTm3w", + "FriendlyName": "Bounty Hunter Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-29T17:07:39.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ayapiP7cAE2QSEcl72wYQw.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,2N0meQR4GEGu9Yy61C0Lwg", + "FriendlyName": "Bounty Hunter Cape (Western)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:06.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1552, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1kkukibngh4tv7hnld1z6zmzk.png", + "AvatarItemDesc": "2ddd2c2b-596c-4022-a488-655b0db1c265,", + "FriendlyName": "Bounty Hunter Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ddd2c2b-596c-4022-a488-655b0db1c265", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:09.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1548, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3j8vqf9bf8mtlc1esp7pjg52y.png", + "AvatarItemDesc": "2fadddc1-babd-4cb3-8f5a-0e7c889c3785,", + "FriendlyName": "Bounty Hunter Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2fadddc1-babd-4cb3-8f5a-0e7c889c3785", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:39:57.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1551, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9sotdnybus40og7ig3za9imke.png", + "AvatarItemDesc": "9ffa6e06-5200-4e53-ac13-f8491aecc864,", + "FriendlyName": "Bounty Hunter Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ffa6e06-5200-4e53-ac13-f8491aecc864", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:07.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 42, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73yfl2xb8qwuws91gzirri5dd.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,", + "FriendlyName": "Bow (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 629, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3suvedjn7frrhme4rxyn56qtt.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,c9589974-c261-485e-b571-cb2f34fb178f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Bow (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:50.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 630, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8hxvd0blvnfweb21vtd3vyy7o.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:51.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 631, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2d53cleqqzz1la8h0m130s58q.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Bow (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:52.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1192, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ftlnff1zjo948sndiuh1x636.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,", + "FriendlyName": "Bowler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:16.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2206, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Z8BRM2D_akKVUKgLncneKA.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,YBpBFNAFGEunM5GX5lFxHg", + "FriendlyName": "Bowler Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:19.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2208, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "wztK_Auwy0SJicMfhG1M-w.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,VyqdOGaUnEa8WzccUIn48w", + "FriendlyName": "Bowler Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:24.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2207, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "w97a0JIhSkGDeHj7x9Tx8Q.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,Dvwz4YRu_ES_rl94y6FNBA", + "FriendlyName": "Bowler Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:22.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 812, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9mi7d2raqzh1s4hk8ya5hwblu.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,", + "FriendlyName": "Bowling Shirt (Turkey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:17.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 811, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5qcfg0698s80wf9u8vhxu0ykx.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,-k9_-2onrEG407or8t1RFw,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Hambone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:13.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 890, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uwainjpb5gco3zqlgd0ofvkm.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,GDvaUU10-EKh3pKUQ9tnag,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Runway)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:16.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 889, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73mnfpvyaf58qnzfmjq9gdmfp.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,kLyM-6otSUy2ql5SEmnF9Q,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Spared)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:15.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ewgjupwpbyyvetn7svdbg3pka.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,8xjU4GQ4OkaqGyad-ypwtw,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:20.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 888, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ib04jli4dwo8vm3pm3qmk46c.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,032pE8s9Ik-2CImFVDrSqg,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:13.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 887, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a8ua2rvt9gw4r4necvamkx7sz.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,M4765UHzsUeNTwH-Pcxp1g,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:12.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1161, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ddse4tp99yydkypotqc8d4hpg.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,FC8ZFBPt4EyTe7IYbv2Tpg,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:03:06.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 814, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2sj414tpbb69wiylcanfba7e7.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,ghM-uFq5-0Ci38AOHa9zZA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Deadwood)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:22.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 886, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e46hwhpywl6eqefwj0lqe1mah.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,Gtq8yRlt1UeFW8U-sc7FiA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Brooklyn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:11.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 885, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0t1wg2mro2gwc3d4flp6re5pk.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,XrSL-v2MbkK0md-FMOweOA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Sleeper)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:09.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 884, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "apdshw5awntmir8e59mptvuwj.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,KqjQ1xRpokGLKp9GVN1EFA,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Striker)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:07.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 883, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3lw7exr10ocq54wlzytnktw7h.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yrQCLXiqRUSwnuQRYX2Rsw,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:06.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 882, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f20xxqkrb5z55ft9ia6vtt5u4.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yZR5aX3gQ0WpRUT6cfK1cA,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Split)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:04.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 825, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2rglb359vpaaizusjkmzxzqiu.png", + "AvatarItemDesc": "GAWURQSqrEW4G5slrPMgZA,", + "FriendlyName": "Dude Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45940518-aa04-45ac-b81b-9b25acf32064", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:20.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 113, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0ua96avoonx4qcwxsrbdl8lqb.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,", + "FriendlyName": "Bowtie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 442, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1ipmq038lb20nmgpf0k7p3lj8.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:06.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 443, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ec965jtympxjh0juzh6uea66i.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:07.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 444, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ey39vqepkupw80nkatijbvxm6.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e69372eb-2fd8-4e17-a448-ee0be61a2c7a,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:08.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 648, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "7lwepk9pha13ajtddc8yjp6i5.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Bowtie (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:32.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1064, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "88tf66ieaq5s0d6ezi59mgoni.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e4IDo-fuH0C9YByzcX5k-A,qCIoeLQw-Uq-SKIetIgIpA", + "FriendlyName": "Soda Clerk Bow Tie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:35.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1250, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "holidays", + "ThumbnailImage": "6ypl01cto10al3qpkqm95fhe3.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,W4a144so90eRzCojUnDowA,rcuK1wZcXkCe0_LGX7E-Tw", + "FriendlyName": "Bow Tie (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:24.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2789, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DIaJORsIX0GaMSsklR3SxQ.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,wF-WrDaY_kmm1AzA20RsZw", + "FriendlyName": "Bow Tie (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:00.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,Q9TGwLgrgk2t8ba8WlpM1w", + "FriendlyName": "Bowtie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1726152636 + }, + { + "AvatarItemId": 1268, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bdv0ee6pbzi0i8z8f5n5zfc2n.png", + "AvatarItemDesc": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,", + "FriendlyName": "Bow Tie (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:47.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1615, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9oo75aubpgwd257zfkh94c7y8.png", + "AvatarItemDesc": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,Wm30oV5BtEuFQZFk3dPK_w,0R3Mh96tS0631EZYs03dmw", + "FriendlyName": "Bow Tie (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:50.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1354, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8z118afr4npaz1lqtevl4cq9f.png", + "AvatarItemDesc": "86af3a97-18d8-4dda-a2f2-cfd3b4224254,", + "FriendlyName": "Bow Tie (Green Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86af3a97-18d8-4dda-a2f2-cfd3b4224254", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:42.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1266, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8nu7ynhikwf10a7dnyr1bqmi7.png", + "AvatarItemDesc": "988e4069-4341-428b-bbf5-023309891385,", + "FriendlyName": "Bow Tie (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "988e4069-4341-428b-bbf5-023309891385", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:39.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0mfqwinkpsyr6llxo7yocofz3.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,", + "FriendlyName": "Boxing Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 437, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dwa5wuhnhdrvml7z2kour4vi.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "FriendlyName": "Boxing Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:05.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 881, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a9oypi8hweb517wrupg36vi1w.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,rzUd1IMTKUmW0HeMPSIOBQ,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "FriendlyName": "Boxing Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:03.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a39t7yom90yxeiwitw6n8iohp.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,", + "FriendlyName": "Boxing Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 438, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3tg5nlsq7otflej53cfartbv3.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "FriendlyName": "Boxing Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:07.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 932, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0to9e0lty6offdt6cnxeq1qpk.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,rzUd1IMTKUmW0HeMPSIOBQ,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "FriendlyName": "Boxing Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:08.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 43, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2c8gnq9h5a6jrn81ru4497q6i.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,", + "FriendlyName": "Boxing Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 430, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cwp28pmovv17mrz6o4w62ejpa.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "FriendlyName": "Boxing Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:57.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 918, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0oaynqpwbmosrcxdikxyfnnm7.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,rzUd1IMTKUmW0HeMPSIOBQ,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "FriendlyName": "Boxing Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:52.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2829, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "l6JiqgrjoU6Pe9nqG_A2Yw.png", + "AvatarItemDesc": "b9987d0c-0cbf-41dc-9a9c-8fdb5165800e,", + "FriendlyName": "Braided Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9987d0c-0cbf-41dc-9a9c-8fdb5165800e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-28T20:16:50.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 823, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9fef5u01k6k4fcki20zg26pey.png", + "AvatarItemDesc": "sL7iHCnjyEuQKzrTvciEzQ,", + "FriendlyName": "Bride of Frank Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ce2beb0-e329-4bc8-902b-3ad3bdc884cd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:52:39.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 824, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "239w9hcx69t1gd8xuj8wathwz.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,", + "FriendlyName": "Bride of Frank Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:17.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1528, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6jn69aku0kec2denwy9axd25m.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,YTR8qybyVkOhNh4iqtU_UQ,H8LXGRJj8UOuH5dil8L9qw", + "FriendlyName": "Crimson Bride Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:15.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6sLaNK31602XAlV-PLUz2Q.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,zNBLaOhCAEKWvgLK4P_X7w", + "FriendlyName": "Mardi Gras Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:10.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e7zfauqbveb04wiazlcnt3nna.png", + "AvatarItemDesc": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,", + "FriendlyName": "Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T20:59:28.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,97f3e262-8bac-4238-9d85-3872f293f204", + "FriendlyName": "Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1497412622 + }, + { + "AvatarItemId": 2290, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "AlSw_umA9E-_tWoN6PsFsg.png", + "AvatarItemDesc": "aeda4e5d-9cfc-44ec-b325-ab8b2d906a8f,", + "FriendlyName": "Bullwhip Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aeda4e5d-9cfc-44ec-b325-ab8b2d906a8f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:35.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 622, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1m40flxsxhcbfatcc8mhbk1kx.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,", + "FriendlyName": "Bunny Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-27T21:42:46.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1950, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4o8g6zqfewfnr504cvcbano60.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,wZC46s7Id0CUICN_XOK-Rg,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:30.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1951, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a0g5v9gzs5d8fpdvoeh8rvqbm.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,yrR3ogjJ50q6ypAnCF-ctQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:34.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1952, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b45xlo71b6cuovl9no7k2cacs.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,ss931x_Hy0-y1au-fT3zTQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:41.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1953, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "95hz7svaey618hpsf9c4or7xo.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,UQfLaKHzo0WqlRul_O61xQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:44.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 454, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e0ix17ip3n8ghjd6zp8tpdsz1.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:19.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 455, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "47orvw4rgb2dcq2uysth7k5kz.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:20.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 456, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9govgvnlf9gabvjvh5rckwqt2.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:21.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 457, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1eq0w8wx46ey92gsw096z68mj.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:22.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 458, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "863juzzox17unyjhagvnbslfh.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:23.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 649, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dftbch1qkdqq457hq33rhhz9h.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,0809a5cf-a0f3-4614-9f98-71518524518a,8212812b-e7e7-4f6d-aa27-d1112af45473", + "FriendlyName": "Business Tie (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:33.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 968, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7x53ntzd6jkkh7xk2h9lwypz0.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,31jelNWFyk6FlWt4HyPAlw,9zGwmle_yk25V-KnC_PJEg", + "FriendlyName": "Business Tie (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:13:07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1251, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "holidays", + "ThumbnailImage": "22ppyftjkt057kgxnmn84jhax.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,W4a144so90eRzCojUnDowA,pHw_MghIYUazSFve6L237w", + "FriendlyName": "Business Tie (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:28.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1902, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "f3b04h0f6kp8xvajy1fhvj8se.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,3svqbYmGt0uyVWnx7vJNmw,iLo1YhFY-06HNWgM6gkB7g", + "FriendlyName": "Business Tie (Neighborly)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:30:59.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1362, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7l8qp4zd4ankofzt9s3j5k8c6.png", + "AvatarItemDesc": "fc96ea5d-b49b-479b-a963-92f1eab44b2c,", + "FriendlyName": "Butterfly Wings (Cool Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc96ea5d-b49b-479b-a963-92f1eab44b2c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:10.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2105, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ypuue6OM0Si-gMkXnm_LA.png", + "AvatarItemDesc": "fc96ea5d-b49b-479b-a963-92f1eab44b2c,opDwtb3OdESsA2Zpfx7luQ", + "FriendlyName": "Moth Wings (Pepper)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc96ea5d-b49b-479b-a963-92f1eab44b2c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:17.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1717, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "345iaeu3y6o2ptwgo6fuggdrw.png", + "AvatarItemDesc": "ec5f5089-7154-425b-8be2-743954ea2d69,", + "FriendlyName": "Butterfly Wings (Crystal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5f5089-7154-425b-8be2-743954ea2d69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:55.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1364, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "99uel8r3j0b7wyrporms60tok.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,", + "FriendlyName": "Butterfly Wings (Monarch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:36.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1591, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1kwmi22q1osg0qzg3f3truukf.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,PTqpWqqU9k--lNlRrmlVbA,yViVKOLHc0qLiPw9Oy1Obg", + "FriendlyName": "Butterfly Wings (Candy Agrias)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:47.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1593, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4rdnvl3uo6e2xmx16jac16cg2.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,nODTglESMEC9tBY1k-LrsA,Do__oD53x0-dNtVeHsr4yA", + "FriendlyName": "Butterfly Wings (Midnight Gossamer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:15:33.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3kupnkadfcqst9wcpz79lvelg.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,1qHXl55Qn0qSb1gAhr8Wjg,GlB_scM-5kuc4A5TmGe6Yw", + "FriendlyName": "Butterfly Wings (Frontier Bluebottle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:15:25.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,10f0c6ce-319d-4231-ae88-446d33eead54", + "FriendlyName": "Butterfly Wings Monarch Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1092022807 + }, + { + "AvatarItemId": 1363, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4mbd5lxxwtmjvp5enggelazu8.png", + "AvatarItemDesc": "27c2eb7f-9fac-4c42-90c7-44e7a33d5303,", + "FriendlyName": "Butterfly Wings (The Blue One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27c2eb7f-9fac-4c42-90c7-44e7a33d5303", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:31.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "620934cd-aa73-42ec-97cd-65dd9eed1226,", + "FriendlyName": "Camera Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "620934cd-aa73-42ec-97cd-65dd9eed1226", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1929411673 + }, + { + "AvatarItemId": 2055, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "oROtFcbPVUKgu5jyR4cmxQ.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,", + "FriendlyName": "Oversized Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:43.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "rf7qGqus3keA_hz_CqguUw.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,bIQKJloWV0SEnBEtoxP_Wg", + "FriendlyName": "Candy Corn Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:28.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2387, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4wbSOraJ9UqV8L-NG1fvjA.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,kKpiR8YM8UG7xcaIgS9aXA", + "FriendlyName": "Oversized Cardigan (Forbidden)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:32.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2586, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2-BK_tv4akuy0tNJWv2PRA.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,W1LdNEXmX0m72Wr7EjJ_gQ", + "FriendlyName": "Oversized Cardigan (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:43:52.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1802535203, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pkxpJOu830etcGZdkiTlFw.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,oQspLDUDxEaiudqzXY7gbg", + "FriendlyName": "Oversized Cardigan (Painted Egg)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1320636581 + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,7d025ccf-f3b9-4601-9dba-9f25b135d11e", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1033755188 + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,2d035c24-8437-4403-a024-4c3bfd0d80a9", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1774245390 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1665457670 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,e5172c0a-3099-4e61-a181-f6b114b0a608", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1632878111 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,bab8193c-f67b-4022-834a-597cbf5dc7dc", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1186975696 + }, + { + "AvatarItemDesc": "5929bc1e-7ead-4818-a9ef-8e6d3bc885b3,", + "FriendlyName": "Casual Necklaces Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5929bc1e-7ead-4818-a9ef-8e6d3bc885b3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1738196188 + }, + { + "AvatarItemId": 2371, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "ZImIysD_H0S8AXDFmb2-vw.png", + "AvatarItemDesc": "1fab9cd8-257e-4825-8b7d-bd8bd281d362,", + "FriendlyName": "Science Shovel (Invasion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fab9cd8-257e-4825-8b7d-bd8bd281d362", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:26.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2700, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "n4mGv9jTj0GUDOgF6nFXVA.png", + "AvatarItemDesc": "1fab9cd8-257e-4825-8b7d-bd8bd281d362,q9hshPgwT0ijshnbKEBKWw", + "FriendlyName": "Science Shovel (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fab9cd8-257e-4825-8b7d-bd8bd281d362", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:31.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2086, + "AvatarItemType": 0, + "Tooltip": "This item may come in handy at concerts, afterparties, or in dark, forbidden lairs...", + "Rarity": 50, + "ThumbnailImage": "TU5LjIU2P0WNnRai4I1qHQ.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,", + "FriendlyName": "Cat Ear Headphones (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:05.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2098, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QRxUhTvrak6Pv3s0nde-8w.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,etbo7k9dq0OyjIrNS4sdkQ", + "FriendlyName": "Cat Ear Headphones (Neon Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:02.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2097, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ov9bwV4XmEuoyMkR3zIVgg.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,7Ww7mumWXkiyj0R3u_-hrA", + "FriendlyName": "Cat Ear Headphones (Glitch Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:02.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1207385977, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Cr0h8X1hoUuexTHM501mLA.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,8br3f77_CEiE2Oetiof1Pw", + "FriendlyName": "Cat Ear Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,8606473d-8d58-4e93-8123-3d6cd1d47ad4", + "FriendlyName": "Cat Ear Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1214875831 + }, + { + "AvatarItemId": 44, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a78j3mr78eeqcg6enr7t2usio.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,", + "FriendlyName": "Cat Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 712, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ee3wf3szcjj03dklm3r2yu3qx.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,37f8378b-b98c-477a-b8c9-9340772800b5,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:11.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 713, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e4o0sq9fto4a26lfver3ezwyq.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,713781e5-ce47-41c6-a24f-94d42a565e30,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:12.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 714, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cs7qiwunx61p8789gg5w4alhp.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:14.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 715, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9k2zja3ouictarzennr87c6lx.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,c1e430c5-fd8d-4032-9e3f-8d87cc8784da,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:15.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1750, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "18xiejb6mr7ln2d5ix9xk4i7f.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,", + "FriendlyName": "Alley Cat Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:28.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2174, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "319gh5kstwntozibq3ylve24n.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,dr6MrV8O30G0i-c7peOoKQ", + "FriendlyName": "Alley Cat Ears (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:25.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2173, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "432bfm0lzuskgxgwj4id7tebe.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,UcK6aipko0iE4THJUO1TvA", + "FriendlyName": "Alley Cat Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:23.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2175, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "10khixt04h6x1ktssnlbwt88h.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,9yNuA59JO0avaKVA6YxQyA", + "FriendlyName": "Alley Cat Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:27.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1716, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7c9vakaf9hozc3kfkfgvgows1.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,", + "FriendlyName": "Cat Hat (Calico)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:20.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1715, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "825i9txrb63ysexwqx6avbebh.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,V1M2-FXZSEG782d6D0AhUg", + "FriendlyName": "Cat Hat (Burmese)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1714, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4c5dywgjhkaf3fk8o08x91iz8.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,wNde5lsob06eQxEH2t-zUw,5KeLrnvaKEWnEiap1xzmjw", + "FriendlyName": "Cat Hat (Tabby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:14.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1942, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5xbgkkfn1legwnjtvtt5uvco3.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,T-JZqXUd3kC1P8L5Pu4ZmA,ao-eeUnb8ECcW2IFGuk76w", + "FriendlyName": "Tiger Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:27:29.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2479, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GsDtJeTdtUqMwfH0lH8bfg.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,NaXiRmxgkkGK1U7IQ4yn8w", + "FriendlyName": "Grinning Cat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:24.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2480, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HbIV9ocBIE6QOVO0wQaSrA.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,VeZQlPI72E6e57IQqAOq6w", + "FriendlyName": "Grinning Cat Hat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:27.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2467, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ufrfw4K9xkuYsOmmzZdOVg.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,", + "FriendlyName": "Chainsaw", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:13.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2470, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U51KTR3VHEuTJ810dMgdVA.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,qWS_znZq2km_MUGvv5QDHg", + "FriendlyName": "Red Chainsaw", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:22.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2469, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01j_eNlSvEmV2Czfp-GEwA.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,jCfhcP5ftk6_ruDnFjIhJA", + "FriendlyName": "Chainsaw (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:18.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2426, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DB_j3Vtx0U-xhO9yu4xlIw.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,5p1Hwa8IE0KIuucb7hV16w", + "FriendlyName": "Chainsaw (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:27.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2468, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T6Ig5zJQmkWoduzZdbHTKw.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,ZqWSyRnr9ECIvoU5yFyqXQ", + "FriendlyName": "Chainsaw (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:16.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1852235451, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "6uJcUEMxpEG8ljDFGcByMg.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,odtEahcf10isGbVQCyc2bg", + "FriendlyName": "Science Chainsaw (Restoration)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1982845644, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "a03e9576-acfc-48d1-82e4-4d320d151a50,", + "FriendlyName": "Chameleon Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a03e9576-acfc-48d1-82e4-4d320d151a50", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 142, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3josy4gpalnfb4bgbv15krqjb.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,", + "FriendlyName": "Cheerful Dress (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 431, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bwdc3w9brd906m9tlr3iwesz3.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:59.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 432, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1hw2ja5gln6hu18squbodnpaa.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,faedf215-289b-40f7-90cc-f5b98517d133,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:00.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 433, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aseuozf59hxlehklnyniqbre9.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,1773d5d8-4714-4721-b30c-97e25d6f2a5a,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:01.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1598, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8z3e22ju1mrd78t30pzbm5z5l.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,05K7dmjigkCByO2ufw9jtw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:05.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1599, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "56blq2erf400cehs8x4zcnwrf.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,yOw1WqEN0EWqR9t53EsQpw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:08.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1600, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cs5izn6ddok4ol512yitiw3cs.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,qoVZuGa5NkOmyfikHjDhXw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:12.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1366, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eum5bolzoy8k5w31k9j0rtzcj.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,yDZFr27Zq0-JH-EsEsxG2g,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:44.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1367, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eppwkjhvfbuczv31sgbty26v0.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,PLRhiOhhb0KhatFH7f3pwA,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:50.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1368, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8q1o0y04vs1ve8bpxz6iqkwui.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,RwgnKTfMEkWrAp_OMGy__Q,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:56.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1137, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e5wnr991h4k0p24z7l5fzcchr.png", + "AvatarItemDesc": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,", + "FriendlyName": "Chef Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1573, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6da55yu9ap4a2qw81ndsgake8.png", + "AvatarItemDesc": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,PR5dh2jf2kqGLO0lXgA69A,-lYK7LhYyEy6wqL1tIE9cg", + "FriendlyName": "Chef Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:48.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1141, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5bz83i3j02o47w4zlz433lomz.png", + "AvatarItemDesc": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,", + "FriendlyName": "Chef Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:35.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1574, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9lxxqrqcchmxcnzz3uf7de4kv.png", + "AvatarItemDesc": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,PR5dh2jf2kqGLO0lXgA69A,VDn1hO6ahky0GDJt5dRmyg", + "FriendlyName": "Chef Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:55.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ogxBM3YvhUu3E19PxYAYwQ.png", + "AvatarItemDesc": "9f2cf07e-f98f-48bc-9c74-784ba4b94527,", + "FriendlyName": "Chess Knight Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9f2cf07e-f98f-48bc-9c74-784ba4b94527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:15:33.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ZkEpc01eUGaW4hzohQxlw.png", + "AvatarItemDesc": "c2362f96-0a73-451b-9a85-46410eefd2ce,", + "FriendlyName": "Chess Knight Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2362f96-0a73-451b-9a85-46410eefd2ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:15:35.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1751, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "az5ok7syap6nxgjegr2yu68bu.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,", + "FriendlyName": "Chicken Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:41.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2500, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_inbDi877kyEOl5VH0d_AQ.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,wl3nqp4FsU2wqEkYFqv3_Q", + "FriendlyName": "Chicken Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:44.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2499, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PO9FzkR9AkerfmOa9FPB6w.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,4v1-TIIgnU-sg5ROppnEPQ", + "FriendlyName": "Chicken Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:42.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2501, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Gh8G1oJ4Zkmt6qsBqaj_XA.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,AvIuVibXj0u7UzWFh90_LA", + "FriendlyName": "Pink Chicken Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:46.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2502, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aRh-bOy1702NIh5iAUUksg.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,fF9qwnew3Eao2gF0xcOqrw", + "FriendlyName": "Chicken Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:48.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1299568996, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PPwy0yri-EypHFSutVm7-A.png", + "AvatarItemDesc": "0058a7c6-04ca-4931-b2ff-03c46f99079f,", + "FriendlyName": "Eggshell Chick Beanie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0058a7c6-04ca-4931-b2ff-03c46f99079f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2870, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DI6H0HT2tUqy7jNzWn9WDg.png", + "AvatarItemDesc": "6a51d8e1-8844-4999-ad8b-06f4d414571e,", + "FriendlyName": "Eggshell Chick Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a51d8e1-8844-4999-ad8b-06f4d414571e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:30.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2871, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "iF4tjDkdYEe3NUEZrY7znA.png", + "AvatarItemDesc": "9eb95934-90f3-4f48-a6b4-d87d75d9bdc4,", + "FriendlyName": "Eggshell Chick Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb95934-90f3-4f48-a6b4-d87d75d9bdc4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:34.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1965, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5gq339wje3p8y6ppfyku4gwyt.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,", + "FriendlyName": "Chunky Scarf (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:14.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1966, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "015zn2g9spl2iit7pplji0g62.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,RvVVu7gWUEmofuJiZqevlg,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:19.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1967, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a3r79cjrswbxqhnvo4sy9xfav.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,g4_xqu9o9US9lIiBsn2BKQ,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Olive)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:22.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1968, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6cyfecuvuiz6xxc0mom9o8a12.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,09zFwIT2AEaQsijLJR2aSw,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:25.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1969, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2wll5s9pcyyfdicg3i1sgsbf4.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,mtIyC_me2EuHSur1ghsKDA,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:27.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1891, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6qgeth2im91d0g6kce2zm8k7h.png", + "AvatarItemDesc": "b1ca1dc4-6867-48b6-a332-4efff06a5db1,", + "FriendlyName": "Cleric Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ca1dc4-6867-48b6-a332-4efff06a5db1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:10.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2yzkfucefna1jy3vuypzb7b3y.png", + "AvatarItemDesc": "b73ae23b-c911-4b09-a803-40999bdef6b8,", + "FriendlyName": "Cleric Headpiece", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b73ae23b-c911-4b09-a803-40999bdef6b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:15.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1893, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ugmcn0xr6htk34ah2hhep52p.png", + "AvatarItemDesc": "1bd891b1-6ec0-4227-9ed8-73c59af4f819,", + "FriendlyName": "Cleric Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1bd891b1-6ec0-4227-9ed8-73c59af4f819", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:25.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 45, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2q5g72y38c6wxtl9ob1g6osjo.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,", + "FriendlyName": "Cloche (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1634, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3ft3mh0blr3stuvbs8p0kyll2.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,w9I-I0GWRE-3RgOUMMlHjg,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:43.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1635, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8dwxff103s58nebb8y2kbn194.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,bmi_S166JEGWhf_7woVL0A,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:45.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1636, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b8f53f91dy30khotxt5f1ktnl.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,dSEiNUAZnUmErO5Vfv51zg,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:54.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1637, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0sm192b6zl94b0y64i6sh5wpn.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,jTG-jnKn0k-Etg_-4Gb9gw,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:01.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2473, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zNy5rjlaD06F8Uq9ifQuGg.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,", + "FriendlyName": "Clockwork Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:15.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2474, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0LdcFFwAQkSh1FSiaDwp6Q.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,0EjPIiuYpEGIWup3pKeC8g", + "FriendlyName": "Clockwork Shoulder Bag (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:17.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2476, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vd7QP1vfykeRmhB2_5D88g.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,dkJNh15DnEuO1BatzpI_uw", + "FriendlyName": "Clockwork Shoulder Bag (Rose/Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:24.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2427, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XOVkcyCEQUWRJU73kORscw.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,XMAOwHGXVECoeKoWb_-luw", + "FriendlyName": "Clockwork Shoulder Bag (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:32.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2475, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "A6d5UocVhE-gR4dNG396UA.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,GAWBaCDBzUqdyTGA-HPczQ", + "FriendlyName": "Brown Clockwork Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:22.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2025, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ES_b9-XkfEGzWShw1v1EMg.png", + "AvatarItemDesc": "06adb976-ca7f-4b8b-8b72-2e03c8bc10b3,", + "FriendlyName": "Clover Head Bopper", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06adb976-ca7f-4b8b-8b72-2e03c8bc10b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:18.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "aeadad3b-b83a-442c-829a-3eaccd9726fd,", + "FriendlyName": "Clown Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aeadad3b-b83a-442c-829a-3eaccd9726fd", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1199232272 + }, + { + "AvatarItemDesc": "6dcc5a53-621e-4f8c-97ed-e7fe42fc86c2,", + "FriendlyName": "Clown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dcc5a53-621e-4f8c-97ed-e7fe42fc86c2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1491263048 + }, + { + "AvatarItemId": 729, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bjg8u844kgyz1lqasebmtopqf.png", + "AvatarItemDesc": "TcgHS_0aPkehrBg7ytMOMQ,", + "FriendlyName": "Clubmaster Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b07c84d-1afd-473e-a1ac-183bcad30e31", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-11T22:30:20.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1748, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6rf1m7pd44oq6kn8h6yf9xeqj.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,", + "FriendlyName": "Overcoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:43:55.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2213, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fZkRsxu0GEq0HJicKi0Gxg.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,_3hxdSy9wU6OUPZJA9kGmA", + "FriendlyName": "Brown Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:32.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1096982540, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FSw9DUpPzECdOB8T9OOx4g.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,B5t5RbzKdEObKDlXLcRp7Q", + "FriendlyName": "Pastel Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1199022146, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QqL58lbLCkSvxjPln1rEsQ.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,OaOC278jEE-owjtDP9ZLow", + "FriendlyName": "Pastel Yellow Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "G1k3m0JjFkWpKsbWb3D4eQ.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,J36t8RwGPUqtLBsF-_UtEw", + "FriendlyName": "Pastel Purple Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1010, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4t0t0evfwd04sjvdwb35fvj5n.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,ojvIFCTpJkeegestF065wA,10fYSIBytk-yUjveDa407w", + "FriendlyName": "Collared Shirt (Plaid)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:36.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 158, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7k5gf8gh1bklveyos3l1ydl68.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,071d1bf3-86dc-4a31-b54a-e6579fab8649,d015cae7-a905-49e4-8823-6dec069689a6", + "FriendlyName": "Collared Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 166, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "b3stfwojthp7css2c8xqi5n5y.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,48abd952-214f-48b2-a8f1-1146f6f69aa2,c2a91a10-2f5b-4ab0-a79f-51408cf4b047", + "FriendlyName": "Referee Shirt (White Collar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1042, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "29urcstxxxjo9a30k66pvi43j.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,48abd952-214f-48b2-a8f1-1146f6f69aa2,RNN0ejjlm0yqGDcEuUkLjw", + "FriendlyName": "Referee Shirt (Black Collar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-03-11T21:45:58.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2026, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cjm3isfczhcb0g2sceven3lwz.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,n4SdiVms4kaqdjXdbjRBbw,YEMLBlOWsEO4tELHseZ7LA", + "FriendlyName": "Referee Shirt (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:22.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 656, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "57y04gj7sxv3ffgs9mbnww3wk.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,", + "FriendlyName": "Conductor Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:17.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1553, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "augnnz7p6ycztoyhc5w3sitrn.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,jwXdjDbEmkOFyKZHT_RYOw,YJGsYfwkik2lp7qWqXyXTw", + "FriendlyName": "Conductor Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:48.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1562, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f11gnls9uil36r3thydrn8hk1.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,LdKaVAveokmp3qRwVBxnFA,YJGsYfwkik2lp7qWqXyXTw", + "FriendlyName": "Conductor Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:34.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2263, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6-G1yAklOU-Vz5BRJzhaAw.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,e-oZt-PbKEOfawVammBQTA", + "FriendlyName": "Conductor Hat (Striped)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:12.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 657, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1xwbdqjl5uws8zmxy544nzuk6.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,", + "FriendlyName": "Conductor Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:19.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1554, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6hvy6jdqfz4iasmq7xqwcfpgr.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,Chiz8maLoEGDpZOJZEtdQg,cQUquXMk5EGt0VUCuPKBgg", + "FriendlyName": "Conductor Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:56.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1561, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "16dvdrgwodptixm96hemhraul.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,2axtF2iOGkez1kfRRGUbYw,cQUquXMk5EGt0VUCuPKBgg", + "FriendlyName": "Conductor Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:21.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1373, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ptrsoi2msj3q9zp0nv3opjho.png", + "AvatarItemDesc": "5154be09-bb8a-4917-95a0-51c91b6a4e09,", + "FriendlyName": "Hatter Suit (Wonderland Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5154be09-bb8a-4917-95a0-51c91b6a4e09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:11:11.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 658, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3jwqqbdhd5j68gzti0f3ov9d8.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,", + "FriendlyName": "Conductor Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:21.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1555, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "btsztyyman7ydhbo8ch7havr5.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,5Dbc8qCaY0OXEuwykM4naA,awWjleyNP0akWNYJoehCLA", + "FriendlyName": "Conductor Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:02.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1560, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1rj6j38s3tlgfuz9dlk7rlcbs.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,HBt3ToOkUUereoPKwyvzuw,awWjleyNP0akWNYJoehCLA", + "FriendlyName": "Conductor Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:18.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "asrlf9nb0mfzjx6vbj87jqc2z.png", + "AvatarItemDesc": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,", + "FriendlyName": "Construction Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1914259171, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "2F2USYfWgkewvbwK6oa1WA.png", + "AvatarItemDesc": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,uym5X1GZgUSzVb8U-pwlIw", + "FriendlyName": "Science Hard Hat (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 223, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7qj8vsrrhy3lfmq74f8h4kp4t.png", + "AvatarItemDesc": "e9913b06-58cb-4b90-bd08-981a3f19f352,", + "FriendlyName": "Construction Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e9913b06-58cb-4b90-bd08-981a3f19f352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1651838298, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "pfFTo8CNSEWIT3J-S7fQHg.png", + "AvatarItemDesc": "e9913b06-58cb-4b90-bd08-981a3f19f352,QXtBrjnRckKJGiRtsy4AVQ", + "FriendlyName": "Science Hi-Viz Vest (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e9913b06-58cb-4b90-bd08-981a3f19f352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 270, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4w02bnkzhi1ylgnq3yy60xyag.png", + "AvatarItemDesc": "3bb50a88-09ae-48c7-a42c-df5879680eff,", + "FriendlyName": "Construction Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bb50a88-09ae-48c7-a42c-df5879680eff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 109, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5xt4rvf95k2jm6bpmmwurfs07.png", + "AvatarItemDesc": "3e139dd9-4757-43a8-8546-8da70dc17f0a,", + "FriendlyName": "Ranger Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e139dd9-4757-43a8-8546-8da70dc17f0a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1921, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b8r15kxxhpkjnd8grlinjcsqd.png", + "AvatarItemDesc": "f2db0ec1-e536-4218-8385-dc6913941880,", + "FriendlyName": "Cosmic Hero Gauntlet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2db0ec1-e536-4218-8385-dc6913941880", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:06.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1923, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5m4wvktjw7xe4tdyw0cs3p4wk.png", + "AvatarItemDesc": "3739a9e1-6dfa-45d2-af54-cc83a58d436a,", + "FriendlyName": "Cosmic Hero Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3739a9e1-6dfa-45d2-af54-cc83a58d436a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:15.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2743, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6daz2-CA9keE0A25Vugkvw.png", + "AvatarItemDesc": "3739a9e1-6dfa-45d2-af54-cc83a58d436a,GpYghupLTUK9cuiL-0uHYg", + "FriendlyName": "Cosmic Hero Vest (Rhythm and Rooms Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3739a9e1-6dfa-45d2-af54-cc83a58d436a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:55.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2005, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ews3jw7aorj2ii9jvvnd7rr81.png", + "AvatarItemDesc": "19984948-b6b9-4d3b-8562-d0624930df6e,", + "FriendlyName": "Alloy Armor (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19984948-b6b9-4d3b-8562-d0624930df6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:12.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2099, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "o9dqbqFcv0SWG6AF4UzpWA.png", + "AvatarItemDesc": "19984948-b6b9-4d3b-8562-d0624930df6e,Owi_JROR80-apNHw-4KNcg", + "FriendlyName": "Alloy Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19984948-b6b9-4d3b-8562-d0624930df6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:05.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202152364, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "11462b96-dd62-4057-a187-49d8322c009a,", + "FriendlyName": "Costume Tank Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11462b96-dd62-4057-a187-49d8322c009a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 832, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "clebbebesxjzf8wnllqxmejvg.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,", + "FriendlyName": "Aristocrat Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:47.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 841, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "80vyoqh3t9fmtu13lcn8r3h87.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,i5RKLo-RbEqisnGII1wpaQ,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:46.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 842, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "40s4vjnqn68azxmiparci9gtm.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,IzkFARlv6EenmrfcNkcOuw,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:47.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 853, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3ky2r4usf4c9jpd0da9kxr9az.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,LyVf9lnzdku1F4iPBGqk-g,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:58.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 834, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "axv14g3ocxvf01h82ybehl3rg.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,", + "FriendlyName": "Aristocrat Cuff (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:49.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 845, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9zr3klyo948zva87e3ynyoij8.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,i5RKLo-RbEqisnGII1wpaQ,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:51.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 846, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01xxmh8nnuu5xc4oom2f1r1qj.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,jwEB6LyEaEekqtPNRc6TVA,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:52.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 854, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "due9stf9zpfkmdoblb2wlfcuk.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,Wj7I2YiVoE6zp8J_HYs8tw,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:59.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 46, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4jywgtwnb8sydqwmy9qd6aszg.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,", + "FriendlyName": "Cowboy Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 47, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0sgisiuk6yb2x3qtliioxly31.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,624c85e5-095a-48b6-85fb-c9bbade19a8d,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 48, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d9rwabvsu3fo05lmoaqnv6udq.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,192ba73d-6990-42da-8fe0-914c2a7181eb,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 49, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ioaonefjjjvtyvufi87rptrp.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 917, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5p88yvfhaz0xqx9u944krzm90.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,teVKVbVEsEyqOSuRdqHQzA,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:51.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 916, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "55ecravwda31t7zdi490k0t6u.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,K5vPqD3BrEuODRDzFdC6Hw,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:50.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2229, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hFGaKfD6BkyXWpL9Z29Htw.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,_6LmNp0uX0Ky4jp2j62U2w", + "FriendlyName": "Cowboy Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:54.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2230, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2nwp59be1lvbayv88iqb1rr4k.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,cNarx7yecE2gk-7mYaoHwA", + "FriendlyName": "Cowboy Hat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:00.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2308, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YisZ-46Na02k8dv191Spew.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,AEqCasEqOUCgetoTwuMTug", + "FriendlyName": "Science Hat (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:59.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2701, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0b-BFgiGEEer1cPxNaI-YQ.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,zoUmQIcOrU6jgBu_0urrGA", + "FriendlyName": "Science Hat (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:32.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 168, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7p83fzhqxxbxo8vh3t9otc9e1.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,", + "FriendlyName": "Cowboy Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 169, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ev3mq9lpdefdivq6ayo76ak58.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,192ba73d-6990-42da-8fe0-914c2a7181eb,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2l9uggmozvea8gtpv9ytjfglp.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "digjcfruws97b6pe7orkqjunq.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,624c85e5-095a-48b6-85fb-c9bbade19a8d,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 898, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dg66aywux3nkmv6q4rwlf4uwy.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,teVKVbVEsEyqOSuRdqHQzA,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:27.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 897, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7bsroab0vzswtlgqzzd5mijnk.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,K5vPqD3BrEuODRDzFdC6Hw,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:26.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2225, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RxR8AFtkNky4E7NUQNbCCQ.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,ODOnI3iYoEKrlQfFabbvoQ", + "FriendlyName": "Cowboy Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:11.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2226, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kxjJxDcwh0u3MPgS6XdCzA.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,BW5oLDA36EK9nLzTm7xViQ", + "FriendlyName": "Cowboy Vest (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:25.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 271, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "aq9fxwenux4k6wi50q7r0yqm9.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,", + "FriendlyName": "Cowboy Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 272, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "408o1nfsbf7uwx8ji3868l7j4.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,17e44296-1cd9-4f8e-9aa7-3837dc69279a,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 273, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "74jyglxoijr47llzzcuyhnhe6.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,c4ebfd1a-fcda-4b98-a151-a941b1801867,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 274, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2abrokbv6i5wmyx0dj57x43zo.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,c0c33a35-494c-4a1a-aa53-d27d9b9ef5a4,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 933, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5lr5c7ooc5e1x5c8f29zbk048.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,wat8f6fs80KptZbW39e8tA,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:09.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 934, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b8w1fmsevjv9eaqf2yjooy8ta.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,wkqKPZmnoU-8TXSWzi2-Ow,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:10.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2234, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "fGnMN9gdtUWyx2MEQZbOrQ.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,tOlpTL8F1Ui_akCjHq5vRg", + "FriendlyName": "Cowboy Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:26.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2235, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "qy6K5v706kGJUmNb4oGH0A.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,rfIzQjxeRU60VdRQXG9Ccw", + "FriendlyName": "Cowboy Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:36.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 957, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6ck4di8cgqlprwg40uowlar1a.png", + "AvatarItemDesc": "vpP2ZvnL0Uy3n-I_Jv3eeQ,", + "FriendlyName": "Cowboy Hat (Hairy - Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66f693be-cbf9-4cd1-b79f-e23f26fdde79", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:09.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 115, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "21rygk1c1r8ccciu1jlzdsan4.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,", + "FriendlyName": "Bandana (Flowers, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 445, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "72m3n2l0f0v5ryummqatwer5t.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:10.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 446, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8pn1j2wbx9ivldclxa1ubgufx.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:11.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 447, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1prcefvfnwi7yewdgbbeapboi.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:12.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1575, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7756o1qqkod0e7anndh2zr867.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Cowboy Scarf Mask (Plaid/Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:45.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1576, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "37j4pw9ip3kccqd36l4be3uw6.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Black Polka Dot Cowboy Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:49.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1577, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5en0adt83zfaj5j1bemmxhldj.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:54.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 448, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9xy3gwxnxiy9pqpf90r0hq3kc.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:13.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 449, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5653hakuvc2f7zkqwh2cln0lx.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:14.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 450, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dh0x7lyldgb793zodhe1sscg4.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:15.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1578, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "exyzdp83a10yb68pl68bc1t70.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:25.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1579, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e7ru80lk5fp2s35uvhj2a8m45.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:28.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1580, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "18r0xqjozrmq3c6iyz17zdqhv.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:30.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 451, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4as6ol0a5z9gfyofshfg9ed39.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:16.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 452, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "59fpaqmmt45bv0ojtuamhjir0.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:17.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 453, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2mmw0lzovs5bx1dwz8tid3cze.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:18.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1581, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "a0g2oy7wel38y6w7abixqi1zi.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:33.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1582, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bv2g4hsg5h2czu7ouxhuzozj9.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Black Plaid Cowboy Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:35.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1583, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "99zusz6icmt0hmbe2pk2wulet.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:37.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2224, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "MUhkmM6xxkKXejZd4iDh6A.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6o6sNWyPoUCcKJBHW0KC9w", + "FriendlyName": "Bandana (Flowers, Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:59.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2223, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "U-3blhOO3kWIzeZFNKXgTA.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,X1OFXyDyXES3CgMpIyV3nQ", + "FriendlyName": "Bandana (Flowers, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:46.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 110, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e01eh3p4tl9606u818mhsjsx1.png", + "AvatarItemDesc": "cca7ab3d-5609-4700-92fd-3280fcea9b77,", + "FriendlyName": "Cowboy Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cca7ab3d-5609-4700-92fd-3280fcea9b77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2227, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gpHXKk3jR061QowF7qKdGw.png", + "AvatarItemDesc": "9d2ea3c2-c199-40ed-a6c3-fab5f8dca7d6,", + "FriendlyName": "Cow Ears Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d2ea3c2-c199-40ed-a6c3-fab5f8dca7d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:40.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2254, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d837uciWekyImW4Ny0wliQ.png", + "AvatarItemDesc": "2a61c844-81c6-4f82-9c75-66cf308ed4e1,", + "FriendlyName": "Cowgirl Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2a61c844-81c6-4f82-9c75-66cf308ed4e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:38.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2255, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "USrRub60Gk6zTcni3MGCjQ.png", + "AvatarItemDesc": "ed695ac2-ff70-4ebe-b1db-18d71a4efdd1,", + "FriendlyName": "Cowgirl Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed695ac2-ff70-4ebe-b1db-18d71a4efdd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:46.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2256, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "sH2WnqiB8E6lk4hZ9s70RA.png", + "AvatarItemDesc": "67c0f944-2f70-46d2-8588-2f67028f8bed,", + "FriendlyName": "Cowgirl Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67c0f944-2f70-46d2-8588-2f67028f8bed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:49.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2587, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "J2d0ZFyToECidRm1EHVhLA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,", + "FriendlyName": "Cozy Winter Coat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:43:57.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2591, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PhvdPQnEikmC4BSDxeHfeA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,B3t4hjeUeEyT17nfbZgxAQ", + "FriendlyName": "Red Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:09.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2588, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tWrlF7BJxU-nPUjBYNlJxw.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,IEnB86oIL0KDa-zzcxhjwg", + "FriendlyName": "Black Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:00.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "m7LntT7DgUy0q0ALmr_4sA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,ZzJxs2oz6UuXCX5rGvLHFQ", + "FriendlyName": "White Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:12.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2590, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "t0ntXj7xqUqXyp7YAxwnGg.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,xLcqKXKH-0Gw1lrSSQe-Ag", + "FriendlyName": "Cozy Winter Coat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:06.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2589, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "doAF6sFqkEWtgY2AjjfQKg.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,nloukujf9kSwK1T2HiwJwg", + "FriendlyName": "Cozy Winter Coat (Pastel Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:03.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2593, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5_Is3y0bw0-_CDD5izOqfQ.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,Grr0Jm6r9kuiZKvnLPz9tQ", + "FriendlyName": "Yellow Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:18.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1472, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6l2rw2xqkm2h6er2jcsouoohm.png", + "AvatarItemDesc": "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f,", + "FriendlyName": "Crab Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:29.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2477, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "spUKGpKq3keVBvBjMRkRIQ.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,", + "FriendlyName": "Grinning Cat Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:15.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2478, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4yiLN6Z-r0msFhufhO36Mg.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,McX_jAOowUmjXNPEHbdS7Q", + "FriendlyName": "Grinning Cat Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:20.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2594, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-iEX4DOt8Ems8CPrv9vFPg.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,4YH_0kN1ZUSVEQmCBiTt7Q", + "FriendlyName": "Penguin Onesie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:21.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,kTyKeivEskaxQNLhfPWo1A", + "FriendlyName": "Crazy Cat Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1791659539 + }, + { + "AvatarItemId": 1625, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vvcu1ehg70m9a5l4pccyi05l.png", + "AvatarItemDesc": "6423f23e-dde3-4428-96f5-6091027c92cb,", + "FriendlyName": "Martial Arts Gi (Cunning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6423f23e-dde3-4428-96f5-6091027c92cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:38.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 360, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dn6xgp8b6mjoilolmwxfn8tom.png", + "AvatarItemDesc": "6b337a35-95d9-4b3a-9891-d9fbb06262aa,", + "FriendlyName": "Curly Mustache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b337a35-95d9-4b3a-9891-d9fbb06262aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:00.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2300, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "jhTnA-cti0mo7tgXVYQI3Q.png", + "AvatarItemDesc": "cf360f73-b349-44e7-b5c1-f175a0f5e70d,", + "FriendlyName": "Belt of Elseware*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf360f73-b349-44e7-b5c1-f175a0f5e70d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:41.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2562, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "bIvb2VlYREWBdsNT4kX_2Q.png", + "AvatarItemDesc": "cf360f73-b349-44e7-b5c1-f175a0f5e70d,zQgy7h3CO0a02zJfhA9wXA", + "FriendlyName": "Belt of Elseware (Shadowy Illusion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf360f73-b349-44e7-b5c1-f175a0f5e70d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:55.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2301, + "AvatarItemType": 0, + "Tooltip": "A mysterious and powerful object from an unknown place. No one knows what it does yet but it seems to respond when worn... (Catalyst)", + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "7YFPA4lmf0iQcexgqXhxaA.png", + "AvatarItemDesc": "f6eee46c-084b-473e-954b-a0c8aa99a02d,", + "FriendlyName": "Amulet of Elseware* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6eee46c-084b-473e-954b-a0c8aa99a02d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:43.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2563, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "egJ0Guweb06skCFq1ExhQw.png", + "AvatarItemDesc": "f6eee46c-084b-473e-954b-a0c8aa99a02d,qbEYPChG5EmnFDUfzwAakg", + "FriendlyName": "Amulet of Elseware (Cleansed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6eee46c-084b-473e-954b-a0c8aa99a02d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:57.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2302, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "hyRSoiE5ZUGnAC91bFgIIg.png", + "AvatarItemDesc": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d,", + "FriendlyName": "Bindings of Elseware*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:44.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2564, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "BA2wuggUAkyxEev61GWUYg.png", + "AvatarItemDesc": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d,cfAfhzaNvkG7wGx0gkaRUg", + "FriendlyName": "Bindings of Elseware (Shadowy Illusion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:58.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 659, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2qs4c3hd1jm20l964lznj7hj8.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,", + "FriendlyName": "Deep Sea Diver Helmet (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:21.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 924, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "970zmxcbriwvube7waktj8lib.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,Su7kiW24d0KapNns96JoFQ,a3Er_BCApUiPtBaGkSH9LA", + "FriendlyName": "Deep Sea Diver Helmet (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:58.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 923, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d47yw12jvdnmgznyxtsb774ss.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,CahBzgN3D0y46AQsvGC14A,a3Er_BCApUiPtBaGkSH9LA", + "FriendlyName": "Deep Sea Diver Helmet (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:57.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2j7gd6uoyus6lyn8g1yvji91e.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,", + "FriendlyName": "Deep Sea Diver Suit (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:28.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 880, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "clcdiuwdbo9i9gcuvogdg36me.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,Su7kiW24d0KapNns96JoFQ,EiIow5mS80qpoS10r1ZdvQ", + "FriendlyName": "Deep Sea Diver Suit (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:00.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 879, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ejlghhpy20pr22d1be9zamyak.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,CahBzgN3D0y46AQsvGC14A,EiIow5mS80qpoS10r1ZdvQ", + "FriendlyName": "Deep Sea Diver Suit (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:56.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 691, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9rtsv91ax2euh05felzvtd1w7.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,", + "FriendlyName": "Deep Sea Diver Tank (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:15.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 905, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c2brbedhjhfswczcszbb6vdn5.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,Su7kiW24d0KapNns96JoFQ,ZpyoWbO6GkGglXlMuYUARw", + "FriendlyName": "Deep Sea Diver Tank (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:37.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 904, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8qnxcx64rh4jdr41v292zqnvl.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,CahBzgN3D0y46AQsvGC14A,ZpyoWbO6GkGglXlMuYUARw", + "FriendlyName": "Deep Sea Diver Tank (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:35.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 662, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8yxna4s3e3firsdmmowshealh.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,", + "FriendlyName": "Deep Sea Diver Gloves (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:29.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 935, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4isq4kmfnatlmnrbk9ylp4ur2.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,Su7kiW24d0KapNns96JoFQ,0p5N9U5qokKdQhW07yYLGQ", + "FriendlyName": "Deep Sea Diver Gloves (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:11.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 936, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ael0gchmqiiy4xijypkebw5gy.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,CahBzgN3D0y46AQsvGC14A,0p5N9U5qokKdQhW07yYLGQ", + "FriendlyName": "Deep Sea Diver Gloves (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:12.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1527, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "a2skv7f3dcpvd1rrpejoce5q8.png", + "AvatarItemDesc": "1a742ff9-c4f7-45d8-8e24-8f24aa021db2,", + "FriendlyName": "Demon Horns", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a742ff9-c4f7-45d8-8e24-8f24aa021db2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:51.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1525, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "badoqabz6dkrchkzj2vkx5fxy.png", + "AvatarItemDesc": "b84f5e9e-11fc-424d-ad39-86901edb9edc,", + "FriendlyName": "Demon Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b84f5e9e-11fc-424d-ad39-86901edb9edc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:43.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1526, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6fwxedm1vpp6m7vdyj1mjush3.png", + "AvatarItemDesc": "26f5e849-14b3-4cb3-ab92-673c94b37c51,", + "FriendlyName": "Demon Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26f5e849-14b3-4cb3-ab92-673c94b37c51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:48.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2179, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZrqDYptat0GjjSq9O6O2TQ.png", + "AvatarItemDesc": "26f5e849-14b3-4cb3-ab92-673c94b37c51,CwObTyhghkWonhm5nTnAaw", + "FriendlyName": "Flying Machine Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26f5e849-14b3-4cb3-ab92-673c94b37c51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:35.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1376, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1vas1a6all7s8fowlh0nithtn.png", + "AvatarItemDesc": "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae,", + "FriendlyName": "Dino Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:37.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1259, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2za37ica0cvevp732s8h5llq0.png", + "AvatarItemDesc": "a5b747b8-c1a5-4bce-999f-e13dbe77bde6,", + "FriendlyName": "Disco Ball Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a5b747b8-c1a5-4bce-999f-e13dbe77bde6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:13.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1956, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b70wx32unnc05ssstrdjeqdl9.png", + "AvatarItemDesc": "4095a06c-ccdb-41a0-ac95-5a97372c9a02,", + "FriendlyName": "Disco Ball Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4095a06c-ccdb-41a0-ac95-5a97372c9a02", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:57:55.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1957, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4qarl3kkz504i1mf21lhb6dhg.png", + "AvatarItemDesc": "84992fc3-4801-492b-8a6c-ffa09ae5b593,", + "FriendlyName": "Disco Ball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84992fc3-4801-492b-8a6c-ffa09ae5b593", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:57:59.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "adbd2eaa-0dd0-4955-be4c-0d5a66d64ed2,", + "FriendlyName": "DJ Headphones Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adbd2eaa-0dd0-4955-be4c-0d5a66d64ed2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1329276406 + }, + { + "AvatarItemId": 2, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "egsz5bap153w61sfwpsx9tgjw.png", + "AvatarItemDesc": "5ac25e5a-8c8e-445c-82a4-7272baec1eb5,", + "FriendlyName": "Doctor's Head Mirror", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5ac25e5a-8c8e-445c-82a4-7272baec1eb5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 224, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2chr298lunemuuurc2ka18725.png", + "AvatarItemDesc": "27e742bf-3081-4274-ba70-301f0edeb4f1,", + "FriendlyName": "Doctor's Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27e742bf-3081-4274-ba70-301f0edeb4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 123, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8lo669z9xca0upv4z2ocdcde1.png", + "AvatarItemDesc": "587ca2bb-dd42-42bf-833f-4e2a97349866,", + "FriendlyName": "Doctor's Stethoscope", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587ca2bb-dd42-42bf-833f-4e2a97349866", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 365, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0czsyf22bw9er1mbnytxi0ytj.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,", + "FriendlyName": "Dodgeball Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:05.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 439, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5dzmypownxu9mz6fttdzw0cjp.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,4a85c181-92fe-47a5-925e-05f0ccb25006,944d934d-5e21-42d7-8196-fff9a81ee469", + "FriendlyName": "Dodgeball Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:09.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 440, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "08j0sgwi31ma53zy69e9h1cvs.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,0ba92db2-690a-44ce-92f4-130e9503b6c1,944d934d-5e21-42d7-8196-fff9a81ee469", + "FriendlyName": "Dodgeball Shirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:10.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 851, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8nnw6vhiwxlys6lvncv55q2xd.png", + "AvatarItemDesc": "h_To9WQYdEusYQxlbMmMmQ,", + "FriendlyName": "Dracula Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8f487-1864-4b74-ac61-0c656cc98c99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:53.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 857, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2l8ozzohqd9a0k2deuxxucq9c.png", + "AvatarItemDesc": "uubY_Cn9A0-Ww4lsIRCF6w,", + "FriendlyName": "Dracula Medal", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fcd8e6ba-fd29-4f03-96c3-896c211085eb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-14T01:26:24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2388, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "k1-iNuM_jUadcrVNAvy83Q.png", + "AvatarItemDesc": "05ab6e79-1198-49f2-945b-528523e306bc,", + "FriendlyName": "Wicked Sorceress Amulet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ab6e79-1198-49f2-945b-528523e306bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:35.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2389, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pR-mOlAIOEiskisbb6ZQUw.png", + "AvatarItemDesc": "05ab6e79-1198-49f2-945b-528523e306bc,aUsPNgV2c0u15VhGBAIyvg", + "FriendlyName": "Dragon Necklace (Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ab6e79-1198-49f2-945b-528523e306bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:37.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1678, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7i6yh1qwt3e3zrrb77zwz3wta.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,", + "FriendlyName": "Dragon Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:49.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2201, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B0mOl7jCckGx4VBglg52ww.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,cbk6h_-1Y0exRpGyrQMDMA", + "FriendlyName": "Dragon Wings (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:59.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2200, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gN3tkjocB0iH14KDkjDACg.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,E_pm-PPj_U-nsfMti-AlCA", + "FriendlyName": "Dragon Wings (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:57.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 3, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a3yzsuscvgxw37qx4iplxyqvr.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,", + "FriendlyName": "Drum Major Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 418, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "81kgcsyk5xpfnseux5kk365d6.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,5a614f97-ef7f-4b70-afff-9ed837a86407,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:20.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 419, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "06rlbrw6875digtc4y8c0l3i8.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,b444ed0a-eab5-4bd6-af34-d080de74a149,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:21.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 420, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4visb1djbytbo881ltks1hzo.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,8053b5e5-0f67-4329-a45d-1ee9fc830820,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:22.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 225, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2hyiw1ost44xvt2vreduatn4w.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,", + "FriendlyName": "Drum Major Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 422, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d1ickhvbj03efs8x35tvbakq2.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,fe5288bc-0e02-4ea3-b17d-dee0e576979d,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:25.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 423, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "93qrv8cwm8k71rmnk1o22mzpq.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,82de1ca3-67d6-454d-84fb-0eeece71fcbc,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:26.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 424, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5la3vym26ex06q0rm9ykg3w1f.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,54d3c589-30ba-44c1-a77c-8f9e16eb0b20,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:27.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 275, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0rq8z5vxkj1jdvbjqezvbckyx.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,", + "FriendlyName": "Drum Major Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 425, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "817bm8rsomfrxcjwad8033u20.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,fe5288bc-0e02-4ea3-b17d-dee0e576979d,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:28.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 426, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3kkimmvnludjevzzo56uhvuk6.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,b444ed0a-eab5-4bd6-af34-d080de74a149,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:29.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 427, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73tjv2ussx3904ivxkhezp2yr.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,8053b5e5-0f67-4329-a45d-1ee9fc830820,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:30.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2735, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "J_F4me5Ad06VJ52Xr7npMw.png", + "AvatarItemDesc": "162a3442-e6d0-4895-be09-40d78af16dc8,", + "FriendlyName": "aaa", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "162a3442-e6d0-4895-be09-40d78af16dc8", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:40.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1439, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7fx9lc7nedtemokvl10u71bvo.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,", + "FriendlyName": "Dryad Dress (Summer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:09.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1440, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ae92fjqai6ajms6tff2mfv81j.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,AHL9ydUbqEqMw1tluIvyQA,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Fall)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:11.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1441, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b660cr3jdsc9mlg5dejtru1o.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,_SPKrYIcnUWhTiSrTFLwoQ,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:12.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1442, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7amwyi3juehdo73arc4mepxm5.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,n2pZHS8A3kSgyFUCI_-8TA,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Spring)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:14.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zg42szCjeUGZC18WPpdf6Q.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,BYbKbXAgUk6vE7H7w4jSMA", + "FriendlyName": "Dryad Dress (Cherry Blossom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:41.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1443, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bzjuuf0r1ocwg6am5t2mtrvxg.png", + "AvatarItemDesc": "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb,", + "FriendlyName": "Flowing Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:19.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1436, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "65ofog1r81v83c1zkimz5dc7n.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,", + "FriendlyName": "Fae Horns (Fall)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:49.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1437, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aom69vkzej29cjh1jcqgxddom.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,M16XTvLPKUWRpiAspgsatQ,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Summer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:57.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1438, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8s3arpmce6wx552qttsj9og6n.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,rXhNZtcZCU21dwEHOCgWOA,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:59.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1752, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bq7qytcrpeczoqxkplbewz1uf.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,OxO9ivoKB0e-fNIEk33Erw,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Spring)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:49.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1506, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bvjmdwvfu70toj5uhdjc9bmyn.png", + "AvatarItemDesc": "2cf6ceee-4c45-4fb4-b9df-cb8437742e85,", + "FriendlyName": "Doubloon Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2cf6ceee-4c45-4fb4-b9df-cb8437742e85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:56.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2717, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4jhq0ihf9i0h357l82r1f4ugm.png", + "AvatarItemDesc": "f8514fc6-93f3-4063-bafc-378ddf454836,", + "FriendlyName": "Edgy Earrings (Dangle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f8514fc6-93f3-4063-bafc-378ddf454836", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:54.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2718, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9ter0n7sprtghgk2o4clgjugr.png", + "AvatarItemDesc": "f641d4f4-e197-4cf4-b477-89069797967d,", + "FriendlyName": "Edgy Earrings (Gauge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f641d4f4-e197-4cf4-b477-89069797967d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:56.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "02dj9jh2t6iwcatksesgdp9v5.png", + "AvatarItemDesc": "2af73868-b619-43f7-9b7a-37a8fe2502bf,", + "FriendlyName": "Edgy Earrings (Spiked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2af73868-b619-43f7-9b7a-37a8fe2502bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:58.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2681, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "GVvKZUlMlU-oAczCFnnkPA.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,", + "FriendlyName": "Electric Guitar (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:48.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2683, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "K9ApHkO1I0WYcDiKg_7bMw.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,zBfCq7T69kqZPTz3P8KKhg", + "FriendlyName": "Electric Guitar (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:54.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2682, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "uZtpsMTIe0WfPlipjC6sfQ.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,liZWUNZih0iakAa7UJkk9A", + "FriendlyName": "Electric Guitar (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 356, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8o02lqyi0dx2nwz9l24ei0511.png", + "AvatarItemDesc": "34654303-9760-447f-8614-1c26506db994,", + "FriendlyName": "Electrician Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34654303-9760-447f-8614-1c26506db994", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:51.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 351, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7mhhpensvbg501x8mtatplhsh.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,", + "FriendlyName": "Electrician Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:31.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 352, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73egrs7lt4bztq4mkimehdlwx.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,b995b419-1897-4c1d-98be-83d551f7ba71,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:34.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 353, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a4ha61zluoi17g7smiinsv5pp.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,67fc9269-8dd8-4d6f-b594-c77e716f2482,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:35.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 354, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ef0ejvn9b4x628qk2dfjie7xo.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,b539777f-48b2-44a2-9d63-17ac703e4d57,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:37.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 355, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "40dmxzroivdmghm7ola3zu9u1.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,5fb2e235-8c67-4335-b432-edb10db2b139,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:40.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1622, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "7m2mi3d8ga6cj0snk6kbhszx8.png", + "AvatarItemDesc": "686ac516-630f-4989-9c99-0b8d055a00f9,", + "FriendlyName": "Holiday Elf Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "686ac516-630f-4989-9c99-0b8d055a00f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:20.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1623, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "av5quwapam4mgd2prq9f64nkf.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,", + "FriendlyName": "Holiday Elf Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:23.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2526, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "tRXo9yhf00enptfGUMyqHQ.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,Hd5IIMd7VUqndR_og0zxPg", + "FriendlyName": "Elf Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:52.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2524, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "QWw5Yklbyk2cF85WzqFl-g.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,8k3q_1d_-kSBzNLCl1HQhw", + "FriendlyName": "Elf Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:45.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2527, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "lNu1DktQikamJcxDfb6XpQ.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,YwossyXZNEm3OPWwXsyTAg", + "FriendlyName": "Elf Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:55.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2525, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "A9c3qAoffkKGpt3NGDQj7Q.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,i3pwJQoNDkO_68pQ_3Y0tw", + "FriendlyName": "Elf Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:48.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1624, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "6m4lkkcj4tnqk0xc8aqctsa6v.png", + "AvatarItemDesc": "61969200-2790-41e0-bccb-d9feff8c7b7d,", + "FriendlyName": "Holiday Elf Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "61969200-2790-41e0-bccb-d9feff8c7b7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:30.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "2e50d8d2-b7fc-44e9-a882-a58bd3b6f099,", + "FriendlyName": "Elseware Cloak Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e50d8d2-b7fc-44e9-a882-a58bd3b6f099", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1829511188 + }, + { + "AvatarItemDesc": "a73e1a56-2710-4448-a594-ecc80571e999,", + "FriendlyName": "Elseware Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a73e1a56-2710-4448-a594-ecc80571e999", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1208807230 + }, + { + "AvatarItemDesc": "c7ea70bd-7681-4b90-b4e3-10a100e4652e,", + "FriendlyName": "Elseware Earrings Ear", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7ea70bd-7681-4b90-b4e3-10a100e4652e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1175771749 + }, + { + "AvatarItemId": 1792, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "26o0a59b3xk9bngu5q6rjwddq.png", + "AvatarItemDesc": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,", + "FriendlyName": "Elven Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:04:59.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2186, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E6CVDPt2mUWUQ6WM1GzI2g.png", + "AvatarItemDesc": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,z1djgGqy502ha9X_uB3enA", + "FriendlyName": "Elven Crown (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:10.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1797, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ss3ohgl1cgeoknp1gcpackva.png", + "AvatarItemDesc": "485bbda2-37e8-45a7-90db-7c35518dcdd0,", + "FriendlyName": "Elven Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "485bbda2-37e8-45a7-90db-7c35518dcdd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:26.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2191, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sR5683LqRUiQsI8U2LXZVA.png", + "AvatarItemDesc": "485bbda2-37e8-45a7-90db-7c35518dcdd0,M5QwJJRRBUy42HLyp6ghtg", + "FriendlyName": "Elven Armor (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "485bbda2-37e8-45a7-90db-7c35518dcdd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:23.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1793, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9pskxjn1i5vvqw267s8jfq4cn.png", + "AvatarItemDesc": "a37c3161-1476-44b0-a9d1-d6eb854db2d9,", + "FriendlyName": "Elven Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a37c3161-1476-44b0-a9d1-d6eb854db2d9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:05.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2189, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DRy_t5nuCUqcj18J33q3Hg.png", + "AvatarItemDesc": "a37c3161-1476-44b0-a9d1-d6eb854db2d9,EmlwkB77pUiiDEmc2pMVaA", + "FriendlyName": "Elven Cape (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a37c3161-1476-44b0-a9d1-d6eb854db2d9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:18.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1799, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "275lkom5he8sdivhtrbsz9ioj.png", + "AvatarItemDesc": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,", + "FriendlyName": "Elven Bracer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:33.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2196, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "42hTRTPSNkmQRqAiceQwVA.png", + "AvatarItemDesc": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,MzNNQ6T1HEqbqcLXZIulLw", + "FriendlyName": "Elven Bracer (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:38.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2493, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vskg3C6Z10uF_tdfqjarlA.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,", + "FriendlyName": "Beam Sword (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:12.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2494, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OodKnoLtC0KlwWUbUQQJjQ.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,u0bLCSxcA0yDHC29XnLZOg", + "FriendlyName": "Beam Sword (Science Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:14.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2496, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7NNMQvKd_02w8L9dd1zvxA.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,6WJKUeFNn0amYNvKMxijcw", + "FriendlyName": "Beam Sword (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:20.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2497, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ObiBqarizkuy3DPPXfx4dw.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,VPvezA1A8UKt2qNoNRW9dg", + "FriendlyName": "Beam Sword (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:38.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2498, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RuBjPNvqok6wRw6kkXH2GQ.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,OIfcKSEil0uHgT5baEFuOw", + "FriendlyName": "Beam Sword (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:40.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2495, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OCi7E50f4UqQMDGTuaWxCg.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,hNFdYhoJYk24MqU2GhQXlw", + "FriendlyName": "Beam Sword (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:16.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 50, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6gyrqvu42014zf0m05yueqgws.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,", + "FriendlyName": "Equestrian Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 632, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0dobf9ialtu2o3i6ia3w9whbc.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,55901f12-d5b5-4fa8-b4c8-e479689ee39d,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:53.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 633, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ebxhd6rhrlk3qem5ly6uri8od.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,4828b50c-95b6-466a-bb25-514891d78202,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:55.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 634, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7w9g6a5fxuoeblfcf3ahgvbs7.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,d6823e01-69f0-4f85-b94a-74894356a2cf,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:56.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1409, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "avhll17max3g5tbutzgkr4j3d.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,", + "FriendlyName": "Night Coat (Zzz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:01.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1410, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "10ks6xzccejyj65uikvyl23xf.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,uDJ5ZqU1f0CiOAH8WY22Yw,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Dozy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:03.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1411, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4g0dvwsd3dz5sthxkhgq6scu0.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,j66We9SYr0SNQ2ufZqIqhA,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Sandman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:05.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1412, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6lrk2hle92tc35cdc3fs9nqhp.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,_jkPDSXFUkSU_yOfutQ7BA,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Zonked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:06.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2066, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "m9a7PytQ6kK8OBPmwOadSw.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,MLB_YJtNtUi2Qw9D4a5i2w", + "FriendlyName": "Starry Night Robe", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:21.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1584, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bbiqjubciywrt9n52sthcrv57.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,", + "FriendlyName": "Fancy Cape (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:06:59.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1585, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "appu9oibb8u3du39aint01ugz.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,crl8VlZ59kGxG01noGEWmA,EckOEU0mJkGIVDvYoKBN9g", + "FriendlyName": "Fancy Cape (Gilded)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:07:02.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1925, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8eli5j86ufvn0nzgy2emp66xo.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,WqX8pAB7LkyPKEzIUCtnNg,8Y2F-cVStkSn9YB47VCoKA", + "FriendlyName": "Fancy Cape (Holiday)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:47.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2027, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-FiTuUGn-kGGXgQjYaAC7Q.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,l2BbgbHCe0S_SDUbfmpSeQ,XHJ0lW4zYkGcL5UyI1HfEg", + "FriendlyName": "Fancy Cape (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:28.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2161, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GadkXWWyHEOkWhpEtq2mrw.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,bTAQ-CkyRUOZoXqXuap2Ig", + "FriendlyName": "Fancy Cape (Basketball)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:28.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 973, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2so63kdd99rxf93sphtoxpre7.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,", + "FriendlyName": "Belt Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:48.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1450, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1n94yhtw4pxw2vp1k9nf43brr.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,whs9olNz9Uani7Gou31mvw,y6aASPUwDkas0W5Yl27ydQ", + "FriendlyName": "Belt Bag (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:29:03.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2028, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7iYxky7EC0WBmVm1yt7YDQ.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,ncERd5bELk23jSRxA7NV9g,N-cAmfOgv0eJZMTo18zJHQ", + "FriendlyName": "Belt Bag (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:32.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2282, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "w2agGc4e5E2zgiHvAsFu7Q.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,eaRDMCA4fE6KJHi639QFvQ", + "FriendlyName": "Rec Con Belt Bag (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:27.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2281, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "UnP3rBcwpEmBR6ED8vVH0A.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,zIIQkllHFEyjEvAGYC0cPQ", + "FriendlyName": "Rec Con Belt Bag (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:22.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1420, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0x4ghshpxazgbb0zpcyu0g22v.png", + "AvatarItemDesc": "4ee2133d-c419-4fbc-9117-746d756a250d,", + "FriendlyName": "Belt Bag (Cosmic)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ee2133d-c419-4fbc-9117-746d756a250d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:26.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 367, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dd3ww7dcaihkg0ags2cxuiyuz.png", + "AvatarItemDesc": "cea5c141-32d8-4dae-80d9-6fb74834a79c,", + "FriendlyName": "Farmer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cea5c141-32d8-4dae-80d9-6fb74834a79c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:05.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 371, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9iudjbzlizfmrgxtwdwwsj6hp.png", + "AvatarItemDesc": "967187b8-df20-4f4f-b48f-10b9ea75cedf,", + "FriendlyName": "Farmer Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "967187b8-df20-4f4f-b48f-10b9ea75cedf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:11.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2023, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kv11sGN1q0uGTiTIyHPeTA.png", + "AvatarItemDesc": "b6cee099-5840-401a-9d74-0b80ff71ee69,", + "FriendlyName": "Feather Boa (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6cee099-5840-401a-9d74-0b80ff71ee69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-02T18:10:57.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rwu9Nb6pTU6Pjd3orGYWOQ.png", + "AvatarItemDesc": "b6cee099-5840-401a-9d74-0b80ff71ee69,an_Z5HMFjkaHyHoZZV4AFA", + "FriendlyName": "Feather Boa (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6cee099-5840-401a-9d74-0b80ff71ee69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1547, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cl2yzoi5qm9yxxli0m8dx34ey.png", + "AvatarItemDesc": "16707919-756d-45ae-95fc-cfb6715b8ae6,", + "FriendlyName": "Winged Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16707919-756d-45ae-95fc-cfb6715b8ae6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:28:04.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1765, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ceggd7cookytj7l34cfoxzeuw.png", + "AvatarItemDesc": "7df5697f-2f71-47d1-bc27-cfa43792ffd7,", + "FriendlyName": "Felted Shawl", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7df5697f-2f71-47d1-bc27-cfa43792ffd7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:36.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_jqJN6iJzUuEi-MlHpRpyg.png", + "AvatarItemDesc": "7df5697f-2f71-47d1-bc27-cfa43792ffd7,GCethPzUyUKYxWdfLykbSg", + "FriendlyName": "Felted Shawl (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7df5697f-2f71-47d1-bc27-cfa43792ffd7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:08.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 975, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dj7x2xk5ttme8r1jpcmeebmjj.png", + "AvatarItemDesc": "BJ2XrGvl40iIl28IbTfbKg,", + "FriendlyName": "Cheerful Ponytail", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ac979d04-e56b-48e3-8897-6f086d37db2a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T02:00:33.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 176, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "41wwwt83ors7iub44o0ftgtzs.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,", + "FriendlyName": "Cheerleader Skirt (Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1626, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4o1fhoua48d5fb5rbrs2ijloi.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,eWf2c62cqEOkRukLzhl_wQ,KFq8d1VsO0KDqBWUj4qRFg", + "FriendlyName": "Cheerleader Skirt (Victory)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:07.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1627, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "edpmi0dav6ft7rwfn86f1km9l.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,a0oe68pikUykAgrRo-axkQ,KFq8d1VsO0KDqBWUj4qRFg", + "FriendlyName": "Cheerleader Skirt (Rally)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:12.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2422, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Hhv0MwY_0EitaME8aQhP_A.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,cFQ1Z7_wp0meS8wdC1hemQ", + "FriendlyName": "Space Suit Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:46.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 132, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5rmads8tgd0mb71na01q6g4l4.png", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,", + "FriendlyName": "Rec Room Sash (Small)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1359256033, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,IodiruapIEqjGym-YcXzXA,LjQ1UKN2xkqJkok40Dxaew", + "FriendlyName": "Female Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.502777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1597463395, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,KUAMuM41hk-YLZoqTiKncA,LjQ1UKN2xkqJkok40Dxaew", + "FriendlyName": "Green Cheer Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.502777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2514, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "F2FsXvPi5UqyVCDKuPzs0Q.png", + "AvatarItemDesc": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b,", + "FriendlyName": "Festive Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:12.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2542, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "fNauUoIJ10uT7PJQCp9Agg.png", + "AvatarItemDesc": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b,1LtCxaCDjUmg62fYCjNUSQ", + "FriendlyName": "Festive Antlers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:58:27.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 179, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "39n1fu8uzgykuoibgylda8en0.png", + "AvatarItemDesc": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,", + "FriendlyName": "Field Hockey Uniform (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "akd6dknn22mbqs5rpgcphqekv.png", + "AvatarItemDesc": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,7b0d6661-e856-4ec1-ab90-d89c28a70826,1fede91a-434e-49a5-882e-e6db107112a8", + "FriendlyName": "Field Hockey Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:24.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 347, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cz08dx8wptcrolonzvozwn4w1.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,", + "FriendlyName": "Fighter Pilot Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:03.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 338, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ef7205mj0de2hjfkm2shgsjo3.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,cea53c7f-24a9-4e3b-80b9-fbf95738f973,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:20.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 339, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5f87fvdl1iouxqppbtsu50ic0.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:21.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 340, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f54hknoj615yks00svpyjvttz.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,8c74ee27-3d56-401b-8a8c-7868a80770e8,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:24.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 349, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1wcxmgwhypyd2evqqigqmes19.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,", + "FriendlyName": "Fighter Pilot Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:07.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 341, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1d9tx3eid2kmtnq3wejxoceub.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,cea53c7f-24a9-4e3b-80b9-fbf95738f973,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:25.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 342, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bfv2kddhqvn7dw1f648zhjf63.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:27.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 343, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8ko7qcft9b1b9aubbimm8cjfb.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,8c74ee27-3d56-401b-8a8c-7868a80770e8,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:29.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 350, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0tlurbm4ftp2t94h35qrfzqtm.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,", + "FriendlyName": "Fighter Pilot Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:08.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 344, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7q0ythybtrbsgg7c2njbi9c68.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,18b52db5-0261-46a0-9214-ff510d455ed9,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:30.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 345, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6328cpp2tizf50t3nsq5yrbz9.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,55b4e661-43e1-4708-82d5-2dd2bebd4f45,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:31.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 346, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7l7k238jg65h9nok7guj535ix.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,8c74ee27-3d56-401b-8a8c-7868a80770e8,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:35.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 955, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "99bp2zxrcuohv18hmmh26iuao.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,", + "FriendlyName": "Figure Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:15:20.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2163, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ls7NQ0Vos0SREhMD3UGSJg.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,dZyVey0_oU-3u6FLbIdGLA", + "FriendlyName": "Figure Skater Collar", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:34.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2164, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kO-X2uWxJEGueWZ1XNqQfA.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,rP0LMoFjqUCU1LWA6_wOnw", + "FriendlyName": "Figure Skater Shirt (Burgundy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:36.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 956, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "arq4ixmsd54yjuazkfe5xglft.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,", + "FriendlyName": "Figure Skater Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:15:24.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2166, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "_J7s68Cf2kKhS_I47a_7Cg.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,ngF6e9k3wkme3HdodiB03w", + "FriendlyName": "Figure Skater Dress) Figure Skater Dress ng F6e9k3 Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-04-13T01:18:42.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2165, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c_ldiPCEC0uVSLKrER_-tQ.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,FpvZjltPtU6AKTBphz3ZMA", + "FriendlyName": "Figure Skater Skirt (Burgundy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:39.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,z7bASCi-JEKuLiOmFx3EFQ", + "FriendlyName": "Figure Skater Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1092505797 + }, + { + "AvatarItemId": 51, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "agf9y9srlnxo896ku56vbgb2e.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,", + "FriendlyName": "Firefighter Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 635, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4jk6yhqfbgqan7m5g4uz672n.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,891d902e-3257-4a8c-9001-858046007997,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:58.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 636, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8mvts9qqf1v3kmd2l4mgbqjkx.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,4c26d1e7-2382-4478-b9c4-a068ed01a607,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:59.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2006, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bnhtf2lj2d5khggvf485zqnxb.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,uahcQNh3IEiooPFq-SQi5w,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:26.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 180, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "573itausg418c5prdz6bv3dpo.png", + "AvatarItemDesc": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,", + "FriendlyName": "Firefighter Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2007, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8gwu9vgybc4jmwudmwkep73uk.png", + "AvatarItemDesc": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,0x7a3hV5XUyzyf8cUuzVtQ,vYAcZMymO0m6bCNQIsI8rQ", + "FriendlyName": "Firefighter Jacket (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:31.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "7ed81aea-0e77-4b38-a87c-729048c6fe41,", + "FriendlyName": "Fireworks Antennae Headband Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ed81aea-0e77-4b38-a87c-729048c6fe41", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1920491330 + }, + { + "AvatarItemDesc": "b734172c-e405-4fc7-88c3-c859309bb4ac,", + "FriendlyName": "Flamingo Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b734172c-e405-4fc7-88c3-c859309bb4ac", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1246829493 + }, + { + "AvatarItemId": 588, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a1vs7a8qu2iy5kaxke59mq5sa.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,", + "FriendlyName": "Flapper Dress (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T18:57:31.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 641, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cu5pah7uuv614frsr65tvynut.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,565c17e0-496d-45bd-a2c3-564e118c06cc,a240d44f-963d-4d35-b22f-980938c7788d", + "FriendlyName": "Flapper Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:19.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 642, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a1844ejer8zz68n1qyajbci5m.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,a240d44f-963d-4d35-b22f-980938c7788d", + "FriendlyName": "Flapper Dress (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:22.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 589, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "acmwuunndj6ex6zr3885jbewo.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,", + "FriendlyName": "Flapper Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T18:57:33.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 643, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5b73p030rhcezdz56gzz5ke13.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,30c61e63-5e09-4a6f-8915-1e1fe4d55cf0,0e42108e-3b08-49b4-86c0-7984719e8380", + "FriendlyName": "Flapper Gloves (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:23.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 644, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3v8m5i8t4wkf4669wvp55t2ex.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,2cf2b644-3fa6-449c-9fe8-d0ab917f3106,0e42108e-3b08-49b4-86c0-7984719e8380", + "FriendlyName": "Flapper Gloves (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:25.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 586, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2q0dg92w8rd2uzk57ib0y1cj0.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,", + "FriendlyName": "Flapper Headband (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T01:07:49.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 624, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e7tt4jrd0jwhdkeastwunm8bb.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,565c17e0-496d-45bd-a2c3-564e118c06cc,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "FriendlyName": "Flapper Headband (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:41.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 625, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uukseprpubic1ed6c0zapb86.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "FriendlyName": "Flapper Headband (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:43.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 52, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6s2gbnnr5jsigj562mzity2yp.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,", + "FriendlyName": "Flat Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 637, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7iezcsxl2wujed3zpx0sad4pp.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:06.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 638, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e688283ak00jg806f3cw5gc0k.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,0ba92db2-690a-44ce-92f4-130e9503b6c1,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:09.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1638, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "24i3534uix45v9rp0lwimi7sf.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9aYT6E9sd0OSavlmkhtkGw,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:20.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1639, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5w6vsff1wv2psbj5naflpxebd.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,H-ypJNyVSka9as7rDFRZ_A,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:22.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1640, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0xi16q5h0z36akxhihlw7prh3.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,-7uPJmO-wUu3cUhqo0caPg,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:24.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "f567f792-e5bf-41d9-a8a9-e3af7aaddaf7,", + "FriendlyName": "Flipped Up Shades Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f567f792-e5bf-41d9-a8a9-e3af7aaddaf7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1877445964 + }, + { + "AvatarItemId": 2218, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TgzWE1dBGkqvgHpdtFU2Uw.png", + "AvatarItemDesc": "c7c85554-e118-4433-8d6b-b61a2971e045,", + "FriendlyName": "Wrist Floaties", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7c85554-e118-4433-8d6b-b61a2971e045", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:04.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c7c85554-e118-4433-8d6b-b61a2971e045,MS0Mglf3qEmzKzbCa6uOAg", + "FriendlyName": "Floaties Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7c85554-e118-4433-8d6b-b61a2971e045", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1483238024 + }, + { + "AvatarItemId": 22, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "20q0kpiy9pbsmzz8qgeyhz0kt.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,", + "FriendlyName": "Flower Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 479, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "548zksu11edcwj6jg6aoag1s7.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,c5884778-a87f-4612-9717-86fbb65d440f,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "FriendlyName": "Flower Headband (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:34:47.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 480, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c1g1jmqtrkrhxhm8c2puubehx.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,827a1538-51bb-4fef-99c1-7f1bebd9866c,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "FriendlyName": "Flower Headband (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:34:50.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1875, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bmw4n7qs969ehy3whhp8d7x2q.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,rZOgFhv59UKssAH7k-UDyQ,eVl7mLA1VkmnR81YLF-ZNg", + "FriendlyName": "Flower Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:39.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 181, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5bmu6mglbinvcbqu05awxhls9.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,", + "FriendlyName": "Flower Swimsuit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 434, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3oz7p1t6fe7pqfgq0hoilmiey.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,cf515c87-3c0a-4443-9f63-5ea1bdbaa61a,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:02.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 435, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3bd0st7c55qfn1i4j4cuevmu6.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,27680277-aec1-41be-a476-9f51cf775580,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:03.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 436, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7kp3fr2rwvgorhc9uaqpahhmb.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,5a96ac4d-03c3-4214-9dcb-c2044ff28f2e,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:04.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 53, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b9vsrdat9b9pm7myqavsy8mbw.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,", + "FriendlyName": "Football Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 54, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6sd4dcoqxuhvhrf3casbx3080.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,d8d490aa-7088-4163-9305-1441367f490c,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 55, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ekahaz7vcxq5ejy0zaxrfx28a.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,61ff7a02-50ff-49ea-b95e-c31b95a30c8e,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1662, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d2vzz38oww35dcnjvsxmoj4yd.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,X24EvVokeUqyDNdqevV2EA,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:25:59.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1504587764, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3jI4Pafh9k2B-YTxx1gQOg.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,UQKS4u4SO0umKOHncLqKpw,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Black Yellow Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1720424941, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "T0KyAJMATUqP3TLksvoBxA.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,Q3bhpoWbVkyUBTKUn5A5bg,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Blue Green Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1663, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7t42ep2l7eezztyekwigomtjj.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,UfhOpLemQ0ypYh7CIC3W1Q,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:07.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1155274878, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "mYOYiE057EOkRmW2vXuTdw.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,_MS-R9DB5kinLBszWle8hg,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Black Gold Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 182, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ewgz9ts3guc8ukhtofeng0haj.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,", + "FriendlyName": "Football Shoulder Pads (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 183, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e8nojpjvm8xk8hxnuivjumxe8.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,32dde6dc-e3dc-43ee-9b7c-987bc6778c17,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 184, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5wkaquhpl7lseclkpi4mgjdkp.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,88c3ed05-ce41-4a9d-a4a7-8a428d240b3f,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1664, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d34fyy9i80gyrjrhwyh2coy4s.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,vMqhT19x302aYEo6E9HhOA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:23.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1192138115, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6FJtINu9eEeqW-_S2Cd5Rw.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,G7B-u9qn5EGqSNskBCVsAA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Black Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1242284106, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Y_q7sebeeUuwLnOb_qMjtw.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,jV1TGp1dHUWCJzVMbJskyQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Blue Green Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1665, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2oj4qcrvtpo7yivbcpx3f2voa.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,kCqthBeVvUKZA9b76kxuVQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:34.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016992135, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cJAAR0vVREamuQT8FfpdHQ.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,dzCc_QmTYUGQ67r4yy3XpA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Black Gold Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1260, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1wfcf6b158y11zvf8rsj18asi.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,", + "FriendlyName": "Formal Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:17.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1hodrkppvhvmxs18btxdu979c.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,rcdwgNvO30Cvx2ycE1GfHw,JmFzNL5v_0q1p0bgoOlBpQ", + "FriendlyName": "Formal Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:21.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1924, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dhy0nd82sozehq400v5il9pjj.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,ktnYOVEhtEmuxmjM1TjIOQ,EqNosRGf5U-5P1nKIRV3fw", + "FriendlyName": "Hallow's Dress (Patchwork)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:26.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2553, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sWwqVZYnhEuU3MEH8_419g.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,oM7qz730lke-off_cQw-cA", + "FriendlyName": "Midnight Gala Fancy Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:41.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2819, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "abYMjMnf3UyRf8ys43UA1A.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,HzTJsuYdLkC_NY3ClszRpQ", + "FriendlyName": "Formal Dress (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:45.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d2403tow1ivxsp7dq769lkahh.png", + "AvatarItemDesc": "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14,", + "FriendlyName": "Formal Dress (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:25.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1264, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3o9n9ip2a7y91q3v2kwe6jpnj.png", + "AvatarItemDesc": "73a119ab-d2c8-4da9-98b2-29edbb38e2c2,", + "FriendlyName": "Formal Dress (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73a119ab-d2c8-4da9-98b2-29edbb38e2c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:32.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1263, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8op0aqlcnxdsbinjfpuqdslgn.png", + "AvatarItemDesc": "73c0e2ba-cba1-482a-89d5-1508997aceeb,", + "FriendlyName": "Formal Dress (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73c0e2ba-cba1-482a-89d5-1508997aceeb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:28.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "80f68c67-2df6-4b58-bf2a-dfd76653a781,", + "FriendlyName": "Fortune Teller Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "80f68c67-2df6-4b58-bf2a-dfd76653a781", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1199731224 + }, + { + "AvatarItemDesc": "d536cf31-5649-4d09-94b0-995c4d6f89df,", + "FriendlyName": "Fortune Teller Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d536cf31-5649-4d09-94b0-995c4d6f89df", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1077066165 + }, + { + "AvatarItemDesc": "a98251fe-3eb9-41cb-9982-6ce44e454d85,", + "FriendlyName": "Fortune Teller Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a98251fe-3eb9-41cb-9982-6ce44e454d85", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1777474640 + }, + { + "AvatarItemId": 1454, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dmas6u15o98831skwq4yiijbp.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,", + "FriendlyName": "Fox Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:17.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2178, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2Q7b4md8JE2YQ8VUXFaioQ.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,A-xoYdSUF0aEr_XZ9Me-3Q", + "FriendlyName": "Fox Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:32.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2176, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "oTmagc6WVkCO9huhLC83Lw.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,db-JiDmRhUKX69weDLiHsQ", + "FriendlyName": "Fox Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:28.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2177, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "puLgwYOR7UaDsU2V90_S0A.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,UZY81cnsV0meMp2lg3_UaA", + "FriendlyName": "Fox Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:30.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 808, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "9w8alcxm82aptgu5w3qz5fmq0.png", + "AvatarItemDesc": "OTnfT-4NQU6Gd_iVPDx8eg,", + "FriendlyName": "Frankenstein Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fdf3939-0dee-4e41-8677-f8953c3c7c7a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:33.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 809, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "3yzcedaeybzym9rpwv7nr1pkg.png", + "AvatarItemDesc": "ZsggwOJ8XUOGfaJltM418g,", + "FriendlyName": "Frankenstein Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c020c866-7ce2-435d-867d-a265b4ce35f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:35.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 810, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "8rb3glamh1kq36xnt8gupn6np.png", + "AvatarItemDesc": "tkbHKZ9wu0W00VGeUbIaaw,", + "FriendlyName": "Frankenstein Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "29c746b6-709f-45bb-b4d1-519e51b21a6b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:37.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2428, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YAStiIqV1kOk0QSNHUcb4w.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,", + "FriendlyName": "Frantic Rabbit Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:34.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2431, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mjpFdsz4lkijguGsysSzgQ.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,J8z15LYBak6jHu3ZaaKAXw", + "FriendlyName": "Frantic Rabbit Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:44.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2432, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dTQ_d68ca0Ct3Gh71yHo4Q.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,QSXXWZrS6UaoAUn3u7hXFw", + "FriendlyName": "Yellow Frantic Rabbit Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:49.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2429, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PKZOkAkm-EK4joFEnKiyWg.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,AcLApWMhAU6tK10O7KoOog", + "FriendlyName": "Frantic Rabbit Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:37.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2430, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SsUBkL6SS0OyRRh-yZ886g.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,dhJm0moskU6urIwG1TXs_A", + "FriendlyName": "Frantic Rabbit Vest (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:41.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2433, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "JmX7fBYzcUyHQStd9MQjHQ.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,", + "FriendlyName": "Frantic Rabbit Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:52.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2436, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XOxmltIN6kSzCdXiXACw6w.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,MVhTW78G_E6z2mY0Y5ZnAA", + "FriendlyName": "Frantic Rabbit Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:00.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2437, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NAR-63A3vUSNmcNgu2Jhyw.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,Adiw7dQqyk6pS0q3T0lcSg", + "FriendlyName": "Frantic Rabbit Wrist) Frantic Rabbit Wrist Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:20:03.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2434, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "Ny4bGecU1kqDWzGnr1B1Uw.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,_4mk8E4PUEGf_3SPuFUy2g", + "FriendlyName": "Frantic Rabbit Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:55.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2435, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "VqjRqu51m0mkMRhHinsJ9w.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,aO5Q4BPpuU-bsNavGii40A", + "FriendlyName": "Frantic Rabbit Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:57.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2228, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b4ixc77euj1q8qnhp0oronjic.png", + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,", + "FriendlyName": "Frog Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:47.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2690, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6s01rm1gm99uf83uib3fvl2fe.png", + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,f-RfHoCWik6QgNKDhCs7Vw", + "FriendlyName": "Frog Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:13.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,f8100fb5-92fc-4520-9755-2deaa5430ddb", + "FriendlyName": "Froggy Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1783947886 + }, + { + "AvatarItemDesc": "2b1f0884-c887-4ec9-9984-426405c50f93,", + "FriendlyName": "Froggy Floatie Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b1f0884-c887-4ec9-9984-426405c50f93", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1878911014 + }, + { + "AvatarItemId": 1286, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8xe1257ru45z0b0867roht3eu.png", + "AvatarItemDesc": "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2,", + "FriendlyName": "Froggy Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:57.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2675, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "bU_6Nph8n0KifAktXPcZww.png", + "AvatarItemDesc": "9117bd15-674e-4f22-9ff8-e7365fa43907,", + "FriendlyName": "Acoustic Guitar", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9117bd15-674e-4f22-9ff8-e7365fa43907", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:12.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2676, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "RhxHB33yikq68wkkfgieQA.png", + "AvatarItemDesc": "9117bd15-674e-4f22-9ff8-e7365fa43907,UdQxfRULDEG6BvJV7HWO_g", + "FriendlyName": "Acoustic Guitar (Dark Oak)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9117bd15-674e-4f22-9ff8-e7365fa43907", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:18.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2691, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "63a52r43lcwxjmtic35ksma5j.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,", + "FriendlyName": "Keytar (Teal/Pink)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:16.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2694, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "exgelbk5b8wyv6c26lb3m0bpe.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,GCwZMIr720iH3TszOb6SQA", + "FriendlyName": "Keytar (Black/Red)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:21.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2692, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "9xylg3wbs72hs1ygy34nllz47.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,LmMg-y8w8kiXdre4vUJrZA", + "FriendlyName": "Keytar (Silver)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:18.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2693, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "28bnag6bw350qkhpaeyn6qxbi.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,ze-eTUX6WUqE-n5nIcH0aA", + "FriendlyName": "Keytar (Lime)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:20.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "309f3ab1-d2a4-4d63-b248-93d9795a8a23,", + "FriendlyName": "Fruit Salad Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "309f3ab1-d2a4-4d63-b248-93d9795a8a23", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1227548947 + }, + { + "AvatarItemDesc": "58d3f945-510f-4f92-87e3-a57e29611e4f,", + "FriendlyName": "Fruit Salad Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58d3f945-510f-4f92-87e3-a57e29611e4f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1528201612 + }, + { + "AvatarItemDesc": "c9badd8f-3f81-4c05-8a31-24abd8839872,", + "FriendlyName": "Fruit Salad Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c9badd8f-3f81-4c05-8a31-24abd8839872", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1617129328 + }, + { + "AvatarItemDesc": "788baa71-6ffb-42f5-8dc8-d0f117b04a8b,", + "FriendlyName": "Full Body Beard Close", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "788baa71-6ffb-42f5-8dc8-d0f117b04a8b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1033187763 + }, + { + "AvatarItemDesc": "83ff0105-37a7-4c71-902c-20f473223d1e,", + "FriendlyName": "Full Body Clothes Boots", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83ff0105-37a7-4c71-902c-20f473223d1e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1780430972 + }, + { + "AvatarItemDesc": "ccae1771-434a-48b7-9b4e-1dcf7b70e0da,", + "FriendlyName": "Full Body Clothes Shorts", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ccae1771-434a-48b7-9b4e-1dcf7b70e0da", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1326708353 + }, + { + "AvatarItemDesc": "a3648455-51cb-4ed4-85c5-9a11f96ad3e7,", + "FriendlyName": "Full Body Hair Emo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3648455-51cb-4ed4-85c5-9a11f96ad3e7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1690082234 + }, + { + "AvatarItemDesc": "c5760078-15bc-4ea6-acb9-0ea295a1894b,", + "FriendlyName": "Full Body Hair Flat Top Etched", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5760078-15bc-4ea6-acb9-0ea295a1894b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1377927688 + }, + { + "AvatarItemDesc": "fb64a2ef-ae83-48a9-b021-0b3f0de2d7a5,", + "FriendlyName": "Full Body Hair Fonzie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb64a2ef-ae83-48a9-b021-0b3f0de2d7a5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1598195373 + }, + { + "AvatarItemDesc": "0d58f3a0-7165-41a4-8d77-f88f393d0003,", + "FriendlyName": "Full Body Hair Wavy Middle Part2", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d58f3a0-7165-41a4-8d77-f88f393d0003", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1183373744 + }, + { + "AvatarItemId": 185, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3tc8n5oesvirh7gtgk7ky4kbe.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,", + "FriendlyName": "Fur Coat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 470, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7yjxqxjcdtwsuu11rr9ijfijs.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "FriendlyName": "Fur Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:38.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 471, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ekhbvef0wyu4wu32a5dvhlqqq.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,643b5f2d-0666-4869-b699-03742de34d16,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "FriendlyName": "Fur Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:39.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 555, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bw125vxk818kcf1j36i5htzc4.png", + "AvatarItemDesc": "e042433a-40a2-4758-8074-3b9d407835ec,", + "FriendlyName": "Romper (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e042433a-40a2-4758-8074-3b9d407835ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-10-26T00:08:07.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2790, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zR88MAKiPkimNE_ZJTgJ5w.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,", + "FriendlyName": "Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:03.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2791, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ih-SEYO0UGR8D_71mKT7A.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,1sxxUbsQMki0sTwxEQIbtw", + "FriendlyName": "Blue Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:06.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2792, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1Ud6HT3jLUC49RkE6B4aIA.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,CEnWMDmcMEyjX5mRdhtXXg", + "FriendlyName": "Gardener Gloves (Floral Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:08.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2793, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "vyZN5MKfwEqqAG1yJQQsbw.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,ySbPHkP8WEee2V_P8lt_RA", + "FriendlyName": "Pink Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:11.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2820, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uc9Y0cmhYUi-BL1OyWYdxQ.png", + "AvatarItemDesc": "2e31fd6c-dffa-4571-8e53-fc28fd0c98e0,", + "FriendlyName": "Gardener Apron", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e31fd6c-dffa-4571-8e53-fc28fd0c98e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:47.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2821, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BC8BhDnhAUWqYwj2dvuT2g.png", + "AvatarItemDesc": "11cec676-0ec3-4778-abe4-a9d399900b54,", + "FriendlyName": "Gardener Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11cec676-0ec3-4778-abe4-a9d399900b54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:50.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2822, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Jr88nPGi9EOu05WlVylKgg.png", + "AvatarItemDesc": "ad6e7c98-8056-4e12-92ad-bfa19a00f4af,", + "FriendlyName": "Gardener Shoulder Basket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad6e7c98-8056-4e12-92ad-bfa19a00f4af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:52.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1452, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7z1qr4njpz9qbv9yoq0metngg.png", + "AvatarItemDesc": "cf1a6f8f-1649-423b-b513-b8d40d242b1f,", + "FriendlyName": "Gathered Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1a6f8f-1649-423b-b513-b8d40d242b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:06.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2083, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "sWYoOZIiY0KBXKZYOQ38zw.png", + "AvatarItemDesc": "cf1a6f8f-1649-423b-b513-b8d40d242b1f,8lwfuCRnU0291wqwgTwgRQ", + "FriendlyName": "Riveter Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1a6f8f-1649-423b-b513-b8d40d242b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:26.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2372, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HpB5RkqKYkWc9i_l8Ul66Q.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,", + "FriendlyName": "Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:28.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1343124314, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c6waJLUmskK9zsdjD-IYZw.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,tp0jNW7f0Uubr9nTMWuhXw", + "FriendlyName": "Gentlelady Hat (Pastel Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1842821000, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "H2YS04F1YEKwQ4HAk1GV9A.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,clKD22mme0y8apzUhHmI_A", + "FriendlyName": "Pastel Purple Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1605291822, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NKtxE0Ix9Emzu3RF2XLuxg.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,9liduuR6IE-6C4YN_FKcZg", + "FriendlyName": "Pastel Green Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2373, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wUMF4BFbWkqp6MofjZVxOw.png", + "AvatarItemDesc": "39dde11e-7aa1-4bd2-8532-62665e8d8dd3,", + "FriendlyName": "Gentlelady Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39dde11e-7aa1-4bd2-8532-62665e8d8dd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:30.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2374, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67o7qPb1O0q7OC60gJfjew.png", + "AvatarItemDesc": "f7764314-c02c-4b46-9c56-8c60351d9b14,", + "FriendlyName": "Gentlelady Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f7764314-c02c-4b46-9c56-8c60351d9b14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:33.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1845, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4wnr9uzel6d0lekcez3cw73vw.png", + "AvatarItemDesc": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,", + "FriendlyName": "Gladiator Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:42.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2058, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjmkJTDtvkWsVfz6feF-eQ.png", + "AvatarItemDesc": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,RjX1VTHHoEiNBfXkkniDgw", + "FriendlyName": "Gladiator Torso (Folklore Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:50.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1846, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "azdd85r2w7xl4v8s9ahpnhll3.png", + "AvatarItemDesc": "1633766e-fa77-48f3-8c6c-a233e56bf320,", + "FriendlyName": "Gladiator Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1633766e-fa77-48f3-8c6c-a233e56bf320", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:54.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1847, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ds517zev16c5r1kkrizriku7.png", + "AvatarItemDesc": "594a69fb-9f76-4f83-b2aa-94f0e72e996c,", + "FriendlyName": "Gladiator Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "594a69fb-9f76-4f83-b2aa-94f0e72e996c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:59.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2056, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "JohKrR2o7kqMPVnZzCfTyA.png", + "AvatarItemDesc": "594a69fb-9f76-4f83-b2aa-94f0e72e996c,GL6frGGo2E25VLcw61otXA", + "FriendlyName": "Gladiator Helmet (Folklore Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "594a69fb-9f76-4f83-b2aa-94f0e72e996c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:46.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1880, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6s6sp9bunolpihm8u9g9a538k.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,BqNgpjP7a0qmm3I3DNbKLA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:54.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1879, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9hfdcf1titio5gbw8o07wtk2x.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,5H0I1anlkkuUYIM28_ZOlA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:52.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1878, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ax893cneyhf2wbm98vxenyxxl.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,9tZq7Xvfj0WhhLBAuAacxA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:49.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2092, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "tD99sEOpYUyYZ-MFVKK3lQ.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,dRQI80Qn4E-kzhPJoW66MA", + "FriendlyName": "Acetate Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:19.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2087, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "SYNkbSO4106ID65XGWzI2A.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,FuMUQFSnnU6awB-vPaeuZw", + "FriendlyName": "Acetate Glasses (Bubble Gum)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:06.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2089, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1lmEHPGbEUmTeG7d_lJ6Uw.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,OBcu3bf1NEio_7t9smqGag", + "FriendlyName": "Acetate Glasses (Amber)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:08.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2090, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "h-G0dl2uBEKQJ4YBI0Sw4w.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,F1IV-zZ22kKtXkr2pHp_YQ", + "FriendlyName": "Acetate Glasses (Seafoam)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:12.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2091, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bV0WFKjHdUGPdgeJ9k_BZA.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,Sad0HQX_Y0eYTx1qqgxKwQ", + "FriendlyName": "Acetate Glasses (Sunrise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:18.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2088, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "yt7jg1pEiUuJZMJuLmP55A.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,-OgDdlvL9EyzeiA50YY5kw", + "FriendlyName": "Acetate Glasses (Charcoal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:07.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "0b38899f-6429-49aa-b626-2696359dc1cf,", + "FriendlyName": "Glitch Wings Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b38899f-6429-49aa-b626-2696359dc1cf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1805065453 + }, + { + "AvatarItemId": 2831, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GenNVM5HVEKt_DZK0VQsQw.png", + "AvatarItemDesc": "f90773c6-d0f0-417a-9af5-183d1e5a657f,", + "FriendlyName": "Glowstick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f90773c6-d0f0-417a-9af5-183d1e5a657f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:37.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2832, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "veozF1-boEi1gm3EPXGN2Q.png", + "AvatarItemDesc": "c51780fd-9eb5-4bc0-9da0-34ab22d800df,", + "FriendlyName": "Glowstick Bracelet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51780fd-9eb5-4bc0-9da0-34ab22d800df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:39.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1916, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "084qi740gtrdi1jyi3yjt5q6r.png", + "AvatarItemDesc": "b772a5a9-dd79-43e4-8d95-f965d81bed8d,", + "FriendlyName": "Kevin the Goblin Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b772a5a9-dd79-43e4-8d95-f965d81bed8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:19:16.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 116, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "04xgdtlhrslfn0o41so5fnjoi.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,", + "FriendlyName": "Winner's Medal (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8xn26bulod52740rbedbiwok2.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,ERnUPsyT6kuEnIRZF8zPwg,wzt-5C_LjEmIQWUVPcYWhA", + "FriendlyName": "Winner's Medal (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:40.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 906, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4rm8hpw76k9han4s5k58jc9gc.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,iaDL2im4lkCXUo0NWM-F3g,wzt-5C_LjEmIQWUVPcYWhA", + "FriendlyName": "Winner's Medal (Bronze)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:38.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 503, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3dzbcs7n1malcbemyi60hae9g.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,", + "FriendlyName": "Golfer Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:30.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 504, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bwzoz4dtuwlngxk9f0tu4ubak.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,48c18418-01dd-40ff-9dfe-c4cf5f4e1245,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "FriendlyName": "Golfer Hat (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:31.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 505, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1iqnljjvv128u9d8mlok69amn.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,6584dada-499e-4d7c-958d-466955b20640,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "FriendlyName": "Golfer Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:32.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2309, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "V_xCN9YtNEqAF6pAvOkE9w.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,2Mgx6S9f8k2H1AAYbkzjQg", + "FriendlyName": "Golfer Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:01.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 506, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67p6qo2ys9s1k0dg867wt3hlo.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,", + "FriendlyName": "Golfer Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:33.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 507, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "arl9g7pg2z44m7uqebdjk9ool.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,4c845dbb-26c3-48a1-b562-e28a7416b154,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "FriendlyName": "Golfer Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:34.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 508, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "01ylknc03jnajh84tyy03knwg.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,96e2b29e-e553-40f1-9eb6-8a9f7e64da39,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "FriendlyName": "Golfer Shirt (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:34.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 510, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eqstzzls6wlgjfdkwtcpj53gp.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,", + "FriendlyName": "Golfer Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:36.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 511, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6etin2gx8z6mi85m8niz96so2.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,72a010c0-bddd-4e5d-bca6-9821d82de9a6,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "FriendlyName": "Golfer Gloves (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:37.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 512, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bi4tmrffhhhfm5xmt03sfyvnu.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,32a5e42d-2b60-40b1-a145-ed96fc91af8e,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "FriendlyName": "Golfer Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:38.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1085, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "02907rdlfgdak15kydocegpp7.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,", + "FriendlyName": "Graduation Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:19:58.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1163, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ll333kyucn9aqhff96o0txse.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,FaWMLkJJgEKFIO493fzGxg,yzL0M_N5Q0eAa8IxACHPAQ", + "FriendlyName": "Graduation Cap (Maker Pen Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:05:53.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1803, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2lx5rfp2pcfj2z8z1wr7f1uuu.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,nCPVMrWXGkeeIN-969xzuQ,yzL0M_N5Q0eAa8IxACHPAQ", + "FriendlyName": "Graduation Cap (Video Creation Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:53.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1114, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bm925en2c6cq90rczyqx8jtek.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,", + "FriendlyName": "Graduation Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:20.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1162, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "44739pgupptu8ki1u1i0efr2i.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,iZy7RmfAz0603oMO-jD-WA,ica0LZyM_kOox8gxW7CaMw", + "FriendlyName": "Graduation Gown (Maker Pen Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:05:43.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1810, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "91oh3z6dug9f7buh2555h4ggn.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,4D4ge1PYokaj_g96k4oasA,ica0LZyM_kOox8gxW7CaMw", + "FriendlyName": "Graduation Gown (Video Creation Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:36.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1065, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "98c5cktigvhw45kiu5db444wj.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,", + "FriendlyName": "Grillmaster Apron (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:36.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1066, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4z1alsikbw52tg9t7o4b0tmf8.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,eV3Nv6SWS0yl3f-fgjPb-w,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (Denim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:45.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1067, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f08vx9xbl5n8d0cdwaqunjhs5.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,Lxx3q6JRnUW_cPG_kGG4ag,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:48.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1768, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4ulmpmgcp45eu79inqrlhz2d.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,vek37vRROUKnSJKVP0CH6w,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (Charcoal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:22.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1076, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5gw83joytwxpoarjjrzas61ip.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,", + "FriendlyName": "Grillmaster Mitt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:09.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1077, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "10xu47yb16kjm62v7fasn937i.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,90qQe0AR3kOou0MsuoTmPA,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:11.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1078, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uhjnjl997sivm0uih15zk8ug.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,hZKPr4sg10SRj9DLuEgOrw,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:12.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1782, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4oo9dvqgiu2yehkvk6dey3jdc.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,x3Q0vVeG_E6juqGM1o0SOQ,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:38.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 961, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2i4t0ybjtnnq1pyn2a2zmo32b.png", + "AvatarItemDesc": "m5Plrduzr0yj_9mTn1x3Vw,", + "FriendlyName": "1988 Jacket (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ade5939b-b3db-4caf-a3ff-d9939f5c7757", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T22:07:36.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2736, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PEHKs2wQe0SFdV5ekTdSkA.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,", + "FriendlyName": "Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:42.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2741, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "h6jjmTpeBkGLdb3Sbt_-0Q.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,dtuR9xLXVUGzSi2G1DMh7A", + "FriendlyName": "Red Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:50.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2737, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GjQ7GjOgmUSLmyl9DH0hjA.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,0FlA_T8GsUe9PdiZamlP_g", + "FriendlyName": "Guitar Pick Necklace (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:43.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2740, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-IEuy3vB4kK_xxTTWTsQXw.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,U8ZZiMEeZ0-qJfcpqBP72A", + "FriendlyName": "Guitar Pick Necklace (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:48.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2739, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rVsH83vYHkO80KaIwEVyyw.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,Gct9Vt-L4UiTjAl61hsebQ", + "FriendlyName": "Guitar Pick Necklace (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:47.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2738, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EfUdI1XtV0eRy8PsZBG2dg.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,2r_gYLhQrkS0bjwaxLecfw", + "FriendlyName": "Grey Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:45.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2185, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NboCAaiehkigdPqWgZxRoA.png", + "AvatarItemDesc": "2d07c0d0-b277-46dd-8980-382bdaa4749e,", + "FriendlyName": "Angel Halo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2d07c0d0-b277-46dd-8980-382bdaa4749e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:07.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2554, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "z5qNLmnhd0aHhDdIxO-wsg.png", + "AvatarItemDesc": "2d07c0d0-b277-46dd-8980-382bdaa4749e,f6O5cw0vc0ineEptRysocg", + "FriendlyName": "Science Halo (Invasion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2d07c0d0-b277-46dd-8980-382bdaa4749e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:44.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2742, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "i6sQmmZpgEWvZNkWaYpaWg.png", + "AvatarItemDesc": "fe16e054-9cae-4a97-a0c0-256b51cdb0e5,", + "FriendlyName": "awesome sauce", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe16e054-9cae-4a97-a0c0-256b51cdb0e5", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:52.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1147, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2y48o7b7lqi2zpho283lmy51n.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,", + "FriendlyName": "Runner Harness (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:00.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1146, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6xgus9q1g8stsgzfpsinr3li4.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,sX0ZZDcCUkyFSJISfvq0vg,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:56.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1145, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5fi6lrfurv47a5jv8vcz3fmxv.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,TnEDe1QoMUCUdqPL_7CxTA,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:51.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1144, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "axtrgokadupvoy6xhgle28790.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,AC3zfm38JEyBZWuXDh4Ukg,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:47.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1143, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ke3ipxnfopjshcyjl3kc7mhu.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,RfZNDPUok0WyQEa3Ko08ow,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:43.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1142, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c7vhxic6odjal3d4elovf6li9.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,dyEaPloE8kazWC7Zblv8uQ,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:38.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,E7zTfb8zbUKuld0rxNt7fg", + "FriendlyName": "Harness Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1003112581 + }, + { + "AvatarItemId": 1164, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "435egcfj9uj17vp4n7ey9u4rv.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,", + "FriendlyName": "Runner Wrist Guard (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:29.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1165, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bwo3vsz07ltq6bsv2c8bszrgs.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,sX0ZZDcCUkyFSJISfvq0vg,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:32.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1166, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "aiygi6wtiasforjki3lnkg1ab.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,TnEDe1QoMUCUdqPL_7CxTA,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Lavender)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:34.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1167, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4poast86ikgyo8gx5xw0v6tl8.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,AC3zfm38JEyBZWuXDh4Ukg,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:35.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1168, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3qn2bl4qw76u5zafxwim8hslp.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,RfZNDPUok0WyQEa3Ko08ow,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:37.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1169, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9ny1060mulzukvbnj8r8jsk77.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,dyEaPloE8kazWC7Zblv8uQ,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:38.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,MvC6bRHxmkaXvWS6Q0cIFg", + "FriendlyName": "Harness Gloves Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1884541935 + }, + { + "AvatarItemId": 1153, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ac4yc0p8960f0pcub6ys93vk1.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,", + "FriendlyName": "Runner Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:28.913Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1152, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "diwahrdx0ag6e8a70yal6uos9.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Euf1h1qsZ02evDsyoHYbPQ,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:19.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1151, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8i6nmv636c2mf1lxl17ox1d4i.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,lsyYlxY9o0a3TbUrHkxeyQ,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:14.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1150, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6urhwi4bkj3jxkd4pq9lxzapp.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,JVZHZm6nGEqE2zEEPoHbwA,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:10.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1149, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6m4gs8ysz42vgc0rn06moyk71.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,zlqLrGEa8E2WW9ey_szREA,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:06.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1148, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a22f3ny3x4czh17wi8o7cxnp9.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,L2iFQcL170OajqfW7t6sVw,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:03.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Iz6cWpY2B0uOQ9BnY3fSbA", + "FriendlyName": "Harness Helmet Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1073107630 + }, + { + "AvatarItemDesc": "8705d28f-49f4-48d9-9946-13f18aabfe4b,", + "FriendlyName": "Hammer Head Shark Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8705d28f-49f4-48d9-9946-13f18aabfe4b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1285875244 + }, + { + "AvatarItemId": 1088, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6t7adsd6rb8v7w110axmy2fpk.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,", + "FriendlyName": "Big Head Bow (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:16.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1089, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e6lw7lptzoy6kedqecx8jk0ob.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,8iZO162KNUiMC8l_z3OC_A,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:21.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1090, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzilh25e7rcim8kgspfpj95ba.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,f_c50saStkaGJuhLjJ9lfA,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:25.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1091, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "18wsz87rejkgdxqq1wc65icjt.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,FUP4iRnoc0ikNmjrFD06lg,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:28.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1092, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d40z3ipsfgbe9hux480cvd5a2.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,GmS0wKwzVEqdf3NoTMHG5A,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Teal, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:32.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1093, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3bok5be6yuo76i8jwnvzr1kf2.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,ZNKGkou_gU-1cxBFy7pDKw,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Red, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1094, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bo0hnyxq2ym7xxw3uicmaydgj.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,-Iqmonry4EKoqAULCKzRPA,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (White, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:38.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1294, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9y2nffs1iso5dhjr38gstlhpq.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,D-SngK7hhkOpoZ2aMWv9EA,mkDVc0Mz4kmuLZ7powBHpQ", + "FriendlyName": "Big Head Bow (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:35.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1355, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "824gaa1rig6e7wnf2tax1j1j9.png", + "AvatarItemDesc": "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3,", + "FriendlyName": "Big Head Bow (Green Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:45.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 563, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ckopqewba1tguffkue6s78rc3.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Headband (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-13T20:56:57.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2076, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "JGqSITx8dk6mMtS4gTrMaQ.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,TeSudgm-NEy3DXe3z_VRqg", + "FriendlyName": "Headband (Retro)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:11.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2077, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dl4fJip8EEukov2aRWgT7A.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,RnrXuIRvMkeW5w1lcNiuaQ", + "FriendlyName": "Soccer Print Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:14.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1095, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3cc5bj2gxpsblgaruo3t9egwm.png", + "AvatarItemDesc": "J6IQt4G1zUeN8v9Wn50BUw,", + "FriendlyName": "Tied Headband (YouTuber, SoulFox)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b710a227-b581-47cd-8df2-ff569f9d0153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:43.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dn3d4707ue1k2osfrlcvavlzo.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,", + "FriendlyName": "Notes Headphones (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:43.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1045, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6gdfkv6dgyb7aup991rfx4ns8.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,viL4CPch2ECwG-RNaWcyZw,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:47.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1046, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ftwtrq2ong4idyrsil0vdmek.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,8ANbOhNnO0yLQOCIBsDwfg,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:48.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1047, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cph7oedpx7hgjd38qq4qtgoal.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,a5nUvJZgTEGBUvdHx0Y6Ng,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:49.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1048, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "60mqpyf61x722pn9if4oa70qi.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,JWBK5v78Y0uRVEXlD-Qk6Q,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:50.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1502, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ecbfwjrpe9e79d5vrdubmh2c4.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,thG3Dc1PFEqAvT9xCVshuw,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:44.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1049, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6k8vpwyhep8o2yv8j2grqd8h5.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,hf0gkFVS8ESvceRsC3LAXQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Psy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:51.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1050, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bhaum9si19ovo5u6f0ik7fcyv.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,4scG_UBlzE-5DWRhSrtm6g,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Chillout)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:52.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1051, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f012aaez937gwxfcp6amwbh1a.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,OqsbZ8Xls0i8uA6ha7H3qQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Trance)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:54.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1052, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0qoenhowtvhtxia42kmo22j5v.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,nX7krD8e2km_ZlOLWcoxdQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Lounge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:55.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1053, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "75w0v1yh3q3o0dfoelib0un32.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,ns5tIRishUOp-nYqLedcwA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Techno)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:56.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1501, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1czj45wblivuiu4m5plehpt8f.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,k_ZYy0SjtU2TB7IFdq2vDA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Synth)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:40.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1054, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "48dwhvntvbcgfw5d6yly1wr9y.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,I1J-xY4YNU2dB_od4P_7Dg,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Heat)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:57.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1055, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2jwdp3bzhm7xcqru8enpkveu7.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,2sf-FU4WckGBDWUkVz4z1g,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Sky)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:58.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1056, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7yuwkss6may3hvmhg1sepbsvw.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,3PfP661Fg0CruzRxis9EiA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Dreams)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:59.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1057, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "19ts2iqffk8o10duwieerytd8.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,d9mwowRelE2lDKEA0UQMZQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Dawn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:01.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1058, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3lnjdd8bye13tx2sv12v4bisy.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,Uz2hrptCa0erjqyPoiZxew,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Void)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:02.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1500, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4dh2u6nja9oh06bc83r9s4pqb.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,PQTvQiYu-ESRUldnxgm5BQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Jungle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:35.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2209, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8thOMYf1lUybc3FN_bY8WQ.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,oNtLjorrZ06t4yIdWqKCcQ", + "FriendlyName": "Headphones (Wooden)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:26.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2795, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SbQkirL3sUidVCVTEkiS0A.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,pAlEt5sqD0mKgFd_0b7RgA", + "FriendlyName": "Floral Headphones (Blossom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:16.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2796, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OjJJcLC_l0SxYuXsaT4blg.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,PNH0VNS1qk6nWVecYmVDDQ", + "FriendlyName": "Floral Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:22.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2797, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "snOakm9_R0upwnPGZDeEuw.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,sXF1BZr8X06AYnWUrWxy1Q", + "FriendlyName": "Floral Headphones (Lavender)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:25.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,e5954339-a379-4510-a54e-18f64e29ffe5", + "FriendlyName": "Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1452629529 + }, + { + "AvatarItemId": 1083, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c0aetz7tzn1vg6rqcopztwq0i.png", + "AvatarItemDesc": "akAhnN0cFkecj8q3spixkw,", + "FriendlyName": "Notes Headphones (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c21406a-1cdd-4716-9c8f-cab7b298b193", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-12T01:24:56.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2375, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ONlwGgbS3EG8In6YslOpGw.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,", + "FriendlyName": "Headphones Mic Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-30T17:42:35.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2376, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NY7YBusIgkCX1gRBRn_KDg.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,M9ZKWX59e02037Qy4rgmAA", + "FriendlyName": "Headphones Mic Hat) Headphones Mic Video Contest Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-30T17:42:37.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2565, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "46_hhqT9P0isgoM6SBDAVw.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,u2nh8EAZT02TxPTWVl59UA", + "FriendlyName": "Science Headset (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:01.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2151, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "730oGQopMUGTrJNdUPTkeA.png", + "AvatarItemDesc": "1db08f02-d559-40ea-9bae-dd11046694a7,", + "FriendlyName": "Video Partner Headset", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1db08f02-d559-40ea-9bae-dd11046694a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:18.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r0K8LjSj0U2lRyRwptpOoQ.png", + "AvatarItemDesc": "fbf74c15-4b42-4cd4-94dd-d0e86f11541b,", + "FriendlyName": "Heart Antennae", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fbf74c15-4b42-4cd4-94dd-d0e86f11541b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:00.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1955, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1uytf301ssh2nyytxcnm0r2v1.png", + "AvatarItemDesc": "5944a66e-b386-43d9-a4b6-7b4a8e987fd8,", + "FriendlyName": "Heart Sunglasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5944a66e-b386-43d9-a4b6-7b4a8e987fd8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:15:27.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2239, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6Wo-wGbct0y9F6OdVvsZog.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,", + "FriendlyName": "Anime Hair Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:26.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2557, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5VAj7sHGqkWRLz4ktf2ibQ.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,YfRfaC49J0yW0wnSJf5DwA", + "FriendlyName": "Anime Hair Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:50.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2555, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gK3kKpiWpkeCTN3muHzInw.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,0WW04NMMfU2LLahCq0jQ6Q", + "FriendlyName": "Anime Hair Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:45.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2556, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HPGiZzkfbUagfHZSMVdpIQ.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,4kYWnAwZo0SDZFmiU8jJwg", + "FriendlyName": "Anime Hair Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:48.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2576, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d7RSXQeGNE2FITMNS4x9lA.png", + "AvatarItemDesc": "f09be0ec-19eb-4443-843c-b387b6731e48,", + "FriendlyName": "He-Man Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f09be0ec-19eb-4443-843c-b387b6731e48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:32.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2577, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KeJx2mtu3UWduFaoj3_lzQ.png", + "AvatarItemDesc": "28f00eee-d3d1-4cac-ac70-fca064fcbaf0,", + "FriendlyName": "He-Man Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28f00eee-d3d1-4cac-ac70-fca064fcbaf0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:34.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2578, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YgFKySGtSUOhM_WsMBYbNQ.png", + "AvatarItemDesc": "9ab4ea8a-1b16-4969-9b80-8e36b1f782ca,", + "FriendlyName": "He-Man Bracers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab4ea8a-1b16-4969-9b80-8e36b1f782ca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:37.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 56, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "37a5o4unxmplzke25isjr0n4n.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,", + "FriendlyName": "Flower Hair Clip (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 485, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "chwhfus6b1dkb3vtb59wuymig.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,ce465d53-3807-423f-baa0-bdba2a75ceb3,65b89eae-347e-4eff-9ff3-7f25cace0125", + "FriendlyName": "Flower Hair Clip (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:05.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 486, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "23uhduy56xwn0r399ed4oscrw.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,596097a8-d545-4256-959d-1d3cd96fc64c,65b89eae-347e-4eff-9ff3-7f25cace0125", + "FriendlyName": "Flower Hair Clip (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:07.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1763, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0lhij515w2a2pi1bjg8pqzztr.png", + "AvatarItemDesc": "d15139de-5e57-423b-b8da-335ad7a4df5a,", + "FriendlyName": "Fae Horns (Hidden Worlds Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d15139de-5e57-423b-b8da-335ad7a4df5a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:29.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ehfh8lkiq0cqwio8dxk9kdg0y.png", + "AvatarItemDesc": "f51abeae-7c5d-4065-aa2a-b2e7025b3fcd,", + "FriendlyName": "Fae Tunic (Hidden Worlds Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51abeae-7c5d-4065-aa2a-b2e7025b3fcd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:33.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2251, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CHOox5OrjECPbDwbhliATw.png", + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:05.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,N-C_xBTZeUq1YsdELln53Q", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1435331028 + }, + { + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,G4MuMrpiKEWN0uEBcGlX4w", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1797238548 + }, + { + "AvatarItemId": 2252, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nonED7GaH0qPJ67vtt7QxQ.png", + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:07.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,VfSI4zfHbECwcrQmcbelTg", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1842931803 + }, + { + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,qRIbIelHBUOU71eoK2XQeg", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1992229999 + }, + { + "AvatarItemId": 2253, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwCEXZw8HkeluG48ThKdoQ.png", + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,", + "FriendlyName": "Hiker Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:11.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,IcgjR69rYUuNhsb3s9qjYA", + "FriendlyName": "Hiker Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1494615195 + }, + { + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,yMkN0om7l0-_47gDrBWxiw", + "FriendlyName": "Hiker Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1664824131 + }, + { + "AvatarItemId": 57, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0ky7ceo16g76hoi2vudcp4u2d.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,", + "FriendlyName": "Hockey Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 639, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7rm6qzjybqpssdvxptzb3cq12.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:11.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 640, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ap6srpnsg5qje8yjd0k6xoq5v.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:12.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1718, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "08ly4egzp8znsf9rr74dqyzom.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,qdA34j6p_UqphqChTCHEYQ,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:09:30.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2078, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HfG5sUm4_EC1LqHmdMhF4Q.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,", + "FriendlyName": "Hockey Helmet (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:16.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2715, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yVFjcpLMhUGC2rFphSKjxg.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,Z6o5ySTsmkmwAGslDH7k4w", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:06.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2716, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "l33ddE1N5kSh2m4vYm-lhw.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,cLKDeDSwd0OvF27bW_wBWA", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:08.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2714, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hGYlm15Q8kS3Qwi0ugLJdw.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,m4t_QKkV4EWTVEH8biQuEA", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat Purple Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:03.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2079, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DLbcAnoh6kOcI8vlUfbceg.png", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,", + "FriendlyName": "Hockey Jersey (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:18.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2702, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "R1XZo-34Wkm75mdVSeCFPw.png", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,c3Su0qnh-UK-ymcGaUy4Uw", + "FriendlyName": "Hockey Jersey (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:34.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1547381390, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,zhgbWHad00aE2KNFM3FNWQ", + "FriendlyName": "Hockey Set Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.507777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 186, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3c1iifcjs873h8qwb9pekkmpu.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,", + "FriendlyName": "Hockey Shirt (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 496, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8j3mq83a7m2bcd1ue5tzv6r2o.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,fcbdfbfd-254a-4538-b119-43e23404b8b5,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:19.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 497, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bkzswe4hfhdvak21tbjhe6icc.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,b1ea158d-1663-4db1-bb09-9847f6e9d1e6,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:20.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 498, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "91cioy61054580v8ruloocj3l.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (Red, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:21.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 752, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "964z64p0djst7okp5yy4qnb5n.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,", + "FriendlyName": "Rec Room Hoodie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:24.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 787, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9d2j5ikkdbngspuc7tki5lecu.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,mvMeyStxGUGlNLSYNeSubQ,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:22.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 786, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4jf3ogeghh4h207uwsgs53ay.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,u_Gzj9Nro0WS48xKnH9few,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:19.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 796, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "14fpnws4q76d3svlzzp3ogr29.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,JTgXKeT7w0K2Mw4Jk2HCqg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:26.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 793, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ccdrg5p5ytm15zierfd33d677.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,rNhOQoP3XkOBHV28ze2NpA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:50.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 878, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "89cwe9sw6lx2jml1bc48d9io1.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,TEgugZRs80iJbhL87RdYwg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:54.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 877, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "81kofnq5cb3fvgyozz15k1ro0.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,ytmSzjhCr0Sm58MSNbwJUA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:39.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 876, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6w9zh5x30uv01aj0rnudzyicy.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,yM5xRrd9OkmDGyQddwOP6g,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:37.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 875, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2xo1n1x3mq2iry1ptnih904dt.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,-Nf7WM2TeEKujB637h3tRA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (White - Starter Pack)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1612, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ngrs0bphj486q6105peoygyq.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,LuYelZY8NkqmOp1pnxPXBg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:23.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1809, + "AvatarItemType": 0, + "Tooltip": "For earning $100", + "Rarity": 50, + "ThumbnailImage": "5yo24l49i1ul66r6i2yec4ggz.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Z10TTlOeW0mumezbqXKGzA,Ht0WRvpguUaLZ1tigCzCMA", + "FriendlyName": "Rec Room Hoodie (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:33.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2153, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9ZVUjjO21U2ujrZzoNX9SA.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Ist3SXFwKkCCvxirWLLjng", + "FriendlyName": "Video Partner Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:21.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2202, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6TEq-Xvb8kirly5K2g96vg.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,9jB8U3yxjE6hanEZgMdvtA", + "FriendlyName": "Rec Room Hoodie - AAPI (AAPI Heritage Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:02.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Ebi3A88h2U2yiygzWBBL_w", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1692692324 + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,gPPg0QrOlEmVmvEc_OheXA", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1976989717 + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,d022fcd7-850e-490b-893e-eb713e5848b5", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1155409075 + }, + { + "AvatarItemId": 1375, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aojr333s0451ovrb3sccpajtm.png", + "AvatarItemDesc": "5362d685-fc55-4b43-85a5-cc3d6cad3f69,", + "FriendlyName": "Rec Room Hoodie (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5362d685-fc55-4b43-85a5-cc3d6cad3f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:10.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2245, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9EjzWesDb0qc78bYLR4gXw.png", + "AvatarItemDesc": "ac957e1e-676b-45c9-a39d-b238d2329a03,", + "FriendlyName": "Horseshoe Bracelet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ac957e1e-676b-45c9-a39d-b238d2329a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:58.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2246, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ewhdfsr2ocppmfgs5a2ddeb9h.png", + "AvatarItemDesc": "9c4976c1-a0eb-4a1e-b5cd-08d44a4b3011,", + "FriendlyName": "Horseshoe Earring", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c4976c1-a0eb-4a1e-b5cd-08d44a4b3011", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:00.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2247, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5vzoo1Ope0ayRkVlKiJDZg.png", + "AvatarItemDesc": "cd77c8ed-ab60-4f42-9bee-0141c7e3e626,", + "FriendlyName": "Horseshoe Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cd77c8ed-ab60-4f42-9bee-0141c7e3e626", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:02.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2377, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "Vv4sv_u_EU2qi4FLQuoCmg.png", + "AvatarItemDesc": "b6d10952-d492-45e5-b54c-de3ab138be0b,", + "FriendlyName": "Hot Dog Hat (Mustard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6d10952-d492-45e5-b54c-de3ab138be0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:39.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2378, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "N2RqYRINKU-RJEE5tp5BNg.png", + "AvatarItemDesc": "b6d10952-d492-45e5-b54c-de3ab138be0b,2KCpbel8JkaeAGQRgvpt2w", + "FriendlyName": "Hot Dog Hat (Ketchup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6d10952-d492-45e5-b54c-de3ab138be0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:41.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pR32NwZrJUWfiKEdw3CYeg.png", + "AvatarItemDesc": "4c52478a-b2c9-4520-8f88-52f2262cc6bf,", + "FriendlyName": "Hot Dog Suit (Mustard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c52478a-b2c9-4520-8f88-52f2262cc6bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:43.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Oy7uq9Y6VEi4oYPxwG-aMA.png", + "AvatarItemDesc": "4c52478a-b2c9-4520-8f88-52f2262cc6bf,YR7P-KQLGUKPF7JbW85K7g", + "FriendlyName": "Hot Dog Suit (Ketchup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c52478a-b2c9-4520-8f88-52f2262cc6bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:46.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b5221aee-22ca-4855-81d9-0b4491229060,", + "FriendlyName": "Hoverboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b5221aee-22ca-4855-81d9-0b4491229060", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1868649629 + }, + { + "AvatarItemDesc": "2c09d6b4-039e-4475-b013-0630740d820d,", + "FriendlyName": "Ice Cream Truck Box Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c09d6b4-039e-4475-b013-0630740d820d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1514562461 + }, + { + "AvatarItemId": 952, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9zxq5okpcuze5r8vwhknknnfd.png", + "AvatarItemDesc": "W7AJ4UYlVkWF5nLbmpmRBg,", + "FriendlyName": "Ice Queen Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e109b05b-2546-4556-85e6-72db9a999106", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:39.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1939, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8tqs6ckiohne11n3sui6d7dvq.png", + "AvatarItemDesc": "W7AJ4UYlVkWF5nLbmpmRBg,5Hk7UR60rkquUNxpQ8O0Gw,zMXDVNsKn0GhVd2Tjho0-g", + "FriendlyName": "Ice Queen Crown (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e109b05b-2546-4556-85e6-72db9a999106", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:31.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 954, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "da9hnrgqj5nbv2mz69que9ixy.png", + "AvatarItemDesc": "ilDgmey2ZUWlFiuEAN7YoQ,", + "FriendlyName": "Ice Queen Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "99e0508a-b6ec-4565-a516-2b8400ded8a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:50.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1941, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4n3khnydgbrm5og1sy02ak2rp.png", + "AvatarItemDesc": "ilDgmey2ZUWlFiuEAN7YoQ,K9wPZ9LHo02GPMtf02rANQ,U7jMLMX4_kKIy1K9IcHOXA", + "FriendlyName": "Ice Queen Gloves (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "99e0508a-b6ec-4565-a516-2b8400ded8a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:55.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 953, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7lqx31eaisz33b3nvpajuxr47.png", + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,", + "FriendlyName": "Ice Queen Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:48.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1940, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "edfuldgko8416a06hqug8ckhi.png", + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,iqSyLD37IUejWYa8ndvqnQ,qrXO8rdEaUqUCXC1QVkhsw", + "FriendlyName": "Ice Queen Gown (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:48.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,TRpbSTiI9Ua2E6H7XiT0lQ", + "FriendlyName": "Ice Queen Gown Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1042519908 + }, + { + "AvatarItemId": 2695, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5YfnXd6wCEGAI8JfuYuzDQ.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,", + "FriendlyName": "Ice Skates on Neck (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:23.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2698, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vZa1r1ZS0CNjfN_JffQ5w.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,ENYQugZRP06SDh6d-oJcjQ", + "FriendlyName": "Ice Skates on Neck (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:29.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2697, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VLP_xmNQBkWJGvDaXrvrQQ.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,KdsZQVCA-E2TiEozsSUv7w", + "FriendlyName": "Ice Skates on Neck (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:27.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2696, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cdkQ8Fl7h0-dBHorHAVq7g.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,uQjh0DySZE6M6DzD2VUPgA", + "FriendlyName": "Ice Skates Neck) Ice Skates Neck Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-21T19:38:25.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1505, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "21f1vt9sfsmvu7i0m9fa5v25j.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,", + "FriendlyName": "I Heart Rec Room Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:52.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1586, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e2e7acypymae4je7p6sgzp5cj.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,4oywwjb3-kifr-um9x87Pw,11ZEZTFhy0ONpoXb3kiewQ", + "FriendlyName": "Suspicious Goblin Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:38:26.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1695, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dpd8v8j4srhnyyau92pk9e677.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,i4Zu9uZuWkWwm8IVGZwkbA,RmjxL1O9bEGOuYxY9kzHEA", + "FriendlyName": "I Heart Rec Room Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:32.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1694, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a4crcmj7ke4jvau32zw1jzrfl.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,HQOKcDCvVUCItkAKo2ygGg,RmjxL1O9bEGOuYxY9kzHEA", + "FriendlyName": "I Heart Rec Room Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:29.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1922, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "dygizmsl47ijdpie6x1q5tspr.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,Rd-iQjCjUUW8_o2kIb1rFw,Ez3OIIGl0Eqys2JGnSnQHw", + "FriendlyName": "Pumpkin Pattern Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:09.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1988, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5nmucss74wx9jq66yxgs5bstt.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,lYsxZfxd0EadrnjliEj4Ug,xnZj7ipl60WO8e_BXK-mlw", + "FriendlyName": "Lunar New Year Sweater (Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:43:34.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2778, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GVwVHoCBbEqlEsScL_SXfQ.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,ymRNNF0h3UOv-yH0ILxhiA", + "FriendlyName": "Rec Rocks Sweater (Ethan Bortnick)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:39.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1514, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "36ltpv6ah0ck4rjqp3q1ypw3k.png", + "AvatarItemDesc": "5873814e-c9a1-4042-b96d-2aa0e0d52e2a,", + "FriendlyName": "International Thief Coat (Mystery Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5873814e-c9a1-4042-b96d-2aa0e0d52e2a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:57.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1385, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ev03rmx75zjo4wrysupitq82o.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,", + "FriendlyName": "International Thief Gloves (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:15.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3hl7xtr4md2vxqqi3d3z5zgxe.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,DtFYrQwaCky5C1ZycqZbiQ,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:17.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1387, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3gmgmoqsor5tij57yfuqtj9i0.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,9SAPupKy_EO6YeG0SMkI5A,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:19.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1388, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "29vqlnj8n33qtfh0u4ka6qbtz.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,hiMYlmk5sEqh-sElOY3A9g,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:21.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1381, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7oh8ujd818cghfg99enk8x1d8.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,", + "FriendlyName": "International Thief Hat (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:54.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1382, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d0flbz3gdxvalp78ggvuj2x8p.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,RBRFHN8_rkuImbyDAfcHWw,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:58.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1383, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2jtzah4nvld7knjcyuxcgml69.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,QJSPgq2KXEOWNynY8uqQvA,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:01.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1384, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0bjo5i6bxmha24uhgq15xq71t.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,4RH8_Z1mqU-JIVvx31wOsA,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:05.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1515, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "87ayi0litn96xap15w1n2iq85.png", + "AvatarItemDesc": "d12e5738-00c7-47e7-9334-8399eec5aa89,", + "FriendlyName": "International Thief Hat (Mystery Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d12e5738-00c7-47e7-9334-8399eec5aa89", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:19:01.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1377, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "alszurr07un4fg5ta4aw0z10o.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,", + "FriendlyName": "International Thief Coat (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:44.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1378, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzgwcv1v83e9lql441lco77up.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,YB7kXMyLJ0mF_5WidhmH8A,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:47.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8r2mn0ynnf5yr8p2yifhlpskz.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,bQyNgFWAe0SPttPri6hxyg,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:49.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5lo58vo2p2eebv8s1aunra4xy.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,wRPoHTsXFESK5pjkxVMqGA,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:51.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1588, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0h4tnqaliavr973k1vxic8j21.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,", + "FriendlyName": "Jacket Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:15.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1589, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "688mit0sflv394soqbkisg5u9.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,v_5qFi-vAUSDd1fLNL7pvA,X0_GvGrZGkSP9Ub81CHgqQ", + "FriendlyName": "Jacket Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:21.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1590, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e1v40txlz8i12cmhyjswztu53.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,y5O8zRJy4kuhnXzATSWKew,X0_GvGrZGkSP9Ub81CHgqQ", + "FriendlyName": "Jacket Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:33.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2492, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4DTZdxI0zUWUUrv5xARVCA.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,tLbmGIoldE2CC5Qi1oyqyA", + "FriendlyName": "Jacket Dress (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:08.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1656, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b8nie0lojas6v6embnj3tteiq.png", + "AvatarItemDesc": "85e03c16-4288-45f4-a579-c911437d27d4,", + "FriendlyName": "Hoodie Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "85e03c16-4288-45f4-a579-c911437d27d4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:20.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1284, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7b7sjloem429tv2xpco7r45jw.png", + "AvatarItemDesc": "105a5100-cb57-410f-a26b-1ee21002f067,", + "FriendlyName": "Jack Frost Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "105a5100-cb57-410f-a26b-1ee21002f067", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:48.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1754, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "36iyxpz2ym6qs0k43ssehyv05.png", + "AvatarItemDesc": "105a5100-cb57-410f-a26b-1ee21002f067,prEZHWvOM0ysaNfAe06ywA,q0nBwoIx_UWPe93wHDWTYA", + "FriendlyName": "Jack Frost Coat (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "105a5100-cb57-410f-a26b-1ee21002f067", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:03.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1283, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xvu0kz1tmmaoifwn7fvdtl6o.png", + "AvatarItemDesc": "e6216087-fb37-46f1-bda4-a88c4640f339,", + "FriendlyName": "Jack Frost Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6216087-fb37-46f1-bda4-a88c4640f339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:44.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1755, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6swgo4j116b5mqtoe4dl3pyh1.png", + "AvatarItemDesc": "e6216087-fb37-46f1-bda4-a88c4640f339,KwhdQ1deNE6Y6jzO9vaAnQ,byvLynH8x06OQ7GNCXOVYA", + "FriendlyName": "Jack Frost Hair (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6216087-fb37-46f1-bda4-a88c4640f339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:06.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1282, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7e934oy5eg0xzkuoeyk090u8w.png", + "AvatarItemDesc": "fbdca48d-4ae6-41ad-8c33-b70a9e33fd77,", + "FriendlyName": "Action Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fbdca48d-4ae6-41ad-8c33-b70a9e33fd77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:40.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1285, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "extmqwfwviflhb677k7y0j5vw.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,", + "FriendlyName": "Jack Frost Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:52.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1753, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f5anyvib4zotf2hlzp6twhrpq.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,H6fcuLxeBUSY3oERFc-plA,oGrNKZLpMU2bLwNEe4MPnw", + "FriendlyName": "Jack Frost Wings (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:02.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1851, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "23wddlluexp562bjt32f4humv.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,dpfTnOvsrUePTVs0XZD9qQ,gOt7BYXvw0uUS-U53KThAA", + "FriendlyName": "Carnivale Wings (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:59.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,3XVjOeAlF0K23sB1krWo7g", + "FriendlyName": "Jack Frost Wings Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1956877502 + }, + { + "AvatarItemId": 84, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "en7nrykz2qqj8buybdl5qjmo7.png", + "AvatarItemDesc": "49397e16-1027-4286-9bc4-b59eae565ff0,", + "FriendlyName": "Side Pony Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49397e16-1027-4286-9bc4-b59eae565ff0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 187, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cbcx8aneit1a0djh7mi7a9pd1.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,", + "FriendlyName": "Jazzercise Shirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 652, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ln4084il57kp9glk7af3u8u0.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,3b2cfc85-4e2a-4e0d-b738-c296ae8692f8,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "FriendlyName": "Jazzercise Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:39.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 653, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "axojfsb5b2u9dmz817cfa6h7z.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,78eb0969-f246-4cac-acd6-295df7c092e4,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "FriendlyName": "Jazzercise Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:40.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1850, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dhyvjxkfgg2fqqwac0tztvlo1.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,oF_wUv-ShU6GP34Ch0MnlQ,YTd0lIQLcU2Y2trwuLhJTg", + "FriendlyName": "Carnivale Shirt (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:46.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2159, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HIXUSEOjTEymO-YYuEyRWw.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,zwKRET9pP0O5uhPU_yHrlg", + "FriendlyName": "Ballerina Leotard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 236, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4tf3fmonukbqbhbj46tlqwovk.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 237, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6e2rsyvdnxb0lqz1t1x527ji2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 546, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6yhybki2x1u4frp9d3mj5057z.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:33:21.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 548, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3404czywuocyttg1yeivg4l6d.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:29.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 549, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bjb4do6miad7c1uhkxlvmvtdu.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,d2b40639-5d34-45b6-904e-6dd29030ff44,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:31.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 550, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "68baneyontwji279r6ya132th.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,724c79a3-822c-422f-9462-a5374ee0211c,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:33.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 551, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "88z32iu1vqx5t79u8rbzxb2jh.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,c72911d0-453f-4732-b1bd-dad520220a06,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:34.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 552, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e8kd5l3itqg0vfla6oyfwyug2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,90bc2d31-1715-4d1a-813a-7c57e66cb017,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:35.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 553, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "00krwbc9mtk0c5c3rczo0ba1i.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:36.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1666, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "elfjxtuklom8p26dxh4behbx3.png", + "AvatarItemDesc": "1631eb78-6718-478c-9b62-abbb0537e09c,", + "FriendlyName": "Jester Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1631eb78-6718-478c-9b62-abbb0537e09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:30.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2745, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pRrGRfj8XE-CVSQQ4C_hmg.png", + "AvatarItemDesc": "1631eb78-6718-478c-9b62-abbb0537e09c,9Yj55hS4gEOB34bggoRnzw", + "FriendlyName": "Jester Hat (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1631eb78-6718-478c-9b62-abbb0537e09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:03.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2029, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0sChD_Bp9Eq36jZRpwRRUw.png", + "AvatarItemDesc": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,", + "FriendlyName": "Jester Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:50.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2747, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YNqKRBfsS0CFwf6ZTMjyvw.png", + "AvatarItemDesc": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,g_jYTusUbU6W_6jHif-CKw", + "FriendlyName": "Jester Shirt (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:11.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2030, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "v__U3oLXhEWjGpKh_L8MoA.png", + "AvatarItemDesc": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,", + "FriendlyName": "Jester Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:53.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2746, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "FhANeW3-fUqUVfzO1F7Ujg.png", + "AvatarItemDesc": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,YzJL2s9S0Umv5XH_g5PG_g", + "FriendlyName": "Jester Gloves (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:06.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "96781359-a8e3-4cfb-be5e-79456ead88c6,", + "FriendlyName": "Jetpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96781359-a8e3-4cfb-be5e-79456ead88c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1129156756 + }, + { + "AvatarItemId": 2187, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wsQ3uMHglEWj0o4orNrwvg.png", + "AvatarItemDesc": "17e64798-f6ca-47b9-84ed-0c83b8678056,", + "FriendlyName": "Karate Gi", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17e64798-f6ca-47b9-84ed-0c83b8678056", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:12.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2188, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "oPruDc0j2EGwzv9I9FWS5g.png", + "AvatarItemDesc": "01107a3f-9a4c-4fb9-8918-e6195e346d42,", + "FriendlyName": "Karate Wrist Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01107a3f-9a4c-4fb9-8918-e6195e346d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:15.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2276, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "abgo5oixia5zua1xh9pr9knb6.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,", + "FriendlyName": "Back Keytar (Pink)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:08.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2277, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "8jjzm1iwi6v9h63b4saad9462.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,njOH4bbntk6THrxbd9TDDA", + "FriendlyName": "Back Keytar (Orange)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:11.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2278, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "aiaec0zo6aruhuugqrw4xr6p2.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,0aNp_2sWHUS_08TMe-_AzQ", + "FriendlyName": "Back Keytar (Purple)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:13.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 238, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7fmex9oqxz42zd52m3gbxb3l6.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,", + "FriendlyName": "Knight Armor (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 239, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2p5x227st2nelqfya7ywpcppx.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ed134bee-d199-43eb-98bd-206571603f40,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 240, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7hajn9eszpqh00q654d0iqds1.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,59866578-d4e0-417b-9483-233ab108b1ac,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 241, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4d9p7yhef4c8w1jrj3cd61gbp.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,22dc463a-a89b-46ad-9a0d-557c742b4a80,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 242, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ce1v4ja422emiw67599tvacqd.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1113, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e00pks3p8szzbwf082p7udhb7.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ITH2OpzjP022vZ5JgxV_iQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:17.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1312, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58uqy0e4o8nv424g5aory3woy.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,PMa9370LPEig_7pKwCceEQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:08.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 117, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "23zm48ii99k5gviu6wd03hbo7.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,", + "FriendlyName": "Knight Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 118, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6zfzza7fqr56kicscf9e2lil6.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ed134bee-d199-43eb-98bd-206571603f40,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 119, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7rnan20ml6o94ezwuvfv1cpqu.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,59866578-d4e0-417b-9483-233ab108b1ac,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 120, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1r4tyyoovhhxpcatrb3ixmuwl.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,22dc463a-a89b-46ad-9a0d-557c742b4a80,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 121, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d1fd8ubrjvta346f7rqtixtxy.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1119, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3dqqaqnta15d1edl9k3vlzu5f.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ITH2OpzjP022vZ5JgxV_iQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:56.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1308, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6hsc92hhcj6jhxh9avyxdqvj.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,PMa9370LPEig_7pKwCceEQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:46.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 276, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "85cm7w6an4z42wlzdgr9k9d3m.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,", + "FriendlyName": "Knight Gauntlet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 277, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bohc8ayxvepselhmkomofmhb4.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ed134bee-d199-43eb-98bd-206571603f40,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 278, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "37g3go1ko7zez3ua49s4tjpbf.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,59866578-d4e0-417b-9483-233ab108b1ac,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 279, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1q79g6q9p9to9xi0qtaj9a9s3.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,22dc463a-a89b-46ad-9a0d-557c742b4a80,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7fy3687o43lmb87xgdmjslpcy.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1109, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bvdx04lp2rrweg7mjd5hl2bnk.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ITH2OpzjP022vZ5JgxV_iQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlets (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:07.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1318, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9tsbb3bjtxr44158yh7ncrn5n.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,PMa9370LPEig_7pKwCceEQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:47.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 58, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0gmqaftgdlsoph92078ffbhpq.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,", + "FriendlyName": "Knight Helmet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 59, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "acqok07g1d8homim9mnjivxuf.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ed134bee-d199-43eb-98bd-206571603f40,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 60, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0r1snvqxftybe0m39s41incq1.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,59866578-d4e0-417b-9483-233ab108b1ac,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 61, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a4l2zz1yv4o8nlxgjjcyczfza.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,22dc463a-a89b-46ad-9a0d-557c742b4a80,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 62, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c2t0mn4y8uzp3mnvpdd9z0unb.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1122, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "15u1a4mvdkbujoe41brggozje.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ITH2OpzjP022vZ5JgxV_iQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:07.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1302, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eb444rmgn7q2ygy1kaxe6tsh2.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,PMa9370LPEig_7pKwCceEQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1917, + "AvatarItemType": 0, + "Tooltip": "Survive any game with the Squid King watching your back. Go Krakens!", + "Rarity": 50, + "ThumbnailImage": "5gf9eq3qeqfuxeh3zi34kmtt1.png", + "AvatarItemDesc": "9176086b-a0b4-4298-a7fb-eb91ea421e89,", + "FriendlyName": "Kraken Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9176086b-a0b4-4298-a7fb-eb91ea421e89", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:19:29.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 243, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5zxnu4wl529tzcip7nwk8im0o.png", + "AvatarItemDesc": "39278302-b52a-48a8-a566-ed756a5fa1a9,", + "FriendlyName": "Lacrosse Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39278302-b52a-48a8-a566-ed756a5fa1a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 244, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0pmiv1zb13od6nl9giu03eizj.png", + "AvatarItemDesc": "39278302-b52a-48a8-a566-ed756a5fa1a9,e3454bc7-c88d-4958-8b81-db3fe5472a1e,da36bbcc-2a9a-4ba2-9e4a-6f52c9b1ef74", + "FriendlyName": "Lacrosse Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39278302-b52a-48a8-a566-ed756a5fa1a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 4, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0eekw2et1by72nmm01x4haan4.png", + "AvatarItemDesc": "7134c60b-0640-463a-8d97-caa9184bef93,", + "FriendlyName": "Lacrosse Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7134c60b-0640-463a-8d97-caa9184bef93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 5, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6cznt9a4cfpxfot00vquk2gb5.png", + "AvatarItemDesc": "7134c60b-0640-463a-8d97-caa9184bef93,e3454bc7-c88d-4958-8b81-db3fe5472a1e,7e688b30-ed13-4790-9ad8-c49885d1b7e8", + "FriendlyName": "Lacrosse Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7134c60b-0640-463a-8d97-caa9184bef93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aqhhqebivzflcpyzyf70m1608.png", + "AvatarItemDesc": "c859a371-5d34-4e53-a96f-a6f7f3377aba,", + "FriendlyName": "Hero's Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c859a371-5d34-4e53-a96f-a6f7f3377aba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-29T23:24:59.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2155, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kNf3tlW1kEGHxU4EDJABMQ.png", + "AvatarItemDesc": "c859a371-5d34-4e53-a96f-a6f7f3377aba,aMOwLebcBU6SuPigtovUYg", + "FriendlyName": "Future Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c859a371-5d34-4e53-a96f-a6f7f3377aba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:24.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 576, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "500ajcxzkdjvi2db9ev74rstc.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,", + "FriendlyName": "Laser Tag Visor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-28T21:55:28.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 580, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "81x4cfhnckh1a4ni8mxn9trzj.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T19:04:48.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 740, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9z5r4i4elegluux9nocggv8lh.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,oT1vLrHW6UC2hWLIT3f_SQ,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:02:48.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 741, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1mu93bk5rhp7yqekc2k5aukn9.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,-e6bmXnwZU2EVwZ8RupKbg,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:02:52.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 574, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0mhwz2ogugpedmv34mtp8p649.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,", + "FriendlyName": "Laser Tag Sensors (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:34.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 575, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1gwx6x19onp32q7iolw1hdeck.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,08b7c12d-3317-4d27-9b40-34737a709454,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:35.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 742, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4b2c3lu6x2lix8wwu23es1z9t.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,jkuw9EhCpEKn-2lKJXWd3Q,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:05:11.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 743, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2mtxs666msasqmqw57iol4efk.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,2Z_FxSPiyUODtlFZWF9DFQ,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:05:17.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 579, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "15nrb536jxz09ck6emxavel7l.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,", + "FriendlyName": "Laser Tag Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T18:30:43.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 581, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dzm37a1dmgdsj0aetwi19afes.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T19:04:55.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 744, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0qqunbtr2dxrzl797zjamcpg9.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,EC0ttvk6eU2HEMacLAx2Iw,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:06:17.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 745, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "13cf7bho89ewryskk93gh33it.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,mhsglVEtREmElF5wuLw3NQ,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:06:20.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2248, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eYDxcWNR4kOlLWhyBLjM2A.png", + "AvatarItemDesc": "665d4fe9-88dd-4fae-97ad-201884a6ca02,", + "FriendlyName": "Lasso Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "665d4fe9-88dd-4fae-97ad-201884a6ca02", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:04.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 958, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dl9x5bl0fz2s03jn0xnqwzao0.png", + "AvatarItemDesc": "mfFLpKQRE0anhrmeUvW0bA,", + "FriendlyName": "Laurel Crown (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44bf199-11a4-4613-a786-b99e52f5b46c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:24.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2608, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KgPqo5yaDkibaNuclBpcJA.png", + "AvatarItemDesc": "mfFLpKQRE0anhrmeUvW0bA,p5TStfnDXk-EY7k6UdHxnA", + "FriendlyName": "Midnight Gala Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44bf199-11a4-4613-a786-b99e52f5b46c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:04.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2855, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5Zki1DsZ0UW_pMecyP3JoQ.png", + "AvatarItemDesc": "37b68987-73d9-4aae-8e3a-edd24ae890a9,", + "FriendlyName": "Layered Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37b68987-73d9-4aae-8e3a-edd24ae890a9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-03-10T22:40:41.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2856, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_uhKCE9kyU6LOgc4Sixxqw.png", + "AvatarItemDesc": "37b68987-73d9-4aae-8e3a-edd24ae890a9,UCxz68a5E0WaZkocDfEf5A", + "FriendlyName": "Silver Slugger Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37b68987-73d9-4aae-8e3a-edd24ae890a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:43.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2199, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZN_075PMnUq0_A0OEIo51g.png", + "AvatarItemDesc": "32b7a724-1112-476d-9ddc-2d04655c9df0,", + "FriendlyName": "Leaf Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32b7a724-1112-476d-9ddc-2d04655c9df0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:54.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 759, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b89jss76zlj72ms617ah00h3b.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,", + "FriendlyName": "Letter Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:24.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 874, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6m42sjjil9iqwyjnymvy516a7.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,apSQiQ1eO0mEzRsbugeu9w,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:32.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 873, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6mcbxskzp5pfwcnetac762guc.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,VpqHNpPiY0CfRJEjRRM-mw,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:31.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 872, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eq8wyyutlzcjeneqryph1cfzk.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,Z1F9rjKqCE-Hd2-Ip5g3gA,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:30.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 871, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4xku2dldqk5zfbjc6m9uw2v07.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,iufpnteVCEiiZu_m-yPovQ,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:28.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 762, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0luffpfh5ls3knh5u8r6w0x5u.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,", + "FriendlyName": "Letter Jacket Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:27.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 937, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "amfc2bzt0jj15t76n2lnhv25e.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,UUyjVU4mvECPhI9xTMYR1Q,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:13.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 938, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4hbdjfasjaponwtrx2tmjnwa3.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,HWc25-d8dUmNN2F3n8mV5A,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:15.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 939, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "39guqwt3fqru9qq6psoua1yff.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,Y0e7o0_BKUm7XJSLediN5w,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:16.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 940, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8uiva9ffm7qrizcvqx99frs29.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,MS7EPNLJEEyMLC6_yHQYtA,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:17.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 756, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7hwrwj6lty2rur9opjp05fgej.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,", + "FriendlyName": "Plastic Sunglasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:21.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1641, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8oaudlg81glmnhd3cqy8t650s.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,z-yaXP9ImkysLOGuCQiblw,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:46:59.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1642, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ec4tf5t9oqy8loqqvqfrw3rl5.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,SmRcrycMZ0qYNsHuQQOORA,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:01.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1643, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "egy0vqy333fqme7jsrxachsgt.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,0svDrGn2NkmhUjC1VJUgRg,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:03.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1644, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0hvhuyqd3i33hptb6ljvlazeo.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,Aq4fjEW5gEyzetXMoG2tdQ,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:05.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1832, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ew0vvmubkulgd7xjvlqhfuc2.png", + "AvatarItemDesc": "affdc9ad-8312-4421-8f56-e35dfdaeb63a,", + "FriendlyName": "Lifeguard Whistle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "affdc9ad-8312-4421-8f56-e35dfdaeb63a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:16.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "e97f05da-57d3-4927-aa22-c124a94d8432,", + "FriendlyName": "Life Jacket Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e97f05da-57d3-4927-aa22-c124a94d8432", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1703069062 + }, + { + "AvatarItemId": 1900, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ct8ojf0jqnnc7an3o2t5zs1a5.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,WWVPdeMMi0S4_Dmn85yEqQ,FRUp0aYZTUmAXsybauOs7g", + "FriendlyName": "Director Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:06:09.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1417, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "75icni7eg78l4eyuy6hwwrzzk.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,", + "FriendlyName": "Luau Dress (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:21.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1418, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "63oehgwwbgnddpx5bauhi2ztm.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,ZXeI9jooq0-dQaX-FlkNuQ,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:23.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1419, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "08kjv45btrzy2apeo276vys08.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,IzdUC8x0F02i3BhPfEUOYw,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:24.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1423, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "du4ay6j6cuhxjyg0f72e9dwtt.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,nPUQvBsxu0mzhQg9YnWEgw,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:51.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1422, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67fl0ih7xgt9itoq9vvq1op9o.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,wyKAZSZwT0u9cuSaQ_-IHg,vUgpsaXrO0ak7huhkHqlUA", + "FriendlyName": "Luau Dress (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:48.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5uifh9814ayb3sywh90qxniz4.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,_OM7UNZENEejgO5h7H1ztg,vUgpsaXrO0ak7huhkHqlUA", + "FriendlyName": "Luau Dress (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:46.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2236, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "jWeFkQl2_Uu7fhibuWdtXA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,D9U3j_XgSUesfHtSNoAdLw", + "FriendlyName": "Luau Dress (Cow Print)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:35.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1149680221, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "s-VTHTHagUa_Xh0Oa3PAJA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,I8SCbauJX0OLAKWOjy78yQ", + "FriendlyName": "Strawberry Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1876125901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PO4F_PVCVkSGejhtWCvxVg.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,SiraUm3ZaUOCeXCekunJuA", + "FriendlyName": "Pineapple Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1965281381, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FVH53dlIV0yvAH9GU2tgxA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,9Z95fSKS00GscDxK8-ARlA", + "FriendlyName": "Blueberry Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1480692753, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0_EW1WuF1U-UBFVVcVjAGw.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,wLpOk1g2rUyDeCKb5uOeSg", + "FriendlyName": "Gamer Dress (Raspberry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1421360180, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aKVQYVQFDkCP9Sy-U3FWXQ.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,RKftA1Fw30-hriuZNMGsoQ", + "FriendlyName": "Gamer Dress (Grape)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1341801044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "A8eUc7t5xEK6b8yAZPGUdQ.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,P6w6X7-Dak2G6O8HDRwlVg", + "FriendlyName": "Gamer Dress (Lime)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1444, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9hmp8axdgbs7r6mungzjw2kzt.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,", + "FriendlyName": "Luau Headband (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:25.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1445, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0gg1ctgab27af7zjuvqitr0zi.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,fV4H_1ZXx0-OVtlasI-xGQ,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:27.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1446, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7nuouk86sz6hwwhfoh6ca9e3a.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,B9DJPqTeYUuPnPHRbKzEJA,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:29.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1447, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1u22xuxo0l7hbu4dipegf91f6.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,yXZO-donI0SHB9-TT0jUdw,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:31.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1448, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4sf5uwmo0j53djcg0konx31j5.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,J5VsmblfWU2MeUjNo0fz1Q,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:32.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1449, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "afqbc9aj7hwgkwft5kyonc1xk.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,Asfe0ZRi50eryD5wlJzgzA,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:34.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 189, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "beio49fzwv1l86rp7tcvhz8t3.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,", + "FriendlyName": "Luchador Singlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 499, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eazhrt40qa5kdt73v7vcqhsdm.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,916384ca-e209-42a0-bd64-c64f5a40b56f,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "FriendlyName": "Luchador Singlet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:23.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 500, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6m59lwkz3pdm7ylfg5q1hwbzf.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "FriendlyName": "Luchador Singlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:24.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1733, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28qzx00rr1mcsdfj73tvv70qd.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,TLrw3oncCE2MeuVifXSWTg,-_ViYCHHRUip7oN_AnH0Og", + "FriendlyName": "Luchador Singlet (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:45.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1513, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0dhyx3nk36z445fgi9v0vyvce.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,7Jx8mrxoi0aQtZEb03CdRA,-IgxYllxkEyr7V1gosWXUA", + "FriendlyName": "Luchador Singlet (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:37.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 122, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8dvvtd9gw9krmqu0dvvlk1rv0.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,", + "FriendlyName": "Luchador Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1734, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "72gv1b98ofujgx42ctd0lwdf3.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,_VfdBTi7PU276qzsYKayow,qSsEcadcm0-twR1TvVcgYQ", + "FriendlyName": "Luchador Cape (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:53.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1512, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2xop370rhid69r8mypwbzzbe1.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Dtis2HpVzkelZb6M9mW5TQ,iqGv6iRJ-UGbofv8-aHKVA", + "FriendlyName": "Luchador Cape (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:30.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1609, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6gheyta77ei8tn3jvtdoimp48.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,xNjX_6-aREm0cYZObVLiAg,OE-PtL0-0kWcJPwn1wvuVg", + "FriendlyName": "Luchador Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:08.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1610, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "32o16d0w2ku6u0tavwqbw9lq2.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Kcyp8sv0202Puym1Wf86xg,OE-PtL0-0kWcJPwn1wvuVg", + "FriendlyName": "Luchador Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:11.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1679, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e1cquzpfu1rcke1xofv7ks1oj.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,r_QZLd-Aw0SsdQb_uYroog,eYGWiwqSGUG7Hx1R3I-11A", + "FriendlyName": "Luchador Cape (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:24:48.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 63, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bgxz7thjzppr81bia4pwvg2qh.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,", + "FriendlyName": "Luchador Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 487, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4jk1xibdjjddzpvrwcw05usiu.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,916384ca-e209-42a0-bd64-c64f5a40b56f,96b36c98-cba1-4337-91dd-c74c00316f97", + "FriendlyName": "Luchador Mask (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:08.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 488, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "59dj5f6fpgmoilwpcpugnwomn.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,96b36c98-cba1-4337-91dd-c74c00316f97", + "FriendlyName": "Luchador Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:09.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1735, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cwfcqra4nyurw86ksh4qv0cjq.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,RA7cizxxYE6J8yV_0XwPxA,VSLcLM0TJU2kJU5gS4KZGw", + "FriendlyName": "Luchador Mask (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:56.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1511, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a6g3l9h3i3ads39fgxf0lnwaq.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,5_UnQ7vZ9E2T-EhJ7M-mQw,jL-LAsd03EeN1J-Zp8opew", + "FriendlyName": "Luchador Mask (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:23.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 64, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d7reyk0b2dqeh51xmv3wcihld.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,", + "FriendlyName": "Lumberjack Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 829, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cjqz06mkg3l90vu87fnh371yt.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,7amHTgEv-UOJSGl0ofo4mw,lb5tuJOqnUKp9prjjlgHmA", + "FriendlyName": "Lumberjack Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-23T21:21:48.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 830, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "drjic5tpew7fpjaelvkqowvgg.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,cKHAetNcgUOxFfDkAPPF9w,lb5tuJOqnUKp9prjjlgHmA", + "FriendlyName": "Lumberjack Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-23T21:21:49.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 245, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0t92rww5x13bmx7jh4n78jqtx.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,", + "FriendlyName": "Lumberjack Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 775, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8u0ou663lluquhfy88m3nuvu9.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,7amHTgEv-UOJSGl0ofo4mw,CUCKsNVnIkavHjcspWAv_A", + "FriendlyName": "Lumberjack Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:58.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 776, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ba5xrtahc2k6jzt6i6q17wg7u.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,cKHAetNcgUOxFfDkAPPF9w,CUCKsNVnIkavHjcspWAv_A", + "FriendlyName": "Lumberjack Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:00.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 107, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6fn7llrs7vzf0mr1pws7ys253.png", + "AvatarItemDesc": "61ba3c90-c81e-4deb-bc79-50c0f1fe3e83,", + "FriendlyName": "Lumberjack Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "61ba3c90-c81e-4deb-bc79-50c0f1fe3e83", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 768, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "301onvpom3avfdh6fxup3q7fv.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,", + "FriendlyName": "Anime Tiara (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:49.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 922, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2oaezhhf77m3tlzy73ehvtapu.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,lhfOKGDAgEKWucerCmH7Fg,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:56.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 921, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dzietotul0k7x0rgoxmeqn53w.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,kLv_wsbbGUmlfIUO1QLYLw,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:55.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2oen9cny4wzp5qnb6rdx5nchw.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,f0pjsV0XMUWoE_z2kqOlSA,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:54.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "79soc27032oiz7ezyzb7bj6w3.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,EDLxuGjEVEGdOPXDqyN-xA,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:53.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 771, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d9jbl9u6drfoj1reb16xtye8f.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,", + "FriendlyName": "Anime Bow (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:54.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 903, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "13c35i4i2yh37p6u3kwbblvup.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,Ot9EG-FfKEauT0u6pSZuWg,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:34.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 902, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "54xi44ymvq9amsim1078bvqlt.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,i3rD_hnQG0a-5f6Ve4AfWg,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:33.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 901, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "35wve61we8efcghfumwpogrnp.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,S9uymuvraUCOM8gmB7bnEA,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Dark Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:32.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 900, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "31o9bg37u28wwxm2fpf5h095x.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,_uh3kj6hKUyBKOSR3R8dog,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:30.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1822, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dc2oyr445lq5s6rcxguz7tjdd.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,kaRBW1u3dkODGjPMePW6Uw,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Rose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:49.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 777, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ouv3swjffh6u9o5pks8yim6q.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,", + "FriendlyName": "Anime Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:01.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 870, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8pxavigoe6em3oy4686ym8ue2.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,VQjkyb5aoEKHKo_eZsq9ug,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:27.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 869, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1b7zb62caqbw5yuafz8hg7nxn.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,kTP3xKhzBkuQF3SqvmYFKA,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:25.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 868, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b79pggvoulv5z0dkn70z5syru.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,hYu9itGe1Ee5Neanhj9VtQ,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:24.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 867, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3unj20ah5pwz86qh67lqz9qvh.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,FE7_gQNBt0CXzMN6di5HWA,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:22.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1820, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c52ga28yxp7v5z0t8ow8e4rtw.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,dYPaOkwy3EGAfvl8RyN4pw,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:39.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 781, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "by0enw83tqi7qq14gmpytpavy.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,", + "FriendlyName": "Anime Glove (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:06.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 941, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1qj8ezhvwgdu6g4waborypyet.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,zaSTWOUy_k6cnoVVYzypWg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:18.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 942, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6tq3bm6g7idt1uv2itnhf5qb9.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,1FcyrVGyGEC9dmOq_51nFQ,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:20.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 943, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6qwl1qaaam1kkaku033yyeppd.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,xpkEQ6yjhU-WfFPSjvPdHg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:21.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 944, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2dr1z7istjp1d0bnqdoebk8ne.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,N9CBuYaMtUOrCks5UcOycg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:22.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1821, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9y4m58z1b4kasw620juo4nihy.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,LDjFJtM-WkeYhtwA7cyDOg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:43.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 190, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dl8ieqkdftxjtgfey8gb41yid.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,", + "FriendlyName": "Cheerleader Sweater (Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1645, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aidgb349qpp5nx0tlmqvfmxvy.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,6secgNPvmUyFnI3NC5dIYw,5V_1JEDrRUa1UPKhtrSB2w", + "FriendlyName": "Cheerleader Sweater (Victory)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:50:04.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1646, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "120gze52rf81cvsx2extomyi8.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,X5SuBHIGBE6wy8N8KDop8Q,5V_1JEDrRUa1UPKhtrSB2w", + "FriendlyName": "Cheerleader Sweater (Rally)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:50:08.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2424, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zwCsgKFNm0KCFbci6h6_ag.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,bc-aVFn__E61pyA4SvUoGA", + "FriendlyName": "Space Suit Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:51.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1952325815, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,dDSR-6ck8UGOeCSXR8Xnug,OFIqUvE4r0yMLHSMnlWMhw", + "FriendlyName": "Male Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.513779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1964834389, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,EaJMu_M0OUyUZ_3x-X-d7Q,OFIqUvE4r0yMLHSMnlWMhw", + "FriendlyName": "Male Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.513779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ckirjfll5885ahrmczp8ry2l.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,", + "FriendlyName": "Class of 2020 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:08.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1890, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dhfwe25d3r0oxtup5xd695cyf.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,oMz5rMzCxkiw9690ZkvlGw,gW2zFv0sFEmiodfw8p9P-Q", + "FriendlyName": "Early Access Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:00.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1903, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "7c0xvai9f3e3orr592s3eq4da.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,zlBPAWMbOE2RuKoIy1Ps8Q,UwQiLub0-kqf4dNvQc-R0A", + "FriendlyName": "Rec Con Shirt (TFO)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:31:04.757Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 192, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "au05f75bl482johdfb0fvjxjw.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,5864d23d-1a86-40a6-a52c-46ee670e4508,2ee3944d-e9fb-4959-973e-570a7fc6cc57,167ba0d4-4fac-4143-b5ae-1dbd8e02dc76", + "FriendlyName": "Class of 2016 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 336, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01iubrr0qyuaj94by5oj4xg3m.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,14afd295-bcd0-4595-80dc-d477618b9ab9,2ee3944d-e9fb-4959-973e-570a7fc6cc57,a74e6c0d-1300-4928-9141-977a711b71c4", + "FriendlyName": "SFVRCC Promo Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-27T19:16:00.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 337, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "eyc48682dl6k4obf67w3822z8.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,f8635fe2-90bc-42c8-9bbe-3387b5157e18,2ee3944d-e9fb-4959-973e-570a7fc6cc57,49934059-cc45-46ce-8277-f96959e1f6ba", + "FriendlyName": "awesome sauce", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-27T19:16:03.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 362, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e03fl2rpw0xd46aawjspj2i92.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,922f229c-124f-4f07-b4e3-19138420ca19,2ee3944d-e9fb-4959-973e-570a7fc6cc57,ceb07e4f-50a9-4ed0-b50b-c59d92ba66b5", + "FriendlyName": "Birthday Shirt (1st)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:02.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 565, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b1k80npgw88paq57cw285zhgv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,0c496f32-0011-4c4d-9778-59446f70c032", + "FriendlyName": "Rec Room T-Shirt (Crew Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:23.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 561, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2stqqgiaqmpxhz7kyzokxny1x.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,c0ff7719-c56a-4934-831b-506ad0f51cca,3259238f-4a9e-4e06-99de-6e7324fa0894,e38294cf-6279-4863-9aea-1a19aeaf8476", + "FriendlyName": "Laser Tag Alpha Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-08T18:59:57.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 562, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "69q96bwnfq08dvg7z8i3423dd.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,381a9f13-5b44-48e6-8a49-221422a963f5,0440f08f-ef1d-49d8-942b-523056e8bb45,9e782751-71c6-4208-87da-3a3cffa6a4f0", + "FriendlyName": "KO Skull Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-08T18:59:59.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 567, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5fseh1kw7c5ej962dbiugr5ns.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,befa2f11-b9fd-457c-b1d5-9d82e90b70ce,0440f08f-ef1d-49d8-942b-523056e8bb45,770e7674-6f9b-41f8-b023-ae79738db0d3", + "FriendlyName": "Laser Unicorn Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:26.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 566, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bp1j53987blav97zjuw9die57.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,bde08851-a9e4-4faa-8f04-a0a661672472,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "FriendlyName": "RECS Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:25.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 578, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "afeyng27plnoeojutrac3dvvj.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,5e7a48ed-8f08-4990-a3ae-05722c9f59a8,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "FriendlyName": "RECS Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-29T01:25:03.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 568, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "92ns2hqejpkog0560clmw3qjv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,4d52bbd6-ef74-45ae-a6c2-bde10ab8eb16,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "FriendlyName": "Robo Logo Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:27.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 569, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3tf587nd10cq6wv4dr1lk0r9p.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,bda7a57e-5227-49f4-a119-e3f166604738,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "FriendlyName": "Robo Logo Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:28.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 570, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "85nms7ezyti5i4wp8l9azl90l.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,6c395d04-2aa6-4a3e-8c2c-695932bc9736", + "FriendlyName": "Squad Goals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:29.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 571, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "19u0u4m2c6wsyaocczd4hv6pd.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,703ff56b-560d-4ff4-8c63-d195e879a328", + "FriendlyName": "Alpaca Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:31.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 572, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dckjq7bzoptwnhc7299yac8sm.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,fe304452-b57b-45bc-9c1c-b85b70bcec12,0440f08f-ef1d-49d8-942b-523056e8bb45,6a007c73-d930-408a-93df-041a715e0dd4", + "FriendlyName": "KO Skull Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:32.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 573, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "eu5x1j13qp2b88ukdkh7eax1v.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,8f19d7f9-4f28-48ab-a1c5-d8f4f788514c", + "FriendlyName": "Hang In There Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:33.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 594, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dtf1nt1wah54rmo77jxzohkg6.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,75162f12-44e7-499f-bc6c-9b0612e9164b", + "FriendlyName": "Class of 2017 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-11T18:54:38.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 623, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "26xtdwd2iy25ifneaxeal9nji.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,ba6b6e1a-a09a-4ba0-9523-552869f03336,0440f08f-ef1d-49d8-942b-523056e8bb45,d461ca71-45c9-415e-8e09-ba93e8d73450", + "FriendlyName": "Happy Tree Shirt (Blue, Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-04T17:02:17.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 731, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2w7aitx6z9si6pezgai622y01.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,rnM1qDlYBU6_Dl1bjg0J1Q,0440f08f-ef1d-49d8-942b-523056e8bb45,OZb8bb5FNUuFYOkwANvxAA", + "FriendlyName": "Rebel Rose Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:48.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 732, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4yagacwve48lrxa8yoo14k83r.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,0qh4mcxJH0Wsrb3nz8UeGw", + "FriendlyName": "Emerald Syndicate Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:51.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 733, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "70j1efvr06uolk29w6i31vbbq.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,Ib_iMwvUm0CJ-t_Thwt77Q,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "FriendlyName": "Cyber Trash Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:53.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 734, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8o1cue9ixf7cfijq9eqg5xk8j.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,yaQ7AlUc3kGq8_RW3eTSmQ,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "FriendlyName": "Cyber Trash Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:54.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 735, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2ad06ofz4ewso7onvvyfzerog.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,dDLQbD9jtkOmw3PR5QJdfg,0440f08f-ef1d-49d8-942b-523056e8bb45,2tFUNn-4kkK6ICo_Zp065g", + "FriendlyName": "No Laser Ham Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:56.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 736, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "aqkgdsqeztkohtgzqy9eu3ee6.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,g4diCmk0wEmIKUVozk9tDA,0440f08f-ef1d-49d8-942b-523056e8bb45,nHg4Uifww0GEX15L27WonA", + "FriendlyName": "Laser Graffiti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:57.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 737, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8q53ymq8bhq82a6s9fqt8a814.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,MPfe2wahQECCDGfhZE-drA", + "FriendlyName": "Hack Graffiti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:58.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 738, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5k6xiv9l2oaeteyfht5ak4epq.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,nLRxEIBhUkyQsfrk3nzsDA,0440f08f-ef1d-49d8-942b-523056e8bb45,n4hqSCv9OUewI30KnbPhFw", + "FriendlyName": "Cyber Junk City Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:16:03.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 739, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "agqloaqzftkyi0wa9v9j439d0.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,M1JVd5ur7U2ZtpvhEhPNhw,0440f08f-ef1d-49d8-942b-523056e8bb45,tzMFIJyxZE6cZNuLdaKs7w", + "FriendlyName": "Hyper Noodle Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:16:09.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 965, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58ls1z246ivu3x64bsenly4v9.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,6kPcLZtEqU2Hs4yBFvc_sA,0440f08f-ef1d-49d8-942b-523056e8bb45,NxMm0sSBdUatynU16o0_Zw", + "FriendlyName": "Class of 2018 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-22T23:43:30.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "978ivwbrlhtfjt9lpibr7eqx3.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,TtncjvYcK0qHqWN-IMMoqg,0440f08f-ef1d-49d8-942b-523056e8bb45,hHhjZeLcaUGN1OyFoYHznw", + "FriendlyName": "Happy Tree Shirt (Purple, 2019 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-04-05T19:47:41.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1130, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a9mzwqn1pzmy5ptemp2sjk6r3.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,fwIgUAM_C0WhKds0O6DyPg,0440f08f-ef1d-49d8-942b-523056e8bb45,fJVxsLWuTESd-2xTCJPZ2w", + "FriendlyName": "Recollage Shirt (Pink, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:28.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1295, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ansni4asvqpm50bcuomwb4i4l.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,ZNPzHmrU6kGVQW7Q5ei03w,0440f08f-ef1d-49d8-942b-523056e8bb45,fJVxsLWuTESd-2xTCJPZ2w", + "FriendlyName": "Recollage Shirt (Yellow, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:42.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1246, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "971lp3mdb0j453fkjy0cckqud.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,d3-sxPTJOk2rKo-Jgy3wvw,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest T-Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:36:17.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1563, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ded2g4m0rj9eastu0comvx8bw.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,v12eJ8louE-cW7-pCGzVLQ,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest T-Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:32:35.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1kt3y6b06rh6qdox2huzn0c6f.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,6kPcLZtEqU2Hs4yBFvc_sA,0440f08f-ef1d-49d8-942b-523056e8bb45,br9hzhDoZUmIevgl7SJkmg", + "FriendlyName": "Class of 2019 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:11.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1369, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3d9h318l8ubxs4x703bpvjqcy.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,TuAjeDmPuUmFr_ESx4q6WA,0440f08f-ef1d-49d8-942b-523056e8bb45,AXtRUZcYWU6g1ZtM-L4vfQ", + "FriendlyName": "Video Contest T-Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:07.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1499, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "anq7dknee5z82yqq5bymquby0.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,LrQ1z-mTHE2vIePBwm8l_Q,0440f08f-ef1d-49d8-942b-523056e8bb45,omS7gF10C0ec0ozEoJGJXw", + "FriendlyName": "Recollage Shirt (Blue, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:19.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1839, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0jdw82n61uuapunyx297i85ng.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,owoIOIEM70e6WxnYdlKYWQ,0440f08f-ef1d-49d8-942b-523056e8bb45,t2do8WSejE-Y0aiEirKw9g", + "FriendlyName": "Video Contest T-Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:19.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1840, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "84fxnyn9eqldulf6rpki5uuz7.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,eb1vYTP_KEGQH-f33_whQg,0440f08f-ef1d-49d8-942b-523056e8bb45,t2do8WSejE-Y0aiEirKw9g", + "FriendlyName": "Video Contest Shirt Reward (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:21.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1841, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zhwxqyn2zie0fpfnaf18z5ve.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,evgH6VWUgkKJNews32hUxQ,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest Shirt Reward (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:24.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1889, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4dsx1k2farwthtgsfgem735gx.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,UUlB4S3GVUGyil6ZK-EbWQ,0440f08f-ef1d-49d8-942b-523056e8bb45,omS7gF10C0ec0ozEoJGJXw", + "FriendlyName": "Recollage Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2021-08-18T21:22:47.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7zgp3njcd8tpgi1f8j1679bwa.png", + "AvatarItemDesc": "c8a97aeb-e8dc-40ba-b6bc-a7c7be907381,", + "FriendlyName": "Feathered Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8a97aeb-e8dc-40ba-b6bc-a7c7be907381", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-30T20:20:27.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1324, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ucp3apcgf7mvka7m2albckl8.png", + "AvatarItemDesc": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,", + "FriendlyName": "Mardi Gras Mask (Orleans)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-31T00:38:06.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1651, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3lmgppocqd4j182ijbbxl0eo0.png", + "AvatarItemDesc": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,mkkk2N_2-EKX3pVfk7U5aA,UQe6v62i6ESImdpc7gB_8w", + "FriendlyName": "Mardi Gras Mask (Parade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:35.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1325, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjo835jgyqtwaijuhly8otljo.png", + "AvatarItemDesc": "43354e59-4938-44bc-81e0-4fba47d0ac12,", + "FriendlyName": "Mardi Gras Mask (Carnival)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43354e59-4938-44bc-81e0-4fba47d0ac12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-31T00:38:11.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1652, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9x7rlsmit2ca70h5xx2vgtwez.png", + "AvatarItemDesc": "43354e59-4938-44bc-81e0-4fba47d0ac12,4xp-xZyNWEi_jJCIhAPI3g,Ulie-RkcJEmblLyoucx-JA", + "FriendlyName": "Mardi Gras Mask (Revelry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43354e59-4938-44bc-81e0-4fba47d0ac12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:38.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 725, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bt8tu5acrblwkf3nc68uhi3jl.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,", + "FriendlyName": "RecFly Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-27T22:48:20.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1084, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "66rxidv3887cwdjojesos240j.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,kkvNbVRjcEKtChpy3W1bqQ,nbhzQy_Hdkeohh4S1IezwA", + "FriendlyName": "RecFly Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-12T23:52:26.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1671, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4gn26corqk34q37e2hx5ouc9q.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,GVoh3mdWf0WNA9lfqL1GEw,w9qG5gHlAUqh8f9ZgnezsQ", + "FriendlyName": "RecFly Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:05.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1672, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cyarqq8aeox95hehu1py5nbbc.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,x4PPvuhg6USE6GSFgZZnCg,w9qG5gHlAUqh8f9ZgnezsQ", + "FriendlyName": "RecFly Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:08.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1681, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eewujb3it57bm2qlk725l2aji.png", + "AvatarItemDesc": "d4f86b2b-0fa3-4264-9eac-cf15b62463bd,", + "FriendlyName": "Resistance Shades", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d4f86b2b-0fa3-4264-9eac-cf15b62463bd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:33:42.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1680, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ekvm0nia4j7hfolms572ortek.png", + "AvatarItemDesc": "c4622dca-80dc-423b-ae22-2065174e817b,", + "FriendlyName": "Resistance Trench Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c4622dca-80dc-423b-ae22-2065174e817b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:33:36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1931, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3iw1qqexx0h5n5ksh78jfpf4h.png", + "AvatarItemDesc": "c4622dca-80dc-423b-ae22-2065174e817b,pCEBoSP2pESLl9ih-J-55g,8SzXFoWD8k20cekFe6cLKg", + "FriendlyName": "Resistance Trench Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c4622dca-80dc-423b-ae22-2065174e817b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-20T21:24:53.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2279, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lQCcPfTrxkuFrwpWyZRiiw.png", + "AvatarItemDesc": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,", + "FriendlyName": "Mecha Helm (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:15.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2703, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b-rwdReQyEOex38lYG4k7g.png", + "AvatarItemDesc": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,GhulL0k9q0euEooMTUKTHQ", + "FriendlyName": "Science Helmet (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:39.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2487, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "PaCn_gU9XUWR1kli2ZdIYQ.png", + "AvatarItemDesc": "40258a3d-0d5b-4b67-bb99-4bafc6410311,", + "FriendlyName": "Science Helmet (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40258a3d-0d5b-4b67-bb99-4bafc6410311", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:04.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3clewgmvjgao6vqaxfjuzx41z.png", + "AvatarItemDesc": "b74ef8c3-405a-4d68-a2bf-01579a9cc397,", + "FriendlyName": "Mecha Helm (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b74ef8c3-405a-4d68-a2bf-01579a9cc397", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:03.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "deey8zsy6ga64seu2ecdgbscy.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,", + "FriendlyName": "Mecha Armor (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:11.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2289, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8u2ve7984kGPN_F3hbDzFg.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,eG1Lf1vG60qNQiCBRsAhqA", + "FriendlyName": "Mecha Armor (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:51.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2489, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "zYe-zV8o90qtz6ryq0Q3aA.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,a9oUAXZpHUaW1_ncqhkYZg", + "FriendlyName": "Science Armor (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:09.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2706, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wWzp1pLLbEq0Zrt_O6yaMA.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,CeebPv2TZUixq8cmVD5lUA", + "FriendlyName": "Science Armor (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:48.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ar7fmx8roe1n7n2hcnjomo65l.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,", + "FriendlyName": "Mecha Wings (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:05.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2283, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QGWG7sL090695CW4JWWvvA.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,cS-oBNlLE061SIB40QjiDg", + "FriendlyName": "Mecha Wings (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:30.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2488, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "JEZj0fwAP0a4qOcclGw70g.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,rFcMAp5UvkuGalprkr_ARQ", + "FriendlyName": "Science Wings (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:06.757Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2704, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sQ5AG_RCAk-j6MKfyvnriA.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,oSoW8v6emEW4g1IduUNzOA", + "FriendlyName": "Science Wings (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:41.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1466, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vpmmt96jkto0ifnjtv9nwy2a.png", + "AvatarItemDesc": "16359b51-96c4-4be0-b598-319c7549e18a,", + "FriendlyName": "Mermaid Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16359b51-96c4-4be0-b598-319c7549e18a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:44.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "16359b51-96c4-4be0-b598-319c7549e18a,3ya4VDfxLU-krfBh_H0VAA", + "FriendlyName": "Mermaid Crown Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16359b51-96c4-4be0-b598-319c7549e18a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1142113680 + }, + { + "AvatarItemId": 1464, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5rcywgkgsd6v3ov5jeez13xtz.png", + "AvatarItemDesc": "fd7774bb-b29c-40b5-84b1-5c2bbe574255,", + "FriendlyName": "Mermaid Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fd7774bb-b29c-40b5-84b1-5c2bbe574255", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:35.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "fd7774bb-b29c-40b5-84b1-5c2bbe574255,499irXZltkSvv9pWsdBSyw", + "FriendlyName": "Dress Mermaid Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fd7774bb-b29c-40b5-84b1-5c2bbe574255", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1339048088 + }, + { + "AvatarItemId": 1465, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f44q2mf0bi5g9hcmaw85jm3j6.png", + "AvatarItemDesc": "c8c00394-4cd9-4bad-9774-a825ded78f80,", + "FriendlyName": "Shell Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8c00394-4cd9-4bad-9774-a825ded78f80", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:40.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c8c00394-4cd9-4bad-9774-a825ded78f80,Qevoxmtsa0eNzGss_aG2gw", + "FriendlyName": "Mermaid Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8c00394-4cd9-4bad-9774-a825ded78f80", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1547269846 + }, + { + "AvatarItemId": 2067, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "k7-wf2BSPUW8hyIsgm1DCg.png", + "AvatarItemDesc": "51b85276-3a40-4c40-93a1-9df307c3e78e,", + "FriendlyName": "Messenger Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51b85276-3a40-4c40-93a1-9df307c3e78e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:24.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1958, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wtce4myle20jpvnkao5yy6q4.png", + "AvatarItemDesc": "745fd5ef-705b-40f0-bfd7-dbf4af5a82e8,", + "FriendlyName": "Mighty Princess Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745fd5ef-705b-40f0-bfd7-dbf4af5a82e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:09.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1959, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8icc0glgl3g068z6zkzlc81e7.png", + "AvatarItemDesc": "7652db86-bc67-4665-8937-f5199d27b5c7,", + "FriendlyName": "Mighty Princess Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7652db86-bc67-4665-8937-f5199d27b5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:12.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1997, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1rltz0xd7rtu9knzw8dxe4tnf.png", + "AvatarItemDesc": "7a5cb589-5eab-4d95-9040-e9bfdc9590fc,", + "FriendlyName": "Miraculous Cat Noir Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a5cb589-5eab-4d95-9040-e9bfdc9590fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:14.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1996, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bkqrk31ohvuvc5euhf8ftmopj.png", + "AvatarItemDesc": "dece6d88-b6b1-4e01-b341-8ca4171353cc,", + "FriendlyName": "Miraculous Cat Noir Ears Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dece6d88-b6b1-4e01-b341-8ca4171353cc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:12.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1995, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2dpq3dcn5tt6gohq1e92zn647.png", + "AvatarItemDesc": "f0abcb90-d8e7-414a-b81b-a2aafacd4dac,", + "FriendlyName": "Miraculous Cat Noir Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f0abcb90-d8e7-414a-b81b-a2aafacd4dac", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:10.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1994, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "56uyxkfexq3rv8uwgrvrv1uc2.png", + "AvatarItemDesc": "e441a297-22ba-4a43-92ef-3549c6cc45c1,", + "FriendlyName": "Miraculous Cat Noir Hair Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e441a297-22ba-4a43-92ef-3549c6cc45c1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:07.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1993, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2qk8k1ry8kme3fai13fs7y5na.png", + "AvatarItemDesc": "fe85784d-070d-4b33-82a2-e0875da053d6,", + "FriendlyName": "Miraculous Cat Noir Mask Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe85784d-070d-4b33-82a2-e0875da053d6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:03.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1998, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0swzhsru2itov2t18lj5ptr8y.png", + "AvatarItemDesc": "19d83cda-692f-4ec1-8da0-15211f3a0c34,", + "FriendlyName": "Miraculous Hawk Moth Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19d83cda-692f-4ec1-8da0-15211f3a0c34", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:17.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1999, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "01v0cofrfhvsecj4d83kd6qn6.png", + "AvatarItemDesc": "a28d3819-e5e0-457f-8363-09aaed6a1382,", + "FriendlyName": "Miraculous Hawk Moth Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a28d3819-e5e0-457f-8363-09aaed6a1382", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:19.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2000, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "53f1e75rivkkk1v3yiqcezzwd.png", + "AvatarItemDesc": "696f46ae-eaf3-43b5-9635-70a78972826e,", + "FriendlyName": "Miraculous Hawk Moth Mask Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "696f46ae-eaf3-43b5-9635-70a78972826e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:22.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1992, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ddhkud4mfi72rzypwa7k6q892.png", + "AvatarItemDesc": "26b8c05a-e960-4039-9e50-4e27befaa44c,", + "FriendlyName": "Miraculous Lady Bug Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26b8c05a-e960-4039-9e50-4e27befaa44c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:56.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1991, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "e055b8bmrxrpkco54wa2vg5e9.png", + "AvatarItemDesc": "9815999a-c4d3-4741-a34f-2545b0cb8e11,", + "FriendlyName": "Miraculous Lady Bug Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9815999a-c4d3-4741-a34f-2545b0cb8e11", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:54.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1990, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4f78dqx34vgtcv34rpbpetpea.png", + "AvatarItemDesc": "34023e02-f59f-4e90-b245-e298cbc1a6d8,", + "FriendlyName": "Miraculous Lady Bug Hair Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34023e02-f59f-4e90-b245-e298cbc1a6d8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:51.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1989, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6yostvo78m555vx70ouaibzah.png", + "AvatarItemDesc": "ad21788e-1995-4c2b-9c7f-610bbc1019c4,", + "FriendlyName": "Miraculous Lady Bug Mask Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad21788e-1995-4c2b-9c7f-610bbc1019c4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:46.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 348, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bgvzostookysj7axrphthrkbq.png", + "AvatarItemDesc": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,", + "FriendlyName": "Mirrored Glasses (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:05.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 392, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0p43fda4clqqc4kkkt9xq6vsz.png", + "AvatarItemDesc": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,f841cdfa-45ba-4f09-a117-503eef34cb4b,9c55b609-b7dc-4070-a3ad-6351a0054a06", + "FriendlyName": "Mirrored Glasses (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:30.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1160, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0y8i0p2err8v094d39pt4hrsl.png", + "AvatarItemDesc": "a4c26527-8f75-44da-b150-7aaf8ef739c1,", + "FriendlyName": "Stunt Shades (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a4c26527-8f75-44da-b150-7aaf8ef739c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:54.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1136, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "12z70obzqdrm08ikd55tdgqj6.png", + "AvatarItemDesc": "d57cf97e-b5d2-4e47-8231-a226a161f6e3,", + "FriendlyName": "Stunt Shades (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d57cf97e-b5d2-4e47-8231-a226a161f6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:15.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1139, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9t35j4q9ykj4qblmofdh0c5vg.png", + "AvatarItemDesc": "87792b6e-1468-45e1-b28c-83f27ada7299,", + "FriendlyName": "Stunt Shades (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "87792b6e-1468-45e1-b28c-83f27ada7299", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:26.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1135, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5e5ro3jz91njip3g49yxza47y.png", + "AvatarItemDesc": "973dc863-b9a5-40d8-8f15-4096e75bd2cb,", + "FriendlyName": "Stunt Shades (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973dc863-b9a5-40d8-8f15-4096e75bd2cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:10.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1138, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0bc165hvvokoljesm0z26hcxx.png", + "AvatarItemDesc": "e2620c2c-c28e-45ce-a83c-0588329b9c81,", + "FriendlyName": "Stunt Shades (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e2620c2c-c28e-45ce-a83c-0588329b9c81", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:22.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1140, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6k0yaoht3qdepv964z904hrm7.png", + "AvatarItemDesc": "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5,", + "FriendlyName": "Stunt Shades (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:31.913Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 557, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "02t4ai6twkgrds4d3p7uhoirv.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,", + "FriendlyName": "Mobster Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:50.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 626, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5durkqoejr41mlqicbjoralgg.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,40283127-2ffc-4989-82f6-29c3381bf9e2,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:45.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 627, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bcipyj02mi2pt13ubgaeou2w9.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,1421caa9-9a0d-42c2-b847-1500ba2e483a,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:47.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 628, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cyemgjkr87sifsyr57umkly9v.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,390a476a-5545-48eb-955c-1bf1a16012bf,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:49.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wy0v5yb2753kb3b6g2r6wmdp.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,46970903-9799-4d25-b90a-7162c1f5dbef,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:09.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1129, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b4yf4gpicr0pa3pz4cdefjtu3.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,PZx7Iae0pkiDaxT9t3-vgg,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:13.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1462, + "AvatarItemType": 0, + "Tooltip": "A hat given to the ethical hackers of Rec Room.", + "Rarity": 50, + "ThumbnailImage": "cjcge17sgy7ehaobbb1d90lb2.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,8XajS2adOEOqzqnLy6-zcQ,P12CUkIg8EuFihGQsvPlxg", + "FriendlyName": "White Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:11:31.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,_Fl6LKUUykSBrdEgaN-GFw", + "FriendlyName": "Mobster Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1396740312 + }, + { + "AvatarItemId": 558, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28199zgfnoo8pyinqru3bjurg.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,", + "FriendlyName": "Mobster Suit (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:54.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 559, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28rrxlerexeuvbdhwah6qw82t.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,531b4297-b38b-4362-9854-4f2139ea43be,33ee376a-586e-4aff-a7e4-4213445a3e92", + "FriendlyName": "Mobster Suit (Herringbone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:58.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 716, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cqin0ca6oahx1odk9wqkjoqzv.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,46970903-9799-4d25-b90a-7162c1f5dbef,c2052d8c-fbcd-4462-8f08-bcf5b3dfde2f", + "FriendlyName": "Mobster Suit (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:17.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 966, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a6mox6qxfhus2uphu75sycnf6.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,4VkPVbBVLE-eJHilwGMAbQ,HTjlzAlWZ0ezY6w3ngSQSg", + "FriendlyName": "Mobster Suit (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:12:59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1131, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2ohuwbw1u9rnkyon9psv94x54.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,newGzX_zA0eGUX3Iayq5Ag,6Jv4AJGBYUGP_n4sDcz2Kw", + "FriendlyName": "Mobster Suit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:30.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,352obeXyMkyLIv62h2g-Ng", + "FriendlyName": "Mobster Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1791921697 + }, + { + "AvatarItemId": 560, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "76jy5kgg3ywcce4irqjp40e7o.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,", + "FriendlyName": "Mobster Cuffs (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:35:08.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 717, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5qsejkrvzfabn61cbd7tzrav2.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,531b4297-b38b-4362-9854-4f2139ea43be,c2a9c007-2748-4fa2-ab9b-0081eef792f1", + "FriendlyName": "Mobster Cuffs (Herringbone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:18.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 718, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bo4sinu9h61ugjoyoi0qu6aqc.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,46970903-9799-4d25-b90a-7162c1f5dbef,05cff8b8-d437-485a-b4b8-41d35becd022", + "FriendlyName": "Mobster Cuffs (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:19.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1132, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7oklc7x4ju4r3afyx70po80cx.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,newGzX_zA0eGUX3Iayq5Ag,QBzMp7MdAUOLIofvddui1A", + "FriendlyName": "Mobster Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:32.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2825, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1Z97coFGmUSgGU9P-pU5KA.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,6ItbfT4igECHl2eLuhGLIA", + "FriendlyName": "Mobster Cuffs (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:41:02.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,326QgcQbnUGJBs2xzFKVOA", + "FriendlyName": "Mobster Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1214487053 + }, + { + "AvatarItemId": 87, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2l73l9kt7me20f6m5vhkstse2.png", + "AvatarItemDesc": "e49d5d78-157e-4bc8-b878-ff7c4963fdc4,", + "FriendlyName": "Mohawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e49d5d78-157e-4bc8-b878-ff7c4963fdc4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2212, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kykH4YwaaUSlrQTBZsT8tA.png", + "AvatarItemDesc": "e21b9dd6-c3d1-42cb-a90d-1b60ffb826fd,", + "FriendlyName": "Monkey Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e21b9dd6-c3d1-42cb-a90d-1b60ffb826fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:30.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1806, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25fe9p8vszllgyike2qzq66fc.png", + "AvatarItemDesc": "5f6a7dee-0b44-4138-b3a7-a079dbc63e83,", + "FriendlyName": "Monkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f6a7dee-0b44-4138-b3a7-a079dbc63e83", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:23.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2291, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "emdth023j0ib2692bu9zs42fg.png", + "AvatarItemDesc": "3bceb51c-ef29-4942-8b8b-a15ed4a41451,", + "FriendlyName": "Moon & Stars Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bceb51c-ef29-4942-8b8b-a15ed4a41451", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:37.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2292, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6l7ba33mqkha8zbtzg8cty02.png", + "AvatarItemDesc": "3bceb51c-ef29-4942-8b8b-a15ed4a41451,4lwF00Rvb0Kr3FJz07HlOQ", + "FriendlyName": "Moon & Stars Earrings (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bceb51c-ef29-4942-8b8b-a15ed4a41451", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:39.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1509, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7gjdy5ae0sascpzzv1vx2hn97.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,", + "FriendlyName": "Moons & Stars Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-08T21:59:42.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1930, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1453pgb9cjdtfwanmhqks0fn.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,eutz2irupECSYGMtru3NMQ,Gruqp7EGM0WJbAM_F8hWbQ", + "FriendlyName": "Snowflake Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T19:00:38.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1700877762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qhUHWc_oWEanxQNBSskKJQ.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,4PI6XssggEKRxkGlWeo8KQ", + "FriendlyName": "Jigsaw Moon & Stars Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.517784", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1209731358, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "APU0Kt0HrE6E_zMnX0MMJw.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,Mp_TzaiSTUaULweziW9_AA", + "FriendlyName": "Dress Moonand Stars Dress) Moonand Stars Dress Shirt Jigsaw Aurora Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.517784", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "eb91dbb0-0dc1-497e-b6d0-b7cd4482bacf,", + "FriendlyName": "Moon Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb91dbb0-0dc1-497e-b6d0-b7cd4482bacf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1407377184 + }, + { + "AvatarItemId": 2857, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CXVseWtLVkWtwM2EQ4f2Rg.png", + "AvatarItemDesc": "ab446412-84cd-490d-84e0-2e666633b72f,", + "FriendlyName": "Dirt Bike Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab446412-84cd-490d-84e0-2e666633b72f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:44.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2858, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "p0HdGaW4sE2gqP6I_UnOTA.png", + "AvatarItemDesc": "3028ba83-0c85-4e27-a47a-56609fa58b33,", + "FriendlyName": "Dirt Bike Chest Protector", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3028ba83-0c85-4e27-a47a-56609fa58b33", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:46.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2859, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Mz1ko5b7O0GghTIcaFyRTw.png", + "AvatarItemDesc": "40c1f20f-182c-4751-afd1-b572b0fd061e,", + "FriendlyName": "Dirt Bike Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40c1f20f-182c-4751-afd1-b572b0fd061e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:48.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 359, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2o15q3ks9rbgmv8h3mf4bdpj3.png", + "AvatarItemDesc": "2b7f4dae-51da-4af0-b009-12d6005c85c6,", + "FriendlyName": "Mullet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b7f4dae-51da-4af0-b009-12d6005c85c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:58.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 815, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "056efjnqghgyjgzcgyuz5vbq6.png", + "AvatarItemDesc": "Lli8-aqt1EmCmyXwBRQaYA,", + "FriendlyName": "Mummy Head Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f9bc582e-adaa-49d4-829b-25f005141a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:39.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 816, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "549e5xz7cvnjwkcrv8qnxssm6.png", + "AvatarItemDesc": "oRUh266ohkWQil14uxcEdQ,", + "FriendlyName": "Mummy Shroud", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db2115a1-a8ae-4586-908a-5d78bb170475", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:42.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 820, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "32o1jvjp0zv2e13meoz6ly7pc.png", + "AvatarItemDesc": "zpjkkb1HtUORoPaODFuuxg,", + "FriendlyName": "Mummy Hand Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "91e498ce-47bd-43b5-91a0-f68e0c5baec6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:52.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2760, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70axtgsjoojk0df4289594v6q.png", + "AvatarItemDesc": "25e76b00-4c56-4259-968f-046c14fb36fd,", + "FriendlyName": "Earrings (Musical Note)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25e76b00-4c56-4259-968f-046c14fb36fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:06.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2761, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bah-hC34O0mkCKiY2pH72A.png", + "AvatarItemDesc": "60cc933c-5bc8-49c1-aa6b-9c2adfef088e,", + "FriendlyName": "Necklace (Musical Note)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60cc933c-5bc8-49c1-aa6b-9c2adfef088e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:08.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2160, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CCrSa0uGc0u9LuDrRzUoeQ.png", + "AvatarItemDesc": "f4f372b8-33a1-462e-a9f7-8a0e85b2c0d7,", + "FriendlyName": "Mystic Mantle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4f372b8-33a1-462e-a9f7-8a0e85b2c0d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:37.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2119, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hVhS_lAUy0O3ga-alhshaQ.png", + "AvatarItemDesc": "b79a6f2c-d960-4d8a-b05f-24e818ec5f60,", + "FriendlyName": "NBA Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b79a6f2c-d960-4d8a-b05f-24e818ec5f60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:38.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gq5kauTaAU2sDicXqi98Qg.png", + "AvatarItemDesc": "fe2721b5-16c8-4d9d-bca5-0233ab008fbb,", + "FriendlyName": "NBA Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe2721b5-16c8-4d9d-bca5-0233ab008fbb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:40.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1270, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "brfgb9cf9tp8tgva2k3qzm6iv.png", + "AvatarItemDesc": "5e9bbda6-d681-42b1-9cc4-5758acf34bc0,", + "FriendlyName": "Formal Necklace (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e9bbda6-d681-42b1-9cc4-5758acf34bc0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:55.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5fwhywu2l5mxei877fhrhwhq1.png", + "AvatarItemDesc": "b95678aa-351a-4929-8a0e-0274a183eb18,", + "FriendlyName": "Formal Necklace (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b95678aa-351a-4929-8a0e-0274a183eb18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:51.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1274, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0rq3ue0rxap0g0375himga11m.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,", + "FriendlyName": "Party Hat (Shindig)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:12.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1564, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aal6en3akiu96g363ok04sx99.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,AfqXBWLBQEiiag4cEtoEiA,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Hoopla)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:37.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1565, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aiqpa9xefa8z86zduzd2gyjw8.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,RROvzwtshUGyRudVIBn8bw,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Celebration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1566, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e9evvmcwo9jnfy5clp9ztr4vz.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,69vv21jfTUu1E_LjK5bytg,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Jubilee)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:46.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,IUDG__cu9EG9wK57ajRTHQ", + "FriendlyName": "New Years Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1348963899 + }, + { + "AvatarItemId": 1273, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9alk9hbmjw74ccjkcwn2i7s3a.png", + "AvatarItemDesc": "86dd9ff9-643f-4f11-8243-93ad948db4cc,", + "FriendlyName": "Happy New Year Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86dd9ff9-643f-4f11-8243-93ad948db4cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:08.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2611, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cbi2t8gd1f5tzytciebrrxndh.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,", + "FriendlyName": "NFL Arizona Cardinals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:35.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2612, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxeoecs08mv3iy5dgdogqbb94.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,G9cFrBRvw0S-nDp-Fn494g", + "FriendlyName": "NFL Atlanta Falcons Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:35.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2613, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7n3s5s977ulc5z7jhiencdnrw.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,KcsFLjkhCUmg8HbamtChbg", + "FriendlyName": "NFL Baltimore Ravens Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:36.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5n2ngxovy8uj9lfz3essaxn29.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ye7GYcd6PkObqAsla6BJaA", + "FriendlyName": "NFL Buffalo Bills Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:36.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2615, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eslxy84me9m2oxtnk8o18n5nn.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,xVPRKfkYG0-u4MFhJYGe9g", + "FriendlyName": "NFL Carolina Panthers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:37.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ah2mm6n1rgh6pvzk4fx9rnj6.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,84OVMNAiv0mQwX2bpkw2yg", + "FriendlyName": "NFL Chicago Bears Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:38.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2617, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "831h9zt2yylg0ao93kyxmy4t3.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ishXZTP3bkeVNk68PBZAiA", + "FriendlyName": "NFL Cincinnati Bengals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:38.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2618, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cz3bgo3si4042vf9xb2dtypxf.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,cLswsDJEmkCqYDGI8DJmPQ", + "FriendlyName": "NFL Cleveland Browns Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:39.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2619, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "220fwlypah2hrdvg8fi0gswf0.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,fdmR0dt0hEakaNp8mpjGmg", + "FriendlyName": "NFL Dallas Cowboys Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:39.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2620, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ca68ce9flmrpl7ig3zwhbqrv4.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,5mFA4uHLmku8oCc1rDJmXw", + "FriendlyName": "NFL Denver Broncos Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:40.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2621, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7g7ym06cw3m98l2f9ziwdlpia.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ry0fWzcysk2fsr-zIvzjUg", + "FriendlyName": "NFL Detroit Lions Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:40.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2622, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1p6ky76pvprr7cwbobzngw01f.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,S9YQnGF2WkaFSG07d-5TUw", + "FriendlyName": "NFL Green Bay Packers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:41.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2623, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bmav2pqie439kaf4223zzgxlg.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,pcEwV_ZPxkSKP39UgPh6fw", + "FriendlyName": "NFL Houston Texans Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:41.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2624, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f2apsx992xa88goodejq341lr.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,nHa0WjWfl0-g-sknorb10g", + "FriendlyName": "NFL Indianapolis Colts Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:42.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2625, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "68ou7wz2ezhyh6y39hxqeyihw.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,w_8Bff5J_Uej5iCAz2WTBw", + "FriendlyName": "NFL Jacksonville Jaguars Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:43.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2626, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dz7a5njd87o8hv2l5etgyisnj.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,kdExSysXREOAYqobK_Wzpw", + "FriendlyName": "NFL Kansas City Chiefs Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:43.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2627, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "351j7myhryxcppvy8gvdz4oix.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,XlkkIjP4EEKq0ObjXYGtBA", + "FriendlyName": "NFL Las Vegas Raiders Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:44.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2628, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ddaagd1il03gi4nrec2vd1xei.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Rlxn533MdUqMefvUSoZrrQ", + "FriendlyName": "NFL Los Angeles Chargers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:44.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2629, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ndck6ysf75lg3kvjiz1bhh3w.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,_BV42pO3n0e0V-eOf9CyGg", + "FriendlyName": "NFL Los Angeles Rams Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:45.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2630, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5uj5qvvm6izou0whgm634hnme.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,g6PcjAOH0UinJtrGmxVuyw", + "FriendlyName": "NFL Miami Dolphins Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:46.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2631, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d32jj9kz3n5jceagnlpmq5vpa.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,i1e3GavcdEu7EJFzwpBXvg", + "FriendlyName": "NFL Minnesota Vikings Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:46.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2632, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "44ohtkl0bud9zkb1hd75qdgph.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ebjoqQ_pnU2qdFVR1yDIzA", + "FriendlyName": "NFL New England Patriots Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:47.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2633, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8t9dbig6zpecho4qe8scj3bpt.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Fr--3nBIUUuD6EYmj1iklA", + "FriendlyName": "NFL New Orleans Saints Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:47.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2634, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b0joufd9x01y01brs3e7krk43.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,hrTGnJLb4U21P9PCi7u3xw", + "FriendlyName": "NFL New York Giants Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:48.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2635, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwlxb29ug9oqs1apya0icyjsc.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,coRfJXTxhUKsFs1tzSBgUQ", + "FriendlyName": "NFL New York Jets Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:48.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2636, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxnxsmeklw8dkemz9vrpajd7t.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Y75a0D5Wj0qmpfxG18LvWQ", + "FriendlyName": "NFL Philadelphia Eagles Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:49.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2637, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5t2128zn5sfwk2uc2qclkhlb9.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,VExSaZXZ9kan-0qjP6YPzQ", + "FriendlyName": "NFL Pittsburgh Steelers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:49.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2638, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "du30i4p3o8j2i1t6evwaw6x3b.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Yec2FLXhpEmrocbVUOkhhQ", + "FriendlyName": "NFL San Francisco 49ers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:50.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2639, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "40mqyx1i4dzjcumjlzblnr5jl.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,EziQI39wZUWqIy8zs5I7LA", + "FriendlyName": "NFL Seattle Seahawks Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:51.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2640, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5gvpye8hcsb60wbuk15bi7r5t.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,PWMkuXHWFEKGKfSiT8rWNg", + "FriendlyName": "NFL Tampa Bay Buccaneers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:51.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2641, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9ftzba4l162233dbbcqiaszc4.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,xdO8lwFuG0e1v2FRmza4dQ", + "FriendlyName": "NFL Tennessee Titans Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:52.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2642, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "30ubxldr0r27wafqn2ejkmtgv.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,vtQ3bx6n7kSFdES121M5IQ", + "FriendlyName": "NFL Washington Commanders Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:52.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1413, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "09zvd3ohdxjul97glak50v2zt.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,", + "FriendlyName": "Nightcap (Zonked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:09.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1414, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5oixy16bep4bh46cp4ma645d0.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,2vawoQvJPUegnClmr2RNAQ,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Zzz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:10.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1415, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wpy1gh40rv5ibcltdnowpd05.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,kIcd-yEnsEqxoeogquvmRw,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Dozy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:12.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1416, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0415gb7q368nr2qadddcg1i0k.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,qHyKfCsrbUqSWomjLXf7Hw,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Sandman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:14.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2064, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "g4VIHBisj0-q7bWIQ6IMfA.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,ZvUwd0aVfk6SjA7oyouXUQ", + "FriendlyName": "Starry Night Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:16.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1569, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6gfimjrsbl126ir365lqnmv8n.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,", + "FriendlyName": "Nineties Outfit (Da Bomb)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:09.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1568, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1oix7x6qvyo7ps4kk7lftxq9j.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,ulOBBvFEJ0ioFA1qmL390A,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Aiight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:07.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1567, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1czdhhr33h8ztob05qmon2562.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,V4CVhCB8eU-nrfYgticVOA,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Booyah)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:05.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1920, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eakv6vkwmx6lwc95u5k9tvb07.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,6ZtRAbD-xUufWNLCrlrrCg,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-29T23:25:09.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2566, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e1-lO7c1i0m5eg3UkCPczg.png", + "AvatarItemDesc": "a600fa80-ed4e-4acb-8d6d-72fa87d2bef3,", + "FriendlyName": "Nutcracker Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a600fa80-ed4e-4acb-8d6d-72fa87d2bef3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:02.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2567, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6-Sz9f-O00uY5pWih9NC_w.png", + "AvatarItemDesc": "beee4e09-9d93-4023-bc78-c5dc551a0ab8,", + "FriendlyName": "Nutcracker Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "beee4e09-9d93-4023-bc78-c5dc551a0ab8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:05.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2381, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "pEnzLpycJU603AzcfO2Mnw.png", + "AvatarItemDesc": "9b636782-47f0-4218-9a1d-be65e550dc44,", + "FriendlyName": "Bongo Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b636782-47f0-4218-9a1d-be65e550dc44", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:50.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2382, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "YzXiKP_YPkW_dray4uvVpQ.png", + "AvatarItemDesc": "96ca8d50-0721-47a3-a988-018a1c157677,", + "FriendlyName": "Drumset Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96ca8d50-0721-47a3-a988-018a1c157677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:53.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2383, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "I_TDOXrCVUe1V3HKxNvzAw.png", + "AvatarItemDesc": "c8d2503e-5d43-4828-8d1d-40c64a577753,", + "FriendlyName": "Harmonica Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8d2503e-5d43-4828-8d1d-40c64a577753", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:55.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1838, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5aq3d9icptakxqxo8zitb57dn.png", + "AvatarItemDesc": "caf1e229-9660-4abf-8074-f706709f4097,", + "FriendlyName": "Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "caf1e229-9660-4abf-8074-f706709f4097", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-23T17:41:53.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2860, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "A6Gyszd5SkyByb7oryYm_w.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,", + "FriendlyName": "Overalls Skirt Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-03-10T22:40:50.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2863, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bGS9YZZvhkSZTQaLBEnbYA.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,sCj73U9p5E6DQreOsbutqw", + "FriendlyName": "Red Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:58.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2864, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "jAr4ImFmQ0u4xa689IeaXA.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,HDpCyFn40kOxJ0P_xJr4-A", + "FriendlyName": "Yellow Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:41:01.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2862, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T3O9LKgezU2qgWjCAoDPfQ.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,OZ1v-s_XFUyfMA5mfd47FA", + "FriendlyName": "Green Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:55.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2861, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LP6loSF2uUmBukTEKkAN5Q.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,e8aNxGFlBUWy59Bfrvvv7w", + "FriendlyName": "Brown Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:53.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1561517472 + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,08593d9f-aaed-4662-973d-0f30e77f1955", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1085700839 + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,e002caa2-3447-4102-8d9f-570d1942101b", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1362900866 + }, + { + "AvatarItemId": 1351, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "729s6a9tw6yv1xyun76b5zwkz.png", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,", + "FriendlyName": "Paintball Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eda29bce-5a07-4439-a2a7-cafa11e2ed62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:51:06.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 105, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "42cr1t40qzbgiloer9d71sb28.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,", + "FriendlyName": "Paintball Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1946, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "52yxsadxjnh09e02kzlza70z7.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,kS7jek5tPkSkIgjIF369Vg,_Jx5VqGbVUCPwl_SaUCTRA", + "FriendlyName": "Paintball Goggles (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:35.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1947, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4bndj29hiu03hkt63qwabedlj.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,3HAMDzfFO06jtYNXm2Bsjg,_Jx5VqGbVUCPwl_SaUCTRA", + "FriendlyName": "Paintball Goggles (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:38.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 281, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "agzh2svq1rj75i5f37dk13i34.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,", + "FriendlyName": "Paintball Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1948, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "841fudm9eezzeqyfx2wqdrpj2.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,-Moek0heVkWtKpTw9oZ0EA,FsirX90YM0y5lk7mnqDnEQ", + "FriendlyName": "Paintball Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:40.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1949, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cvkzqrgyihp9bv1n8b6ie95fl.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,D-XJyw1BPE-m0gXpvcXrQw,FsirX90YM0y5lk7mnqDnEQ", + "FriendlyName": "Paintball Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:41.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1332, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1xj0gybaafqrogg7hnsgqbscb.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,", + "FriendlyName": "Mechanic Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:27.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1333, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0q7ysb2tsjy0mbxx1nes33jdx.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,C9cPjOoiCUCHrCBi809CMQ,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:32.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1334, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20bhcftlu43jwrw06imhg37ox.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,24zVmiGiaESrM5qcRfF6gA,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:36.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1335, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0v5t8d3m8790n1lzjjp3qd9z5.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,LOeHr4xoik6VOv1fOeImjA,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:41.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 197, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "94farwa7bctza7e4db865is8x.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,", + "FriendlyName": "Paintball Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 654, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "274io1pn2peum3e1dm08i0tlj.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "FriendlyName": "Paintball Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:41.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 655, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0hdenghs8yfyhmtnz229g0hd1.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "FriendlyName": "Paintball Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:42.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2941, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b4RkTVS6UeA0lrNcM_BAg.png", + "AvatarItemDesc": "07116e9b-b182-40b4-90c2-99738e7aedd6,", + "FriendlyName": "Parrot Purse", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07116e9b-b182-40b4-90c2-99738e7aedd6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-24T15:25:33.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1255988719, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "74e6878f-94e0-41f2-93ea-de6412238385,", + "FriendlyName": "PartyBand (1710)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74e6878f-94e0-41f2-93ea-de6412238385", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.519776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2568, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fuCyF2LQ6EaCTanw2VqOwA.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,", + "FriendlyName": "King Rat Cape (Rouge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:08.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2571, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VoZTTqgfBEOCWqIxKoS-Pw.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,gYXppJSpDUG9MB0xej4-kQ", + "FriendlyName": "King Rat Cape (Jaune)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:22.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2570, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_Hj47OdDmUeTB3wTf2r2Kg.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,VF7QrfNdC0ex5p62bkdt7w", + "FriendlyName": "King Rat Cape (Vert)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:18.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2569, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WEL2fL-6q0qFtXp51G5Wng.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,eEsEJ78Me02dXnp6gTuZRw", + "FriendlyName": "King Rat Cape (Blanc)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:12.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 760, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3r88aoow0xky1yenciez1enyj.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,", + "FriendlyName": "Peacoat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:25.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 791, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2i0jzvjgsic4vsv7uod1p0o5k.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,fvoaMN4M10WS6jiA6XlOJQ,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:46.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 792, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6fdpcq0zhuz9xr09wlcj3j7bx.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,PwsLxCnzOkSW-RGz3tNb1w,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:48.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 794, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "beldcz68emei41q0ndhzzbtr7.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,uiBklotTUEqRI2F67vyJyA,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:11.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 817, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dhqntf98nka1vp9oh5hi5ybtn.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,jXBi28cRV0CEQgKIrpD0_A,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:43.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5db418c3-1aae-42d3-af59-4ad03a9a3bf4,", + "FriendlyName": "Peacock Gown Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5db418c3-1aae-42d3-af59-4ad03a9a3bf4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1042258582 + }, + { + "AvatarItemDesc": "e457f946-b985-417c-9dec-0d8d3a7da71a,", + "FriendlyName": "Peacock Gown Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e457f946-b985-417c-9dec-0d8d3a7da71a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1725552604 + }, + { + "AvatarItemId": 1271, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjtkko8cs4inet2bswk24bky0.png", + "AvatarItemDesc": "7be2eafc-21ea-4266-ae64-8888c06798d6,", + "FriendlyName": "Pearl Necklace (Freshwater)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7be2eafc-21ea-4266-ae64-8888c06798d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:00.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1272, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4kmcbo8n4ht1kadrspll7f1oy.png", + "AvatarItemDesc": "7be2eafc-21ea-4266-ae64-8888c06798d6,sqPMH0LhLkeG-4XkxpU-sw,URWHffwjGUKgXeV01upePA", + "FriendlyName": "Pearl Necklace (Saltwater)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7be2eafc-21ea-4266-ae64-8888c06798d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:04.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1479, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6a428adukettbzkckyu9updp2.png", + "AvatarItemDesc": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,", + "FriendlyName": "Pegasus Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:17.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,RRmRsm6R6kOyn5HfbXNJWw", + "FriendlyName": "Pegasus Wings Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1841210169 + }, + { + "AvatarItemId": 2603, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zzqrxKCL00W27uhhyfZuqA.png", + "AvatarItemDesc": "b66cb611-c079-42f0-ae04-6f51384dbe90,", + "FriendlyName": "Penguin Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b66cb611-c079-42f0-ae04-6f51384dbe90", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:46.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2264, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Av7hkeKA7Ei1Stn-Fk2txw.png", + "AvatarItemDesc": "3cd52c5d-0c46-41c0-8254-c4542805da31,", + "FriendlyName": "Pioneer Bonnet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3cd52c5d-0c46-41c0-8254-c4542805da31", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:14.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2265, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2o_-7a2C30eYta_KTWhvIw.png", + "AvatarItemDesc": "ad55bb62-671a-4a29-aec8-36a1e3104f05,", + "FriendlyName": "Pioneer Cuffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad55bb62-671a-4a29-aec8-36a1e3104f05", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:16.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2266, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "-__TuY2TCUu0TPihPV1SqQ.png", + "AvatarItemDesc": "bb6ee7fa-7a31-4a16-a97e-be6eac42c595,", + "FriendlyName": "Pioneer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb6ee7fa-7a31-4a16-a97e-be6eac42c595", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:19.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 600, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bu0ii93b6dmvvc93szjfnal02.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,", + "FriendlyName": "Pirate Captain Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:24.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 611, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0rvymvsae95r30k0psb64iavr.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,0f71b9a2-dd88-435c-a571-4503d12dcf2d,9b2102f0-e16b-464f-9f1c-f068de52bb89", + "FriendlyName": "Pirate Captain Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:47.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 612, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0pyrhi595qqyqin50b6zfj2ws.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,f4ad4b9e-dd42-4711-9437-5beae51966f6,9b2102f0-e16b-464f-9f1c-f068de52bb89", + "FriendlyName": "Pirate Captain Belt (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:48.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 599, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dl4lc9lujy638xdphs121fg3k.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,", + "FriendlyName": "Pirate Captain Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:22.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 603, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5o6eyu7tmzl4ffq27fn0ucofj.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,17040a88-87f1-4613-b439-3316e33dbd48,71fd06e3-cac8-4923-a09c-ef91183712d2", + "FriendlyName": "Pirate Captain Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:32.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 604, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b90z3pydklkb08tidovz5jd1g.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,f9aaa7c5-59bc-486c-9a6e-27a8ac618f84,4cc0ea9f-65af-486f-8319-02b85ab5197f", + "FriendlyName": "Pirate Captain Hat (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:37.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1128, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7zbbv8oqo767y92w7mp1p6brm.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,jQwNBXyoU0Gk4FyNw6jFxg,4cc0ea9f-65af-486f-8319-02b85ab5197f", + "FriendlyName": "Pirate Captain Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:01:03.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 601, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5ynt63fbqq9vg0dnp6obm10jk.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,", + "FriendlyName": "Pirate Captain Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:26.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 607, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5ftzyq0wpssi4a8hsrmla5jml.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,556141f7-6fbc-4065-9faa-8d8240a93d7f,00ac4e37-6eac-4ed5-b46c-2c075038bc3f", + "FriendlyName": "Pirate Captain Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:42.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 608, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "69yr1pl69y8de2fpn7tvc0f34.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,d81d0b62-e052-46c2-9252-4bc5fb219f53,00ac4e37-6eac-4ed5-b46c-2c075038bc3f", + "FriendlyName": "Pirate Captain Jacket (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:43.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1112, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bm2ft9q0v5z0q0i1x8nutm0yw.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,xGmnUGF-Ck-jP43r8tVVjg,52053afd-9f12-4fec-a358-7eb3fe8c19bb", + "FriendlyName": "Pirate Captain Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:14.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 602, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cwpa2fe7uhjhy2fg8z2bjoqqf.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,", + "FriendlyName": "Pirate Captain Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:28.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 615, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1ouywwp83tgc11yi3x9wro2bd.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,556141f7-6fbc-4065-9faa-8d8240a93d7f,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:52.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5384s8w1fosxnurfe756wgd0n.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,d81d0b62-e052-46c2-9252-4bc5fb219f53,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:53.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1105, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cijsojywwytastkrfmnur8m2l.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,xGmnUGF-Ck-jP43r8tVVjg,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:48.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 619, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dfy3wgi2sfer51uhs3af8y5da.png", + "AvatarItemDesc": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,", + "FriendlyName": "Pirate Eyepatch (Skull)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-28T01:18:41.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1121, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dkbvx8r9yxtlxb2uhcv5qn1az.png", + "AvatarItemDesc": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,clFrpYWFQEmDkE14f3o_Ig,DE1C-RJkuEWIvpR0q1i42g", + "FriendlyName": "Pirate Eyepatch (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:04.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2884, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "j7uEUd9qPEerGchhJ6rmaw.png", + "AvatarItemDesc": "11b3413f-2c18-41cb-8ef2-55fb825ed476,", + "FriendlyName": "Plush Bunny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11b3413f-2c18-41cb-8ef2-55fb825ed476", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:59.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2885, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B08PvxHhZUOcI_bnrefOFQ.png", + "AvatarItemDesc": "11b3413f-2c18-41cb-8ef2-55fb825ed476,DNGqrCGe8kKrNmJaGzjjew", + "FriendlyName": "Plush Bunny Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11b3413f-2c18-41cb-8ef2-55fb825ed476", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:02.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1823, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0xxjg720qofphga2otdghooga.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,Ris_kNVcgU-EK4SoMckC2Q,74J7skzs9EaIq09bZArjiQ", + "FriendlyName": "Reclympics Shirt 2021", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:53.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2017, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6JmHNL-jsE-M9URUaMcvuA.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,PkMkvHzXek-yKyqJESZ3tw,nbgwLqKBnUyvieWX0i47dg", + "FriendlyName": "Scrubs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:10.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2018, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bCXA7saMUUuD_C4_Qy0t5w.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,P49CJ5imfEuxH1rryrfFRA,nbgwLqKBnUyvieWX0i47dg", + "FriendlyName": "Scrubs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:12.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2685, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "efVnvXfV-0-RgOpfnNi_bQ.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,logtSrZSTUqwEmBw9i0qjA", + "FriendlyName": "Rec-On T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:00.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1884, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eafwn78k3h4uaugzy9kay26kr.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,tMsCUhDslkmdNBfogeSz7w,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:18:04.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1883, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "crpsa73v3nvk6saa71tdx8k9u.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,cypMpMaVeUuRjm4FLH4czQ,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:18:02.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1882, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7h6z4ycgbt833y56ccc3rtl3p.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,whWKAUSGp0qprsJomq9Nwg,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:58.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1881, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "84jgglt9m8uawzj1481xfk4fw.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,LTXy6g4xe0abuFzOLpVDUg,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Dark Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:56.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1848, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4hpfukjpch71fzmfokc3tx3s6.png", + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,", + "FriendlyName": "Poodle Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:44:57.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2558, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BHgotDTbX0umzVt4i36gBw.png", + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,tPAJqFHiX0yUQpDOE0MJ6A", + "FriendlyName": "Planetary Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:53.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,zhLs4Hgw1UCrcNekAWd4RQ", + "FriendlyName": "Poodle Skirt Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1708534829 + }, + { + "AvatarItemId": 1070, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5sb23yvxjkh7ozjfothb1ul6f.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,", + "FriendlyName": "Floatie (Creamsicle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:59.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1071, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agoyhlxxj14bkqor8pthssxge.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,LRv1X0ZrjkqO7azGpZ7uMg,sx10-Hsfl0OS62pIFvHH8A", + "FriendlyName": "Floatie (Donut)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:01.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1461, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c5zj93bwe1qme6eb14dtwbcjt.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,NMmOn0zQZ0mJLxCMnsbIVw,v1ZOE9G9gEWAqNJX1A9f2A", + "FriendlyName": "Floatie (Watermelon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:09:35.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1844, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2iyjpxumims1bzvq7x6kqrmqx.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,1w_u4d4AjESxw5dxRkKHrw,CylZksC7KUGXF2VeJhlLSw", + "FriendlyName": "Floatie (Cheeseburger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:37.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2158, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dVpzRz2OY0avaXnWbmbm_w.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,6wCu0bPODE6tayAY_aicRQ", + "FriendlyName": "Floatie (SummerCamp Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:33.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2244, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "n9H1eWtdiE25SebY5g4Ukw.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,7BMBl3iA6USKijNCKfCXqA", + "FriendlyName": "Floatie (Neapolitan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:56.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2299, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SlacPLiWtE2CpmO5WHd-RQ.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,akqD9mYQQ0qks9SJe80MbA", + "FriendlyName": "Science Floatie (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:38.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E_MFRZ-dI0GiU60k_m8LSw.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,RCit-BlLyE6cvzpbfCVShA", + "FriendlyName": "Science Floatie (Invasion 2)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:28.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2817, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QMhjhzpkIUOPw6-mpbsclQ.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,1Z24Obg1MEm6dC_blOy4IA", + "FriendlyName": "Floatie (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:39.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,KrNRgSR3qEq9f6hfcsTFQQ", + "FriendlyName": "Pool Floatie Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1941160248 + }, + { + "AvatarItemId": 1072, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6o6o3vze30qmak32kdlkeus4l.png", + "AvatarItemDesc": "awOFE0_GSECr8GBNBo6a_w,", + "FriendlyName": "Floatie (Giraffe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1385036b-c64f-4048-abf0-604d068e9aff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:03.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1073, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aasr0sd35kmv6vhgbs9znxafg.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,", + "FriendlyName": "Floatie (Flamingo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:04.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1074, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxj3pn647bzr2jpxm9myh6ret.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,k3dbqP5z30KU37MVPpsEPQ,GtoHkTpyA0iGIHsxphwivg", + "FriendlyName": "Floatie (Dino)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:07.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1451, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8t2f1nmayxxozgvqs90t59neh.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,hM0OmxjWLESbsJ-M0Z3QvQ,qZzkrGXpPkGq_nnGLZorwA", + "FriendlyName": "Floatie (Parrot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:05:42.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1824, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bs25xo0gnja1akbu4eg7q8dor.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,9WpjdWJbhkCoEgDljqZkdw,Uyn3QOl1zkGR2_OUOhNWjQ", + "FriendlyName": "Floatie (Rubber Ducky)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:36.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,zxDWWqb0S0uDJgYeVQQ3oA", + "FriendlyName": "Pool Floatie Bird Head Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1424015386 + }, + { + "AvatarItemId": 2106, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LHx48xU5E0OkqfpqJBes6A.png", + "AvatarItemDesc": "2565d597-9d17-4c47-a66f-b683583f0cf8,", + "FriendlyName": "Floatie (Bunny)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2565d597-9d17-4c47-a66f-b683583f0cf8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:19.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1825, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bdeaw4skm15u253skgs4rtpe9.png", + "AvatarItemDesc": "efac28ae-ff44-4322-9bc0-61eaaef95399,", + "FriendlyName": "Floatie (Dragon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efac28ae-ff44-4322-9bc0-61eaaef95399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:42.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1453, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9srp5d5aba29z5vzoj3qkqmpg.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,", + "FriendlyName": "Floatie (Zebra)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:11.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lLR7SX5N30af5zpgdD5M6A.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,p379ObEL9Eea0DX9Ld4qnw", + "FriendlyName": "Horse Floatie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:18.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1158295129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0vpwih9r_UeG9tov9Mm6VQ.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,ljbO0JAKVky8M97MsjKnPg", + "FriendlyName": "Floatie (Armored Horse)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.521776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2190, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dOpBUgepYk6c0zUHhvZxfw.png", + "AvatarItemDesc": "187569d3-23b4-4118-862a-ace7f50fec45,", + "FriendlyName": "Rammy Floatie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "187569d3-23b4-4118-862a-ace7f50fec45", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:21.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1471, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "chtv23889bkxqdhplm9mrfmxz.png", + "AvatarItemDesc": "7a98503f-0460-4d25-a0d5-72a32db8f4c2,", + "FriendlyName": "Floatie (Turtle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a98503f-0460-4d25-a0d5-72a32db8f4c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:25.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1954, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c0c3e51sc58rcze7q10j1aiq5.png", + "AvatarItemDesc": "7a98503f-0460-4d25-a0d5-72a32db8f4c2,A8ht7G1iqEeLvP0SW6SwyQ,QppxPdf550-jT7dK7XrTzw", + "FriendlyName": "Floatie (Penguin)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a98503f-0460-4d25-a0d5-72a32db8f4c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:15:14.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1075, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "64nsdc03lld08y8ouiugpbt3a.png", + "AvatarItemDesc": "UGkQa4atNUe1cE5TN79s4Q,", + "FriendlyName": "Floatie (Unicorn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b106950-ad86-4735-b570-4e5337bf6ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:08.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2231, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "47gm8nyvqd8aiokq2iddqfdko.png", + "AvatarItemDesc": "e633d56d-5121-4492-b606-40759108a00e,", + "FriendlyName": "Ice Pop Earrings (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e633d56d-5121-4492-b606-40759108a00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:03.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2310, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d54dk7h9h3ru312k9t4ncpscd.png", + "AvatarItemDesc": "e633d56d-5121-4492-b606-40759108a00e,6CFwXqWT50mH1v6WueFsjw", + "FriendlyName": "Rocket Pop Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e633d56d-5121-4492-b606-40759108a00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:03.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2579, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rDwV9FzDSE-8tcpVeQsaFw.png", + "AvatarItemDesc": "7a97838f-24ac-41a0-8390-0c9308644eb1,", + "FriendlyName": "Power Sword", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a97838f-24ac-41a0-8390-0c9308644eb1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:38.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1843, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1lnhgu9j4uf3ypg48ra7v0by1.png", + "AvatarItemDesc": "76a73866-5b60-424d-b751-a105e030f46e,", + "FriendlyName": "Prankster Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76a73866-5b60-424d-b751-a105e030f46e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:34.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1842, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aijv697pz04ho70d9l3lxcnvd.png", + "AvatarItemDesc": "5b3cae00-88bd-4179-b75a-9444c5a52fa5,", + "FriendlyName": "Prankster Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3cae00-88bd-4179-b75a-9444c5a52fa5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:28.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2060, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XSHhlJsxRU2OwZD3eTZWJQ.png", + "AvatarItemDesc": "5b3cae00-88bd-4179-b75a-9444c5a52fa5,ZM6W70pGt0aAPLYf4DykeA,icHpJch4uUeGdhoLkawyyg", + "FriendlyName": "Prankster Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3cae00-88bd-4179-b75a-9444c5a52fa5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:55.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2019, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wf_PLXUTzU6UXiDZCDulRw.png", + "AvatarItemDesc": "06475461-f3ab-4e92-ad92-243c00b19ff0,", + "FriendlyName": "Prodigy Princess Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06475461-f3ab-4e92-ad92-243c00b19ff0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:25.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2020, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "SdP7P425CkCbPhAIfZCtaA.png", + "AvatarItemDesc": "076e98b6-dc1b-48f8-bb02-aa65d26b33d1,", + "FriendlyName": "Prodigy Princess Bracelets", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "076e98b6-dc1b-48f8-bb02-aa65d26b33d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:29.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1456, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e08mipx3bu50m127eoa5c3js9.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,", + "FriendlyName": "FNAF Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-19T23:01:58.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1587, + "AvatarItemType": 0, + "Tooltip": "2020 Rec Con", + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "27ormucbs1iox5kvr9tka0fpc.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,DOY6LpHq8kui8yDWGsNgZw,48yVsRt8sU6jf3P4wlPA8Q", + "FriendlyName": "Rec Con Hoodie (2020)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:11:27.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1783, + "AvatarItemType": 0, + "Tooltip": "2021 Rec Con", + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "0w037o55hb7xrlbbz5ndkt3qk.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,HUxqkzeE-Umq96TgSn0Hbg,48yVsRt8sU6jf3P4wlPA8Q", + "FriendlyName": "Rec Con Hoodie (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:47.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2162, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IezDzyHOWUy4vzhhR8erIA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,QMScBauef0uOE48-YcDcCg", + "FriendlyName": "Dino Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:31.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2237, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "oYY2GYyfUUmrxi__mx94Gw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,25X6s0sTZkyzUOKiIt4cGQ", + "FriendlyName": "Creator Conference Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:39.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2284, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "sg_RSadtBUepyNJrDn81nA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,tkuP0sNwjEO3XH5Lf_KqkA", + "FriendlyName": "Rec Con Hoodie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:33.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2733, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "84yMtUUfQUGCcvFe82Y2DQ.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,G_QPd8YBe0-n8ywAGLMKBg", + "FriendlyName": "Hoodie (Jessia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:25:00.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2684, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ja4C7EX3zkKj0VnySl1Muw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,Kuh-HrhzTUmuGaw7cpLUMg", + "FriendlyName": "Broken Heart Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:56.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2705, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "JpkOsmJyvk23HfK4fWMwJA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,SJ7DV0meUU6QjksIcAAeRw", + "FriendlyName": "Pullover Hoodie Shirt) Pullover Hoodie Shirt Color Block Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-21T19:41:44.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2799, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "grtmSgDkekKbMf5XDtoA3g.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,bS-St6XLHkSwKt41J7BmKw", + "FriendlyName": "Alo Yoga Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:29.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2823, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cDunfbR1wE-D8-LUSD_mdA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,jir883uk5k-cEgyAPDtQww", + "FriendlyName": "Pullover Hoodie (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:55.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1177382318, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pPyQA-HfpUirnUHijRJeYw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,a2mUuKOr-UOM5nPpR8dSMw", + "FriendlyName": "Hoodie (Em Beihold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.521776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,501dae9f-2fc4-45fe-a276-f606b93f434f", + "FriendlyName": "Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1545540299 + }, + { + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,b8cfa1ab-13bb-4e71-afe0-490820aef449", + "FriendlyName": "Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1335772728 + }, + { + "AvatarItemId": 1522, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "44boqpgj0k38m24uqczos4upt.png", + "AvatarItemDesc": "9ad40310-7cb9-473d-920c-5c18d7972030,", + "FriendlyName": "Pumpkin Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ad40310-7cb9-473d-920c-5c18d7972030", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:17.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2368, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "KFjbHzLz7UezIItKG7vasQ.png", + "AvatarItemDesc": "9ad40310-7cb9-473d-920c-5c18d7972030,mQTpoW0-AEufdFmApqtRXg", + "FriendlyName": "Pumpkin Cap (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ad40310-7cb9-473d-920c-5c18d7972030", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:12.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1523, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "9vaxzwj4b0l1ty30cspscr3di.png", + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,", + "FriendlyName": "Pumpkin Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:26.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2369, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "BtXG-VrTYU-1n1VDsKOgjQ.png", + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,Be2v1bpBSEu4lwKJePscPA", + "FriendlyName": "Pumpkin Costume (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:14.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,p4X1ilkxU0OjEWXdmQrVwQ", + "FriendlyName": "Pumpkin Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1015871793 + }, + { + "AvatarItemId": 1524, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "asfkh306x4wyzvnccvqkhby0d.png", + "AvatarItemDesc": "7e6e24b6-6f9c-4315-b457-73eba52d824c,", + "FriendlyName": "Pumpkin Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e6e24b6-6f9c-4315-b457-73eba52d824c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:31.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2370, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "F7i3Au-z5UmKBdm2ZZxm_Q.png", + "AvatarItemDesc": "7e6e24b6-6f9c-4315-b457-73eba52d824c,Mnpd7r3eu0CNmZYSPTMASg", + "FriendlyName": "Pumpkin Gloves (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e6e24b6-6f9c-4315-b457-73eba52d824c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:22.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2445, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "94mq42cmonwiia61g4kx9jsmu.png", + "AvatarItemDesc": "07bebdb7-bbe1-49ea-a746-c1d00e11f174,", + "FriendlyName": "Pumpkin Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07bebdb7-bbe1-49ea-a746-c1d00e11f174", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:30.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2446, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "0qumzncb3mqqivji6bma4w0vd.png", + "AvatarItemDesc": "07bebdb7-bbe1-49ea-a746-c1d00e11f174,ciwVtCaRHEO72BFPe4Ujmg", + "FriendlyName": "Jack-O'-Lantern Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07bebdb7-bbe1-49ea-a746-c1d00e11f174", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:32.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2800, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ae49cxd8vs4g8bpfmd9l2uiv.png", + "AvatarItemDesc": "cfce35b4-d182-4505-a631-5a8e5011f550,", + "FriendlyName": "Pink Pop Rock Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfce35b4-d182-4505-a631-5a8e5011f550", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:33.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2801, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZOOhVVu5AUmtTX571xSvXg.png", + "AvatarItemDesc": "98e4c868-df73-4755-84b6-c4d12723b811,", + "FriendlyName": "Pink Pop Rock Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "98e4c868-df73-4755-84b6-c4d12723b811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2802, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lA0R461bQ0ST6HUFBbaWCA.png", + "AvatarItemDesc": "cb061cf3-05fb-4494-be5e-091ddb080271,", + "FriendlyName": "Pink Pop Rock Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb061cf3-05fb-4494-be5e-091ddb080271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:39.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 368, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8zf6s3vsds90klh8gnb66rq7t.png", + "AvatarItemDesc": "ce250e13-8137-4f62-9e52-2b1cebe7d0ad,", + "FriendlyName": "Punk Hawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce250e13-8137-4f62-9e52-2b1cebe7d0ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:07.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 372, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d4xgo6yv2c0ciprp97kwmfjbz.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,", + "FriendlyName": "Punk Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:12.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 373, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f4451n0e2f0tbm5fmb0h59n72.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,4169bfed-8403-4590-a29d-4941275ac0b6,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "FriendlyName": "Punk Jacket (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:13.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 374, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ovn4a6rs6eyhc8cqwowjycsp.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,0724b166-9ace-46ff-a191-b13f135b00a8,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "FriendlyName": "Punk Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:14.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2755, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "rlFO-9ZKCE2yfkHlavGzig.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,MFsWxydybE-dw3KO2hkuZA", + "FriendlyName": "Yellow Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:36.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2750, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qEYJHcXWwkKASHqpFug-TQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,9bb0x4qgokyQLbt1jA25Wg", + "FriendlyName": "Punk Outfit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:22.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2752, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d21ms4sSZUe8LXA5sCgB6Q.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,YceWQmfB7U-R7OeBIr2Ogw", + "FriendlyName": "Punk Outfit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:27.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2754, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "30PBljeph0mO3nz4ycwRAg.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,G2ncsS9U8E6fyjvliFqdOw", + "FriendlyName": "White Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:33.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2753, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "XTqgvYBK3UeAHs9ot0OlpQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,AdJ3ExsUq0ixzaKDHK5RUQ", + "FriendlyName": "Purple Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:30.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2751, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f4hUyaoXvUGqD6pHI4u8WQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,TC79rbThdkCZHGWaxk-qYA", + "FriendlyName": "Olive Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:25.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 376, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4d1njsjeb0jzje2jyt4qwnpjf.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,", + "FriendlyName": "Punk Wristbands (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:15.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 377, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1px8h9mdfc4sqsqsgszmxrx0h.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,a6796264-9c7e-4605-830c-06db8d0dc780,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:16.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 378, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8b0gvz7zqfujt7dgbozqj2clf.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,f3b2330a-b981-4c7e-902f-1f57382f2388,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:17.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 394, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0kiv90a8q8p0qlzhtvo821u6h.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,baffdefe-de21-4551-9330-b50873965bdd,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:30:01.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 202, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2amyw5vvgk1c9r6w9pdloakyg.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,", + "FriendlyName": "Long Coat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 472, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6nqdiagvhy4jttofvcepefq2y.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,b42d903b-b393-4f49-af5e-38d0fb6c42b0,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:40.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 473, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cdi7d06wp2qv0t93m630vtp9s.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,8cab8cc6-9a2d-434c-8b72-e66edbdd223c,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:42.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 474, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "asgwfydw5ghc9650zs32gm2b3.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,657f28d5-672a-4f27-86c6-3cb00f25bb84,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:43.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1628, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3q0iocg562i3lzbn5n65t13lw.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,w_aK2K9bHEeU6oSQ7BK0qg,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:28.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 969, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agu0otilxkbnfpbo3ad7xiozv.png", + "AvatarItemDesc": "e2ObiQFqCUOdKniKL9a1EA,", + "FriendlyName": "Cupid Headpiece", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899b637b-6a01-4309-9d2a-788a2fd6b510", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:01.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 971, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d3mpn71rzkxnchgl4zc2c29lm.png", + "AvatarItemDesc": "BqpRbr_T1kO_YJqB1787kA,", + "FriendlyName": "Cupid Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6e51aa06-d3bf-43d6-bf60-9a81d7bf3b90", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:24.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 970, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78t1ffywj0qqumsmn2pc9yr9b.png", + "AvatarItemDesc": "PryEX4MEvkGKwST6dqJHjA,", + "FriendlyName": "Cupid Toga", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f84bc3e-0483-41be-8ac1-24fa76a2478c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:10.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2899, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nSSGXZLjwEGclxA93atFUw.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,", + "FriendlyName": "Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:25.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2903, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MD5DVsHC4U2RMM-e36mYDQ.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,pqtwCB_c20yRmOEs6jlH3g", + "FriendlyName": "Purple Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:38.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UhJ8FHTUiEWTy2_6oM5T3g.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,SSstW2ZPk0SB47qYlgbZzg", + "FriendlyName": "Black Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:32.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2902, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dkNHvcM-PUeaHpc9SxCaTA.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,p5zYbKLGU0m0ZutOicfjsw", + "FriendlyName": "Blue Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:35.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2900, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eJWJ01dRW02aA6rSCX8mJg.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,wgIwYsXqiUekieE6pSt1Ng", + "FriendlyName": "Beige Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:28.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1321, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "357bwxxf0kvoz6yd778gmm1k6.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,", + "FriendlyName": "Regal Crown (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:46.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1322, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8kgbf2gxx8ajp1uw1zrx306an.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zljmtj5WqUaRvB3XQTgJXg,XFuSiGupZ02Fut1Dxum_Iw", + "FriendlyName": "Regal Crown (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:52.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1323, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5zb2ksxrxwp3n2qlecml4u0l5.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,rmq1AOQvtUGOHvbPUximwg,XFuSiGupZ02Fut1Dxum_Iw", + "FriendlyName": "Regal Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:59.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1895, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "18orrg4k9zm7dsjfj9k2dz23q.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,rxMCIkLb2UO0Z3tsqtYuSw,x8nPf0xEwUiRdBQU00Bu1g", + "FriendlyName": "Diamond Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:55.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2423, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "jlklbsk_UEW29bsjWRwmUQ.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zu3hTm6JyEa0sE72KtVGXg", + "FriendlyName": "Space Crown (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:49.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1296, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "52v2ad9kyewcmaefknqmm6l5i.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,", + "FriendlyName": "Quilted Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:56.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1297, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8je25aruza2779n4njv1gmsgd.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,QBglgj80zkuvUkk55Y5Feg,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:00.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1298, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bldx6nf85lql3l8l0v9q3wmag.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,-HYrY2A5Tk2UWMi4_hmYbg,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:04.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1299, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7i0g7hs6aziwfqxrm0xnv67gg.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,IjRMopsYlUqSE6327I9TIQ,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:10.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1936, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ntv07mm6o9xzz2hcltgnxkur.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,zTsJHduQ5kirH9-6mewV4g,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:36.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202199258, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "GriWpcHg50eSLAxY5yc47g.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,ons8s4ILoUmkdtliouAscw,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quiltedcoat) Quilted Coat Shirt Teal Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.523776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2559, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "2GOgnQwKxk2NkTSIxV9t_Q.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,j3I0vVz0yUOYLX9wCOMyyA", + "FriendlyName": "Science Coat (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:56.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1675, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d4c3lyj93p5m9r7r9lvo7z2br.png", + "AvatarItemDesc": "be75b46c-ce22-4758-8810-c409c5aa1c50,", + "FriendlyName": "Puffer Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be75b46c-ce22-4758-8810-c409c5aa1c50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:38.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2008, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "87cdvvpwz27kl5fqj4a53il38.png", + "AvatarItemDesc": "be75b46c-ce22-4758-8810-c409c5aa1c50,ZLaDizWS7kOmnHVGlLLJ-g,TUZ21v0C9UK9GPsfbRwpsw", + "FriendlyName": "Flannel Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be75b46c-ce22-4758-8810-c409c5aa1c50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:04.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1853, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chi1z36iwxalp98kkl4coqu8h.png", + "AvatarItemDesc": "45695fba-5abc-4c5b-b9eb-949d5174d1f5,", + "FriendlyName": "Racing Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45695fba-5abc-4c5b-b9eb-949d5174d1f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:12.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1899, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7trthor2vpnt3r9e6aywi0ohc.png", + "AvatarItemDesc": "45695fba-5abc-4c5b-b9eb-949d5174d1f5,oFjt2tSsa0CJ7j0s3P6b4Q,XD9dH8LF_ECmL0TwXJW1zg", + "FriendlyName": "Racing Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45695fba-5abc-4c5b-b9eb-949d5174d1f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:41.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1854, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5ll88rg9iiispe9c611fj575e.png", + "AvatarItemDesc": "9fb62d0c-67ff-4675-85d3-a2869c2330aa,", + "FriendlyName": "Racing Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fb62d0c-67ff-4675-85d3-a2869c2330aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:22.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1896, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b5jt7jrhmi5ch4z1axfan8u95.png", + "AvatarItemDesc": "9fb62d0c-67ff-4675-85d3-a2869c2330aa,ijCbiOeGD0C4uznegq9CXw,VzyoPsQyCUaW6RSusgVUGQ", + "FriendlyName": "Racing Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fb62d0c-67ff-4675-85d3-a2869c2330aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T20:34:43.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1852, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7lyaxr5m3766nt3tdurlv2gik.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,", + "FriendlyName": "Racing Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:08.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1898, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1ohpxd83hqnj4iz7zqf0eqy3w.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,lhqUg1VMNkC8nrsGZF4edQ,uFERkSfSl0i9BPFBVOICeg", + "FriendlyName": "Racing Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:38.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2115, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qvzY9bthF0SXys1g8FCeNg.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,yO5pgI2quE2i3tNBlvJPGA", + "FriendlyName": "Racing Suit (Sponsored)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:25.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2484, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8uEeIbBPq0eGrt1TFDEHTg.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,jGNpSO25HkO7KbR3v05nkA", + "FriendlyName": "Racing Suit (Astronaut)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:36.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2002, + "AvatarItemType": 0, + "Tooltip": "This item may come in handy at concerts, afterparties, or in dark, forbidden lairs...", + "Rarity": 50, + "ThumbnailImage": "es10uj7wkevzcr8uv905avgbr.png", + "AvatarItemDesc": "e4888429-6f5b-4f57-9b9b-2559d3a54856,", + "FriendlyName": "Rainbow Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4888429-6f5b-4f57-9b9b-2559d3a54856", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:38.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2928, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZZVGN49VWEOBaIP-aV-9gQ.png", + "AvatarItemDesc": "24781418-38ad-411f-925e-2321fe616b51,", + "FriendlyName": "Rain Cloud Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24781418-38ad-411f-925e-2321fe616b51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:16:03.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1357, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1c69e4rsxwolxiw7iq522lr5k.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,", + "FriendlyName": "Raincoat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:41.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1358, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2y3vygtoygwgkb4vvi4ueelz8.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,GH9q8_0O9EqjGdz7lGWD2g,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:46.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1697, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7uwfpp71odpm2zjf7dopjufm1.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,M-N9vQt0CEefqgmeLp3l4g,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:43.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1696, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "647nsozzr00smthvpc1qrymze.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,q_HcScqHgEG-x70g1wtfLA,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:42.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1482, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9oppuuz5leb1br8st56q8pzgc.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,gDP3y0J2EUayzVvBT9-92w,dMDMDExSmEKOF_kQIHaowg", + "FriendlyName": "Raincoat (Getaway Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:47.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2809, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6MqtAecx3EmgN2hug-kDWA.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,kBmmFoqo-0eLSSAhen-VLw", + "FriendlyName": "Black Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:53.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2811, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "zxxN4bCyXESVjKRUDYv7iw.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,tVbu4hUFHkmEKEz38GG2Nw", + "FriendlyName": "Pink Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:59.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2810, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3yrQDfBRi0i3HHeiqNPY3w.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,m8x6fiLEw0m4pJywntEpzg", + "FriendlyName": "White Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:56.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1359, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5slbcv7tlqy8kpl75ol21jidq.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,", + "FriendlyName": "Rain Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:50.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1360, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0ds3vsjpe01l47ks32nxbst5q.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,v3af60p1Q02NpdCqIRO9lw,1hVOI7IuY0mKvtr2PZXLww", + "FriendlyName": "Rain Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:57.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1481, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agse5pe0hx0ij3ujgprl3xcsz.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,kX7ocqn8X0-4M1nw-KrwFQ,qC66Hu6yKEu2Res2o2z1Kw", + "FriendlyName": "Rain Hat (Getaway Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:44.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 688, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eabi5cm8xoboqpioo9h705aih.png", + "AvatarItemDesc": "f277af6a-0807-4716-8948-1bee236ffb74,", + "FriendlyName": "Ranger Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f277af6a-0807-4716-8948-1bee236ffb74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:08.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 697, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "58ktghs1gt48t0xa8cgyrvrhr.png", + "AvatarItemDesc": "f277af6a-0807-4716-8948-1bee236ffb74,d0bb468c-b78f-4e80-916d-65fec3ec0010,0272eb19-fc0e-4ad6-8db9-cc0cda2528b0", + "FriendlyName": "Ranger Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f277af6a-0807-4716-8948-1bee236ffb74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:47.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 693, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e2oxz7ylbhkwldqs5mqnxnqzb.png", + "AvatarItemDesc": "fb907eb2-91c8-45ff-84a5-462bb86ea7de,", + "FriendlyName": "Ranger Uniform (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb907eb2-91c8-45ff-84a5-462bb86ea7de", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:18.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 700, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6r3jp2iifvxn0dpu7pmhfzw0a.png", + "AvatarItemDesc": "fb907eb2-91c8-45ff-84a5-462bb86ea7de,b415fe60-3e9d-4f4d-a53a-ca15eac6af7d,d21e1b6f-9c9e-42b4-bad5-201ec43161e9", + "FriendlyName": "Ranger Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb907eb2-91c8-45ff-84a5-462bb86ea7de", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:51.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2572, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "i5YnDBV6w0ef6GvGX7uWBw.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,", + "FriendlyName": "King Rat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:26.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2575, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ma2zBsTYbEu0Crl3zZaDJw.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,RTrcYpLtukm83lwAfWi5WA", + "FriendlyName": "King Rat Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:35.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2574, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "m90YcY3tGkWS_3aDgwpp9g.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,3oar1K99PU2OTGaT0i7Hag", + "FriendlyName": "King Rat Hat (Vert)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:31.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2573, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T9OIhzV2uUiPCPipuBhCkQ.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,T5z4W4H8sU-2NxpepVH0Qg", + "FriendlyName": "King Rat Hat (Noir)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:29.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1769, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "dt1vtd0wu908uat9x5cnwqgmz.png", + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,", + "FriendlyName": "Rec Con Lanyard (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:29.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2152, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "QyumHCBqFkSaxEc2T7bMaw.png", + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,cxwdcbGTfUS_LYQoXpcQng", + "FriendlyName": "Video Partner Lanyard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:19.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,b6b5aad9-1211-48ce-ac2e-8053a879ccbd", + "FriendlyName": "Rec Con Lanyard Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1103472285 + }, + { + "AvatarItemId": 1829, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1efy3zkdik26gdvivvvd0ap3k.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,", + "FriendlyName": "Reclympics Medal (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:00.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1830, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vqxnzgn42vejyf08m7zft4f3.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,zNLzEvpTpkOIi0J3fIvh1A,OqC4ny2rk0yJJOHemspV7A", + "FriendlyName": "Reclympics Medal (Bronze)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:03.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1831, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "alczfhgjdnbcvf9orafqcp6ag.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,mVj9t_XVCEqdx89v2zdjHQ,kJ_pNxSWKE6HZX4Sc7Dv0g", + "FriendlyName": "Reclympics Medal (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:06.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1667, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aya5t0pnayzq9jz4mfs6jidhc.png", + "AvatarItemDesc": "9656a307-0c79-4b8f-910f-e67c454faefb,", + "FriendlyName": "Rec Pods (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9656a307-0c79-4b8f-910f-e67c454faefb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:46.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1668, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0f7hl0uorhvk24n6ag35m0pxu.png", + "AvatarItemDesc": "9656a307-0c79-4b8f-910f-e67c454faefb,mG0HW8fJkUOE5u7Whg4kag,3CLmVNB_p0Gd5N8dN74TBw", + "FriendlyName": "Rec Pods (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9656a307-0c79-4b8f-910f-e67c454faefb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:49.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2037, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X2KqX8rMj0WzdutVcCYmhQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,", + "FriendlyName": "Rec Trooper Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:54.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2038, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4iczDP9r4EqWpK0IGEcJ8g.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,WkI_v-Sua0WlhmMf8R4aGA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:56.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2039, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nmOyhPcHDkG33U0RQ55jZg.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,3bqY_T4_HEy406HcXrfLuA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:59.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2040, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rVOFYXLD1UOM6Gx-rJT_-g.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,I4lNZy7lR0uyUN5Vk3fabA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:01.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2041, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1825duyZL0qQprGQokp2Lg.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,SMeMLRUF-UWrOcXYJzgScw,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:03.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2042, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "I1pEVWdT3E-DLY2hkN88qQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,aaaG7Fg8fky2dkrwVsA30g,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:05.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2081, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5lE9WIsg6U67neLntaKrvQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,GSO7t0cNs0SIbU-jQjnX5g", + "FriendlyName": "Rec Trooper Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:22.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h3kstogj0skzqb4uky1jpnsn.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,", + "FriendlyName": "Rec Trooper Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:07.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b94th4lvtw4iu7e3pxussoiwk.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,WkI_v-Sua0WlhmMf8R4aGA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:10.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2045, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "07vec7ppqlcv6a60pn9voj1va.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,3bqY_T4_HEy406HcXrfLuA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:12.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2046, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3v252zqwrqkn4wxjcllebsnwx.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,I4lNZy7lR0uyUN5Vk3fabA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:14.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2047, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "afspou17ntsll0egoozdkp3md.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,SMeMLRUF-UWrOcXYJzgScw,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:16.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2048, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25yoikgkjnlbo9c84x1r7rvuf.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,aaaG7Fg8fky2dkrwVsA30g,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:18.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2082, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "14cf5es3wgsam3e771iwjkzbm.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,d0D2htQZvEKajd0oAesNow", + "FriendlyName": "Rec Trooper Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:24.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2401, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b5ur52zpzgwpmifcd9wxjdhq.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,W7E9KfRTLEK-1EZTNKM3Yw", + "FriendlyName": "Rec Trooper Vest (Space)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:39.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2031, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "glEms9DPgUePBYRHEkwVCA.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,", + "FriendlyName": "Rec Trooper Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:39.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2032, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cHR8FF5MZkaGrOTZcAvR-A.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,WkI_v-Sua0WlhmMf8R4aGA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:42.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2033, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3kbfjdmvbkav8SJ0HVhIfw.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,3bqY_T4_HEy406HcXrfLuA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:45.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2034, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GLCx4lFj6E-Z-FE9wkiDoQ.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,I4lNZy7lR0uyUN5Vk3fabA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2035, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NVXRDfl_ZkOY9c5Nq_W1Wg.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,SMeMLRUF-UWrOcXYJzgScw,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:49.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2036, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5bl8JvLg7Eqp39q9_7m5dw.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,aaaG7Fg8fky2dkrwVsA30g,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:51.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2080, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ziuv9nfKPkOE__m_o9Hvxg.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,9ir4Es5d0Ue-8bn2EUcCZQ", + "FriendlyName": "Rec Trooper Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:21.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2400, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NXhmwZeIG0SGdfTzbBGsaA.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,XaNgcjkwP0uxS5_S1JaQrA", + "FriendlyName": "Rec Trooper Gloves (Space)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:35.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 203, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3q137204ani35lep2nt9a2rx9.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,", + "FriendlyName": "Belted Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 204, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cm21tbv0v6i0hf0k70bpbha7b.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,5b58d879-2bc4-47ed-a996-1af2ace16425,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 205, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "74y7j6vz7569lkpcqb2fje2p0.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,d320ad9b-ba5c-45ea-b2b8-0d3e409a9db1,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 206, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2ikovokydhafpprj55cpdyk5m.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,d61711fd-589a-4669-b991-9408a58fffd9,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1607, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5l9wcvzyx9vvzrqe84ohl0s72.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,oJFfs62tR0WVRry2Smt8yA,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:39.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1608, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8ur3yl22591shtrv2px510hqi.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,MIfCkcrFOEasJqfDmxThdw,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:43.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 207, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "djd1hn8umzgdrbijc8oam3pqa.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,", + "FriendlyName": "Flowered Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 475, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e9vo4xrmjapg3t56dbabbraqj.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,1acb1f56-4509-449f-9c6a-065bdeaa9ee3,2d7361d2-31a2-404b-90da-10e89114e1f2", + "FriendlyName": "Flowered Dress (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:45.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 476, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "930kpdjun0nt4qnb2awrj922c.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,e98c3f98-7844-4d6e-a21c-05027033c18c,2d7361d2-31a2-404b-90da-10e89114e1f2", + "FriendlyName": "Flowered Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:47.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1790, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4i7ompfsh19q8naq86uvyacc4.png", + "AvatarItemDesc": "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4,", + "FriendlyName": "Red Panda Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-05T17:42:31.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1964, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5m6k9qeuysh54wzun74y56isp.png", + "AvatarItemDesc": "51e5467f-d310-44f8-80c3-17d7e27450bf,", + "FriendlyName": "Retro Gamer Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51e5467f-d310-44f8-80c3-17d7e27450bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:31.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016431692, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "06VrP-0hN0-WkSvHS02YfA.png", + "AvatarItemDesc": "51e5467f-d310-44f8-80c3-17d7e27450bf,pue_FwobQEiaVubgc5eMUg", + "FriendlyName": "Dark Retro Gamer Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51e5467f-d310-44f8-80c3-17d7e27450bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1963, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ehoq4v330bykz8stgk9bf58j.png", + "AvatarItemDesc": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,", + "FriendlyName": "Retro Gamer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:29.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1134686645, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IFDGFDNDlUCCJx3FM5dBHQ.png", + "AvatarItemDesc": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,ZYjUekG8JkC9uPkm4xH8ww", + "FriendlyName": "Dark Retro Gamer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1962, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5y0de11wldvhcpeo87sdq0vyq.png", + "AvatarItemDesc": "352b49fc-99a2-4f05-a688-866ade1cd394,", + "FriendlyName": "Retro Gamer Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "352b49fc-99a2-4f05-a688-866ade1cd394", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:27.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1132364555, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "q-bZ8zTCIkOyfHecJwDDng.png", + "AvatarItemDesc": "352b49fc-99a2-4f05-a688-866ade1cd394,PabSuDB_GEejMahhLvLkJg", + "FriendlyName": "Dark Retro Gamer Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "352b49fc-99a2-4f05-a688-866ade1cd394", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1961, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ehevha4vehw8jlosk83rag8tj.png", + "AvatarItemDesc": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,", + "FriendlyName": "Retro Gamer Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:25.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1933117342, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uxAsLya8MkWuyusZMQmV0Q.png", + "AvatarItemDesc": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,EM1x0p66L0Gf6BNBP8-1kA", + "FriendlyName": "Dark Retro Gamer Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1960, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "em7ibip6ffvp7q0jpp5qcx4j5.png", + "AvatarItemDesc": "4885683e-a87e-4d27-990a-c16f1df7a28f,", + "FriendlyName": "Retro Gamer Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4885683e-a87e-4d27-990a-c16f1df7a28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:22.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1138935250, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_uzRLVwLNku8gCel1chHsg.png", + "AvatarItemDesc": "4885683e-a87e-4d27-990a-c16f1df7a28f,hKBSHovOeUy84yCbvutkOw", + "FriendlyName": "Dark Retro Gamer Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4885683e-a87e-4d27-990a-c16f1df7a28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "e036f042-92c1-4c35-ab71-e9c7d5fbf301,", + "FriendlyName": "Ringmaster Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e036f042-92c1-4c35-ab71-e9c7d5fbf301", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1683507107 + }, + { + "AvatarItemDesc": "73994140-58f1-40b8-a7a4-baca020ab2fc,", + "FriendlyName": "Ringmaster Mouth", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73994140-58f1-40b8-a7a4-baca020ab2fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1011431567 + }, + { + "AvatarItemDesc": "53a73a1a-70d1-447b-a594-3c96890f52ab,", + "FriendlyName": "Ringmaster Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "53a73a1a-70d1-447b-a594-3c96890f52ab", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1084249225 + }, + { + "AvatarItemId": 1807, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "duw3kiu7wtbc2uhma5er2l480.png", + "AvatarItemDesc": "1fed823a-22bb-4225-bdc4-491da6eba875,", + "FriendlyName": "Apocalypse Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fed823a-22bb-4225-bdc4-491da6eba875", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:25.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1802, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5dszhnzwez2lmqbracuxzqjsf.png", + "AvatarItemDesc": "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a,", + "FriendlyName": "Apocalypse Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:49.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1804, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3k1od2jqyltyokb1zgvbyxoll.png", + "AvatarItemDesc": "3e307eca-0850-4d85-8cc1-f5c176bfa7e9,", + "FriendlyName": "Apocalypse Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e307eca-0850-4d85-8cc1-f5c176bfa7e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:03.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1808, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dqdlvwys8ok8v51xsyvdtnxs.png", + "AvatarItemDesc": "23944856-b567-4075-815f-261cb4655cff,", + "FriendlyName": "Apocalypse Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23944856-b567-4075-815f-261cb4655cff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:29.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a6631bxfoa0zyvs7fxyzubtji.png", + "AvatarItemDesc": "23767c66-31de-4e6f-81f5-b17769a0fd5b,", + "FriendlyName": "Neighborly Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23767c66-31de-4e6f-81f5-b17769a0fd5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:13:49.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "7f4317a3-0f47-4fcd-bfeb-ff62e33d37c9,", + "FriendlyName": "Robot Arms Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7f4317a3-0f47-4fcd-bfeb-ff62e33d37c9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1505598127 + }, + { + "AvatarItemId": 1719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "446uvbhzordij4cso3587acsi.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,", + "FriendlyName": "Robot Gauntlets", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:10:59.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2111, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "shxxE1X6sE6_4lH3hEXGSg.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,gIDTSqjSvECgxxd4YtOZIA", + "FriendlyName": "Alloy Power Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:27.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2110, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SIkjnmq_P0mIn1aA3HyRTg.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,BoaDgMfzEEycbxVmWneHNA", + "FriendlyName": "Alloy Power Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:26.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1874, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4syp3adr5p8obc2cv6ef22vvr.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,", + "FriendlyName": "Rogue Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:34.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2442, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OzofyASWI0-3nEAN_uzMww.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,C64nh9MHv0mlUEP9-s7sJw", + "FriendlyName": "Constellation Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:17.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2444, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "K8ehibem7USe2u0ui6wmDw.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,ay9ktLnHfUKdj0-q52Gvyg", + "FriendlyName": "Constellation Cape (Sunset)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:24.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2443, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "C3GGuNb9x0qmewSkRHP33A.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,WJ27X_UnfUmFKpY6mRk_Rw", + "FriendlyName": "Constellation Cape (Aurora)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:20.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2560, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "zW972rrvQ0-DezwkPcNYvw.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,YVkcCfJZekq7GvdMUy2IWg", + "FriendlyName": "Science Cape (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-07T21:47:00.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1870, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5f0fab7drkewxktsldpgux1wv.png", + "AvatarItemDesc": "4b72736e-6fca-4961-98f2-1d4e199eb1e0,", + "FriendlyName": "Rogue Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b72736e-6fca-4961-98f2-1d4e199eb1e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:20.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1872, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "edcczm6mc1nc1pcanunxzgprq.png", + "AvatarItemDesc": "155c13b9-3c72-4314-85ac-6fbac19cdc76,", + "FriendlyName": "Rogue Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "155c13b9-3c72-4314-85ac-6fbac19cdc76", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:26.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2803, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dWn_mPxj7kqBYwHjUkgI8w.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,", + "FriendlyName": "Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:41.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2807, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7TM3VMXrDUSYrIPIMneOSA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,P8w3-vcH5kaHaMEsFsKDjw", + "FriendlyName": "White Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:49.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2804, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-PKF_-QCLkiKRwC3epBnZA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,KUSVg33DSUK8aYXxJzOTdA", + "FriendlyName": "Rose Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:43.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2806, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "myhvbSN3lEG_bApVoStEcA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,g8h43tuiN0CCifW9XbnFhQ", + "FriendlyName": "Red Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:47.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2808, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kABVMxEknUWZArqSsxRLFg.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,T0PMM2iQqkCCMZoznrdkug", + "FriendlyName": "Yellow Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:51.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2805, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FWfZEwnuIUSoM_qCk6O_ag.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,sA0KRBVMikOo6BBfk0dC1g", + "FriendlyName": "Blue Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:45.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2285, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4h1x24hz3beovhoamej45mz90.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,", + "FriendlyName": "Rose Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:37.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2286, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bf3m7280dfcfs560g1sg22rd1.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,4ve7AWD5pkGjvmtC6JRdiw", + "FriendlyName": "Rose Earrings (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:41.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2288, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "63yh6x5s2jp8owgzlji3dlq1b.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,10nOM4HBRU-e7Bb05SrtLw", + "FriendlyName": "Purple Rose Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:46.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2287, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "044d8yuos4umj6mmduxqxzgkx.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,F3m52YaKyEqwbgUyPVfFxQ", + "FriendlyName": "Rose Earrings (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:43.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1221, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "emi0fu2fmbr4yo7ar29206bpi.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,", + "FriendlyName": "Round Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T23:28:26.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1915, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "conhirr4han3hgcb2cd702mbx.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,2pnfLqkYZ0-GbpeE8g0vVA,hGtyDIIWckGoRgCaSG6GBw", + "FriendlyName": "Round Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:29.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1914, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "99d174z75khtsr89u9oy0e8ac.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,vw2iLomK9keTfEWsqJrmhA,hGtyDIIWckGoRgCaSG6GBw", + "FriendlyName": "Round Glasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:27.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1913, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8i5jwartl3iqnxvt1wo4k0n5h.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,t_y2xmFYLU2v6Jb6PX6v_g,4J9GPTKjjUOjyLVPmJyxWg", + "FriendlyName": "Round Glasses (Tortoiseshell)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:26.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2180, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gsJJq-rFYEqEYVYeX8ig2g.png", + "AvatarItemDesc": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,", + "FriendlyName": "Basketball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:37.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1806077292, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Jo3RL0WeG0e_NtvrpG87MA.png", + "AvatarItemDesc": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,G_fqC7KPaUWFM4Dk3stJYw", + "FriendlyName": "8 Ball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.527778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bf58ryhvrbk2aecunq5n3d31i.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,", + "FriendlyName": "Royal Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:08.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 772, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxnimeiegto1m9zphsy7qlcz3.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,5_-QLURc_0W8qeeN3Q3iGQ,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:55.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 899, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bi9ik7y9m6zg5fxaf10v08uj.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,mqdncbYcuUSGFvFqEU0z4A,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:29.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1309, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "auudmnyvmvid9ohssear4bwrb.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,MbusQ9bte0i9zirlxtNkSQ,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:51.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1310, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7h38lx0795mk0b8xq52d9qxpl.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,_9QlnTPXuEO5OQ3H0CwFkA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:55.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1503, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ttt8tqohiquycu310oyhuoql.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,cKLBS6pbf0SFuavWyeZ7GA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:04:18.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1758, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bwspz3moa7kwzc2t95xaqy4m.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,DZyW-gOG9U-QLGOMFBhOoA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:31.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1805, + "AvatarItemType": 0, + "Tooltip": "For earning $50,000", + "Rarity": 50, + "ThumbnailImage": "7na5kk0hvnqgktgkcfeeae46d.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,pEiU6yZm2U-X9ip_CM1SAw,PIV6RtD7Pk-UTcUp2wF1ww", + "FriendlyName": "Royal Cape (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:07.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "76b2m8j7wi2u4yhfscwdacit8.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,", + "FriendlyName": "Royal Crown (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:00.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 962, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "auqhtl8pre7pp954pnbtgfdm6.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,E1GfXDLUaUKaG9hTLAfFPA,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (Silver, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:27.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1303, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6e9fx1i4uk389yevktgtc9s2e.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,wCe2yC2U1kyYvPd8Z1rX2w,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:15.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1304, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aq5apzhwgqow1ci3zzpz4gmlg.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,QkfA71-1z0qOiZAVrbzhtA,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:20.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 721, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2awsas0vcecc7a11lr9fprbmy.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,", + "FriendlyName": "Royal Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:10.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 866, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a73qlfdd1y677wh2wfqn4hheb.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,0ps19D7b_U2iejdKQFORyA,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:21.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 865, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4chofv403g5ompt8exr51v7ai.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,t1MeoroFOUioYmPfT9mCeA,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:20.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1313, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "856n9ywg3kp1796sqc8zm9yvz.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,pjkTGVHkIE-BzCVi1KAesQ,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:12.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 963, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4sv3incjefp9fawup30vx3ih8.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,QP38EMygY0S-KIvdGatARA,m5Vw_MMCWUiSOOCL94QO0w", + "FriendlyName": "Royal Dress (White, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:35.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 722, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c7mqflmgoajhkoupate9o25ll.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,", + "FriendlyName": "Royal Tabard (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:11.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 864, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "247il28eoprdj9km80orhaqg4.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,5TJPDmvEBEOkP82ZWLFqtg,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:18.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 863, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9q4xzeoi13tu32l6zxmci5daj.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,rCeRk8Pdl0W5M47JAQPhpw,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:17.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 964, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6i0vxy286tiuc0bmfbvghl17k.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,8zXWUxwZHU6tThCV4p5iGg,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (White, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:37.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1314, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eq5nxte0f2lf5p4vau1f14d8j.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,s2kCppLsN0uY7Hy6qjQc-g,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:16.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9s3qbhep0iagvjuzfxnb10ab1.png", + "AvatarItemDesc": "418ed21e-766f-4c69-9d57-6f866dcc7feb,", + "FriendlyName": "Safari Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418ed21e-766f-4c69-9d57-6f866dcc7feb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:19.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 389, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ah0cj8axzd6ex6de2x2474x3m.png", + "AvatarItemDesc": "418ed21e-766f-4c69-9d57-6f866dcc7feb,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,0dabda0d-842c-4d39-a6b9-a8ca3ec20825", + "FriendlyName": "Safari Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418ed21e-766f-4c69-9d57-6f866dcc7feb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:26.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 375, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1v72ao5s3ko2tz94379807tki.png", + "AvatarItemDesc": "1041d745-c5e9-4218-a3e1-198f5c9c418c,", + "FriendlyName": "Safari Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1041d745-c5e9-4218-a3e1-198f5c9c418c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:15.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 393, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "702vaqfirb8tw9cqhy7977bcq.png", + "AvatarItemDesc": "1041d745-c5e9-4218-a3e1-198f5c9c418c,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,84c016ad-133f-45e3-a412-cac35612540b", + "FriendlyName": "Safari Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1041d745-c5e9-4218-a3e1-198f5c9c418c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:29:49.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 370, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "28rrsgo2cs68zome98ef6on9s.png", + "AvatarItemDesc": "f47f81e7-fcdd-4687-9b99-013147bef30f,", + "FriendlyName": "Safari Belt Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f47f81e7-fcdd-4687-9b99-013147bef30f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:10.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 385, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9d9fdme5qc72avvqaj8m7u2b1.png", + "AvatarItemDesc": "74365234-cb72-409d-8cf5-9a7209f707e1,", + "FriendlyName": "Safari Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74365234-cb72-409d-8cf5-9a7209f707e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:24.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 395, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cpfts5y9qfb5p04u9nnjxia23.png", + "AvatarItemDesc": "74365234-cb72-409d-8cf5-9a7209f707e1,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,fa9a3d8e-16c9-4a7e-9166-3226e0d13c39", + "FriendlyName": "Safari Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74365234-cb72-409d-8cf5-9a7209f707e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:30:09.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 382, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e4rsuhl9ws520phjximdfsour.png", + "AvatarItemDesc": "02a5b48c-abba-4385-967b-ba900424829e,", + "FriendlyName": "Safari Monocle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02a5b48c-abba-4385-967b-ba900424829e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:22.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 246, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bbp0mhypju37l680pb4b7mrdr.png", + "AvatarItemDesc": "2c679f89-c76e-4cfb-94e9-448c8fd44d55,", + "FriendlyName": "Saija Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c679f89-c76e-4cfb-94e9-448c8fd44d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 282, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6mjsnrcz0704f1ablk7vhwper.png", + "AvatarItemDesc": "50c9c6f8-2963-4ef3-95d5-e999a898269f,", + "FriendlyName": "Saija Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "50c9c6f8-2963-4ef3-95d5-e999a898269f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 65, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "98wzkbq0ttvls6my69cezxa1o.png", + "AvatarItemDesc": "274cb9b2-2f59-47ea-9a8d-a5b656d148c6,", + "FriendlyName": "Saija Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "274cb9b2-2f59-47ea-9a8d-a5b656d148c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1904, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "670kc3b5s4g784sztj0kyl4xe.png", + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,", + "FriendlyName": "Samurai Belt Sword (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:42:56.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2054, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mFZE17grJkezYN_vmo5ZnA.png", + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,XIluUn9vQEabkEpqyYBG0g,9sSCSLRkYUCs4CCHK8FfAA", + "FriendlyName": "Samurai Belt Sword (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:41.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,d6525663-1028-463a-be52-7b8ca5de99ba", + "FriendlyName": "Samurai Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1465178662 + }, + { + "AvatarItemId": 1905, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f278x818nbgl8s8jjebtz9s9z.png", + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,", + "FriendlyName": "Samurai Helmet (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:42:59.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2057, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "huMYQIkj9kGAZLeIjvaUTw.png", + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,tdECtZp1UUuRRKHV60coyA,PIg3ywG2J0CiGUPpm1A6Kw", + "FriendlyName": "Samurai Helmet (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:48.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,689e15e9-7e7e-4fd5-927d-c015d69c1178", + "FriendlyName": "Samurai Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1144250768 + }, + { + "AvatarItemId": 1906, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6u8vpdm9um3l371w3nl9q0j0g.png", + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,", + "FriendlyName": "Samurai Armor (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:43:02.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2061, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tcgeftujX0my-JOBuLu7kA.png", + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,c7wiHNZ-X0KazEW4cY9PeA,5fVeR6S6R0yo_NcT-TB1lQ", + "FriendlyName": "Samurai Armor (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:57.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,5d21a059-fdc1-4edc-898a-5fef98eed137", + "FriendlyName": "Samurai Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1797755030 + }, + { + "AvatarItemId": 1907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0n5obvj9pnbfm6a7x8t010av0.png", + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,", + "FriendlyName": "Samurai Wrist Guard (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:43:05.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2062, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AShCqsZgOUmxqJasgumjXQ.png", + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,h-uMOz2Hz06ymEjtz4wuzQ,4Ooi0h3SrkObClvS2cGk9Q", + "FriendlyName": "Samurai Wrist Guard (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:04:00.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,ec8b5518-1fda-450c-ae57-d0cf06700532", + "FriendlyName": "Samurai Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1208756254 + }, + { + "AvatarItemDesc": "f0fd87b4-4891-40a0-b463-7ce1da2c8929,", + "FriendlyName": "Sandcastle Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f0fd87b4-4891-40a0-b463-7ce1da2c8929", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1197877174 + }, + { + "AvatarItemId": 1934, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "8fzadq5f6ni6nboo4jmt4z0wb.png", + "AvatarItemDesc": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c,", + "FriendlyName": "Jolly Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:06.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1935, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "8qodk6xu0694kgb6xwrvd2pb9.png", + "AvatarItemDesc": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c,veF50UiWeUS8ugHSm6g_3g,7KDavHUPDUKTJI0fFzJHpQ", + "FriendlyName": "Jolly Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:13.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 596, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4b0c6i1j57butbjhen02dvnxd.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,", + "FriendlyName": "Scallywag Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:13.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 613, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3rvrjylez3infpqjran7vpvjs.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,53dd9e44-7096-438e-a7c3-11d7c307426f,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:49.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 614, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a8hpjaq8s1osstq81y66f1yk0.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,c3c9f89c-580c-43c4-b38b-07394044e0e1,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:51.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 595, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d1wurf50yoyooz47syoelb2ri.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,", + "FriendlyName": "Scallywag Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:10.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 605, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7qkfqsjmmjcd9v4hxmipdbu36.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,53dd9e44-7096-438e-a7c3-11d7c307426f,a4903da3-249d-4f0a-a746-b547fda371ff", + "FriendlyName": "Scallywag Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:39.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 606, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3llu49o19577q8gvi3uv7qu29.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,c3c9f89c-580c-43c4-b38b-07394044e0e1,a4903da3-249d-4f0a-a746-b547fda371ff", + "FriendlyName": "Scallywag Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:40.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 597, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a6uf8rc48kcjj4lj2fsir9vsr.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,", + "FriendlyName": "Scallywag Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:14.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 609, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3m6lx9rzs88omzsy182j1yuxb.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,85eb7f47-bb31-4c66-a8b3-562d35028e6c,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf", + "FriendlyName": "Scallywag Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:44.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 610, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00u595ofjuu0ky28diplmypee.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,39f04eee-3a58-4e53-9f5e-7e995d46ed71,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:46.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1111, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "80jjzxrcdq2pf4nuv4ev13nx0.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,6y_fzLhG7E65Px3-CYiyXQ,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf", + "FriendlyName": "Scallywag Vest (Crimson)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:11.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 598, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6ko9exiyd39fsdh9zptnhw8ba.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,", + "FriendlyName": "Scallywag Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:16.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 617, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "f0s9ro8uu6ibrjgn1bicm5vf8.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,cdca593a-8840-4140-9145-fe962732040c,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:54.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 618, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cxo3ckfyyp09zazkttzy0mjz4.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,62bd5dda-08da-4528-83c1-f0ca3812dc6e,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:55.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1104, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00nybvpdwpb8lqxe6gkzo9y7x.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,X3MkhL55Rk-0UK6h4dw0Eg,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Crimson)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:44.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1178, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cbojb5iebhb2p1tra7gh61j7y.png", + "AvatarItemDesc": "d960b47d-f8bd-4c2e-813c-a6a09876ea18,", + "FriendlyName": "Scarecrow Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d960b47d-f8bd-4c2e-813c-a6a09876ea18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:03.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1179, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5saxy9nrha61uuej237wtnnsh.png", + "AvatarItemDesc": "d960b47d-f8bd-4c2e-813c-a6a09876ea18,TZ3oQBp_3kWY4Vzscgv5Wg,xCpdHx1iM06Gg0tYoGv0DA", + "FriendlyName": "Scarecrow Hat (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d960b47d-f8bd-4c2e-813c-a6a09876ea18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:08.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1174, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "acu72xlzacl1k4mnepqsfd2td.png", + "AvatarItemDesc": "1fcaf4fd-9012-407e-b691-fbc3ccf22421,", + "FriendlyName": "Scarecrow Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fcaf4fd-9012-407e-b691-fbc3ccf22421", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:25.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1175, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9mad5t9bu3npfwut2s2mhtvkp.png", + "AvatarItemDesc": "1fcaf4fd-9012-407e-b691-fbc3ccf22421,S0AKRB_m2k-I7snHnUigtw,uTkbswCedUGyuZcaX0W6Ww", + "FriendlyName": "Scarecrow Overalls (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fcaf4fd-9012-407e-b691-fbc3ccf22421", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:31.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1176, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6vc9gb07nhld11lkb3agtr3xq.png", + "AvatarItemDesc": "3c9acb9e-3353-4359-aa5a-ae9512b086b4,", + "FriendlyName": "Scarecrow Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c9acb9e-3353-4359-aa5a-ae9512b086b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:52:54.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1177, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8otr4d3cafso2wpwxnuskm5o5.png", + "AvatarItemDesc": "3c9acb9e-3353-4359-aa5a-ae9512b086b4,dro7VLV8BkGjqpPAbJheYA,bNA44H2wzUy-FkWgyOq1Kw", + "FriendlyName": "Scarecrow Wrists (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c9acb9e-3353-4359-aa5a-ae9512b086b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:52:58.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 94, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2t2ujgsm8dnv8j9ax7bmlsnsd.png", + "AvatarItemDesc": "f17a2f9e-1eab-44e8-b10a-1f53c5d0778c,", + "FriendlyName": "Scientist Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f17a2f9e-1eab-44e8-b10a-1f53c5d0778c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 247, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "17egpclh4qfjwpauvfot65edy.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,", + "FriendlyName": "Scientist Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 795, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xvbjj5mp2xmnu3w4g7m7c9px.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,KTidYfH0ckWxJGs5qzUBTQ,w07bkcUoQEiNJTVDeSywzQ", + "FriendlyName": "Scientist Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:23.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1100598754, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "oDUFSXJI5E6fmcsxJqMskg.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,X0nzvhAtgEKqzjQPb9d3qQ", + "FriendlyName": "Science PPE Coat (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.529776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,8OdkUQPXzkGqXFa3LNXupw", + "FriendlyName": "Scientist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1664244968 + }, + { + "AvatarItemId": 99, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9g1cba0qnig6whctstb0qaxyc.png", + "AvatarItemDesc": "11e508a7-fe7d-470e-b270-2dba33e2f7a1,", + "FriendlyName": "Scientist Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11e508a7-fe7d-470e-b270-2dba33e2f7a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "11e508a7-fe7d-470e-b270-2dba33e2f7a1,chxai_T-BEOc0-uZv6OAQg", + "FriendlyName": "Glasses Scientist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11e508a7-fe7d-470e-b270-2dba33e2f7a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1577926849 + }, + { + "AvatarItemId": 283, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3sd1nkxa7ilnnjgnwm2xtqw6b.png", + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,", + "FriendlyName": "Scientist Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 802, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6r1rr4giperexziw91ocwu6f9.png", + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,KTidYfH0ckWxJGs5qzUBTQ,VSZX99mSt0e7gPRM2_XzMw", + "FriendlyName": "Scientist Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:45.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,mRNobpJiWUCpih1zETyaZA", + "FriendlyName": "Scientist Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1306056020 + }, + { + "AvatarItemId": 1510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aun7sd8qe3iqtu86eeyec9uq1.png", + "AvatarItemDesc": "380a5e35-b4d0-4119-9721-8ce4c0840fbd,", + "FriendlyName": "Starship Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "380a5e35-b4d0-4119-9721-8ce4c0840fbd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-08T21:59:46.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 102, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0lm206mtws500pk9e4v0zaxuv.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,", + "FriendlyName": "Space Visor (Nova)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 909, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "av1v53eoyk53w8zi6bwd633xb.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,SXG9rdEkxkOR31Nk0aTOww,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Quasar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:42.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 908, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c9qgvf1jfrdwzsjud14bqa9pp.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,FFIpxInE-EKbk2GY-pIkyg,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Nebula)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:41.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1800, + "AvatarItemType": 0, + "Tooltip": "Unlocked by visiting ^WelcomeAndroid", + "Rarity": 50, + "ThumbnailImage": "1x302shgihlyh5d4ewddkiln8.png", + "AvatarItemDesc": "8691fa78-3069-41e6-8431-01c294e8d56b,", + "FriendlyName": "Space Visor (Orion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8691fa78-3069-41e6-8431-01c294e8d56b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-21T18:25:26.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 783, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9cfmiay0o7q5lwqa8wx2pm6ug.png", + "AvatarItemDesc": "KNZ1DvCLC0WHyH4opYAXtw,", + "FriendlyName": "Space Visor (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0e75d628-8bf0-450b-87c8-7e28a58017b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:25.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 914, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "80gor8nb4uaucnbfr9tmpumkr.png", + "AvatarItemDesc": "KNZ1DvCLC0WHyH4opYAXtw,FwiizGjYnUaM1VJ53R1cCQ,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Eclipse)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0e75d628-8bf0-450b-87c8-7e28a58017b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:48.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2483, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "BUTSu7FlzEOaiirSftT3Aw.png", + "AvatarItemDesc": "54ab4eb0-e28a-438f-9977-1ae8d642d136,", + "FriendlyName": "Space Visor (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "54ab4eb0-e28a-438f-9977-1ae8d642d136", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:34.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1235, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4eqc3t4t4z5gnf92sqf844nea.png", + "AvatarItemDesc": "1736a60b-6fb6-49bd-948a-fd3f3fc1be50,", + "FriendlyName": "Space Visor (Neutron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1736a60b-6fb6-49bd-948a-fd3f3fc1be50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:22.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "66dcdd7c-7dee-490f-b2d6-1113daa782a7,", + "FriendlyName": "S Cloak Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66dcdd7c-7dee-490f-b2d6-1113daa782a7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1696752929 + }, + { + "AvatarItemDesc": "50721e23-67e6-4f16-8e70-b0eccc2d9c3d,", + "FriendlyName": "S Cloak Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "50721e23-67e6-4f16-8e70-b0eccc2d9c3d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1144628819 + }, + { + "AvatarItemId": 690, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1x9c7tsi208t2ykfb3m4oc9je.png", + "AvatarItemDesc": "f60e093f-33be-4527-b827-060adc77b1f4,", + "FriendlyName": "Scout Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f60e093f-33be-4527-b827-060adc77b1f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:13.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 698, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "29hnpj9lbliny7rio545fdave.png", + "AvatarItemDesc": "f60e093f-33be-4527-b827-060adc77b1f4,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,464fcf20-361f-4dc9-b074-150c3bc27523", + "FriendlyName": "Scout Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f60e093f-33be-4527-b827-060adc77b1f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:49.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 694, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1p79pekt3n19qb30ijvjrm1pn.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,", + "FriendlyName": "Scout Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:28.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 701, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "46gvey2edecat2kkb5igdbv3b.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,5814a331-6155-4df4-b921-3ffc6059e44c", + "FriendlyName": "Scout Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:52.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2390, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "O9Zd5keRSkKyg2NA2V-QEQ.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,OX1ituawfUqYC-caC1WcCg", + "FriendlyName": "Zombie Boyscout Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:38.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2403, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "hsrSnSftIEikNWVCh1v1GQ.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,5owofMF_Y02WX2vwVL2aew", + "FriendlyName": "Zombie Boyscout Shirt (Mutant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:48.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,b3LMvviE_0-ObF1x5s1dtw", + "FriendlyName": "Scout Neckerchief Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1494988446 + }, + { + "AvatarItemId": 689, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5p66sv7hukzmxmyh14z8b5781.png", + "AvatarItemDesc": "7c1552a6-bc36-46e6-884c-1de6b8111a92,", + "FriendlyName": "Raccoon Cap (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7c1552a6-bc36-46e6-884c-1de6b8111a92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:12.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 704, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3x3ft6vz0rl2rj8hpyhovqfpw.png", + "AvatarItemDesc": "7c1552a6-bc36-46e6-884c-1de6b8111a92,926ae943-76cb-46dd-9c66-a6ee44ea9194,f1cd10fb-3d0b-436f-a173-65bc0d88d8bf", + "FriendlyName": "Raccoon Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7c1552a6-bc36-46e6-884c-1de6b8111a92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:08:41.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 692, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "10h045rqcvgang1sy8hxigaa2.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,", + "FriendlyName": "Scout Sash (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:16.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 705, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9bcfdhumbtjizrtjd0clcoxgy.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,67490e3b-0f9d-4bf8-84fb-54957b981c8c,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:49.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 706, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1reomg1o3fiimjissaj3cz8gd.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,b520f3bc-599a-498c-8a4f-97cc25aa741f,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:52.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 707, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e9pz1n0sr3t8a3p5llhnnh5l8.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,639f5e2c-6fbd-4ab4-aa10-7772e0932798,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:55.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 695, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8usltv63g871msvqw5fh4v055.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,", + "FriendlyName": "Scout Skirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:30.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 702, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "636348tvnldqryf6iouybgnoy.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,2bb65868-ee12-4871-b50d-64bbc9066f0b,e65530f7-543c-43e9-bf12-b6cda2bc755c", + "FriendlyName": "Scout Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:53.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2404, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "KhdXc-odtUiy82uWA9Hyfg.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,XxVqGAlweUKta__sKdMP3Q", + "FriendlyName": "Zombie Girlscout Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:52.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2405, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "3krD9e7_gUezvWyLO30zZQ.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,VL_OnsbYNkGgGAV16x5tQA", + "FriendlyName": "Zombie Girlscout Shirt (Mutant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:56.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1242, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "768szgyob0dtu1479hlls39oa.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,", + "FriendlyName": "Scuba Goggles (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:39.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1243, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3uvxr9vhe7g8piy0o8yyctuu7.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,DRS8xu3AqUOy2L0RKF3f7g,A7Y52vGIN0ijEKsWR_GQLg", + "FriendlyName": "Scuba Goggles (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:40.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1244, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1qslsywz60fw9vjpnsmkdf7gi.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,ZSOuWuKVCkqaIKc5d23G8Q,A7Y52vGIN0ijEKsWR_GQLg", + "FriendlyName": "Scuba Goggles (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:43.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1236, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4bal97uh9so459loudao1d5pk.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,", + "FriendlyName": "Scuba Wetsuit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:24.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1237, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6n0p0vohzjctpgowbi5u1zyvq.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,VqtOYBiKRkaSW4xAlBxQgg,xTqtLPQVyEKjAQZYl6HGow", + "FriendlyName": "Scuba Wetsuit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:26.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1238, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0xnsnma7zo835x2f5qjbn78m1.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,J-K34UX_B0yObjQI9tn0vQ,m0R4Jtf4HES8krxMnwIWFg", + "FriendlyName": "Scuba Wetsuit (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:27.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,ib2g1jTBgEeuj_s_uD1LKA", + "FriendlyName": "Scuba Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1519911932 + }, + { + "AvatarItemId": 1239, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7f4gij6i4zbxcp7r843b74p12.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,", + "FriendlyName": "Scuba Tank (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:29.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1240, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "29lilphy1aywcuix2e39k2ll3.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,V_duN2oWaUmmao1xHhzCJA,Uvarm_CwTUWWQYZvzI8OaQ", + "FriendlyName": "Scuba Tank (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:35.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1241, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "64h9st85tv92639yx7n2hvkgm.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,GWi1rEebqkCV_TS2WzQhcA,4zPm9miF5UOouEWFpOxYZg", + "FriendlyName": "Scuba Tank (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:37.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 66, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ajleo52n1fi9sfdklgfwqj0fo.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,", + "FriendlyName": "Captain Hat (White, Blue, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 67, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "abjufv7nbd5ja0vwk6hsiu952.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,f5403c89-9769-479b-8a05-c71607f9b189,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (White, Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 68, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ifixksiv9byqkl1zzr14g6m8.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,36e63797-5a42-4779-a5cc-ef5c2e7d17d4,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (White, Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 69, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8v6gk9097bsjdumvvi0xe4uk5.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,70112887-dc8d-49d5-96ed-0845204e0b58,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (Black, Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 554, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6hqhj2g8w7mvm85ibz82mgre5.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,2738ab5f-8c94-46b3-bac6-4a8111c41c21,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (Pink, Black, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-26T21:31:46.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "af4dh1nguqj29w27x7rk4ev5y.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,hhs9GY3XZkWb-8TXODvo6A,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:17.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 208, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20un14zo8e1w83oiatx99lwpt.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,", + "FriendlyName": "Captain Jacket (Blue, White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 209, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "41op70bhqlf5s2l320femq7fa.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b9f51861-ad10-4878-a9b1-d849bccf532a,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 210, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5iuznrq2zpc14fctl8tnp4odk.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b5d62931-e756-4a8a-8f3b-760beba49319,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 211, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9c3baiau5sgxnhlikff1pph09.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,6145292a-9002-41c6-8cd5-43679b6f542f,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 212, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "45br9sqobjiwgoh4alz5l9qf8.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,7c1aaa7e-503b-46ef-b1c5-0c61a75b916e,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 213, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "00jh263iuxouyk40imb4wjv0o.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,27af51bc-8226-4911-8d47-22d28ea8e378,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Blue, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 214, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7ergraacf4272gslnvieytiw3.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,857fdcd8-704f-43d0-92b9-bca26619b153,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 215, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3jnhwwxklxb0ojoszaexejbfx.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,3df5d13b-ba77-4b51-90b2-7a946e4d0be7,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1245, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3jysjn20nxxud7yahb9m78toy.png", + "AvatarItemDesc": "d86e2e65-5d87-466d-b885-8c52ac2eeeb0,", + "FriendlyName": "Captain Jacket (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d86e2e65-5d87-466d-b885-8c52ac2eeeb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:45.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 111, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5whnrhprjsn09q0089w2ztby2.png", + "AvatarItemDesc": "c161dc73-6ce3-49c4-8d15-45e6911f84e4,", + "FriendlyName": "Sea Captain Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c161dc73-6ce3-49c4-8d15-45e6911f84e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1871, + "AvatarItemType": 0, + "Tooltip": "Drops occasionally after Rec Rally games.", + "Rarity": 20, + "ThumbnailImage": "aqft3txgw2utq98o392d8z647.png", + "AvatarItemDesc": "9b62cbca-694f-4a32-b910-14da66bee9bb,", + "FriendlyName": "Settler Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b62cbca-694f-4a32-b910-14da66bee9bb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:24.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1869, + "AvatarItemType": 0, + "Tooltip": "Drops occasionally after Rec Rally games.", + "Rarity": 20, + "ThumbnailImage": "dcmbdw7xs41bspzcqqgmm66z0.png", + "AvatarItemDesc": "8cc01536-2f43-4de4-ac63-7304d8beb73c,", + "FriendlyName": "Settler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8cc01536-2f43-4de4-ac63-7304d8beb73c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:13.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2834, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1jTfJYB_7EGuMfrAABvtrQ.png", + "AvatarItemDesc": "e27d33ca-8a81-4c16-a3c1-ffdbc9104d7f,", + "FriendlyName": "Pincushion Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e27d33ca-8a81-4c16-a3c1-ffdbc9104d7f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:44.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1133, + "AvatarItemType": 0, + "Tooltip": "Doo doo doo doo doo doo", + "Rarity": 50, + "ThumbnailImage": "8y0zbo7xaq7ymt78orz2j8b42.png", + "AvatarItemDesc": "13e70c80-69a4-4ad5-90e5-4dd5e03da677,", + "FriendlyName": "Shark Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "13e70c80-69a4-4ad5-90e5-4dd5e03da677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-07-16T18:20:43.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2197, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "znzGBCjJOESSUfgrMXG-dw.png", + "AvatarItemDesc": "13e70c80-69a4-4ad5-90e5-4dd5e03da677,ln5aTG458UOVh4sR820FJQ", + "FriendlyName": "Shark Hat (Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "13e70c80-69a4-4ad5-90e5-4dd5e03da677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:48.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1877719043 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,ppkByMiIi0inxbXKX4fu4A", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1679852489 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,KaTmQ95fV0KAUM-nvOdHwg", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1933742111 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,1GKZY3pku0Wuq4_vdiO21Q", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1603771490 + }, + { + "AvatarItemDesc": "45e9bd7b-b1df-4751-b3ac-994652a5745a,", + "FriendlyName": "Sharky Fin Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e9bd7b-b1df-4751-b3ac-994652a5745a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1869424493 + }, + { + "AvatarItemDesc": "c0e3d978-c4c8-4606-a1af-b37de8995dbc,", + "FriendlyName": "Sharky Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e3d978-c4c8-4606-a1af-b37de8995dbc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1467914069 + }, + { + "AvatarItemId": 723, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "597y9xwas90z92z2w9b6wwif0.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,", + "FriendlyName": "Detective Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-21T01:05:53.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 747, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73q1jtkz7ele0ps0zjryw0veq.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,fEPwvuJCWkGBe4IekXpmLw,axPBGoqorEe77Oqqjfzb5w", + "FriendlyName": "Detective Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:11.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 748, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5x3p1zbvxjej9uzfpfkzw5g75.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,MQOxmUk-mUG-Ee9m9nX31Q,axPBGoqorEe77Oqqjfzb5w", + "FriendlyName": "Detective Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:16.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 724, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "abhyjwyie97h1rlznlk29up36.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,", + "FriendlyName": "Detective Coat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-21T01:05:55.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 753, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0dwm2kiido15zo6nhtcwi0vma.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,Z1Ib9r_fkUqgoZk0uid_Xw,9ZP617DQk0mZ10J3d5q9oA", + "FriendlyName": "Detective Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:25.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 754, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ec3q3cbtla7cn866qybjjfdaw.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,msOFHoFN9UORkeXXtdqBsw,9ZP617DQk0mZ10J3d5q9oA", + "FriendlyName": "Detective Coat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:27.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 959, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4dej37re0qtqjy0r8z93pdoo.png", + "AvatarItemDesc": "pNOfTDOgJUGQvGAlTjBoCg,", + "FriendlyName": "Wide Neck Tie (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c9fd3a4-a033-4125-90bc-60254e30680a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2215, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "62A9TZ7RIEOpOwFfOh9vXA.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,", + "FriendlyName": "Skateboard on Back (Skull Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:38.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2233, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IVFZ9-hSgUC7-LCqkg6e_Q.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,RDkLPQ8OukGIuLgTwnyqfw", + "FriendlyName": "Skateboard on Back (Vaporwave)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:25.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1511682151, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QhCcLdIsJE6qCUuMXJNKzA.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,7WzGZTf51EWClLTLNhhGVg", + "FriendlyName": "Skateboard on Back (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.531778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,655a48e6-ab7c-4c77-9c52-262f5bb8cbc2", + "FriendlyName": "Skateboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1710365958 + }, + { + "AvatarItemId": 2910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0GJO9X6LHUCt29vIeqSe6A.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,", + "FriendlyName": "Yellow Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:52.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CuNGqjAKSU6GCIYRVZDJjQ.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,y6NrbfS070WvASkKmPK-9Q", + "FriendlyName": "Burgundy Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:57.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SbLa3ck7dE2KhA1vlS01Hg.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,MVXeErsEE06KenrYaEm7Kg", + "FriendlyName": "Blue Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:54.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2914, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1kMIu6ND0CfZWLprY8CHw.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,7IYi1twr6kO3xJiQo5CB4w", + "FriendlyName": "Skater Fit (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:02.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2913, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hvNdEmSkiUWJ0ctgd3uXlw.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,qwQ8OV3E60WG_VEl9ECHoA", + "FriendlyName": "Green Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:00.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1182, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "21qmin6ucgfg45pvh611izmvq.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,", + "FriendlyName": "Skeleton Mask (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:30.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1183, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "08urzyypklh9ip5h98tczkuaj.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,s6DQxJ3gY0CG8L7JmcmRTQ,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:33.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1534, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "02lomjpecz0msvhsrrykghrca.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,DJnKyNXtuEW1tbKBFscIng,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:38.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1535, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "965z4xrxm4jg3xkv7a7vkdam0.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,ZY0rVNemjEuEMGMjuPNZyA,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:40.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1536, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "23626wp454o5pqka9wlihhylb.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,jhPUIkf5bkinMSsqPTK3fg,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Yellow Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:43.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2307, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "Z6ugLoGUCEiPuvn_pWpROA.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,FO32QjZ17E66PiSEDyf0xA", + "FriendlyName": "Cyborg Skeleton Mask (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:56.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2303, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "KlT8BALxYUisE34P3vbNZg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,rv8cpfd8kUCK5NbwjNmExg", + "FriendlyName": "Cyborg Skeleton Mask (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2304, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "i_i6gp2yhEyV8tCehNVavg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,kHPSqeM7JUqzMDtVxBGyBg", + "FriendlyName": "Cyborg Skeleton Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:49.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2305, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "4Sc2MIKVHkWz1i42WTaUvA.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,7BrllPDzNEyD9UZgdgM1Dw", + "FriendlyName": "Cyborg Skeleton Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:52.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2306, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "LKhhC901_Ei4OyKUfa82Hg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,Ftonil9tzEWQnmRQuTxwxQ", + "FriendlyName": "Cyborg Skeleton Mask (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:54.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1184, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "4yv9lieuu39ybfs061keni4bw.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,", + "FriendlyName": "Skeleton Glove (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:38.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1185, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "5yfuk6ffcuv1z2vqg6ffd7b69.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,s6DQxJ3gY0CG8L7JmcmRTQ,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:41.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1537, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "ejkvm0j7lbzn3f6m3h8vzq464.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,DJnKyNXtuEW1tbKBFscIng,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:47.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1538, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "bac6n0vzhk6sgr3r9ltyf5xm0.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,ZY0rVNemjEuEMGMjuPNZyA,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:49.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1539, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "dl5czy3ptauoh49mbo0ae06zs.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,jhPUIkf5bkinMSsqPTK3fg,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Yellow Glow Skeleton Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:52.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2322, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "5hcwuha7qbinh2y6q3kskmmp3.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,GYeYHzKcekODxx_zbtk8Qg", + "FriendlyName": "Cyborg Skeleton Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:44.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2323, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6g7hoxuvnw5065l9r0fptljij.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,lwyGrpMk-kGZwIkioHr8Sg", + "FriendlyName": "Cyborg Skeleton Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:46.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2319, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "3clh44ctdz1digv1oiwv505ae.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,KWYMlbpZiEWRUb0F7CYDjA", + "FriendlyName": "Cyborg Skeleton Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:36.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2320, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "369wpmjr71m46vryx3x1ynzm7.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,CsPK8sqkHUSV1u9d4LijHw", + "FriendlyName": "Cyborg Skeleton Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:39.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2321, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "7x0z1jskh28m0i22yy1hel8xd.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,dPM-UHMtSEKC3rN8l4JsUQ", + "FriendlyName": "Cyborg Skeleton Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1180, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "1b7g9kq7p2543j7d970f1bkw2.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,", + "FriendlyName": "Skeleton Hoodie (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:21.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1181, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7hrs6xlvsngnvndq0px6erei9.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,s6DQxJ3gY0CG8L7JmcmRTQ,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:24.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1531, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "0s8dsvnn6kc6q646bv4ipcqzy.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,DJnKyNXtuEW1tbKBFscIng,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:29.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1532, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "5z8kh7zr2w90iw14n6828rdeu.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,ZY0rVNemjEuEMGMjuPNZyA,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:32.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1533, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "8duxsgrggyqxtmflb6g47toll.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,jhPUIkf5bkinMSsqPTK3fg,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Yellow Glow Skeleton Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:35.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2314, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "fU2o342vgECqLjsyiP8vCw.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,x_UvGO3yQEmprFEXhPExDg", + "FriendlyName": "Cyborg Skeleton Hoodie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:18.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2315, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "uHnG7V2W1Uu8H2VhCYGJgA.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,WRi0rK1O60qEpDPgOH5P6g", + "FriendlyName": "Cyborg Skeleton Hoodie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:22.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2311, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "cFJR0iw4gUGSumlZz_qxXA.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,NvnHqZ-nb068DOSrxZPYIQ", + "FriendlyName": "Cyborg Skeleton Hoodie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:04.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2312, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6Oec9FiiLE-7m4bafgo2Jg.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,2mwfYNx7iUGbSf6u8rRXwg", + "FriendlyName": "Cyborg Skeleton Hoodie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:09.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2313, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "nRtJQf3QR0iu2fICET7LtQ.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,xtjgHdnYREm_gx_tLsTpvg", + "FriendlyName": "Cyborg Skeleton Hoodie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:13.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2580, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "k9Vy4pHmyUWpUmPbGE0xuQ.png", + "AvatarItemDesc": "6936d8e0-9d4a-4e22-99b6-04e5182c1109,", + "FriendlyName": "Skeletor Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6936d8e0-9d4a-4e22-99b6-04e5182c1109", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:40.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2581, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SAPYQD9BqUWYP73yXsGZkg.png", + "AvatarItemDesc": "ba96458f-84c3-42b7-acc0-e4d96f31ec84,", + "FriendlyName": "Skeletor Hood", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba96458f-84c3-42b7-acc0-e4d96f31ec84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:43.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2582, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2cM1bj5ynEux57OhmNIcfw.png", + "AvatarItemDesc": "7ee02f33-1135-4adf-a626-44a1b6d1b0cb,", + "FriendlyName": "Skeletor Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ee02f33-1135-4adf-a626-44a1b6d1b0cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:45.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 946, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "81f46rtgrs77zl3to2piot49a.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,", + "FriendlyName": "Ski Buddy Goggles (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:16.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1276, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "948ckejd6ibxj5plgmycvqr20.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,fXNkMWFMV0aXalK_y_8SHw,SSY8ZYMgaE2Bl4oFmMjKQA", + "FriendlyName": "Ski Buddy Goggles (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:39:56.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2597, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4doxnMsj30aGV6CnhP58sQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,n741vKj02kyt8B5fT6s2og", + "FriendlyName": "Ski Buddy Goggles (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:30.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2598, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eqLzMORoeEGX-z-yYZtUsQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,iDnij3VaEkSAnpt5QRevig", + "FriendlyName": "Yellow Ski Buddy Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:33.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2595, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "VFE9I4VMiU6FDWdRZaZlRQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,oCB1ZeT0hU6uQGhHNsEvkg", + "FriendlyName": "Ski Buddy Goggles (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:24.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2596, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-KcCp_tuMkalrEo6T6ql_w.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,IQMO6E5ofkyC3S-IgrKyIw", + "FriendlyName": "Ski Buddy Goggles (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:27.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 947, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b3xepw6yux7925w5til94vmmw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,", + "FriendlyName": "Ski Buddy Ear Muffs (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:19.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1277, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "502z6klf3k4u48eow06006kxl.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,KwCEye0nsESFs-yj4h9sjQ,c_t9XDbn-Eqml1O-wC5zJQ", + "FriendlyName": "Ski Buddy Ear Muffs (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:02.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2601, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aiYHJsbXHku2qBmKihSApw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,m2znGrdH-EOhrbLbFsmyOg", + "FriendlyName": "Ski Buddy Ear Muffs (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:40.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2602, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "JHSLD9otmEODYVGGix00gQ.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,ojrXnWkUkEOGM3_oT6V29w", + "FriendlyName": "Yellow Ski Buddy Ear Muffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:43.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2599, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Cl-lyYb_7EyL8pudY8bgOg.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,sipxCGA1uUu72KV07vAWFA", + "FriendlyName": "Ski Buddy Ear Muffs (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:36.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2600, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cEOrlpC5uEi63XRZAeiOxw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,1_vRe9nJMEWcNX8kAwgh9g", + "FriendlyName": "Green Ski Buddy Ear Muffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:38.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 945, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7e765tmsry0a7xzqwbwtih0ga.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,", + "FriendlyName": "Ski Buddy Jacket (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:14.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1278, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2rr9ojhz8lb65xzu05oejy4i4.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,fXNkMWFMV0aXalK_y_8SHw,HQQWccdyakaEmntPhvC6CQ", + "FriendlyName": "Ski Buddy Jacket (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:15.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2606, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zPU9jyLBykuvxdbYwUE6Sg.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,1BQdC-mTdUqfm4vUf0lx-g", + "FriendlyName": "Ski Buddy Jacket (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:57.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2607, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Tk070G0_FU-z-K1AbYodRw.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,S7C2A8nga0SlIeOEkeYaUw", + "FriendlyName": "Yellow Ski Buddy Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:45:02.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2604, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "h2VEGWuIQke-wT_k7W0sTA.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,vpxBkYv6LUSVV4NhYAS1Fw", + "FriendlyName": "Ski Buddy Jacket (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:48.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2605, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "klVYrqwIukW57n4kEMxC6Q.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,0mwtvRpkmECaT5_4aqCttQ", + "FriendlyName": "Green Ski Buddy Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:52.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2721, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "iCZlDOC3T0yMCT2lIaEPrA.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,", + "FriendlyName": "Skis Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-10T00:14:03.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2723, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nT7I-G6ixUSQ6Om25gsUGA.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,sNtmG8EODkOoiAdOi5Hetg", + "FriendlyName": "Ski's on Back (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:10.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2725, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5I7iBS_W3EG0-nf--ECAJg.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,HQ1M8kINaEu-0HJtz7f8Gg", + "FriendlyName": "Red Ski's on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:16.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2724, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LdbyV0Td6Uuj5MUaG1720Q.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,hPVbRnQq-EW3g0q17N6G5w", + "FriendlyName": "Ski's on Back (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:13.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2722, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BP7vd50iWE-mLN5Go0g5hQ.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,OScwXIzJHUKzb-NC6gUnxw", + "FriendlyName": "Ski's on Back (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:07.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2726, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lCMwxmYnFE2_25yjGunDzg.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,QVsGyxlefEeomtJO-dFAOw", + "FriendlyName": "Ski's on Back (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:19.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1201, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bb4i0uki9qbf8p96v4jfo349e.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,", + "FriendlyName": "Trucker Jacket (Sleeveless, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:49.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5hykjpkvkk03yf3qjii3sm1ii.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Sleeveless, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:53.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1203, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0x06arb0upzvorpzdr0ci4qjo.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Sleeveless, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:58.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1204, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3t7w6vu3h429ps4eurav4diui.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Sleeveless, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:01.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1205, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dn6y0cqf5ukxig24sgivzxa2i.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,4Eck_dNL1UKlIZWsA1YppQ,WeyKw_WS70Cx3NZdOaxGeA", + "FriendlyName": "Trucker Jacket (Sleeveless, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:04.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2763, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AmL-fduRx06s08e_Pzs1tg.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,wMo3Bq1aXkCMFSRJft81BQ", + "FriendlyName": "Trucker Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:13.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2272, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TulnHZMqkE2HIMXRNacnsQ.png", + "AvatarItemDesc": "a79e855a-c4fc-4ffa-be2f-9f4fef0ab1d5,", + "FriendlyName": "Outlaw Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a79e855a-c4fc-4ffa-be2f-9f4fef0ab1d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:19.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2273, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3zjJ5Sl4WkCfXJUE--1kTg.png", + "AvatarItemDesc": "cc30f7f7-6c12-4a96-acfd-11aec984c2e4,", + "FriendlyName": "Outlaw Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc30f7f7-6c12-4a96-acfd-11aec984c2e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:21.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2274, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4nxCvy9s-EWMBpdXowp34w.png", + "AvatarItemDesc": "86e0a4cf-b112-40d2-94b9-7ef35abe5a61,", + "FriendlyName": "Outlaw Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86e0a4cf-b112-40d2-94b9-7ef35abe5a61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:24.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 70, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ero4520aj78qgzdiz2knkg422.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,", + "FriendlyName": "Small Brim Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 489, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1z40s482g9xq9sirwfx26sfce.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,67fc9269-8dd8-4d6f-b594-c77e716f2482,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:10.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 490, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "azus8th8ecez3uybgfgvqq0o4.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:11.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 491, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "77ytai8z0lk4qz5x8il84ybvx.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,d6823e01-69f0-4f85-b94a-74894356a2cf,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:14.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2481, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "R5_LD0bgPkWiDdYxa1A_mA.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,08r7prRREUi-Hvp1Xze-wQ", + "FriendlyName": "Mini Droid Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:30Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "C2N_ATMr50e2W4aeVk04iQ.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,7qVilU94X0Kldpvku2mcng", + "FriendlyName": "Alo Yoga Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:27.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 310, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "16fi077y627ijhrcmqr0t298e.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,", + "FriendlyName": "Smuggler Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 311, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zkofpgk5o77fd65ks8x95cxv.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,e821aa74-2d47-4388-bcfb-1893b8a83a52,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 312, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3seplqtowg3fi113bsetxytpu.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 313, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5iogm3iyqg7sp05x3hgihphdy.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,90869e86-cbc5-4024-9d08-fcdc41c64eb5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 314, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c02xchxpap45jxgdksgsouq6h.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 334, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ed58vssma43zkdcb28nce7hhe.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,002b07bb-4256-44ce-8ca4-25510e70b6b5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1110, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ce642u8nppm5zamly1hq59fs.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,bzYdPb8GWUS-CgT4wrziIA,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:09.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 321, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2bncfuckcjmwaa7wano2sn1od.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,", + "FriendlyName": "Smuggler Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 322, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "648nagff1d7nh5vgo6tx3tpqr.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,e821aa74-2d47-4388-bcfb-1893b8a83a52,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 323, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7mr1xqf0fhgcv6v8p96yba2sx.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 324, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "97ynsh86vqwidiuno90esdvqs.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,90869e86-cbc5-4024-9d08-fcdc41c64eb5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 325, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7botyxuklwv4dbcddyrwdpcbc.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 333, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9vn2nq6cpu4mjyomplb7mld09.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,002b07bb-4256-44ce-8ca4-25510e70b6b5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "de8u98lwjl6796f9eeq8g7zfr.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,bzYdPb8GWUS-CgT4wrziIA,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:41.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 305, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aipia6o2kym88ur3brk8ufjud.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,", + "FriendlyName": "Smuggler Goggles (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 306, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3940162c53c4psfxm652thn6s.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,e821aa74-2d47-4388-bcfb-1893b8a83a52,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 307, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20x5x09hzrg6c4zcpys1iykz4.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 308, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "72lfd8j7a44mhv0viedavv2ht.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,90869e86-cbc5-4024-9d08-fcdc41c64eb5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 309, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7a494od9prmwajsibg29z7jaq.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 332, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ayx5bk4f6mojyvfiarqfavc3n.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,002b07bb-4256-44ce-8ca4-25510e70b6b5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bkz14lxmqlga9phjxk98i0j7.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,bzYdPb8GWUS-CgT4wrziIA,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:00.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1068, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d8r0drrwrxs7ot1bgzz145sm6.png", + "AvatarItemDesc": "rJ9zPPGdlk2i24sxLfkqng,", + "FriendlyName": "Lifeguard Uniform", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c739fac-9df1-4d96-a2db-8b312df92a9e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:51.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1063, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5bzqn0b3ouxa0kemvmre0a06k.png", + "AvatarItemDesc": "22pyI3Kz-UullhcebC6DLQ,", + "FriendlyName": "Snorkel", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23726adb-b372-4bf9-a596-171e6c2e832d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:25.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2707, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lhJKy4WMsEKemSq-lsUulg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,", + "FriendlyName": "Snowboard on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:51.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2710, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Gb6e46akCEKEXpH2qSvTlg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,PI3_sfTcNEW5TcAmHbsUOw", + "FriendlyName": "Snowboard on Back (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:57.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DsRdBkQ4c0WsfkMYTXQsXg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,K1-5YGg2rEiJFRWUKEfEMA", + "FriendlyName": "Snowboard on Back (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:59.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RlqSyYcF8UeGktdGy3dvmw.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,DSZKCNhXAEiAxMM1Gqfhsw", + "FriendlyName": "Snowboard on Back (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:53.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sf_MJ_CbKUCFSn6Iky3HxA.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,E6md4xTNYkS9geB8zkNRmA", + "FriendlyName": "Black Snowboard on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:55.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2712, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rfwXR61Q1k2Kyd4vTEsAyA.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,2dGY5MHfxEunzr7jtSFRVw", + "FriendlyName": "Snowboard on Back (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:42:01.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1660, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3bu1frdg68km45i2m32p4tuf5.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,", + "FriendlyName": "Snowy Owl Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:56.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1s8l5fozxazwvdbf7an9mewb0.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,W83EASFUS06FBVMfL80ruw,Ydd6tdb9dUGooM6WAczNjQ", + "FriendlyName": "Snowy Owl Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:38.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0mjop3uc4xo79ox3fcqpa6wjj.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,8M72ydR_uUSsli43PH2Eyw,wQxO4xmXCUGeC9ZHfDNGTw", + "FriendlyName": "Snowy Owl Cape (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:43.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2257, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U8zL4biQO0utrAL1LCl_Zg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,F0o3eeWbCUaUAGRUdI0z7A", + "FriendlyName": "Floral Cape (Midsummer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:51.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 5880, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6crHGcPzUUefWWZmQFnzOw.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,NnqAnqqLmUypWnNR2s-_sg", + "FriendlyName": "Cottagecore Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-10-10T17:15:03.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1543394911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5Rk5icw_oEqqERltIynyTA.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,qvK8xgbyuE-dW3--Ku-mvw", + "FriendlyName": "Floral Cape (Midsummer Obsidian)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1633983614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2jE0M2FxUUKRdwB-Z981Dg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,hRl4UTGoBUi5dmu0f4s76A", + "FriendlyName": "Floral Cape (Midsummer Verdant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1144281523, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4YKttOYfZUCJWTM47OGSsg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,07qFFGmwm0qmIikemncBkQ", + "FriendlyName": "Floral Cape (Midsummer Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3li2gob8aeawnh0bgtt1lfs2q.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,", + "FriendlyName": "Snowy Owl Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:58:36.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1710, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "48ikxhz5n438dh6imyqdnqqdx.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,I_3bHRSG_EmC6novYyy2dA,Cq8BOWcHb0GTDLvXl1iIcg", + "FriendlyName": "Snowy Owl Mask (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:46.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cabsqekth6kemqe8zn7s45v7k.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,HWAWBHI3zkGsh06he9CoPg,UAeSz-XeZUaZsk9FNlPX5w", + "FriendlyName": "Snowy Owl Mask (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:49.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 509, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2vqxpnzd5r6nuyacqrsrw5loh.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,", + "FriendlyName": "Soccer Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:35.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 778, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9bshjyb8x1qk9xkany8c3ngke.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,OnQeVxdstkS2zGxLBAuM3Q,i8bvbGAKREmAEbjKO-geOQ", + "FriendlyName": "Soccer Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:02.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2316, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "rRsmZUSOC0q9ITRtTz45Zw.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,-v3ddzUMF0aKxJR82SDevw", + "FriendlyName": "Soccer) Soccer Shirt Recpro Orange Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:26.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2317, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "mJIUWEd9WkWLnKurCXXFEA.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,mtR99SoHQ0aJLJfXPsKW1w", + "FriendlyName": "Soccer) Soccer Shirt Recpro Purple Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:30.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 513, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0we6tjqz2a38v2ojhop58m34f.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,", + "FriendlyName": "Soccer Goalie Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:39.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 782, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "azz8fjlgu2rjy5q07n278fb0b.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,whEHo6Rhr0CxsDBsBD32WA,av4PwLz6YkmpBRImQZgn4Q", + "FriendlyName": "Soccer Goalie Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:07.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2324, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ZftdQQ7kwkKCDZuTV-DWFw.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,3by5X5XZ4EW7eaERsaBJKg", + "FriendlyName": "Soccer) Soccer Wrist Recpro Orange Combination Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:48.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2325, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vupu6d_gGEaywmPePUvf1A.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,l2g5qO9dokiNepH92oPmug", + "FriendlyName": "Soccer Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:50.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1059, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bij80e36orzdtzehef7fmjeru.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,", + "FriendlyName": "Soda Clerk Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:04.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1326, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "crm789xheizu2cbu14b3wavpq.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,XFCpIdeDXUqeYCIgh4fGtg,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:47.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1327, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8etl00yx6u11l07jed4sl7xnc.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,oKxmH-1s2EyIDkZoOQSQQw,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:52.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1328, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5tdjwtkcesfgqls3gzy24y3hf.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,MxnAq_XOw0uTzQ8IIxqb_Q,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:56.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1069, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7x52aisqe0e84v27jn0madibj.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,", + "FriendlyName": "Soda Clerk Uniform (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:54.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1329, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2cvsw2qbbncs9zl5vsv7mxqkk.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,XFCpIdeDXUqeYCIgh4fGtg,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:07.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1330, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "447rhi816e4hfny2r4ktjynv5.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,oKxmH-1s2EyIDkZoOQSQQw,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:12.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1331, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "di4jb7lfnmlgt0x975ppcx8h0.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,MxnAq_XOw0uTzQ8IIxqb_Q,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:16.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2384, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0MfRmLg2yUGhr0zEhMC8Pw.png", + "AvatarItemDesc": "341dcb4a-818c-4d7c-9ae3-1dabbb79519a,", + "FriendlyName": "Solar System Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "341dcb4a-818c-4d7c-9ae3-1dabbb79519a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:57.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 315, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "d0pc71o2msynlo2gg6zblsdb3.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,", + "FriendlyName": "Space Marine Armor (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 316, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bn2zkvbfzcikfewpjgtd2bgl2.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,c8cc9589-4831-4e73-a865-ef8c139a2ba7,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 317, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9matrdtcoznln9t9wjrus86kx.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,a089bcc6-798d-4777-98e2-7afd290dc6b6,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 318, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2z7ww93ccq8rsr8l3bu6m7ntd.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,a36d252b-16ac-4a82-bece-de145147e7d9,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 319, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1a78ihg1rn6i5jh196fh166vv.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 320, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "90qc2umq7k4h8g9c9wgxcr18v.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,68273c9e-96d6-4ddb-aaab-d99229e864d5,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1101, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5rzpff3hf3m92tijpthgokni5.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,4ElTihrMA0ipcgeb9UG01Q,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:32.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2490, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "tmcuRuyNAkaX-uyikLs9ww.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,1pJN1AfxgUmUN_bTaH3rng", + "FriendlyName": "Science Iso Suit (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:12.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 326, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7izb4xifehd19st4qgaf70jqg.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,", + "FriendlyName": "Space Marine Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 327, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "anqd573o831mzengh9221nawt.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,c8cc9589-4831-4e73-a865-ef8c139a2ba7,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 328, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5zeohu1rh1ox5hoix80fbfl50.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,a36d252b-16ac-4a82-bece-de145147e7d9,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 329, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3qre9n3k1ubvucbztpra48in4.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,a089bcc6-798d-4777-98e2-7afd290dc6b6,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 330, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0k5d0e2dbwkixi838nrtd06ll.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 331, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0u3e41638g5e5afvuyqxror2x.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,68273c9e-96d6-4ddb-aaab-d99229e864d5,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1106, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0crijq94h4xfrkph110y254b5.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,4ElTihrMA0ipcgeb9UG01Q,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:53.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2326, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lmmMFWBKYUqm66trPr35Yw.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,LVnNy7-nFkyvi19nNcdFgQ", + "FriendlyName": "Science Gloves (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:52.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2713, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9nzqt8ExHU6STyCJinXlUA.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,uWe5nxA4qkaicmXm3tMw9g", + "FriendlyName": "Science Gloves (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:42:03.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 299, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2blhcp3hyn511tz9gxh7s982l.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,", + "FriendlyName": "Space Marine Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 300, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6wlpi8vp4n0f8yrjuywdx8dzr.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,c8cc9589-4831-4e73-a865-ef8c139a2ba7,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 301, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2xmq1ji9y74k37fj1ft9nmnvf.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,a36d252b-16ac-4a82-bece-de145147e7d9,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 302, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3v8rhp57dz9s41uhg5l4rytbv.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,a089bcc6-798d-4777-98e2-7afd290dc6b6,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 303, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8syjfjzce4brzrmudgrhv1mvo.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 304, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7dx5tt4aplt5y8oxs4j4ojbqc.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,68273c9e-96d6-4ddb-aaab-d99229e864d5,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1086, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ebj5kkpndee5u9d3b5fcifo7s.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,4ElTihrMA0ipcgeb9UG01Q,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:20:13.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2679, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KnhWwAmT6kOTA9oXrhmGMQ.png", + "AvatarItemDesc": "bc0a1c68-2eb5-4dde-a242-c4e19940699c,", + "FriendlyName": "Space Monkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc0a1c68-2eb5-4dde-a242-c4e19940699c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:29.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1507, + "AvatarItemType": 0, + "Tooltip": "Unlocked during Xbox launch", + "Rarity": 50, + "ThumbnailImage": "6t5em8ljarz9lnn8erh5ndc1b.png", + "AvatarItemDesc": "b37582a1-7904-4288-a47a-7a63a3cb28ae,", + "FriendlyName": "Space Soldier Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b37582a1-7904-4288-a47a-7a63a3cb28ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:06:05.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1508, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ey0vb0vehvdu8qhub0g2unvrw.png", + "AvatarItemDesc": "88ae6d12-6d39-4770-ac2a-f8c4602a10a5,", + "FriendlyName": "Space Soldier Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88ae6d12-6d39-4770-ac2a-f8c4602a10a5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:27:18.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1760, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ba7xldqaol3atwgv9n84yszra.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,", + "FriendlyName": "Gunslinger Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:19:11.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xe9d7zlk5k1ybhox7zl3n47i.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,jXRXO3CVE0-H_iaxbMhFhA,VZyHWGKNJEGSxDT2GO3ebA", + "FriendlyName": "Gunslinger Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:46.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1812, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8e5uyhuugy9bj5wqx2g8ucwlc.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,YSnoHFmJDUa_XCK6JMMh2g,VZyHWGKNJEGSxDT2GO3ebA", + "FriendlyName": "Gunslinger Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:43.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-ejrJeBy7kaQ8QEETufdXg.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,JilOXG1I_Ei65mw6TzFWgg", + "FriendlyName": "Gunslinger Hat (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:11.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1761, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "11cf76c54n13whwtqa3rjs7s1.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,", + "FriendlyName": "Gunslinger Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:19:21.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1817, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2dhxtutyxumnzuwg5rz5nbm1l.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,1-_ssucgeEalgr6EDL3S9g,Ru4HN3stVUutQ9rUh7ro1g", + "FriendlyName": "Gunslinger Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:59.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1816, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2mqqtndrscn1090v5nnmhizyz.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,cbn6vE0gDUOuklVhJv1vNA,Ru4HN3stVUutQ9rUh7ro1g", + "FriendlyName": "Gunslinger Vest (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:57.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2271, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "x5M_A1sHQE-nk0Uj7ghTzA.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,2eTBbHTkf0uxy9JqA507Hw", + "FriendlyName": "Gunslinger Vest (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:16.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1759, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "el2614e47hfl8u1fca9qvtyp2.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,", + "FriendlyName": "Gunslinger Poncho", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:18:35.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1815, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5yb187ueehdy6eu34my5nep7x.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,Gep43Ix44EWybJ80XAyDbg,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Gunslinger Poncho (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:54.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1814, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6m083hwotwj7o3sa08b291vyg.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,EmTrJKm8cUy8_BHAvP8Meg,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Gunslinger Poncho (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:52.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1403059708, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,VnRSTH4HqEqADLtryqSQNQ,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Spaghetti Cowboy Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.537778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2270, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BEUiX6fKYE-xt4747ftpcA.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,T670e7nOakqKl3L6LTlTtA", + "FriendlyName": "Gunslinger Poncho (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:14.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1572, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4shtbpjixq5n883npkahv8c5v.png", + "AvatarItemDesc": "e29d044e-79fb-4280-bca9-aa1e1257e968,", + "FriendlyName": "Martial Arts Gi (Speed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e29d044e-79fb-4280-bca9-aa1e1257e968", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:38.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2203, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "-yUmlgdAekKg0XMSaP8K7g.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,", + "FriendlyName": "Sports Scarf (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:05.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2204, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bnXH-F2F6EG-wTVhwSmaaA.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,ggpNxFtZMEaHlvWXsDl9Tw", + "FriendlyName": "Sports Scarf (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:09.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2812, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KiAiCi-3AkKa90OHtKGapg.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,Jd_ahlwRskehLggn1T-GdA", + "FriendlyName": "Sports Scarf (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:02.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2109, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XMdVSPwYlEm9q62jRsWEOA.png", + "AvatarItemDesc": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,", + "FriendlyName": "Spring Bloom Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:24.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2504, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nRaGYENtIUSgiJpKpJtfdw.png", + "AvatarItemDesc": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,MtbewwIGSEu-F9Ks_Ibb4g", + "FriendlyName": "Spring Bloom Antlers (Solstice Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:53.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2915, + "AvatarItemType": 0, + "Tooltip": "First stage of growth", + "Rarity": 50, + "ThumbnailImage": "QrS8jVr1HU2v-T0h7SMZgQ.png", + "AvatarItemDesc": "c2df65e0-2e85-4666-bea1-e8efa654b155,", + "FriendlyName": "Gaia Clip (Sprout)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2df65e0-2e85-4666-bea1-e8efa654b155", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:05.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2916, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "PXVbvtMXvUuHbJItHMqazg.png", + "AvatarItemDesc": "d1de5e76-0367-42f2-be88-6f28a140e5f6,", + "FriendlyName": "Gaia Clip (Bud)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1de5e76-0367-42f2-be88-6f28a140e5f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:10.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2917, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "QbAsltVVv0qUrxJJRoYbRw.png", + "AvatarItemDesc": "4fb6e083-ae10-4c71-898a-6a55138a68a3,", + "FriendlyName": "Gaia Clip (Flower)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fb6e083-ae10-4c71-898a-6a55138a68a3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:12.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4t9vou7x5a8b92tyr0k4wzzza.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,", + "FriendlyName": "Square Glasses (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 646, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2p1yfnex9n9efclfrdg5v75cm.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Square Glasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:30.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 647, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9rp7mz69wmhupgeve23u4kbt6.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,c5884778-a87f-4612-9717-86fbb65d440f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Square Glasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:31.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 750, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ccjied0pmdne9fu4abyc5giwn.png", + "AvatarItemDesc": "kghEf_iMKUSxPO-h5iRhSQ,", + "FriendlyName": "Classic Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7f440892-8cf8-4429-b13c-efa1e6246149", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:21.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9q6v1yay56q7ckhj5we9h1ksn.png", + "AvatarItemDesc": "899f1d17-eee1-4b7d-a112-fbbba597e738,", + "FriendlyName": "Squid Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899f1d17-eee1-4b7d-a112-fbbba597e738", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-07-30T22:14:05.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2198, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "x-RovcwIAUOk4uruqtQEUg.png", + "AvatarItemDesc": "899f1d17-eee1-4b7d-a112-fbbba597e738,t8kKXvBU-kSM_MHaqZYpkQ", + "FriendlyName": "Squid Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899f1d17-eee1-4b7d-a112-fbbba597e738", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:51.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1655, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c5xy43pxfjwaaum08f6rtf6um.png", + "AvatarItemDesc": "34f45c5b-fea7-40b4-add7-bc7a37000ab5,", + "FriendlyName": "Star Captain Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34f45c5b-fea7-40b4-add7-bc7a37000ab5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:02.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2168, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PbHPcqrg4EqJp9-zB9HpRQ.png", + "AvatarItemDesc": "3ce86805-987e-4958-bcc9-57d89c4a5b1a,", + "FriendlyName": "Star Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ce86805-987e-4958-bcc9-57d89c4a5b1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:49.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2779, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Qs_cSEoU-Uy3_fGkJeIHKg.png", + "AvatarItemDesc": "3ce86805-987e-4958-bcc9-57d89c4a5b1a,PGZihr096kC4XZvHfmVMzA", + "FriendlyName": "Star Glasses (Popstar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ce86805-987e-4958-bcc9-57d89c4a5b1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:42.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2780, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bsf3ipTBlUC2I-jUSFPw9Q.png", + "AvatarItemDesc": "b4c93141-f387-4e42-9b04-12b7a97c9416,", + "FriendlyName": "Star Glasses (Popstar Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4c93141-f387-4e42-9b04-12b7a97c9416", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:44.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 784, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bts86kl55yir0z5wcudycjvob.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,", + "FriendlyName": "Steampunk Topper", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:45.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1079, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eb13jqd3eks4b9bwsooqjxlp4.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,AAbGHOOitUu9enweOXbrkw,fXnd_COSzUeznwe7P4ZYRg", + "FriendlyName": "Steampunk Topper (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:53.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1827, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3gjby0eo4e1tmv2wuecnz6rnf.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,Jy2TN1KBTEeoVehy_x32Ng,fXnd_COSzUeznwe7P4ZYRg", + "FriendlyName": "Steampunk Topper (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:17:50.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 805, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dmp4y6ycyxupuom25j3hzjnwn.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,", + "FriendlyName": "Steampunk Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:54.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1081, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ozwlaf5193fr6v3ysxikeo2e.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,fD5vtKxBWECH54Jj4FbnUg,C8gCmBZ8RUG83vvDw_UYew", + "FriendlyName": "Steampunk Vest (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:57.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1835, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8b66w4zmh5zq171rxofxcml3h.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,gy-xeYKKREaBlx9fUmuW2w,C8gCmBZ8RUG83vvDw_UYew", + "FriendlyName": "Steampunk Vest (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:35.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 803, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b1x1g3wbr7kr9cld4mtw2wn1t.png", + "AvatarItemDesc": "sOREgAGs-06CeGGsG-WIkw,", + "FriendlyName": "Steampunk Pauldron", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8044e4b0-ac01-4efb-8278-61ac1be58893", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:48.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1837, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9a9ta5xg1e227zgwpmygwy1cz.png", + "AvatarItemDesc": "sOREgAGs-06CeGGsG-WIkw,rSzG7s1ocUmx9_VrJLOcHQ,ISycihytUUyoypVb-kXN_Q", + "FriendlyName": "Steampunk Pauldron (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8044e4b0-ac01-4efb-8278-61ac1be58893", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:43.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 806, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7p4x9oq3c0f415llgnjzustxv.png", + "AvatarItemDesc": "qxovV6Vym0ufL2cTCyKjOQ,", + "FriendlyName": "Steampunk Cuff", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "572f1aab-72a5-4b9b-9f2f-67130b22a339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:55.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1833, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "en36simwv139x0vu9nj5hit04.png", + "AvatarItemDesc": "qxovV6Vym0ufL2cTCyKjOQ,64Hvmzrb3Uu9X_dCA3M3Jw,rPpYm5fIzky4vqVB26guAw", + "FriendlyName": "Steampunk Cuff (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "572f1aab-72a5-4b9b-9f2f-67130b22a339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:20.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ajxapi81nzit5fp49tbhidav5.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,", + "FriendlyName": "Steampunk Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:39.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1080, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4e35e3ndy0mrxjrfnrs3kay0.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,vuSwk04zykCe3ANKN838OA,Uhw5NBQy-EOIiCWiZFiL7g", + "FriendlyName": "Steampunk Hat (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:55.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1828, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3k0gmjgtjhawlaqbc76p3n7c4.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,ZUMIXcyOpEGiHgslbHQbwA,Uhw5NBQy-EOIiCWiZFiL7g", + "FriendlyName": "Steampunk Hat (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:17:53.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9a1u3xocxzaogyzpudo3nopsn.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,", + "FriendlyName": "Steampunk Corset", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:48.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1082, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bcv3r4hoktif6vyvq5xi14rez.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,A6VnGtpY9Ey0atf3zaLo3g,GJRG8sCM7kSGfUd9gLwpgQ", + "FriendlyName": "Steampunk Corset (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:58.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1834, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d34cbgen23yzb0kmri6koo0j4.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,rsUfUnTwbU6zrjrHWps1yA,GJRG8sCM7kSGfUd9gLwpgQ", + "FriendlyName": "Steampunk Corset (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:23.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 765, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0dl5ao69a8mzsn6jlyoei1qmd.png", + "AvatarItemDesc": "CdeM6RJmPUumXtdmdM3RXA,", + "FriendlyName": "Steampunk Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e98cd709-6612-4b3d-a65e-d76674cdd15c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:41.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1826, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bu4jy4ulmhtqc9baxhaqtabrf.png", + "AvatarItemDesc": "CdeM6RJmPUumXtdmdM3RXA,mZ2ASMrs80axStLp9g1enA,dGzmSibucEe7i1HD7lPcNQ", + "FriendlyName": "Steampunk Goggles (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e98cd709-6612-4b3d-a65e-d76674cdd15c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:49.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 804, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bq9tt6z6r54gl79e7oun3l7dw.png", + "AvatarItemDesc": "iBQ-v6xaqkipUlfzkSHEeQ,", + "FriendlyName": "Steampunk Satchel", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf3e1488-5aac-48aa-a952-57f39121c479", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:52.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1836, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f1t23z0rwzoefrj56j19gnhgr.png", + "AvatarItemDesc": "iBQ-v6xaqkipUlfzkSHEeQ,bH3dgwnwt0KEw8oCDVIcEQ,erPtGSgq_kmqfeSlZWw4VA", + "FriendlyName": "Steampunk Satchel (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf3e1488-5aac-48aa-a952-57f39121c479", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:40.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2727, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tP86FtoqYkyI7ZU9u21Ryw.png", + "AvatarItemDesc": "42f1a780-1fe1-402e-bf11-176b768e33b9,", + "FriendlyName": "Strawberry Side Purse", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42f1a780-1fe1-402e-bf11-176b768e33b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:22.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2728, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mAe4p7EG9kGOltV99JqDCw.png", + "AvatarItemDesc": "42f1a780-1fe1-402e-bf11-176b768e33b9,jzGTu3tSB0WM430AiMGAAw", + "FriendlyName": "Strawberry Side Purse (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42f1a780-1fe1-402e-bf11-176b768e33b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:25.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1571, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "89it9c90iy4bi8otctbxs4kl0.png", + "AvatarItemDesc": "25769d1f-4ecb-4d92-b692-322b38c013b5,", + "FriendlyName": "Martial Arts Gi (Strength)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25769d1f-4ecb-4d92-b692-322b38c013b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:33.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1279, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "3faagp8a1v4tthau00hha26mx.png", + "AvatarItemDesc": "b795c631-f075-4d32-ac87-c732b36cb932,", + "FriendlyName": "Light-Up Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b795c631-f075-4d32-ac87-c732b36cb932", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:21.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 23, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8vw62vvj318mjoz88zhu69qod.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,", + "FriendlyName": "Sun Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2210, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "SwnYTGNYiUG4Ua78QnfFEA.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,YS-fXtVLTkG-wl40HPBLOg", + "FriendlyName": "Sun Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:27.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2211, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FoMbe_63g0WNL9Sl7y_rJw.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,dzH_40F-MUCy_7gATqL4PA", + "FriendlyName": "Sun Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:29.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1483, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ijpwku25zvpdgkp4y12e3yyv.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,", + "FriendlyName": "Sunflower Sundress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:41.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1484, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9iwljaztbqbk7v3c1w2zryiah.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,OJYpuFw810Sg4bTw0XQf-g,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Lodge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:47.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1485, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d1rcizuvnlnebb16r91lfnd5i.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,t4gMAKp4L0WBaTGf_rtqFw,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Yacht)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:53.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1486, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4j8oqc693u6bw3exf1fz40gqg.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rGMFdPKmK02_OvIIFYlAxA,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Veranda)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:57.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1487, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "17odpv44phqhpwrrvimszr6y6.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,wxvSDMommkenIl5hTY2fdQ,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Seafoam)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:04.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1488, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8vqcr3rvn7ctiddmrvtkcq1oc.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,-sqfCQpzpUyjEShEXH9JYQ,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Sunrise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:09.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1489, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7lu5zwmfs5awwdn6oo40jy7z0.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,as9fjqDWiUi1w7yYavKZXA,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Sunset)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:13.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1490, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "96vw242yfg6iw9nr4lt97iqqg.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,sz-J7RjesE-ke3bKwumzZQ,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Meadow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:17.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1498, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5ov29z2kh47zws4vv1r89tx28.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,QlZt51mdn0WuiqKoJaIRyA,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Vineyard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:10.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1491, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4iobr7kq8wwwa2wclut3agqo3.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,6PyJEQxyd0K9TPRnvt_86Q,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Estate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:30.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2262, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "EPFpkkBDokyWw7QwCJe9Hw.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rsh2uKli90OoEnJZRFc2Ww", + "FriendlyName": "Boho Sundress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:09.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2826, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3e4iynr3546eem9i3x1apmhk4.png", + "AvatarItemDesc": "e0c4c93d-ea3e-420f-9e8f-7890b5bd4b6f,", + "FriendlyName": "Sunflower Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e0c4c93d-ea3e-420f-9e8f-7890b5bd4b6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:54.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 104, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f0xdfrv8p6p8tly3s9tdqx0kh.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,", + "FriendlyName": "Sunglasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1877, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ebmp0xvyd2y9i4tqvuzl5ns1v.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,dZM1bs6_MU2F6Vz1mH0QKg,KfM7veOVlE-dNZmsbDNZBA", + "FriendlyName": "Sunglasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:47.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1876, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "59lin822pwng10iwcthz1bb9d.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,0Ytm7G4_lkmZWPA_YfaIeQ,KfM7veOVlE-dNZmsbDNZBA", + "FriendlyName": "Sunglasses (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:45.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1468, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ainuo5affylw7gah48hs640la.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,", + "FriendlyName": "Hero Gal Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:11.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2012, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "diaifnoquzrh4x2mmkz1mf8zh.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,zw-evwOmg0q14P040EYYiA,a3I7EyRNVUilLfE6hriYtw", + "FriendlyName": "Hero Gal Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:24.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2100, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wBjDMVQgx0uxNDVdCLu4qQ.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,bhYZTPD1nEaEercLIZRucw", + "FriendlyName": "Hero Gal Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:08.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2101, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "P8mS2NTJS0e0H0VqDKvZUA.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,_C4RxcDBQE-mHh2oyL9vGw", + "FriendlyName": "Hero Gal Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:10.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1470, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cls959ibdrc0xabl1rbe41fux.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,", + "FriendlyName": "Hero Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:18.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2011, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aiplwveih9o7f5q4ok7hr4a2t.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,-K940hWJXkee1HDrlurepg,ItJCR1utoU6i-syjJpxQqw", + "FriendlyName": "Hero Crown (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:20.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ejV0XPkWEuQwpYQ8UBwQg.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,SdSjmCkq-ECoboR2PwxJfQ", + "FriendlyName": "Hero Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:15.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2104, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vXnwcxkJbE-7MComJenMwQ.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,iGhiA5wU8kG3NH6P9NZvGg", + "FriendlyName": "Hero Crown (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:16.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1467, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6lr4h9o5e2b37vr6d9nn7tkw9.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,", + "FriendlyName": "Hero Guy Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:06.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2009, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "56tgu6gfzoy6bro091y5liea7.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,JY4zXsTbX0W1sU6g1o4kuw,Y-XQzHOjIkC_-it1j9m9jA", + "FriendlyName": "Hero Guy Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:11.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "iVc5S7VHGE6_DP6V_NdRXg.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,W_AVltwf7kSzmftSsGdijw", + "FriendlyName": "Hero Guy Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:20.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2108, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bGfb7o8B7U2W6pcvZdElGw.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,trFPTiehdUm2Q3bdvBQr7Q", + "FriendlyName": "Hero Guy Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:22.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1469, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d6wng88epebl3fbw0v5p6llyl.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,", + "FriendlyName": "Hero Bracer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:15.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2010, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "duphs7n0pfrpd3ttpnqv1dzot.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,oCeStQfVkE2ZwCcQjtV1bg,r94RX2106E2TRK2wQ-u8-w", + "FriendlyName": "Hero Bracer (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:17.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2112, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dfP4cWSPL0qZYIPW3aBHmA.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,nNJ8Tkg57k6pTS_0HSLkUQ", + "FriendlyName": "Hero Guy Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:28.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2113, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "xukCmMKpjUaYrqcBdb18OA.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,VmHQJIt7HkO4INJEXCBbNA", + "FriendlyName": "Hero Bracer (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:30.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ffddbd79-0158-4f93-b8ec-dd5573c035a7,", + "FriendlyName": "Surfer Puka Shell Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffddbd79-0158-4f93-b8ec-dd5573c035a7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1672191014 + }, + { + "AvatarItemDesc": "5b2d0a6c-238d-43a7-a20b-bddcf14315f1,", + "FriendlyName": "Surfer Skimboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b2d0a6c-238d-43a7-a20b-bddcf14315f1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1582373149 + }, + { + "AvatarItemId": 730, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cg6s53psdqilkxbzgkx72gri3.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,", + "FriendlyName": "Poindexter Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-11T22:30:21.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1647, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8c3gd19ccfwts3cqzv6pojvyj.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,lq59KZwElEStrviDEQLLcA,olyS074RHU-ojE0GzBBYJw", + "FriendlyName": "Poindexter Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:24.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2214, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gYtC-UGMRkC9gKrg3689Fw.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,PSpysyn5gEKXCEi7f6gDoQ", + "FriendlyName": "Suspender Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:35.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2888, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IMgfGlThV0q8ilEF8MG5ew.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,", + "FriendlyName": "Blue Sweater Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:10.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2890, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6Ry5ZkLGke8F2jv7aYoxw.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,LVuUiGBtn0mp4Wfs-2F5EQ", + "FriendlyName": "Sweater Scarf (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:20.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XrXf6JrQaU6tG5MV94TFSQ.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,R8VYB2EGPkmQX4dbmGLuRA", + "FriendlyName": "Sweater Scarf (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:27.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2889, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HGZ5flmXY0Ow5pQH-q3L-A.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,wCCq14_wc06hkmjg6TTFxw", + "FriendlyName": "Sweater Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:17.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2891, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MsgTxG94f0-timzwekDXSg.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,0JVWqxXYoUqgI-5ORIr0bA", + "FriendlyName": "Purple Sweater Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:24.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2756, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SWIYABA3u0KSKjMg4tbN1w.png", + "AvatarItemDesc": "97c8e2f3-e786-42e5-8852-4abec5dc645d,", + "FriendlyName": "Sweet Tooth Hat (Milk Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97c8e2f3-e786-42e5-8852-4abec5dc645d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:39.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2757, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WNWMQVjKaEC9CXqo_esiGA.png", + "AvatarItemDesc": "97c8e2f3-e786-42e5-8852-4abec5dc645d,_8OmgC7c80CsvQN41U6XyA", + "FriendlyName": "Sweet Tooth Hat (White Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97c8e2f3-e786-42e5-8852-4abec5dc645d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:41.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2758, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tGRQjD2mEKhf-jbbGpp7w.png", + "AvatarItemDesc": "39854685-4793-439b-9c64-c98825456a03,", + "FriendlyName": "Sweet Tooth Shirt (Milk Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39854685-4793-439b-9c64-c98825456a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:44.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2759, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NRd9D3di1kKciSpywTnJwA.png", + "AvatarItemDesc": "39854685-4793-439b-9c64-c98825456a03,qVeFYveZQEmobBTEMg1RTw", + "FriendlyName": "Sweet Tooth Shirt (White Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39854685-4793-439b-9c64-c98825456a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2583, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "wy-_Y3jSdUC9bKLa63iIpA.png", + "AvatarItemDesc": "a928c6dc-442f-4cbb-b705-cc5a3969b34a,", + "FriendlyName": "Teela Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a928c6dc-442f-4cbb-b705-cc5a3969b34a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:47.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2584, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yCbSGCAoBUyP8osA23LjDw.png", + "AvatarItemDesc": "f159e878-7563-46db-b59d-c838612a0a78,", + "FriendlyName": "Teela Tiara", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f159e878-7563-46db-b59d-c838612a0a78", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:49.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2585, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HwOW8GYsaESO0xHdf6rxQQ.png", + "AvatarItemDesc": "38e7e182-5166-42d9-9ed3-b525b98579c1,", + "FriendlyName": "Teela Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38e7e182-5166-42d9-9ed3-b525b98579c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:51.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 366, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c6y5y8wwcc2dcp81jcdqvac5r.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,", + "FriendlyName": "Tennis Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:06.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 441, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "22s0jdhj08deg2hm1agcg0ap2.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,304e5cf1-5c94-481f-8e49-e6a943e6b300,90e0da8b-1941-4ca8-b7e5-b4e31921873c", + "FriendlyName": "Tennis Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:11.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1370, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1eu2yq3p1x77hmixyzmubdux6.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,NpMkoIK62k63QoLVEXId6g,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:15.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1371, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "duxjnjbu2n1zbarvejwuhhzgn.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,PLRhiOhhb0KhatFH7f3pwA,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:21.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1372, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8zfsf3e3asjbwc8dz1kq9acae.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,RwgnKTfMEkWrAp_OMGy__Q,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:26.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4g834pzncxe2djcfe8ri5ow6t.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,", + "FriendlyName": "Thrilling Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:46.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1529, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6iumwjj0hu0bnlky89lc0xdjd.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,7-lAnMa7M0u0n0nmeIINgw,6GfSjqIb4ECCvIPC8vO8_A", + "FriendlyName": "Thrilling Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:25.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2318, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "inGiSDO8JESERdMjtdk9-g.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,8HR0DwDl2ECtHf5aEkqSHQ", + "FriendlyName": "Thriller Suit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:33.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 821, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5zwxtidy5x8bl647kq837917l.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,", + "FriendlyName": "Thrilling Cuff (Red) - Available again until 10/15", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:54.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1530, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5tmz1m7roox1cl2u7cf3bgrnc.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,Q1knTl-n-EeAUwotFspNew,FHB5XHwNWka7aUt5km3LNg", + "FriendlyName": "Thrilling Cuff (Blue) - Available again until 10/15", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:29.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2327, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "FqT9BAyTM0qDBFE5vkveUA.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,B1IotYmwbkiUAfdZiqzq8w", + "FriendlyName": "Thriller Wrists (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:54.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2293, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "O2g6r5QKqk2REyXvLqLyeQ.png", + "AvatarItemDesc": "7b568bd0-ce59-456f-af69-537364a52f69,", + "FriendlyName": "Lightning Hero Suit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b568bd0-ce59-456f-af69-537364a52f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:40.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2391, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5MjNgKFX1EuMIDbmbkqxZg.png", + "AvatarItemDesc": "7b568bd0-ce59-456f-af69-537364a52f69,_fabGqrxrkW2mfx0bejHsA", + "FriendlyName": "Lightning Hero Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b568bd0-ce59-456f-af69-537364a52f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:42.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2294, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ww6XZOkMQEGPfHNxwJ9pbg.png", + "AvatarItemDesc": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,", + "FriendlyName": "Lightning Hero Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:43.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2392, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kk6TuKH2gEeN-PCQlSGzGA.png", + "AvatarItemDesc": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,46WaeyRZnUCuLkEpxECRMg", + "FriendlyName": "Lightning Hero Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:45.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2295, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2RiLsCbQ0Uubmw-2hVgnPg.png", + "AvatarItemDesc": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,", + "FriendlyName": "Lightning Hero Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:47.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2393, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "JLbQE-oojUqVUNpFNs_Cxw.png", + "AvatarItemDesc": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,8NY4MevLp0C2lP178fIZ9w", + "FriendlyName": "Lightning Hero Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:48.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 133, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "66fwuamhzcs860kylwr1qfv8m.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,", + "FriendlyName": "Tied Scarf (Striped, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 462, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bcknoanm16wf9xuuxd8n236ez.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:27.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 463, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6qg6j1yialrvz8slxa9jeyu87.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:28.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 464, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ehbtcde8j9yfepf412zdnzsqs.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:29.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 465, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "15m86aopz28g8iezgelpzwej3.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:30.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 466, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9dfutygcqtdm7xo7lv2j8y5fy.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:31.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 467, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "a6a5lj41oooaly6njbbnnox5o.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:32.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1267, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f561z4j5s60ih6h3mp3me6ath.png", + "AvatarItemDesc": "05ec4e87-088f-4281-a118-2e63c2585200,", + "FriendlyName": "Business Tie (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ec4e87-088f-4281-a118-2e63c2585200", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:43.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cm7rzgd535abr6dvbm4wfb061.png", + "AvatarItemDesc": "05ec4e87-088f-4281-a118-2e63c2585200,Wm30oV5BtEuFQZFk3dPK_w,tfiSfpRez0y_R4QjAhO0bQ", + "FriendlyName": "Business Tie (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ec4e87-088f-4281-a118-2e63c2585200", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:46.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1265, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "76fk7csv3rzv0frnesf1d575f.png", + "AvatarItemDesc": "0d4e7ebe-efd9-4e68-9900-5645916e7596,", + "FriendlyName": "Business Tie (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d4e7ebe-efd9-4e68-9900-5645916e7596", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:35.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2781, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yn-2YY0OvU-VMl9fBtdbuA.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,", + "FriendlyName": "Tiny Backpack (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:45.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2786, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TQGXsPW7Hki2VKZiU0b-HA.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,AtF5be-huUeADedbRurRcQ", + "FriendlyName": "Tiny Backpack (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:58.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VzzHEVAKM0ChjhmxaHnhow.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,T9XdU5xAFEqX3QV__SjXYA", + "FriendlyName": "Tiny Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:55.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2783, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e-QC6uq5zECcdfkIRxtpiw.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,cxWuO4qBtUOKYmCXI0D5Jg", + "FriendlyName": "Green Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:50.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2782, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9xjIqLGxDkOZsXIexIon3w.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,FIZdMEUgykOHb7fVlOfa5g", + "FriendlyName": "Black Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:48.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2787, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lfkspyvSk0CIkZx0gXhMog.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,5GEhQr7Iy0-kB0yCA3uJ6g", + "FriendlyName": "Tiny Backpack (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:14:00.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2784, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "y91-mA5ypkiHUOTxxjG6iQ.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,wrobkXV-10CD1JJtgyy6Fw", + "FriendlyName": "White Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:53.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,8e5280a8-7771-4278-b985-c325f2490f9d", + "FriendlyName": "Tiny Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1327578016 + }, + { + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,545ed5e0-beab-46d8-9d14-7de0420ff90d", + "FriendlyName": "Tiny Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1500824834 + }, + { + "AvatarItemId": 1504, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "49614bvc1m6rzznnvix44dnbb.png", + "AvatarItemDesc": "00f0b914-aa22-4d14-9af1-996e1af61f4d,", + "FriendlyName": "Tiny Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "00f0b914-aa22-4d14-9af1-996e1af61f4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:46.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2068, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "higzQQvRGEa6dDK9f0TTvg.png", + "AvatarItemDesc": "591af332-a01c-454d-89dd-5ed6ae3ed09c,", + "FriendlyName": "Lucky Clover Top Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "591af332-a01c-454d-89dd-5ed6ae3ed09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:25.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1897, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "56r92qv64w04tvm8920bhyjlv.png", + "AvatarItemDesc": "2f77696c-618c-4fb8-9220-33839cd3bfcd,", + "FriendlyName": "Tiny Top Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f77696c-618c-4fb8-9220-33839cd3bfcd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:30.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2730, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "K_CvxmWWCkugsiOwB8nZAQ.png", + "AvatarItemDesc": "0bd750d9-82ff-4d77-9f2e-7e2651316f51,", + "FriendlyName": "bwahhh", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bd750d9-82ff-4d77-9f2e-7e2651316f51", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:30.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2729, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "UNu6rZ-7CU-S5MeZzybZKA.png", + "AvatarItemDesc": "74902dee-08a5-4a8b-a113-80f450a0de0a,", + "FriendlyName": "aaaaaa", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74902dee-08a5-4a8b-a113-80f450a0de0a", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:27.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2731, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EwmmD_7_NUeiFXLFKImuQw.png", + "AvatarItemDesc": "b0a2e988-9942-4c52-a714-f1bc12fa6290,", + "FriendlyName": "Rec Rocks Shades (Tokyo Machine)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0a2e988-9942-4c52-a714-f1bc12fa6290", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:33.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 72, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7beqslha3807spvktpk1rat13.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,", + "FriendlyName": "Top Hat (Black, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 645, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "d8wvm262ivsmterpwyjcf0d3i.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,bd4a84e2-b67a-4269-a26a-17fb23ddb09e,ccf1ccc1-e229-4157-bb74-f2cdef01e547", + "FriendlyName": "Top Hat (Black, SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:28.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 915, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e8kcw6vyqqzb5temhwr493mtf.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,DzgN67HoikexE2rTlZLdYQ,vm1ESK9ls0SaxSwHplNALQ", + "FriendlyName": "Top Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:49.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1732, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3nihpsb8d8hrh8ovb5pzd5f1a.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,xeK_acj2hE-sYGhsMRwjhw,HwhHShudGkGjXagJxCN2uA", + "FriendlyName": "Top Hat (Patchwork)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:16:15.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1801, + "AvatarItemType": 0, + "Tooltip": "For earning $10,000", + "Rarity": 50, + "ThumbnailImage": "5948oo28re9hcl2wswbr309wb.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,Em__GeYWaU2pAEqWYCBKpA,DG83ruIdI0Wu-GSRg4icYQ", + "FriendlyName": "Top Hat (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:46.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2482, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zcove5uhGUSvwxXtkvlWow.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,n8t0gIYFbUaPMhyQeWkPSQ", + "FriendlyName": "Top Hat (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:32.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1374, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zss1xn4q3bwumq65zgy4xq5g.png", + "AvatarItemDesc": "7dc6c916-b887-4929-93b5-dcea3022e6a9,", + "FriendlyName": "Top Hat (Wonderland Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7dc6c916-b887-4929-93b5-dcea3022e6a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:11:18.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1356, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h9wa7v6ma9wzhwdgvumqlyp3.png", + "AvatarItemDesc": "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03,", + "FriendlyName": "Top Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:48.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 972, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ki0gazw736edmje4kpkv8bvb.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,", + "FriendlyName": "Traveler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:40.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1430, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "31sglupqv6cqh6rzb8embhfwz.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,Mt2a9ygWPkS-DyXj1q-Kvw,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:41.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1431, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5eujgf1g27oi4kl20v8si25ry.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,LEUQnDz-k0m-hY-AX3Z9Sg,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:42.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1432, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1dcjmvlujpg9zj2zccxxccpcw.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,cSFt9MWLj06CDzIzATqrTg,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:44.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1433, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9ozq264dxzs2juj87d64edd1i.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,B6qNcZQ770eJel13RskuIg,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:46.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1434, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6qw0otr02xea0hnbor3sp5kas.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,p3g8YAOhTUKAiE-Wh-X-aA,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:47.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1435, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5seeahrp9r3hyie3y148dclsb.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,SqHAYcqBXU2PtF2urWGruQ,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:49.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1731, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "can59da4g2r699415y5w5vr03.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,Pu_1I6Zq-0G1HBMHa5MrKg,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Tourist Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:57.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1730, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6v72ng5qy6zmim5nx1i6k7hy2.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,7Rw_gt9IUEOYsvVcOTbVBw,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Tourist Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:55.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1729, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e25fr88d2412i6acn3s2e2khj.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,aiBfwCNwpUyJ0rYKCMa7qw,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Traveler Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:51.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 974, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eati57vuo2tgumb24q5t0je5y.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,", + "FriendlyName": "Traveler Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:53.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1424, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0x7hflju5oo8crqrrj3r0b5so.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,otQaNOsODUSTfHlCX0IVOA,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:46.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1425, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bp1h0205pg13g8p8aj2j9vhdi.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,NKf5-r2S_0iP0SaO9bjvaw,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:48.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1426, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chd2q6f8set0nr8jh2vr2m7qd.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,adJVA26PyUSVvmZgxkCAmQ,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:50.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1427, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "56zylqig2v1wahn347cxlipyg.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,4ijeE3LqUUuSplKrMto6Kg,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:52.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1428, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9896kxku8xyotg3kpo30sep01.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,d27e5m1BLUWI42Pxnj6F4A,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:54.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1429, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5a104x4ogdqr0eqzaqirydl55.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,RZFWcTzxPkOfUuTIQ1mzkg,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:56.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1725, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3gp6gte16x8khurqqu7nzhfhd.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,XT_txjZfIUOT400UPHLaRA,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Tourist Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:05.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1724, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a744qz6lz4fslmbrp602yan1l.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,iEvS6I9LjkSaGAmjcxz8uQ,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Tourist Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:02.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1723, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "87fumlw57etytmbtflbzgbrxm.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,gwdtq3NiEEmMKYtn1VPoiw,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Traveler Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:57.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202549920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_a0Yxn41bUmWYogStG-J6w.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,6taO0KRM_kuKoaTnOIsHRg", + "FriendlyName": "Raindrop Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.543783", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,7WLoulmRgEKX3Eshtn_cxw", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1718528065 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,BzzPuw8i0km4VQLd77mXEg", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1462034470 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,er8BswhSkUqh5TVV5fDAng", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1696359360 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,zUZHddmmhE2rXfCVuz_GGg", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1381816991 + }, + { + "AvatarItemId": 976, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "35n8yj17afa5u5gc5w40oz7de.png", + "AvatarItemDesc": "cbN5e-t_jEKa4hzir5JAxQ,", + "FriendlyName": "Traveler Shades", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b79b371-7feb-428c-9ae2-1ce2af9240c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T19:10:59.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2154, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BO1szcANSECxRSF0upetrA.png", + "AvatarItemDesc": "59bbb9d4-4fdb-4400-90d5-244643dca4ce,", + "FriendlyName": "Toy Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59bbb9d4-4fdb-4400-90d5-244643dca4ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:23.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2485, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "9aC9wW3rVkKmZlMNJSwZkg.png", + "AvatarItemDesc": "d834be2b-b107-49aa-b0ec-b54bdb4c61aa,", + "FriendlyName": "Science Iso Helmet (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d834be2b-b107-49aa-b0ec-b54bdb4c61aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:40.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2004, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70dggr2u22pi1bpl844m6ozue.png", + "AvatarItemDesc": "2b7a32da-eda1-45e1-b28a-f9f882cda7fa,", + "FriendlyName": "Toy Jetpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b7a32da-eda1-45e1-b28a-f9f882cda7fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-12T22:10:14.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 251, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5gm9mbr01cwr3bmzl4nsv5ko7.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,", + "FriendlyName": "Tracksuit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 501, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "essmk8w84jurmnzu791bfueh2.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,026ca50e-3a81-43cf-87f5-950687b7bbdd,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:25.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 502, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bvsa18aen05ajtkzw2h9y672b.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,6584dada-499e-4d7c-958d-466955b20640,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:26.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1648, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8v9x6a68s45783jrnnqykohcu.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,uUOO-k5KS0mU7B5BLaOpnA,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:41.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1649, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "222uif2yybv04a5a1ialb8rze.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,Ijf-j7EgEkGYOKo9SQNLRw,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:44.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2182, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SPNmVkKb1UWv0spykc088w.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,J7l1YwWjqkutybwykwih7w", + "FriendlyName": "Tracksuit (Squid Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:41.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 763, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1265j9ozr0rlgadvfmpq5yyzv.png", + "AvatarItemDesc": "-SQLtL4rdEWTJdw2IroQCQ,", + "FriendlyName": "Treasure Hunter Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b40b24f9-2bbe-4574-9325-dc3622ba1009", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-23T00:08:02.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1704, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4nzqsmjss5f6hoabn1ihimj3.png", + "AvatarItemDesc": "-SQLtL4rdEWTJdw2IroQCQ,CoSVCyrI-0qg-EkqB2R0hg,JO9lJOQSREWbPyDBZH_cyg", + "FriendlyName": "Treasure Hunter Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b40b24f9-2bbe-4574-9325-dc3622ba1009", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:20.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 779, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5jvdbqiv7unk1h9fed6w0xzm1.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,", + "FriendlyName": "Treasure Hunter Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:04.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1698, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4mi4ucsloz64q4qidq36cfyf8.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,H0sqS2_VPEieVrHSyMNo7g,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:47.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "05ockzuu7hohv73owaltasco3.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,TjeZwaUp10uhM72q8j4R0A,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:49.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1700, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6gg5wzc84qgzuedb40n52oe0i.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,ZlO_DqkSKUStV4-gGPvQOg,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:51.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 773, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5h0gufhuvlgih1xqqy2oje03x.png", + "AvatarItemDesc": "t_66r_j5oUSh9awIPmdUKg,", + "FriendlyName": "Treasure Hunter Bandolier (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afbafeb7-f9f8-44a1-a1f5-ac083e67542a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:56.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1705, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0s4l4l3cvdmfvz1eza6ts5dia.png", + "AvatarItemDesc": "t_66r_j5oUSh9awIPmdUKg,YTlnBuEjhUG1hSfAs8qwuw,IcR9Z_MxKkS9uh4uFYuFzQ", + "FriendlyName": "Treasure Hunter Bandolier (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afbafeb7-f9f8-44a1-a1f5-ac083e67542a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:26.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 774, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5vu32ph9qbrum8aq8hkvaa7d1.png", + "AvatarItemDesc": "ImwG0snB9ECc_gqRRBoz6A,", + "FriendlyName": "Treasure Hunter Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2066c22-c1c9-40f4-9cfe-0a91441a33e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:57.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1706, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7p5hywqgv256aipqmufecvfc4.png", + "AvatarItemDesc": "ImwG0snB9ECc_gqRRBoz6A,lAjUeUtTLU2-3bG2OXBukg,LFB_YqTym0-tQmUIj-YcYA", + "FriendlyName": "Treasure Hunter Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2066c22-c1c9-40f4-9cfe-0a91441a33e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:30.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 780, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d6no5z0xu0alfa3sfd7tchss3.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,", + "FriendlyName": "Treasure Hunter Tank Top (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:05.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1701, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2squicogn0nbezwccj6ahi6we.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,3qu7mxSNpkKRrYMhsWHVGg,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:52.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1702, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5fzlk5daghmydxowkt8y6x3ba.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,Ij8MXrl1Okip4M7y-gF23Q,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:54.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1703, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ets8ha47bzly0bvux6s0lw1t7.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,qEojxjSXm06CX52hRDJAew,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:56.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ffdda4d3-a768-4335-a883-aa3f9dc897bf,", + "FriendlyName": "Trihawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffdda4d3-a768-4335-a883-aa3f9dc897bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1335410714 + }, + { + "AvatarItemId": 982, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bfcff0ql8rwt35vnexuh55sc1.png", + "AvatarItemDesc": "ReqOfohe6UamzaiHG1x3Ow,", + "FriendlyName": "Trucker Hat (Pickup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e8eea45-5e88-46e9-a6cd-a8871b5c773b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:18.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1365, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4dquic4wqv5qwn7664v0i7fpi.png", + "AvatarItemDesc": "ReqOfohe6UamzaiHG1x3Ow,lDXambcP60CeY2C8qeGDFw,uiAwVt3HK0mRVtWp9B-gTg", + "FriendlyName": "Trucker Hat (Flatbed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e8eea45-5e88-46e9-a6cd-a8871b5c773b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:31:41.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1206, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8b8w29yppr8ire3rmnz7dihi2.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,", + "FriendlyName": "Trucker Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:09.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1207, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xzrzv7jtrqilh8yio1uf4b27.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:13.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1208, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c5h60lx2j7phvk8hmvih50s2o.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:16.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1209, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "71nsut92sx3eet8hzyon7q6mt.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,o2WfRyjJy0CKoUg8YIxNjQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:20.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1210, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3ko6st266abg6sg11o1nsxdxj.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:23.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1473, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a0lqwqfuolj7hkvhf1glhqc15.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,sscD2O3LEUOAKkqjGpGe8g,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:33.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1474, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6v3w4z6b6gofs8vtz0up7olo8.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,hRxyLsKfsESFAsXAyZQjoA,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:37.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1475, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1c2vx4eqcpf3ulizhf8xymy5k.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,kgCKjtSjFEmFq9ez2llxKQ,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:41.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1476, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "22a6bzdqbg2mnctnuw24oqszc.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,KaZqNx5VKkarRrOJcTuD9A,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:45.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1722, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3oy21va74rnfmipzz83x24n3q.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,eOJkmpSL4EypK-AQz7j8gQ,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:48.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1211, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "evjppeeirfwnfcqr1gdexa1og.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,", + "FriendlyName": "Trucker Cuff (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:28.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1212, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9z0jwf8oh2kcafax37xcxp68x.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,yA0KhVg19kWqG1UkyxQogQ,yBNsYcZXzEiuR0u3tWnnWQ", + "FriendlyName": "Trucker Cuff (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:32.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1213, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3d4u0uq00scnglmtqdwyvp55u.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,P9bBSL0YLEyjN8PxMfgbKQ,yBNsYcZXzEiuR0u3tWnnWQ", + "FriendlyName": "Trucker Cuff (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:37.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1214, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "emod4iboh8jp8hcuu07ylugks.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,o2WfRyjJy0CKoUg8YIxNjQ,sHyNs-dpc0-_Ah-HJVczcw", + "FriendlyName": "Trucker Cuff (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:40.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1215, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4hs50cqx2o2w24tay05gzsxem.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,bgRKH1wfqkWVHcuSnFYnlQ,sHyNs-dpc0-_Ah-HJVczcw", + "FriendlyName": "Trucker Cuff (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:44.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1347, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bqm1574wf7rpdwjlok49b0p77.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,", + "FriendlyName": "Mechanic Overalls (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:45.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1348, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ka8fpkswhsrqk4yzqu9zq6hx.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,iokdlBfjuEuAwqIMOQwxdA,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:51.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1349, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dvw88qyim2kmbtb852qiitri8.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,nKauYAyWhkm2AMHFyC-fLg,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:57.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1350, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8l97dx6qhjdo9t7s2dpksymii.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,EiGgN7uVukqoRE7UJcMLTw,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:51:02.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2021, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "deVNr6Y5KU6p3WI9O9RCpw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,", + "FriendlyName": "Tucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-26T17:42:24.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2022, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yD9WlEoR7Uuxoz4pegMXnw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,iIjl6PamKECL_JWvrZNHfg,2IFVtBYV8EqQXs8XHqeqdg", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee Shirt Latinity Mask Tucked Tee Shirt Latinity Swatch", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-26T17:42:27.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2049, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ubK_rEh-IEm7I_Dm8X_6hg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,vtXUyu-xmkaYtOCL9r3PsA,VtepaI2fvUWhcvNmpQ8r6Q", + "FriendlyName": "Class of 2021 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:37.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2069, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fH7sFkQfakOQNGz1LNIeDQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,r4O0JuB0dke1IoOEqW4nkg", + "FriendlyName": "Art Festival 2022 Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:27.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2070, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HafOMtY0B0eYptZba4mZ2A.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,UJRSCFfPq0K0yV0ETS3uSQ", + "FriendlyName": "Art Festival 2022 Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:29.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2157, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3JZYjcH1rEilhQIrqVialw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,3bWtQstEmUeHJXngec632A", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee 2 D Contest Gold Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-30T20:55:50.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2156, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "tgdvdzDpgkOt5c2sU-g6og.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,NEU8mYk_TkqS78l0zY9GAQ", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee 2 D Contest Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-30T20:55:48.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2096, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5xSIXFJJUEm8gEi413DBeQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,K4_AIRWX-U-f-nXsBOc82g", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee Shirt 8 Bit Logo Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-09T20:24:32.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2250, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eAx_UfHqTEmu_aRnqNXzGA.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,9lPZEIVbbUK1hdR1aVPLwA", + "FriendlyName": "Cowboy Boots Shirts", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:09.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ffb9sMGT0EqSNh85meeoJQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,W_RwnySBMk2GlrknM7NDrg", + "FriendlyName": "Class of 2022 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:05.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1489631510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B0Y_RojdNUWlUY10xCmzCg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,LXR5H_-7JUO7F3JkJh3ouQ", + "FriendlyName": "Strawberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.545778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1413305249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_TRYe6zmekyTjU6_Sa9d5g.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,mKWQMgBuTEq9EwXBTusuJw", + "FriendlyName": "Tucked Gamer Tee (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1176199818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qHJWmNTPqUag0af__vphuQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,N2ofIXeSBECHTBAoP2yQ6w", + "FriendlyName": "Grape Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1198761907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LmTBToLyKUyYe-lIJ4jKiQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,rg6sMjE1XkijY4Pq73pjcg", + "FriendlyName": "Lime Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1755749293, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DTvdUNjKGkmhKbNFYXzdkg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,t9-7AjmLgEWawcyWCD4xVw", + "FriendlyName": "Raspberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1511681798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qEEw3iKEAkmL_Kmj7syfCw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,5Q9E76b580CnMWIg6zbPwA", + "FriendlyName": "Blueberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1234, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "3qareqet5g5gwgynxe4tw9gub.png", + "AvatarItemDesc": "9b5406c4-8589-47e4-9f69-4fa4733615e7,", + "FriendlyName": "Turkey Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5406c4-8589-47e4-9f69-4fa4733615e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:19.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2486, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "mt3RP76mEkGxwfRV3p8aoQ.png", + "AvatarItemDesc": "9b5406c4-8589-47e4-9f69-4fa4733615e7,yXDNXmcJvUC4eCx9oGItcA", + "FriendlyName": "Turkey Hat (Snowy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5406c4-8589-47e4-9f69-4fa4733615e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:01.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2531, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "PcxjLeS1vEKxB0-e2aPilg.png", + "AvatarItemDesc": "5da97d5c-0d1b-4b0d-a2be-d255871d833d,", + "FriendlyName": "Turkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5da97d5c-0d1b-4b0d-a2be-d255871d833d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:04.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1570, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "4g2r02n1g5w09re7hl1ba2yyi.png", + "AvatarItemDesc": "002a0f2f-1a24-4439-b578-470818ef8325,", + "FriendlyName": "Turkey Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "002a0f2f-1a24-4439-b578-470818ef8325", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:27.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2491, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "NjxKDOlshkq09A4VSq5gAQ.png", + "AvatarItemDesc": "002a0f2f-1a24-4439-b578-470818ef8325,d-scjeOeCESC0rQMKsozsA", + "FriendlyName": "White Turkey Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "002a0f2f-1a24-4439-b578-470818ef8325", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:14.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b5a83be1-fd78-4797-b3e4-e5fa9730fab8,", + "FriendlyName": "Turntable Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b5a83be1-fd78-4797-b3e4-e5fa9730fab8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1787211725 + }, + { + "AvatarItemId": 1193, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cccfr6msg1shob4s8fngcwqa6.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,", + "FriendlyName": "Turtleneck (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:53.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1194, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "18z0oxaf9r5hu5604hnfsb5b3.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,9b9t8_aKH0-gQXMhZycaOA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:58.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1195, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aafnbk70rbkwsh5xsq4e8fk2j.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Zz4XatKfUEapReRAQHxKKA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:16.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1196, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "biyvmvzdwez09j9045jip174p.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Jt0QqNrAp0qzUR_RQoivVg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:20.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1197, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8o2jy3y3nfrfunsiqadtg7716.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,lBePHNL9LkyaAjEFyFE8Sw,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:23.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1198, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4aqh17g64pqv044ldezph3z37.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G4HIPUjekUigiU0-Kr9mdA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:27.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1540, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9d1oi3isvlv9xgg27pjqx7xbv.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,5mcp7-2ZEE6n3-zN3_CoWw,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:26.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1541, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8hdi7uyevqrhqw4zfu5knwqb5.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,R0UCacpdhUS906Xf7l142A,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:30.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1542, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ackem1gqqmknsbpbqr1r0opsk.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G-VKgepBkkeATcwCaS6Rwg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:33.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1543, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7nm5wdsxclddyf0y5up03zpg0.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,1LonkCbt4kykeAScYKpwyg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:36.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1650, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7sv7ea1zgexf9hov8dp1ky7im.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,IXWd396grEOWHt32L7fCpQ,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:12.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1087, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7swwjtbh6ko8afp6d0nmwspsn.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,", + "FriendlyName": "Tutorial Cap (Bachelor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:20:17.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1127, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b0ck22y2gp6w3ppasjughn453.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,gDH1WmW5rk2RdY4wJYYZow,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Tutorial Cap (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:58.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1126, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5y6bqiyhj3hhqy92tqzvjxo6l.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,wHJnbDOo8ESmUbQDr0k31w,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Tutorial Cap (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:46.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1247, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b9enslfg9k5mul9t9zukxjibd.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,GK3yolCNlEm3B_5DtHkE8w,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:15.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1248, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "amll8glk5rg14851jcmr390x4.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,U2ZbHn8WJEySUXVpzAY6ig,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 1)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:17.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1c3y650gb6irf7rhmbs9cf457.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,L333w3dfIEWDbtt1Yn1gBg,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:20.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1099, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "37ulwbdd8vskdogo0hv1kascn.png", + "AvatarItemDesc": "ASHnWS0h9kOJhbOUwxWKFQ,", + "FriendlyName": "Tutorial Gown (Bachelor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59e72101-212d-43f6-8985-b394c3158a15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:25.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1256, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wia57rfapa2pl00ngn2vtv2t.png", + "AvatarItemDesc": "ASHnWS0h9kOJhbOUwxWKFQ,U2ZbHn8WJEySUXVpzAY6ig,6nmHu9zj8k-tEz0tBHblnQ", + "FriendlyName": "Teacher's Gown (Level 1)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59e72101-212d-43f6-8985-b394c3158a15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:54.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1098, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7x241yml5e8ojjul82gsjcu8c.png", + "AvatarItemDesc": "MJQiQ3LATEOymxCC8wfSjw,", + "FriendlyName": "Tutorial Gown (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43229430-c072-434c-b29b-1082f307d28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:23.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1257, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ac5q83c1bed0a7fr6fqc9zz7e.png", + "AvatarItemDesc": "MJQiQ3LATEOymxCC8wfSjw,L333w3dfIEWDbtt1Yn1gBg,lSDrKR69rkSFULy9GA__1w", + "FriendlyName": "Teacher's Gown (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43229430-c072-434c-b29b-1082f307d28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:58.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1097, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bogc32ezqq4e260fqnwn6hunt.png", + "AvatarItemDesc": "0yFzgVESokC3gFd3hrS8bg,", + "FriendlyName": "Tutorial Gown (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "817321d3-1251-40a2-b780-577786b4bc6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:21.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1258, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bsih24pcifs39vkgb3nj2y01r.png", + "AvatarItemDesc": "0yFzgVESokC3gFd3hrS8bg,GK3yolCNlEm3B_5DtHkE8w,yKb2A8M_m0GQDhFWCRUHTQ", + "FriendlyName": "Teacher's Gown (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "817321d3-1251-40a2-b780-577786b4bc6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:04.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1118, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5frgjfs2olhxlc6p6k1xzi1gc.png", + "AvatarItemDesc": "vP9EKTMXP0yni8U7uCz2-g,", + "FriendlyName": "Tutorial Sash (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2944ffbc-1733-4c3f-a78b-c53bb82cf6fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:43.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1252, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3sux0cg60z82am4w6irvfph44.png", + "AvatarItemDesc": "vP9EKTMXP0yni8U7uCz2-g,FsATqzRVekO9hQ3u_q7keg,Q7hqvEo7DUyD9AqIOunWWA", + "FriendlyName": "Teacher's Sash (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2944ffbc-1733-4c3f-a78b-c53bb82cf6fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:31.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1117, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f20t6rhmbvzsu0byj0ag8itk7.png", + "AvatarItemDesc": "O8VqC11xNkSKpDh6Omp_0w,", + "FriendlyName": "Tutorial Sash (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b6ac53b-715d-4436-8aa4-387a3a6a7fd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:40.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1253, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9y51i5ld8baokfpueqd67oc07.png", + "AvatarItemDesc": "O8VqC11xNkSKpDh6Omp_0w,mvj5RDTuh0qoql0OyBFI9g,YQQamy-tQkCkpZPXKepUrQ", + "FriendlyName": "Teacher's Sash (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b6ac53b-715d-4436-8aa4-387a3a6a7fd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:37.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2084, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rZx9UCnz6kWaUo6z050exQ.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,", + "FriendlyName": "Tutu (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1738760043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ts-p4lQTo0qU3LNY7YIW3g.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,UjSxxUK7z0GGdjvI_NsJ3Q", + "FriendlyName": "Butterfly Tutu (Monarch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1968011120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ItXnzo230qoec69LEGbBg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,lBkOSQNDZUOvoknntnEMew", + "FriendlyName": "Butterfly Tutu (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1920210778, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NpnrKR1oZke3h0i8odmHGg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,sq5Qv4nVAUqzhMdB7U2MVQ", + "FriendlyName": "Purple Butterfly Tutu", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1044129999, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kafLNepzrUqLnxh9kG5Nxg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,6fp7nDdMvU6Ig-EmvjMpIA", + "FriendlyName": "Butterfly Tutu (Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 858, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b1blz138qua11xtiprlgui282.png", + "AvatarItemDesc": "jUpFzO5MPEGTbS-E0a99Nw,", + "FriendlyName": "Dracula Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc454a8d-4cee-413c-936d-2f84d1af7d37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-14T01:26:26.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2610, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Wir5kzMxjkaVf8C17Tg5wQ.png", + "AvatarItemDesc": "a994fc19-974e-444e-ae9e-c2f6c32432bb,", + "FriendlyName": "Midnight Gala Tuxedo Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a994fc19-974e-444e-ae9e-c2f6c32432bb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:21.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "246e6caa-c94c-420d-9805-9f0282c6d052,", + "FriendlyName": "UFO Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "246e6caa-c94c-420d-9805-9f0282c6d052", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1580435119 + }, + { + "AvatarItemId": 2918, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "m4UIuZjNzEWsCP1gpZBgjg.png", + "AvatarItemDesc": "95a519de-f2cb-429c-b014-508477f20d42,", + "FriendlyName": "UGC Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "95a519de-f2cb-429c-b014-508477f20d42", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-04-07T17:07:07.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2184, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "KXfytDhXzES2yco-rwqDSA.png", + "AvatarItemDesc": "c5d70cb4-71dd-4fe4-b719-34fe2073c611,", + "FriendlyName": "UGC Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5d70cb4-71dd-4fe4-b719-34fe2073c611", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-04-19T23:40:30.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2195, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5K9fUPIZ1kaQahcIYrlZCA.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,", + "FriendlyName": "Umbrella Hat (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:36.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2267, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qcRLmi1ST0OCkd8H8YzgYw.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,Ii2WNQxqtUeoIfGItUkVbQ", + "FriendlyName": "Olden Umbrella Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:23.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2828, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_ER1egNWrEmQDiXRt-SAmQ.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,U-KT3nJlW0-JT8GSQuQXhg", + "FriendlyName": "Umbrella Hat (Mushroom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:59.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2827, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "D69pyDco3kKsZJf_pN2FHg.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,3L-AGgM50kCCz2V3UJtErA", + "FriendlyName": "Umbrella Hat (Ladybug)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:57.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1060, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6xa1zsivxbibzmgrkhclxi34k.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,", + "FriendlyName": "Unicorn Horn (Spirit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:16.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1061, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "43eqpst8khtmcw02hx4g56yts.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,FNe9LkWJMUeC3frYJMyEaQ,nW_fBsuZA0OfIjTMXm6ZCA", + "FriendlyName": "Unicorn Horn (Harmony)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1062, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8txs4ivnem6d79jibl2zf6qtx.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,mudt-Z5-5USdnIq-xaDgOw,nW_fBsuZA0OfIjTMXm6ZCA", + "FriendlyName": "Unicorn Horn (Joy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:21.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2116, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rgbXleaBw0Wb9fZ3vTUtbA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,", + "FriendlyName": "Rec Room Untucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:28.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2117, + "AvatarItemType": 0, + "Tooltip": "Mr Beast's shirt", + "Rarity": 50, + "ThumbnailImage": "CF81YKO-nUGbzKI3slBEtg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,vjO5ctMe0UC8GN6p61IC4A", + "FriendlyName": "MrBeast T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:30.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f4svRkfHhE-6a9L_W0YSwg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,hbF6ChMyEk-L8O4f6o_l6A", + "FriendlyName": "Sports Fan Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:53.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2169, + "AvatarItemType": 0, + "Tooltip": "Rocked out at Rec Room's Prom!", + "Rarity": 50, + "ThumbnailImage": "LA32UPI01kqXUcm7xSrlmw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,rQaQALql2E-zdM8072gfCg", + "FriendlyName": "Prom T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:50.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2216, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NkEwfQcQFUiQ_C-0VKPJhQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,y78S5o6VXUGva1Wpd6sp3A", + "FriendlyName": "Untucked Tee Shirt) Untucked Tee Creator Reward Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-05-11T19:02:40.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2241, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E9mf1x4jp0yyg72cw7s-OA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,S_dLd3dzvk2hM6ZA2DKTWg", + "FriendlyName": "Tennis Ball Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:38.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2240, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LvZbawPbAEWmg5aVGi-M_Q.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,GU267AQ5T0m1jco3mNw_uw", + "FriendlyName": "Maker Pen Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:30.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2298, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "VasCRGTf-E-zXE53kLLl1g.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,SV2UmeQDjEaMK_A2Q4tt3w", + "FriendlyName": "Rec Royale Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:56.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2296, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pVwmecVkBEm37eRY3Jc_Qg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,V-vCW0ICl0Ke3QPAGo9GSg", + "FriendlyName": "Bee Yourself Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:49.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2297, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dH9FOddSYkSjUWt2Ux_kSA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,g_f9bH8tykGq3yRwWuQPOA", + "FriendlyName": "KREQ Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:52.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2688, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZOvGH1iQ_kqzVB8EkYBpeQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,Eh1jgOjxpE-AEModcPAFMg", + "FriendlyName": "Jazz Fluence T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2687, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VT-6wtBaC0Gs78iXxiZS1A.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,_aQEMQzp20eMT_Izfc64wA", + "FriendlyName": "Retro Cassette Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:07.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2689, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2_H41S2JX0a9J5AqYS7boQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,ppH8miE39kGaSuACQv34WA", + "FriendlyName": "Untucked Tee Shirt) Untucked Tee Shirt RECDLC Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-12T21:02:17.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2686, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0UwHPWuWWEacVUf3ynSV8g.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,hZvsroTOnU6ew9HBWaqxHg", + "FriendlyName": "Rec Rocks Shirt (2023)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:04.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U0QKGodCxUOP2Vvs0UCeKQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,QQVh8FdTakO26iOFdzGoBQ", + "FriendlyName": "Rec Rocks Tee (Akintoye)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:18.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2744, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UDBwvAX1qUWFKJmxqFGxzw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,mzu-a8fcsUCW591ENABAgg", + "FriendlyName": "Rec Rocks Tee (Haven)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:58.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2843, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XapimDoowkSNnhZVot3imA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,JpaGA1tG6U-VLsHH_vklzA", + "FriendlyName": "Fourth Friday Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:31:05.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2765, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XEh7w_iBSEWOPDJHeE18nA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,42YaouiGVEe3zSjTsvyfoA", + "FriendlyName": "Rec Rocks Tee (Clinton Kane)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:22.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2814, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WtNLFBwRdEqYa6RZtR82Lw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,fzQzxcpDNEW8Rx122zmjAw", + "FriendlyName": "Rec Rocks Tee (Charlie Curtis-Beard)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:09.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,b11GS6Jd4E-UhO8wxhfQXw", + "FriendlyName": "Untucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1640435160 + }, + { + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,cd9d37fb-51a9-45dd-8e87-2756eb62f48b", + "FriendlyName": "Untucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1312778606 + }, + { + "AvatarItemId": 831, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6dn83vim1zahdfgjcl98s0u6d.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,", + "FriendlyName": "Vampire Hunter Necklace (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:46.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 836, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7pryvhofcoe894jd6lj9k3fpt.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,lvDIOeSI10SNxnCO_M_9zA,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Diamond)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:40.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 837, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0e9k0ehhqbwmok3pfnwg4a9k3.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,0S7Nqm2kv0qMDTh-5Wy0MQ,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 838, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8dp37nmbznt4ddn3fpjb7i882.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,poqpmOloPUKotHH9p0YAyw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Amethyst)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:43.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 839, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dhvs55ab5etr8whu54dyu63mq.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,F7luQ18ZXEaITFiCqqwkpg,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Topaz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:44.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 840, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bmf0h0l2wd8hhxzq331rdtd2n.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,okpg4ftsak-wc8Zu83uytw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Opal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:45.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 852, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2qjoypuya1mw4gd0o6jhz9c0k.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,OfenZaa1-UGdQ8YVOrXjzw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:56.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 855, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "czzdjcyf31hpujiah45yuse4n.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,WOxMXWKGrUWhnNInttZ-Zw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Amber)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T21:38:59.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 833, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "84ln91je4llsa10o7904ovuwq.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,", + "FriendlyName": "Vampire Hunter Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:48.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 843, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b6rtey8gih1qt69aci1qc2op4.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,Z8Bfk0MTb0evZehvvgzuBA,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:48.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 844, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "csm6i7gnanlmwjohok0nf865k.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,9QG6qp5FBU-u0-JK-yMMqA,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:50.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 856, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b80np2tvmtp3siyl63qj51enc.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,CavRDqIlmUeTZBtCLn1hZQ,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T21:39:01.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 835, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cimomkml6k4toyowd1voh7hqm.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,", + "FriendlyName": "Vampire Hunter Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:50.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 847, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzkxvaged4oeb5kres30cpf1l.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,D0tP58wC30mJ00vNxw8hqA,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:53.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 848, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5colkn1hn7j9z19pn2vmuyjq5.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,Vxq6v55a_kGPODzwB5MC1g,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:55.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 849, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "79l7swn4ouu72xp8nmx7n1meu.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,iJW8XGJfU0G12iXarjxhdQ,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:56.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2275, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TtA9vfjAmUSSZ0HliWFsDg.png", + "AvatarItemDesc": "3ec19cb7-d0a3-4c35-a7e0-dc1f8609d373,", + "FriendlyName": "Pretzel Vendor Tray", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ec19cb7-d0a3-4c35-a7e0-dc1f8609d373", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:26.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1231, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4ktwnvhbdb6vrphngnp2a7pne.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,", + "FriendlyName": "Formal Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:10.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1232, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3h61nzryygyv3hlalnxl445sd.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,ffHKr5EcIU24PbAtWSnalw,bVrkIyADCUiGNYWdmP_HPw", + "FriendlyName": "Formal Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:14.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1233, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8a8z4irf5f2f0qemw44a9ksy3.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,m4W5D0Oa9kaWa-8I96YYzA,WF_fg80f30qvHit0vGBwCA", + "FriendlyName": "Formal Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:17.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2050, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "l-xQVuiMukee1yhQpZD6Dw.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,9RPbhHJWBkqwpDbMg-QumA,Qzklnrvw0EOiZ1mrj_nAYw", + "FriendlyName": "Formal Vest (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:41.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2561, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NMm13SJapEefUXYk6zAzcQ.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,bJXcVtY7qUK101QUAgr05A", + "FriendlyName": "Midnight Gala Gilded Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-07T21:47:03.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2001, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6ik8yccgzd9kcci799wrss8n.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,", + "FriendlyName": "Vigilante Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:33.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2205, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EsJbgLpY9E2nOTvBpQupHQ.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,mO5wjf7_cEWNCHFR6ymGxA", + "FriendlyName": "Vigilante Cape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:12.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2407, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FTP3W6-lVEm0dHlsJU-OAA.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,EjNmQJBRnUet_ovEQ3HnDA", + "FriendlyName": "Vigilante Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:04.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2532, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8__P61NSO0yRenF0XuCZVQ.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,oy67Rd9BskuB8yloqSv0Jw", + "FriendlyName": "Vigilante Cape Shoulder) Vigilante Cape Shoulder Green Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-21T16:42:06.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2408, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3e_aTt0gJUOLEV5MPnaBFA.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,", + "FriendlyName": "Vigilante Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:07.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2410, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hNQCYaLS6ES3S7MsMnbjYQ.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,eBm7gRXomkivUz4Qg_ab8w", + "FriendlyName": "Vigilante Glove Wrist) Vigilante Glove White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:11.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2409, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0seg4oV6PE-6014-iF3t0Q.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,agkPLQG_Mkmqf3WkDaFaWA", + "FriendlyName": "Vigilante Glove Wrist) Vigilante Glove Blue Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:09.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2543, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yCllwTBTl0yS2I5e-uCk5A.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,", + "FriendlyName": "Secret Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:08.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2548, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5cJBLmj_M020xDeOBjTGfg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,l1cBXE49rkKTDEl1NvmWog", + "FriendlyName": "Secret Mask (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:21.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2550, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dbd2B8AH_ESr4B382zo8zg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,MYCVPJDuz0Od_aZJekuWGA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:25.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2549, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "G-NyjtfwGk6tNxEwpb3r1w.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,kfZalp0wCkypX-QDhF0YoA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Red Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:23.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2544, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "udVHYjEfKUWkjL-2WA5Etg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,2U3ZEvBzoESCb7kk4zSokQ", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:10.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2545, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1oQmhaLu1USd36z3vIXSkw.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,S5IPVluRPkWzapjf7Bdtxg", + "FriendlyName": "Secret Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:11.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2547, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "zAnC5sdLVE27dsVXv-pKug.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,oDkSnfL7Vk64ZdxkuenEmA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Pink Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:20.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2551, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wAPE0uRkEkivSTVbtq2xDQ.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,8kAqQHAgnUypHdxnwVSQwQ", + "FriendlyName": "Secret Mask (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:27.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2546, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WGlj0EddtEuIpKKzqJ0XVA.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,pUHlnnttXUmqP-9Rab8YaA", + "FriendlyName": "Secret Mask (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:13.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2414, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qsnBFyRE20CfNh68iWd0lA.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,", + "FriendlyName": "Vigilante Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:22.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2416, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Xxv_F0hPeE-3CmWWVz9lgg.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,rixBVVHS1E2Q4Y43ix4ySA", + "FriendlyName": "Vigilante Suit Shirt) Vigilante Suit White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:27.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2415, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "jgtZjEvX30q8Y9obf4VFqg.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,zvs7t0P2Q02wIELJzMa2TQ", + "FriendlyName": "Vigilante Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:25.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2417, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "scABHZiEIEOr3uyq1HhN5A.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,", + "FriendlyName": "Vigilante Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:30.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2419, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wWHYtq4i-EeTTmy0RHGJqQ.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,0Ua_WtxcNEWc_KoRuuHhtg", + "FriendlyName": "Vigilante Utility Belt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:34.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2418, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_pw8woutPUabGOMaUuOihw.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,PXssHhcJsUmUKGA8T4u85A", + "FriendlyName": "Vigilante Belt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:32.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 71, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "drqcby4q8dncgmtl1ndocn4kj.png", + "AvatarItemDesc": "c87239ca-a255-4b13-bbc5-1b38236fb4cc,", + "FriendlyName": "Viking Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c87239ca-a255-4b13-bbc5-1b38236fb4cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 24, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bpe9ub501z2dr6rgv1b5zhi1u.png", + "AvatarItemDesc": "8c51fc1a-b1ad-4002-a13c-4313abd92c7f,", + "FriendlyName": "Visor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c51fc1a-b1ad-4002-a13c-4313abd92c7f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1497, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6f370azhmwpl1vkwy7fwf4kzo.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,", + "FriendlyName": "Silly Shorts (Donut Dreams)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:04.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1496, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "65sqa2rlvhzcxamj55t9w8684.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,nu_UGtkJOUK-5MWaW83MUg,pgBFpOtl00K2zsTJcfrefQ", + "FriendlyName": "Silly Shorts (Watermelon Rain)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:01.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1495, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8u3din8ubpb13mug1b7ls4lnw.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,sJioq5ARDUyMHFji_6OxFw,bjXO36Nu6EWD2dU1UC4Uwg", + "FriendlyName": "Silly Shorts (Pina Colada)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:58.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1494, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2lgy30wam8vhg848fzjhiys66.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,56qchovVVEaVwL9S52kneA,BAjMoTHbD0mSCjoG9jmzjA", + "FriendlyName": "Silly Shorts (Sour Popsicle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:49.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1493, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e0hiapycpgqmkydxsy7vminkj.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,zJMD-_HR80WCAUqYGe7_NA,rAzhaLh1nEWMd9v1D-HWcQ", + "FriendlyName": "Silly Shorts (Lonely Island)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:41.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1492, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "49x7h4tmx4xozv5opw2vrtmnl.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,9P8ARRUYdk6hCoGMKhix4w,o__JORNtYEiJEyyoBIimPA", + "FriendlyName": "Silly Shorts (Pink Flamingo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2167, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nIUJ-iQCTECRCeCD0vn96g.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,Pg2cVQonrkmbXZdsNcn-KA", + "FriendlyName": "Wacky Shorts (SummerCamp Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:45.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1463, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "08qct3wo7gai3uz597vre1rid.png", + "AvatarItemDesc": "767684e7-0eec-4f6e-ad58-3d5ae715206e,", + "FriendlyName": "Retro Sunglasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "767684e7-0eec-4f6e-ad58-3d5ae715206e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:11:46.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1339, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "drpze5r42trhdfe0kkmbirisr.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,", + "FriendlyName": "Server Apron (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:05.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1340, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9f3l01qhr5bcg8ed2l0i4d4wy.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,HdwZhjrqxE6wuf68MnDhDw,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:09.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1341, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6k82rsbhcoow0xzix9ty8q06z.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,b7oobDcDtE-69fhM-5g5AQ,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:14.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1342, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3on8a1l5752ih0y8i5uu1zha8.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,Blev_VFjRUysY_nymGWAVg,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:18.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1343, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6840rnvh9m72ksb1xw4y69cp3.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,", + "FriendlyName": "Server Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:23.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1344, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9cewy2sbl6u8ip4456pu20bad.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,GQSpXmD0B02_gwzpq6StFA,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:28.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1345, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2vipnhbzbw003kcmye8831wbk.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,YvPeCbQS2UOcLjPfoIML_g,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:32.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1346, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8657c4mdn19tdzvke8uu9mztu.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,zGOD45zNAk2i6P_zPsNPfg,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:38.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1794, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5c3hj3unazrmeie5zbuj4d1yk.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,", + "FriendlyName": "Wedding Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:09.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2511, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KXWrEO68d0Grp3m1mwkRjg.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,nuR8wvvCXU-EMXjBZuTXJA", + "FriendlyName": "Sugarplum Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:05.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2513, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "of8r-gp3HkCGzkv5aES9aA.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,TqhSo1hbPE6wdB_6lvLhFw", + "FriendlyName": "Sugar Plum Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:10.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2512, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PUAM3Iq0rUqQ6Pi5AYQeYg.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,SExtD3FBk0GwseMZDdGJlw", + "FriendlyName": "Sugarplum Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:07.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,96556f7f-a6e8-48f4-a4e4-3fad9be5df4b", + "FriendlyName": "Dress Wedding Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1724539158 + }, + { + "AvatarItemId": 1521, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "5gcobmtf2n9884p336ea9z10x.png", + "AvatarItemDesc": "05b7af56-71f2-45ba-a377-c105bf6a6f7a,", + "FriendlyName": "Werewolf Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05b7af56-71f2-45ba-a377-c105bf6a6f7a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:21:05.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1520, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "dqy4lj2b3ng70db6wvws3916n.png", + "AvatarItemDesc": "833e8ab3-9b54-4e15-8f4d-ee1bac5823c9,", + "FriendlyName": "Werewolf Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "833e8ab3-9b54-4e15-8f4d-ee1bac5823c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:21:01.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1519, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "abqsaidncml8v5e6w3cus33fc.png", + "AvatarItemDesc": "16f4dc71-d35a-4269-b925-1a0cd85a43dd,", + "FriendlyName": "Werewolf Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16f4dc71-d35a-4269-b925-1a0cd85a43dd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:58.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2394, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "xlMaAOhYFUiyeB9g8Ynt_A.png", + "AvatarItemDesc": "67d8d599-4611-428f-98cd-98d8a527ace5,", + "FriendlyName": "Wicked Sorceress Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67d8d599-4611-428f-98cd-98d8a527ace5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:50.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2395, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zgDeoqR2GkqwnMftPbASfA.png", + "AvatarItemDesc": "67d8d599-4611-428f-98cd-98d8a527ace5,4qSWIO-cy0q5JWHp7tagHA", + "FriendlyName": "Wicked Sorceress Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67d8d599-4611-428f-98cd-98d8a527ace5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:52.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2396, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2591QLU31kirjhVvKJKqcA.png", + "AvatarItemDesc": "6d3be838-85cf-46db-b33d-0985aec9609f,", + "FriendlyName": "Wicked Sorceress Cowl", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d3be838-85cf-46db-b33d-0985aec9609f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:54.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2397, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "89fpSMNTb02Y1qYpw1xf6Q.png", + "AvatarItemDesc": "6d3be838-85cf-46db-b33d-0985aec9609f,Vn2gH3yjiE6bwBRgh5ntZQ", + "FriendlyName": "Wicked Sorceress Cowl (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d3be838-85cf-46db-b33d-0985aec9609f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:56.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2398, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6wrK2BA4GkydRnaEpYN9DQ.png", + "AvatarItemDesc": "730453b8-a352-4e6b-9573-54294690c2ad,", + "FriendlyName": "Wicked Sorceress Staff", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "730453b8-a352-4e6b-9573-54294690c2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:59.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2399, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_Hv84dtkbkuHQuNy36QnrA.png", + "AvatarItemDesc": "730453b8-a352-4e6b-9573-54294690c2ad,4yMvQ3-hnU2zZQf_XT9r-A", + "FriendlyName": "Wicked Sorceress Staff (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "730453b8-a352-4e6b-9573-54294690c2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:12:00.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2505, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pSMu4GZwPkaYjHeVvrdP0Q.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,", + "FriendlyName": "Winged Headband (Frosty)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:56.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2509, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9quIlEDlukSdbauAxP4n8g.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,g14i4cTzkUu6RCOELBZEZA", + "FriendlyName": "Raven Winged Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:20.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CHZHjVfzp0mycor85z3G_Q.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,JUOrYcAd7U-ChkFZRiBeKA", + "FriendlyName": "Winged Headband (Fire)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:22.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2508, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7cUt9iO5Y06r9k3KfGPwGA.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,yxBqgXjlVEeRUOC_6iv77g", + "FriendlyName": "Winged Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:18.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2506, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yMQVOtm_EEeDJjnd-mgvQQ.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,6JihVI8pm0eJ-wDWuXm0sg", + "FriendlyName": "Winged Headband (Hawk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:58.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2507, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "F5yTEJXfq0GxW0BX6wKrmA.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,wwT8NdC-bk-aAWLoJvef5A", + "FriendlyName": "Winged Headband (Ice)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:13.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6a9x79mupkr34kq5jdirl6jkn.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,", + "FriendlyName": "Winged Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T20:59:45.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1789, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "92zq8rl5cyj5107xzihrijuss.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,9Zm7zOFBXkGRHrtp0JnaeA,oEsjaKTnKUq-Zgg4vm-EfA", + "FriendlyName": "awesome sauce", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:07:04.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1784, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "e0ivt24nvmykvpkg7ivnet274.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,0jnL66i2_kGjfzDBc8ZNSQ,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:54.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wipr83z3k4w166t8juiq5q2x.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,pY3RdcA2lU6H5rVXp8csfg,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:56.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1786, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a4nb7rytqdlq8f433qn0cieme.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,1Cbw9819NE-Fmv1NQIYKow,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:57.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1787, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "295vi4pw9jv6gz93ql5te9zw3.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,5JQPQ2JBUkSFyXeYWvr4tA,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:59.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1788, + "AvatarItemType": 0, + "Tooltip": "RecCon 2021 exclusive", + "Rarity": 50, + "ThumbnailImage": "3agkplk68iooewqp7l2qckznf.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,omnSzDLD5ESCU6ndropzPQ,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:07:00.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 6613, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "EkFVi2G8T02B9WxMBsz-hA.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,chqfCVxsEUuuUfp6qIadrg", + "FriendlyName": "Winged Headphones) Winged Headphones Hat Emissive Blue Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2024-03-27T22:53:46.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2440, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "Mx131-oxr0WEOEUxE3QvYw.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,VtsT6sKDYE24bAU-PKkIDQ", + "FriendlyName": "Winged Headphones (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:13.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1544, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "16g4r2mkpxrh4o9sywb6mqjwm.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,tXhgU1o_wkaNQ-7P3KXidQ,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Kingfisher)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1545, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bwisaai6w6aje9vyxb5oi2qbp.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,nn86ATWxekWdIGlPePPgGQ,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Vintage)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:51.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1546, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "22evbpym92jss1q2zzd8hgkb7.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,Rf7CDh2bg0-HdG5n7phGaw,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Thornbill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:55.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1559, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8czrpcicgmpn30oiwunhjkdov.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,oxi26T5a4U6L73pF-Pgrhg,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Grackle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:55:59.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1558, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dym59p1zihb18nmd4gfuzr226.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,6547pqf6vUm2L89AG6D91A,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:55:55.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 590, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "byuimkku1xbrkury4h0dloo3g.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,", + "FriendlyName": "Winter Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:32:56.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 951, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7c8t5ha57jkfu3nt2csays2c9.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,6vjA_vHq70Gclypll_GS_A,nalaCGqtL0SFA4Itwqmp8g", + "FriendlyName": "Winter Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:17.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1669, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aqnqkk7wewefq5ptgg8ky4ll8.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,3UxfTkPsEkezOXO__wz_mg,nalaCGqtL0SFA4Itwqmp8g", + "FriendlyName": "Winter Hat (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:04:21.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1223, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5h97nfxudddfga6xddftkdv7n.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,xVwzV9-m6E-8PVpkg4D-AQ,NH0L99bd00OVKMyltuSnHQ", + "FriendlyName": "Winter Hat (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:30.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1224, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ujv4qof9f4vkfnmm5zhc3yqp.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,mraJrTA4V0eARb_Kq7yYVw,NH0L99bd00OVKMyltuSnHQ", + "FriendlyName": "Winter Hat (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:35.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1596, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d2wg3243jobc3bipqut3250ii.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,09Cg6dOq2kC2-tI5yyGSYg,YFZimVHKuk--lXS8RCrZTA", + "FriendlyName": "Winter Hat (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:29.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1945, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dq0ld9pvcoy11h7778869v6w4.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,u1JNHHTxSU-N53Bs9pRCoQ,0zHD3DaMAUOogqOqyd59SQ", + "FriendlyName": "Winter Hat (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:24.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2677, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ogsxq40HrUGm_XGRT-bDrA.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,w7ybaJQVo0WCZFDPCJAtWw", + "FriendlyName": "Winter Hat (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:21.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 591, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "byw8djj38cmq7xhypade0jdsi.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,", + "FriendlyName": "Winter Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:02.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 950, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8oldwy2zk4m8splglpnb00px6.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,2lD-mjl3pk2wWv24RY2tbA,dUu7b-JFOky9c2lIVCNmcg", + "FriendlyName": "Winter Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:14.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1225, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7vhlvcgeu2orqfj1v5ym2tw6b.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,OrqqGMhK1UKGowFxpJi2mw,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:39.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1226, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c374omg9e5qg2vfc2ferpr6rs.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,-tDd0gdxQkuvL2id10AweQ,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:41.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1670, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c9kkdeflt9if8ses9jc4zceuw.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,TDcZZ2ynEU-Mgmju_qIVIg,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:04:43.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1597, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6vj33whkr0o942bwju915h7tv.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,09Cg6dOq2kC2-tI5yyGSYg,eUktNKKK3kqUR8-gA3rNxA", + "FriendlyName": "Winter Scarf (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:35.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1944, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dg0fgutjfxtnt68jv0ixfe86l.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,cj9OYinX5kyzlhBUQwZKog,AKWH3_SDqkG8SRm5sauROQ", + "FriendlyName": "Winter Scarf (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:21.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2678, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "K2uufB0PC0S-SCX_3wmmvQ.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,NJcJw51-xkq0phT-nlJw2A", + "FriendlyName": "Winter Scarf (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:25.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dll3aenct2b8668hjuionbuff.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,", + "FriendlyName": "Winter Sweater (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:04.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 948, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "779n1128jegvy95s8avlm32fs.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,LDqcPkvNXE2gFXxbpNkfhA,Too5jpb1w0iQenkjRRS-QA", + "FriendlyName": "Winter Sweater (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:03.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1227, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5e6rkamgfx1r7o0j31z8lkrc8.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,XOBIPrKCr02xa4tKj8wN9g,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:00.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1228, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f0y7tht9axiudht3tlhqeysgc.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,i33RmJJCwUem-ADkOwMNQQ,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:02.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1673, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "44uerp68eefsysf8d98xb2sj1.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,fq0xC9BjLECbsQIZfAvK9A,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:17.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1595, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ajy0ulpcn5j6v22cwvub3mj4g.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,09Cg6dOq2kC2-tI5yyGSYg,3fQj_7meDEeQqm_7RfEfxQ", + "FriendlyName": "Winter Sweater (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:26.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1943, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aa8mp2wzw823pnrmg36w1gge6.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,fTWOc1eEl0aVKXwTYYjA9Q,31nK13dsTkaIf3tGFrZlJQ", + "FriendlyName": "Winter Sweater (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:19.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2609, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ZjDS_OAMk6uv03QVqK4vw.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,PG6H6px5HkO4X_xPRd1idA", + "FriendlyName": "Winter Sweater (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:13.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 593, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0hlts8hq83dvp3ifo504f57oz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,", + "FriendlyName": "Winter Mittens (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:07.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 949, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aiitap49m6e7fam5fzxlgszq0.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,SWvJWvMDpE-it5PFoPgERg,jQjfiz2i6kaPfrkjnBGxBA", + "FriendlyName": "Winter Mittens (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:10.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1674, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0iiuzgowl2qneslonnzfktvaz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,83jS7d-cUUC_GNDcGpiDxw,jQjfiz2i6kaPfrkjnBGxBA", + "FriendlyName": "Winter Mittens (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:24.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1229, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "00oq8k2l0ag3pn1lp8zpc36rz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,afWY5VGiF0isGn6qpo4KeQ,_yvss6L59US4-8QYZgcCdA", + "FriendlyName": "Winter Mittens (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:04.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1230, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9uo8iwvaprweq4i5xoa11qb42.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,tyzdtSkR9k-YCnjNvPAT6w,_yvss6L59US4-8QYZgcCdA", + "FriendlyName": "Winter Mittens (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:08.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1594, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9k6nv0za82bel8lvz8220tor9.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,09Cg6dOq2kC2-tI5yyGSYg,xq7bizx6UEGaMTHB9zI3pw", + "FriendlyName": "Winter Mittens (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:24.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1975, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6jg4ugpje9gdl4iyf223r1e0.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,MmGNESXgOkiZ0xCG41E9ig,Nc6jwNeK_kyajH5xKzCNVw", + "FriendlyName": "Winter Mittens (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:21:21.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2680, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zwdzcRCLDEeGDboAcpAzOQ.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,e6hTzFc-fEiJe0L4jDVxRQ", + "FriendlyName": "Winter Mittens (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:33.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1300, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1hz9fyly8abux6bynwxt67g6.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,", + "FriendlyName": "Winter Cloak (Nightwatch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:14.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "90opi030nb86oj7r0um5a2iip.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,pXrZhtzjV02_qvIQoWEFWA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Hearthstone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:57.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1617, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dtxdon4brg2jmucu6b9sga2wh.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,VK4YBLXO10G2_aEl3ZQUGA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Whiteout)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:00.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1618, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8yf569v4byj5czra0vsottv9w.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,A7l6Q2clsk2XyKf62D3dHA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Ironbound)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:02.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 826, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "36zngdpuj89crp4vmobfqwgk9.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,", + "FriendlyName": "Witch Hat (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:24.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "byiqri4xizdtch72sub8lx40z.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,PcUZrdKXhk2XHa5Gy0YnnA,dpBx3DxB7kKJphxCvfmKKw", + "FriendlyName": "Witch Hat (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:50:36.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1516, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dam6qhnya0lx1zj3twhobzuo1.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,0cP9EcZRFEuMOvmZ_Kx9dA,smmfK-N2R0uoDiuIHrhImg", + "FriendlyName": "Witch Hat (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:19.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 827, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "avd2s93fmxs9qkst7n3y7zcxq.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,", + "FriendlyName": "Witch Dress (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:26.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1173, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "csihpdjmklkvoybyrc0do3hiq.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,uTHwo8qd6Ei3Oy4gz_j9Ow,AK4CoAB2IUGkwqECNaO75g", + "FriendlyName": "Witch Dress (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:04.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1517, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7olbww29lo0vqi3gal2gidvgz.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,rZ45-lecBESqHaLX0c7NAw,iDxHI0T4Gkifs3_GY5gdjQ", + "FriendlyName": "Witch Dress (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:39.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 828, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4zz4132pzz51bxxjxf570c1d3.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,", + "FriendlyName": "Witch Cuffs (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:29.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1172, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dab6kmwjuwqeqhmjfdhdx043o.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,Hk6svQ5Sr0GDKLxOyktk9w,IQvHmZ8ei02hVuXZiXb6TA", + "FriendlyName": "Witch Cuffs (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:00.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1518, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0p6vqm6iz2rilzafurlc4bbfd.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,0ZKR_qxrWkiWkjpot8s6Cg,5aX524Lfok-0dC3UFv-OkQ", + "FriendlyName": "Witch Cuff (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:47.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 515, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "17c4sjyyimn5d2gw0f556bubi.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,", + "FriendlyName": "Witch Hunter Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:51.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 516, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4quvkumfq6a1nznmdsezcpwxq.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,64545dcc-a8eb-4c69-a539-78ee73a2d327,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:53.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 517, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4110szxveghcz05ilxuwglfb9.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,e02838f8-4029-455a-b35c-dd40f972b05b,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:54.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 518, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6z583ji4uuq0r1nymbm4h8ou5.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:55.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 519, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2boublopz20zq6qu6ncbx97n7.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,909ed249-ce09-4304-8b02-56794fa7567d,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:56.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1125, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "04egovo7xe0ud9w0z1a6z2glf.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,Tbrc2MlSlE6mKua8qSoluQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:44.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1305, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3wjvw25jvmg34uniq3dxxrv37.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,aBQ3R9boek-mYeSYSNvDbQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:33.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 520, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c6y6kr8pkktfh9j9jrwbdbiop.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,", + "FriendlyName": "Witch Hunter Torso (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:57.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 521, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dbbbxrdwflnaiaj1nx1ovh07r.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,64545dcc-a8eb-4c69-a539-78ee73a2d327,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:58.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 522, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "40hz8pm5dpxvycg72a9eqhqu4.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,e02838f8-4029-455a-b35c-dd40f972b05b,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:59.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 523, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6crngu0sszkwju23mkpdiihjs.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:00.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 524, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cfce1ejdgdy1jvubqb4vyv8k0.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,909ed249-ce09-4304-8b02-56794fa7567d,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:01.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1096, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9hmvdv7ncpyo5f6agy7dh9rxv.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,Tbrc2MlSlE6mKua8qSoluQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:18.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1315, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dci33hnvmb7vrbgaa8u6zvrlg.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,aBQ3R9boek-mYeSYSNvDbQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:26.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 525, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aifzs5jdida9x4pu9dc5p3lvb.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,", + "FriendlyName": "Witch Hunter Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:02.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 526, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8kbnanl6xae6f5zssdhow3qi6.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,64545dcc-a8eb-4c69-a539-78ee73a2d327,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:04.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 527, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "71i7ci2n620of8lk3jxy8p0xl.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,e02838f8-4029-455a-b35c-dd40f972b05b,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:05.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 528, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "99lamaljd63j9ijdm4si9n6nz.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:06.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 529, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eiseqfjnni496s1qlbkfv63gw.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,909ed249-ce09-4304-8b02-56794fa7567d,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:07.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4baeboc0ifrxpbd0ko0ohkilh.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,Tbrc2MlSlE6mKua8qSoluQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:56.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1319, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ao77s83vraqs2p4xiwkoxdk8o.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,aBQ3R9boek-mYeSYSNvDbQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:57.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 530, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dvw439bvcm7yg6u5gwrrduckq.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,", + "FriendlyName": "Wizard Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:09.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 534, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6tvxvhjodc7ixkaqu2ncy473u.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,357eb160-360b-4a63-840d-65d11cc8ffc2,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:35:59.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 535, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7866xyln6cpoizvv5eps253jn.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,a38254c0-00e3-429b-9d5d-b9e6a9812e69,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:01.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 536, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2mgmt7so1oho67truxxyy5key.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,d3a9b576-5433-4305-bd96-b08820c09212,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:02.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 537, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ts6u23lp3vvh9g7zmcwgm6yd.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:04.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1124, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cgrpr9ydok6zdsitoa7a14oie.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,RKmRHk2N_U2gd91nybo6vg,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:32.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1306, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "19rp9i0wdr1ua9vm9hgjiy1f2.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,d3KmqVuMQkCYK6sVTMOTOA,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:38.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 532, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4u8g9x7yeqkmpqikqud6p0889.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,", + "FriendlyName": "Wizard Robes (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:12.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 538, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "a814kjf5tknfmtt221aq0rmcg.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,357eb160-360b-4a63-840d-65d11cc8ffc2,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:05.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 539, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6xg4k21qpb73l0qe9wq3xs507.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,a38254c0-00e3-429b-9d5d-b9e6a9812e69,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:06.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 540, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "351pq2gydedb34tytcs9n87b7.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,d3a9b576-5433-4305-bd96-b08820c09212,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:07.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 541, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c1nbb1yfgeptsgsxxakgr3ejf.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:08.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1100, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dlznhq7lrq71suzc0045grre.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,RKmRHk2N_U2gd91nybo6vg,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:29.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1316, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "47ngqkmtgcfvbds2nhtksozc3.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,d3KmqVuMQkCYK6sVTMOTOA,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:33.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 533, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "42c0usoynxjhmimj6coo11rd3.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,", + "FriendlyName": "Wizard Bracers (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:13.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 542, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4kheccxh9b10611a6leuimejf.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,357eb160-360b-4a63-840d-65d11cc8ffc2,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:09.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 543, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ejoni0f6t3kbisv094qlswolb.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,a38254c0-00e3-429b-9d5d-b9e6a9812e69,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:09.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 544, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4s49nshq56cwk2ptse4evh4p.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3a9b576-5433-4305-bd96-b08820c09212,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:10.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 545, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cz0n5qaa95cagk71hmmypg87q.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:11.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1108, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1n7pr3zdkb6nskzxhpvouxscx.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,RKmRHk2N_U2gd91nybo6vg,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:58.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1320, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0vwlyjwom9p9ef29kfe7nowjz.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3KmqVuMQkCYK6sVTMOTOA,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:16.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 531, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "633t2xbzngvd2f2x91gj77qth.png", + "AvatarItemDesc": "27cdecd2-88f1-40a8-80a9-87abd827c3c5,", + "FriendlyName": "Wizard Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27cdecd2-88f1-40a8-80a9-87abd827c3c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:11.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1873, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dachlbubxeuputz2ewly8je7l.png", + "AvatarItemDesc": "fb2a0e86-3363-4579-9f18-f41138dc85ba,", + "FriendlyName": "Wolf Amulet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb2a0e86-3363-4579-9f18-f41138dc85ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:31.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2121, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aTYlG6TI1ESQrh8Tyzi-bw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,", + "FriendlyName": "Atlanta Hawks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:41.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2126, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vndWqkhOjEiG1tfAoDZoig.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,dpxEW-2Un0eeVIsg5_o6Xw", + "FriendlyName": "Boston Celtics T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:53.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2137, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cTeuIowAx0KzIVy0-WH2cg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,4UTuNYISo0OP70FuGzmOsg", + "FriendlyName": "Brooklyn Nets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:22.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2130, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1KoWvmLOa0m99qKETLTIOA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,yVNoqFjSeU6mPJxEJnzRLg", + "FriendlyName": "Charlotte Hornets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:04.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2124, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r1RzRtaGuUKZ-8XAY3Dh2A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hE5scNMqJ0igY_jhufvNmg", + "FriendlyName": "Chicago Bulls T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:48.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2125, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YC4ouQXwGEOyXXjSiDhPkg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ibmxOUIFs06gOpRYLe58xg", + "FriendlyName": "Cleveland Cavaliers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:51.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2136, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "up6nS7ge3kWq1ROeCQ9GTg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,LpUOG6fHz0uQPtmOLtUlKw", + "FriendlyName": "Dallas Mavericks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:20.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2138, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NMpKzkwrjkmapWagtUmY4A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,sVeZH49mP0yBRKrbVr-0LA", + "FriendlyName": "Denver Nuggets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:25.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2141, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-6_8xc5kqEyRKQ1vuvbNYg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eAq9xK51mE-Nu4RQY0ds4Q", + "FriendlyName": "Detroit Pistons T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:32.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2149, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3ABGwcegD0CZluvQKynPlQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eCEF5P5N7EC_cEjM9-5iUA", + "FriendlyName": "Golden State Warriors T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:52.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2143, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2t_BBgqutkWjsMndffuawA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,OpWc8pumTk6M6x1WSw3a6Q", + "FriendlyName": "Houston Rockets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:37.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2139, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QTk8hWoyH0KjtihHXsQ3ww.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ow_9g2xNj0G33-zWIhjW-Q", + "FriendlyName": "Indiana Pacers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:27.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2127, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-Ct_cbd5DUeGp8zl21aLMA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,2MUexARJAU-Vc-Qebqqepg", + "FriendlyName": "LA Clippers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:56.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lrUa6D6QQUec3po_q7y2-g.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,9bQaIrqI00qtjdtkIkj_3g", + "FriendlyName": "Los Angeles Lakers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:15.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2128, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kf_qLY9VIE2q2gSCp5L1uQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,pvnZdyL9NEaYYX8T12AgVA", + "FriendlyName": "Memphis Grizzlies T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:58.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AGZJkBZVWUy1O-nlqtYI3A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,clStIXndXk2ayewq-1LKng", + "FriendlyName": "Miami Heat T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:01.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2123, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ako-ThG7XEqcnjIt9OZnlw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,oflbr1qvHUyvwUxdFZMcmA", + "FriendlyName": "Milwaukee Bucks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:46.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2147, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "W9BC4K_6K0G3J8tY9fyJeA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,j3S5Gk92mEOKlD424seciA", + "FriendlyName": "Minnesota Timberwolves T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:47.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2140, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NNC2C7t7sUaTbsPQB8wOmA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,JBFu1kZ8C0ya5JZLDh0ynw", + "FriendlyName": "New Orleans Pelicans T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:29.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2133, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MKQctCVd20ONb5z9deG31g.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,a4r3MYNjT0Ok0uQX9BYH6w", + "FriendlyName": "New York Knicks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:12.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2146, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PLv73v1H3EaMsBsQJzBVqw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ZDsylFpkEUi5PijTLSfYNg", + "FriendlyName": "Oklahoma City Thunder T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:45.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2135, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kOzNsMW2mUGpmPEeT0AnUw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,B3wjSG3Ui0ipGgN3h7BFiw", + "FriendlyName": "Orlando Magic T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:17.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2122, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yjtuZ5Fb5kGrGUDMaeoyEQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,H_lV3FQG0EC4ebW5vfnRIA", + "FriendlyName": "Philadelphia 76ers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:43.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2145, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HUAmHnGyBU-bYA17m4hILA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hU2rt41rvUqulLM1H5NX4g", + "FriendlyName": "Phoenix Suns T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:42.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2148, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1lch9FUPrk23Vdmwl61lHA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,49mpP0duKEylQ2v5ZU9SYg", + "FriendlyName": "Portland Trail Blazers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:50.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2132, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Qficbcavm06029T8l_tmgw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hoXpo41nnEOPGq20Tq-t9A", + "FriendlyName": "Sacramento Kings T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:09.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2144, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KwQTRfiStkqFs1xlwiPgUw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,RddbstjtZ0ClxgY-s66tTw", + "FriendlyName": "San Antonio Spurs T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:39.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2142, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ss8R7vuL6UqFDQeWpt-fxw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,kk271nI1MUu-gZjec9djrQ", + "FriendlyName": "Toronto Raptors T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:34.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2131, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kgk79zh1rka-JRcVXk1xnQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,tkHJl8TQHEiSeD-seDV4jg", + "FriendlyName": "Utah Jazz T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:06.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2150, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lqK4K0ia5kCA6osf4p36eA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,M3vXSuDRw0GLNh9A4dF4vA", + "FriendlyName": "Washington Wizards T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:55.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2183, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "K5HHE0xLXUyHcne6GNjNig.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,nEBw9Nh1-kWcOynsm7P8Pw", + "FriendlyName": "NBA 75th Anniversary T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:44.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1894, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2tkpst7269jmrhwyhewrh4t18.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,", + "FriendlyName": "Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:43.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2837, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78b4yr_R40a5Z_V4E85knw.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,B5WV1io6fkaAJdm4GbzM_Q", + "FriendlyName": "Monarch Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:52.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262588196, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RGy9sbGCrkuEWsufq0SbUg.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,bNWm_2_VE0-4fdcF6S0aIA", + "FriendlyName": "Purple Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1968469639, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8VDdpE56dk-9OeT9sGG-dA.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,q9Nganq9nUaV3zuvx9s3nQ", + "FriendlyName": "Butterfly Wrap Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1600494107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DWzLX_RUc0yrZ_icbuAJgA.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,y8yTimlV2EWhJMU-Ay9e8A", + "FriendlyName": "Mint Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1657, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5tzmcps62glt2hqx42rvrgotc.png", + "AvatarItemDesc": "9231a9c6-7638-4fe9-bd2f-eac6c943009b,", + "FriendlyName": "Championship Wrestling Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9231a9c6-7638-4fe9-bd2f-eac6c943009b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:31.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1855, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4510ib9vx62wtdf62c22d4ll2.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,x3kGXCD0UEWkjfXqu_kuxg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:58.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1857, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6utyv84yov0n8z090tcquifk4.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,fWxn_AarpESryNA9UN8wvA,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:02.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1856, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a2n00ipkx8cd6po7pkjti1rsw.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,sEKIlMfbJ0eED6VtcXcQrw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Army Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:00.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1858, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2b2f6frv89cqaeuswhoqzwvfg.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Dg3DxHr8GU2e190TGveXqw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Baby Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:05.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1859, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c4c0hhlbwpkuvdbt67tg68guo.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,5rLtRfNWO0qT7X-0sJkUXg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Lime)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:07.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1860, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bl1czxrbjmc23vhg7hy87v385.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,DiJGFIO-5Ue8rkEhvF5-Mg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Sienna)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:09.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1861, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5s7gwmw9faljog1rm2hdo358p.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Sg5yKkqJEEulQeUvRhFgJg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Prussian Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:12.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1862, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8w15137vxkyf85ljd9clb74kk.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,M0nOQl44DkG_GjTXzAEQCQ,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:14.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1863, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4zk2f1zx0df30rx3r2rafuhsi.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Ai85jH88H06ze74VdWolfw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Pure Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:16.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1864, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0saykzdj1mlrn24gfq57ps3yu.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,NRjJuinZ4ki8qaEPvP84tw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:19.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1865, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8k3ikhmifw4vcf0qeayqmlzby.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Z2L6AqDdcUqxiN1mVw-X9g,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Light Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:22.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 564, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "btwnqsv2torue6ekxyp3zudoe.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Wristbands (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-13T20:56:59.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1866, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "43wffwjx8jpzi7e26q188u0mg.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,j367nSYyg06rOMZ1GmfmFA,FS7kgc4DpkKuar24J9dW8g", + "FriendlyName": "Wristbands (Plaid)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:25.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1867, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0vdopp5i9bqzer09tpkfgi76a.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,2qYs9IkE90eppx1DggNlsA,6Y43EyTpiEejGp0ODWTSjA", + "FriendlyName": "Wristbands (Moons & Stars)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:28.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1868, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c2ypxn1ve868vb7jw2i5vn060.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,JV60jZGerUWuzQ2rRnwquQ,gSb8iLTEZ0mi3g5-jZXifQ", + "FriendlyName": "Wristbands (Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:31.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2788, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0AD75TjNekWbBVUQv0B09A.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,gOP1m4pVHkmlR2k8RrcdmQ", + "FriendlyName": "Rec Rocks Wristbands (Ethan Bortnick)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:14:03.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2072, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "kuIHwuTu10Cr8dCY4ZBlzQ.png", + "AvatarItemDesc": "d9c025e1-561b-4997-9f69-c96eb3c00786,", + "FriendlyName": "Wrist Warmers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c025e1-561b-4997-9f69-c96eb3c00786", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:34.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 256, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3szxfcxg8z8qj8wcaoubbty0z.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,", + "FriendlyName": "Poofy Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 257, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "00voe87lgaogwdjrgkt66bxew.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 258, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9z43yb4xdivb6qyou9niixqtn.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,64850553-cdfe-455a-ac00-dafbe63d613e,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 259, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9c3ziz00oalv0ji0f7tke3j7w.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,31ac9d4d-3d6b-48f2-85d5-dc4b8165a17a,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2051, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kq1OCF490kKmBUv0YF5PPw.png", + "AvatarItemDesc": "459ff0a0-ad68-4337-9399-57e3b03e4bf6,", + "FriendlyName": "Yeti Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ff0a0-ad68-4337-9399-57e3b03e4bf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2052, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "y_99GWjFbUa8UCqUY9X7FA.png", + "AvatarItemDesc": "7524b3f3-130b-41b1-83f6-9c241bd92bb4,", + "FriendlyName": "Yeti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7524b3f3-130b-41b1-83f6-9c241bd92bb4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:49.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2053, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UHgH78a4iEmYmvBe9cKI7w.png", + "AvatarItemDesc": "184368a6-17fa-4cfd-bb57-a2862f6e17bd,", + "FriendlyName": "Yeti Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "184368a6-17fa-4cfd-bb57-a2862f6e17bd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:53.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2118, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f5pXQnYPQUK8GU0CzpeWig.png", + "AvatarItemDesc": "f5b01307-e495-469c-afac-c10f375cffb0,", + "FriendlyName": "Yoga Mat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5b01307-e495-469c-afac-c10f375cffb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:32.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2420, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "xFNT1OMmg0O3cQ3aagkA8Q.png", + "AvatarItemDesc": "f5b01307-e495-469c-afac-c10f375cffb0,lIURXMnmn0GcYFUVSXHzWA", + "FriendlyName": "Yoga Mat (Boogie)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5b01307-e495-469c-afac-c10f375cffb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:36.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1189, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "c9bcud5i7blg02i1i21anii66.png", + "AvatarItemDesc": "9168b3f9-c7e5-40b6-bb2c-9e708596e675,", + "FriendlyName": "Zombie Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9168b3f9-c7e5-40b6-bb2c-9e708596e675", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:07.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1190, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7q88u3fe1zgi5smic4akp7ow9.png", + "AvatarItemDesc": "de19b239-3ed0-4f03-acb6-da69b5aa52ea,", + "FriendlyName": "Zombie Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de19b239-3ed0-4f03-acb6-da69b5aa52ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:11.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1191, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "amgn9mnr8d0gpwg27xhvbuvwf.png", + "AvatarItemDesc": "5f1bc3c2-dfbb-4258-9b10-608535e34db2,", + "FriendlyName": "Zombie Hands", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f1bc3c2-dfbb-4258-9b10-608535e34db2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:17.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2643, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cdu27a9jf28i0va22wre1e08h.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,", + "FriendlyName": "NFL Arizona Cardinals Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:03.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2644, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0znrqqn0ggnpfhcs942kyvrtp.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,VxmHqppE6UWiXVBXL2QvTA", + "FriendlyName": "NFL Atlanta Falcons Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:03.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2645, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eexlvsjg8kezkcwttljr1690x.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,kjDg5GM1b0iepa52BX5wbg", + "FriendlyName": "NFL Baltimore Ravens Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:04.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2646, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "707jfnnrkawaq9bdwniic0pye.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,WlfHWDcnO0SBT5wTjEPSmw", + "FriendlyName": "NFL Buffalo Bills Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:04.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2647, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5lqm8xisnbpdwi6my605vy6uj.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,_egTT9EA-UK0aLkn_QnWFA", + "FriendlyName": "NFL Carolina Panthers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:06.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2648, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1lkfrltzaelfhihaleygfgtoz.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,10nSNj7eRE6XwS9y2OvC2g", + "FriendlyName": "NFL Chicago Bears Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:06.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2649, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "axl8sqe1o3pyczpmutxk61ltf.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XPBWTcpQ9kaS0mabpdvYhw", + "FriendlyName": "NFL Cincinnati Bengals Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:07.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2650, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bya9qdvmk13buz121v7p3omb0.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FokpgM8R5EuXZwe89T78yA", + "FriendlyName": "NFL Cleveland Browns Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:08.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2651, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xj6xkknhas1ka2boco8j8tkf.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,v0nZrzbIMEi3AaGuNjmwDw", + "FriendlyName": "NFL Dallas Cowboys Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:08.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2652, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwh5sx4vh6v0gdfi3kkalxutn.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,TtthSwdIq0CuSNOKDLsldg", + "FriendlyName": "NFL Denver Broncos Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:09.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2653, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7q30iarhlropnxgad6s0kjjdc.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,5fjPcf4xQkSEYn3T-f8xLw", + "FriendlyName": "NFL Detroit Lions Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:10.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2654, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3vg7up51ivibzltebmdutv694.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,sjY_XUbXAk6h2TGnjBGDeQ", + "FriendlyName": "NFL Green Bay Packers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:10.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2655, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9kjwm7fopyhvlu9oq60p8kqh1.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,1nFX70Rqo0-qQo2M_Z1KCg", + "FriendlyName": "NFL Houston Texans Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:11.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2656, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "621yuib76ktgb5dus5p65ciue.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,cOyJcjGyp0miaMLAOpz0FA", + "FriendlyName": "NFL Indianapolis Colts Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:12.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2657, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2w56qylw8p57q7ik25gtk5usj.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,nIUjGqY8L0u0ZbX45b4PUQ", + "FriendlyName": "NFL Jacksonville Jaguars Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:12.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2658, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "85q8el387hlfi6qwrheeqg2g2.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6JY7l1gtpE631NTmT6xv3A", + "FriendlyName": "NFL Kansas City Chiefs Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:13.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2659, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2hbcygm3cqto3mirz4d7og1d7.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,27Fg--SzD0ertvGEv1Jgsw", + "FriendlyName": "NFL Las Vegas Raiders Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:13.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2660, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3inbs0h93lddkdaggk3h26yae.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6EjRVha2i0mCUVLaL_em_w", + "FriendlyName": "NFL Los Angeles Chargers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:19.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "di6np6u43ih96rrsdk18jvwox.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,EtgbhY9a_EqO4j86Al9Kpw", + "FriendlyName": "NFL Los Angeles Rams Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:20.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2662, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5h0qrqjtunac31f0srzvmmttz.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,oyvOdKXeGU65V6sKhhb0qg", + "FriendlyName": "NFL Miami Dolphins Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:20.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2663, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "en0jav3kz8odonu5j54j9ckk3.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,-0DfEFcbNEaNI2uvfHF9qA", + "FriendlyName": "NFL Minnesota Vikings Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:21.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2664, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "32qnngk3wsvsjk35n4vcfap7z.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,DVnSVZcj8kyLr1DXRu7kzg", + "FriendlyName": "NFL New England Patriots Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:22.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2665, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70ki9yd0las1hfb2tvielgtqm.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QgECbFDwyk24sI-e_fXy5w", + "FriendlyName": "NFL New Orleans Saints Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:22.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2674, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tah1l71br2opihsogi3xwk34.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,zMWcGYnGdk6lVqVNuFwJHw", + "FriendlyName": "NFL New York Giants Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:28.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2666, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b7zfebrr9rbe6rsoq8y16fx7w.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,jYJehFukzUCHKzWQAaM3uw", + "FriendlyName": "NFL New York Jets Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:23.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2667, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9j2025b6j83s88cooyi6ui7g8.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,4viU0bMT1UuasZHD8euFKg", + "FriendlyName": "NFL Philadelphia Eagles Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:24.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2668, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2aupjxiovt27lh2rkk8a4q4h8.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,YePtDvVnOk6cZkDBVIv-PA", + "FriendlyName": "NFL Pittsburgh Steelers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:24.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2669, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4pa7azn3fyrni6imh3ip7dpg.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,q-Gjg0zzI0mTHxjn8wi5HQ", + "FriendlyName": "NFL San Francisco 49ers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:25.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2670, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4lmbw9jnhj1x5cb2dj6y7wdx.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QD-SRdKijU2RVu60TcU_jw", + "FriendlyName": "NFL Seattle Seahawks Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:25.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2671, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4c9qxd5hhuxj27pqdxfocik1g.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,3m5Q5crrHUGZJ9_GuxubQw", + "FriendlyName": "NFL Tampa Bay Buccaneers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:26.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2672, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9u34u3inzwswkkhcwc7snict0.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XNTUcdaSVEuS3rbzwIwYNg", + "FriendlyName": "NFL Tennessee Titans Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:27.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2673, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ynu85lp1m1sqc2r1upivy5ox.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FnezvbcGD0-oyFOTkpUo7g", + "FriendlyName": "NFL Washington Commanders Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:27.877Z", + "AvatarItemIdSource": "Existing catalog" + } +] diff --git a/Server/RadicadeServer-Config/consumables.json b/Server/RadicadeServer-Config/consumables.json new file mode 100644 index 0000000..f8d4eb2 --- /dev/null +++ b/Server/RadicadeServer-Config/consumables.json @@ -0,0 +1,524 @@ +[ + { + "ConsumableItemDesc": "frOMH6WxDEG1fBqC4_83vg", + "FriendlyName": "Black & White Camera Film", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "m0bVLwWGj0GuIBSb6wCk6Q", + "FriendlyName": "Dawn Camera Film", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "oG7CdvW7p0-S8sVe9w5vRw", + "FriendlyName": "Dracula Film", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "1c0Djlp090uBDczEobYNQw", + "FriendlyName": "Ghost Beard Film", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "6SyCoJCgo0Wd6qlPlnMOtg", + "FriendlyName": "Goblin King Film", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "Il4VmrnjDkqjmjzddqoIEw", + "FriendlyName": "Jumbotron Film", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "A5M-yf9tgUihq1uab3v58g", + "FriendlyName": "Sepia Camera Film", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "mL3zCEuy2UWZxAV4V-OsMA", + "FriendlyName": "Witch Film", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "-hy0qD-iUk-v4NHxNzanmg", + "FriendlyName": "High Five O'Gold ", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "YEfbJTnsR0yT_p7e7tb_kQ", + "FriendlyName": "High Five O'Gunpowder", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "xHOwjwpXd0GDkvBz2VqieA", + "FriendlyName": "High Five O'Lasers", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "VQSgL2pTLkWx4B3kwYG7UA", + "FriendlyName": "High Five O'Spells", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "lag2tZyB90W04lQ7ol4vMw", + "FriendlyName": "High Five O'Whip", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "xwQWBB_fekmTqRc2LB92cg", + "FriendlyName": "87 Flavor Cake", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "ZuvkidodzkuOfGLDnTOFyg", + "FriendlyName": "Assorted Donuts", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "JxAl1wVJ1UGAjoT3m3ilug", + "FriendlyName": "Bubble Tea", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "iiGTvhOCHkOTNJhb16Zbyw", + "FriendlyName": "Bubbly", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "Efd3_dy4ZEqWr7i6j5x0aA", + "FriendlyName": "Cake Pops", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "EmPvh3I6L0uK_1i8Wy_ylQ", + "FriendlyName": "Candy Apples", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "Sk_1sm88ZU2zpWn01Lv7hw", + "FriendlyName": "Celebration Cake", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "5hIAZ9wg5EyG1cILf4FS2A", + "FriendlyName": "Cheese Pizza", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "BbJb1_0g4EGJP_CeNrjpew", + "FriendlyName": "Chocolate Cake", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "mMCGPgK3tki5S_15q2Z81A", + "FriendlyName": "Chocolate Frosted Donuts", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "lWN_g4OcN0S0C4n6cSZq4g", + "FriendlyName": "Chocolate Popcorn", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "K9RbXo-4U0q6NbGu8VL1Sw", + "FriendlyName": "Chocolate Variety Box", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "KFd3wxfnfUybCVkfj1pz1A", + "FriendlyName": "Creator Bubbly", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "3Z-9qU4n2EuNRzRNl_pqcA", + "FriendlyName": "Dessert Pizza", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "uswFSANaZES0EJMLfOgNSw", + "FriendlyName": "Flaming Popcorn", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "6F-kHEhKp0-jYli7fP5yIQ", + "FriendlyName": "Forbidden Bubbly", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "F4SOI0n8k0eo21jvbxsERA", + "FriendlyName": "Forsaken Bubbly", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "7OZ5AE3uuUyqa0P-2W1ptg", + "FriendlyName": "Glazed Donuts", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "_jnjYGBcyEWY5Ub4OezXcA", + "FriendlyName": "Hawaiian Pizza", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "-_VNAj3P10aWCCJZoVTF4Q", + "FriendlyName": "Invasion Donuts", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "P15H1ONBhk-5DYYjid1ttg", + "FriendlyName": "Latte", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "TG_VYTUdvkeT6zG6o1yZlw", + "FriendlyName": "Pastel Pâtisserie", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "mq23W-RSP0G8iGNLdrcpUw", + "FriendlyName": "Pepperoni Pizza", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "QRx0aSTT9keMFdAJMQHdTg", + "FriendlyName": "Popcorn", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "4TGctDRPiE2GWiP3CKQqLw", + "FriendlyName": "Premium Bubbly", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "YNaldB09Kk2kiWVzrmNUGQ", + "FriendlyName": "RecCon Bubbly", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "hUfeoD0ESUitHLG7juSqmA", + "FriendlyName": "RecConBubblyBlue", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "uMHrUPLYFk2rJOW_uop5Aw", + "FriendlyName": "Red Apples", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "LwaotjVEBUir0-w8126n_g", + "FriendlyName": "Red Velvet Cake", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "JfnVXFmilU6ysv-VbTAe3A", + "FriendlyName": "Root Beer", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "InQ25wQMGkG_bvuD5rf2Ag", + "FriendlyName": "Salted Pretzels", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "jdrRJ7deWk2P9aRRrPy3zw", + "FriendlyName": "Snowman Donuts", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "wUCIKdJSvEmiQHYMyx4X4w", + "FriendlyName": "Supreme Pizza", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "bN7kzl_F2E-U2iWE_3m0Aw", + "FriendlyName": "Surve Bubbly Orange", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "kP6U6RYXf0yhEGhIFz7T7g", + "FriendlyName": "Surve Bubbly Purple", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "lFivdMXwHk2eMhmngMB8yA", + "FriendlyName": "Survey Bubbly", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "4My42w1D1Uu3-98pXB-x_Q", + "FriendlyName": "Sushi", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "YpL6eN6sKEquOYIYnbhvew", + "FriendlyName": "VR Cola", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "ZtZnYBpKkECJlhHmkj4MiA", + "FriendlyName": "KO - Bear Claw", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "g3kxdlJv5kO8PuBXveM48w", + "FriendlyName": "KO - Bees", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "wx--2TPTdEuGAqLCjs9Qag", + "FriendlyName": "KO - Cola", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "XOZcxx-Klkyhe-MDbTqiwA", + "FriendlyName": "KO - Fangs", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "uGVFydNSokCXFAmXu3aceQ", + "FriendlyName": "KO - FryingPan", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "EAhk3ZZdXEmH5wRAXXT24Q", + "FriendlyName": "KO - Grenade", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "Av-wvjXvvkmNVSz7ZZnTiA", + "FriendlyName": "KO - Ranger", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "5AJin8T2iEG7BzOPOgx2HA", + "FriendlyName": "KO - Shield", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "U38Qe6rhEk6mFvArHfYjng", + "FriendlyName": "KO - Skull Wings", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "yvqSbK2czkS2sUCRdrGaEw", + "FriendlyName": "KO - Star Power", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "J1WqFNUWo0OBi4LGKPDHWw", + "FriendlyName": "KO - Tire Track", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "hRdK6NGfAkGBmYSs_xGzXg", + "FriendlyName": "Sparklers", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "xr45B5QC4EyKXThtwZto_A", + "FriendlyName": "Bucket Orange Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "JwJeh15cjkOc9WWaGRibDQ", + "FriendlyName": "Butterfly Blue Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "3R1bzI35fkChoFFRFbponQ", + "FriendlyName": "Cauldron Crimson Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "x9ntSHto50GpNWPwYCLUEg", + "FriendlyName": "Circuit Cerulean Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "4L3JoaqkAUa1kz98nciPXw", + "FriendlyName": "EDM Emerald Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "YaVzcoefhk6zjbNDELLx8A", + "FriendlyName": "Frontier Forest Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "iEtWDLmv7ES87J4NE_TGJQ", + "FriendlyName": "Gizmo Grape Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "jA3zzS31zEitBxtRCk7oew", + "FriendlyName": "Goblin Green Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "P7HixsdkskmzgFMdFJ-WdA", + "FriendlyName": "H2Oasis Blue Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "5Ugg8k19n0WH8GPFCDWTCQ", + "FriendlyName": "Jungle Jade Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "P1kCuPlRc0-NAg6JUlEH_g", + "FriendlyName": "KREQ Red Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "Hh_O_RejGE-_PubyM8xHaw", + "FriendlyName": "Lounge Lavender Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "s2pxukIEt0uDeGVJmZayOw", + "FriendlyName": "Maker Mauve Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "vXL0rrAk70SFxJlaJk0uqQ", + "FriendlyName": "Mousebot Magenta Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "UAOH6ccEbEmehJg8XW_D8w", + "FriendlyName": "Paula's Periwinkle Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "o0hG5O_eaUO0R8vPyW-Hvg", + "FriendlyName": "Pirate Gold Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "xRtKWR-D40GUW43R0EFpNg", + "FriendlyName": "Propulsion Purple Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "AQTYAh1it0GIGdgGO5sUHQ", + "FriendlyName": "PVPeach Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "8vlVslaWaUyFS-iDHhxW9g", + "FriendlyName": "Ranger Roy-Al Blue Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "STFKahjHJ0SQJPfoDe4S1g", + "FriendlyName": "Rebel Rose Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "eJh_BQ5y4UWwiMJzBKcwMQ", + "FriendlyName": "Recreational Raspberry Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "inIuPzhhOEmz1RI8mZtSLg", + "FriendlyName": "Standee Sandy Hair Dye", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "50oCPkzd3EerBd7nYNdkCw", + "FriendlyName": "20% Off Coupon", + "Tooltip": "", + "Rarity": 0 + }, + { + "ConsumableItemDesc": "Tpxqe_lycUelySRHM8B0Vw", + "FriendlyName": "Ego Potion", + "Tooltip": "", + "Rarity": 0 + } +] diff --git a/Server/RadicadeServer-Config/equipment.json b/Server/RadicadeServer-Config/equipment.json new file mode 100644 index 0000000..a5624f7 --- /dev/null +++ b/Server/RadicadeServer-Config/equipment.json @@ -0,0 +1,2872 @@ +[ + { + "PrefabName": "[DiscGolfDisc]", + "ModificationGuid": "19ef59c7-f74b-4c63-935a-1d4b1abd8518", + "FriendlyName": "Disc (Coop)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DiscGolfDisc]", + "ModificationGuid": "2b70a76e-6f48-402e-90ff-40b06aa23eb8", + "FriendlyName": "Disc (Coach Select)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DiscGolfDisc]", + "ModificationGuid": "9d2a3618-12b3-4ca0-be59-ed15d7926d77", + "FriendlyName": "Disc (Laser)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DiscGolfDisc]", + "ModificationGuid": "7877964e-3a2b-4818-8513-0e26a707bd7c", + "FriendlyName": "Disc (Clear)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DiscGolfDisc]", + "ModificationGuid": "bd3f6aa5-c6e0-4e06-901d-1176accf1003", + "FriendlyName": "Disc (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DiscGolfDisc]", + "ModificationGuid": "5d3c3b16-713e-43d8-88e0-7633063091da", + "FriendlyName": "Disc (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "bc4254be-60e0-4cc5-a233-6a0bd4137118", + "FriendlyName": "Paint Pistol (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "33c84c50-7c2e-4a80-ad06-79c4f20f829e", + "FriendlyName": "Paint Pistol (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "7ef5d1d9-24db-428c-85e0-107c1bfcc026", + "FriendlyName": "Paint Pistol (Orange)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "4aff20ee-0b20-434b-a030-d3e1764fe4e7", + "FriendlyName": "Paint Pistol (Purple)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "36751c4a-86a9-40e1-bef2-84f962ab678d", + "FriendlyName": "Paint Pistol (Vitton)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "ec5ef0f3-c072-45ec-bb8f-3781fb16e067", + "FriendlyName": "Paint Pistol (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "b8d5612b-2c6f-46d6-9412-decddac7d4c1", + "FriendlyName": "Paint Pistol (Pirate)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "Dg9PFcg-JUia72PnYJqkQg", + "FriendlyName": "Paint Pistol (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "z0Xw8_BUZUODrGHSZcLHHQ", + "FriendlyName": "Paint Pistol (Valentines)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun]", + "ModificationGuid": "p70jO7t0OEyaCWGrpUQa0Q", + "FriendlyName": "Paint Pistol (Watermelon)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShotgun]", + "ModificationGuid": "61e49c13-b414-425f-8e8b-bdcf3d12a5bf", + "FriendlyName": "Paint Shotgun (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShotgun]", + "ModificationGuid": "f27af5d7-c741-4457-8c98-8e8791f1a41c", + "FriendlyName": "Paint Shotgun (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShotgun]", + "ModificationGuid": "053c24a0-82a2-43f5-b0e7-e56db05a10ba", + "FriendlyName": "Paint Shotgun (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShotgun]", + "ModificationGuid": "ZMAx_-B_7kWy6-TLXx8g6Q", + "FriendlyName": "Paint Shotgun (Watermelon)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShotgun]", + "ModificationGuid": "CwIBKjm3G0-xUGt_9Gf7UQ", + "FriendlyName": "Paint Shotgun (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShotgun]", + "ModificationGuid": "HSAwF_uBgkC0mBXyLSayWA", + "FriendlyName": "Paint Shotgun (Cactus)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "5dcf8a2e-6fa6-4007-ac1e-73dd53c4c247", + "FriendlyName": "Basketball (Purple)", + "Tooltip": "", + "Rarity": 1, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "e6899422-ab2f-4bd9-8e98-aef950ec27b6", + "FriendlyName": "Basketball (Blue)", + "Tooltip": "", + "Rarity": 1, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "d6d22997-fe39-4ffa-b1b3-4955368c829a", + "FriendlyName": "Basketball (Vitton)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "56582acf-c7a0-4aff-b3aa-2fdea4d254a3", + "FriendlyName": "Basketball (Rainbow)", + "Tooltip": "", + "Rarity": 1, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "56c9a6db-0f54-482c-aba9-e265f899771d", + "FriendlyName": "Basketball (Coach Select)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "WOWwmg7jg0mH2g4LJUp6fA", + "FriendlyName": "Basketball (Cozy)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "bNEcMJOzokeEVKDeXk_cDQ", + "FriendlyName": "Basketball (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "G0b2rKv0iEyXCEUVRcxmTQ", + "FriendlyName": "Basketball (NBA)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "Vit6nO2nHEqvoYLf_6jHQw", + "FriendlyName": "Basketball (NBA 75)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "Q7kLwTIpY0yO_EKYnPWEzA", + "FriendlyName": "Basketball Cute Cat", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Basketball]", + "ModificationGuid": "mgpkPuEekUK20ZAnR66fOg", + "FriendlyName": "Basketball Cute Cat Gray", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "174adb53-bc6e-4fcf-b03d-a00fc3deeb49", + "FriendlyName": "Paint Shield (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "fd367329-b8b2-4d44-825a-da16092751e3", + "FriendlyName": "Paint Shield (Purple)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "a6c869d9-8f30-4ce6-a7ea-cb7ea9f9d492", + "FriendlyName": "Paint Shield (Pirate)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "jZ7wlwRxgUuWYuLasbnG8w", + "FriendlyName": "Paint Shield (80s Sunset)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "QTyRLpDB3UuReHQqrKxb5A", + "FriendlyName": "Paint Shield (Gingerbread)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "gfGhbEMx_kCRdO-6vPOh3g", + "FriendlyName": "Paint Shield (Paint Shield Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "ClJApV0LC0mxh0UiYhaXGQ", + "FriendlyName": "Paint Shield (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "TleGVT9enUigvfBoWz6kvA", + "FriendlyName": "Paint Shield (Galaxy)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "nb1BXq0jxE2nXihd0RBgLw", + "FriendlyName": "Paint Shield (Halo)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballShield]", + "ModificationGuid": "a59392cc-8027-43b8-83f5-4a5106bbd4aa", + "FriendlyName": "Paintball Shield Gold", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "a9a3292c-8ccf-4564-aeb1-124c52057ef2", + "FriendlyName": "Shield (Black Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "2389fce4-3ab2-4e05-918c-845eb8538bed", + "FriendlyName": "Shield (Goblin)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "93f5418e-a03d-4074-804c-daa0e374dd9f", + "FriendlyName": "Shield (Purple)", + "Tooltip": "", + "Rarity": 1, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "735b06c6-1b78-4f60-9291-a414a96cd228", + "FriendlyName": "Shield (wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "affbab2d-3200-4512-a140-685fdc5570c6", + "FriendlyName": "Shield (Yellow)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "a363ba1b-2f70-4c5f-943a-f45cfa59122b", + "FriendlyName": "Shield (Rock)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "eb90210a-6d2d-4429-b1e0-3285333685fc", + "FriendlyName": "Shield (Valentine)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "439be0eb-4d1e-4c80-97f8-b4636d0ce94b", + "FriendlyName": "Shield (Pride)", + "Tooltip": "", + "Rarity": 1, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "k0BVix46IEmgVrqUmFU3-Q", + "FriendlyName": "Shield (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "RQCgWA3Pf0KTqznUR58-lw", + "FriendlyName": "Shield (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "-rbKD_ztMUq69Nv4tLrrVA", + "FriendlyName": "Shield (Snowflake)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "L9usPszLFkiA6xs32es2Lw", + "FriendlyName": "Shield (Electric Bloom)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "_uaO1FvF5Euw3tMPGfPo2A", + "FriendlyName": "Shield (Sunflower)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "ff08af8b-2f30-4f35-86f5-de1b40ed799e", + "FriendlyName": "Quest Shield Color Crazy", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestShield]", + "ModificationGuid": "b52450a6-b07f-4e5d-91f7-e982df08eba4", + "FriendlyName": "Shield Gold", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "33a7a3dc-5266-4075-b56f-5eaa2c400e9f", + "FriendlyName": "Laser Pistol (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "db91b61b-ab32-4895-8f5a-0212c1283cbb", + "FriendlyName": "Laser Pistol (Vitton)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "89b1fbbc-e7be-440b-8b22-8fd379c1a568", + "FriendlyName": "Laser Pistol (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "8a2a3219-fed0-4403-9061-451d162307c2", + "FriendlyName": "Laser Pistol (Candy Cane)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "5nQ2kF6h6kunbk5z18Lozg", + "FriendlyName": "Laser Pistol (Frankenblaster)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "0cFcVYCLTU6CKhr9uADrbw", + "FriendlyName": "Laser Pistol (Recasso)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "aByJShU520OzdOXBV8o0Dg", + "FriendlyName": "Laser Pistol (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "dRvcMuM__02iMsRwKEPtXQ", + "FriendlyName": "Laser Pistol (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Pistol]", + "ModificationGuid": "fdqopuA1qUiK_jg-G5gr3w", + "FriendlyName": "Laser Pistol (Galaxy)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_AutomaticGun]", + "ModificationGuid": "546b3a57-13d4-4c35-ab3a-bbb7d466f0b8", + "FriendlyName": "Laser Burst Gun (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_AutomaticGun]", + "ModificationGuid": "bafacc36-02c6-408c-a3f5-8c2aa07a68db", + "FriendlyName": "Laser Burst Gun (Arbor)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_AutomaticGun]", + "ModificationGuid": "u60IW1Gt10eC-8SlZmQUNQ", + "FriendlyName": "Laser Burst Gun (Recasso)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_AutomaticGun]", + "ModificationGuid": "dEmh0tniCkeYBY1EdD09jA", + "FriendlyName": "Laser Burst Gun (Zombie)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_AutomaticGun]", + "ModificationGuid": "jzgz_HrPAUqOeqx3uNXVUA", + "FriendlyName": "Laser Burst Gun (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_AutomaticGun]", + "ModificationGuid": "TK9ybLf7xUSi6F1bVfyaRg", + "FriendlyName": "Laser Burst Gun (Physics Construction)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "55a9c1e8-07e2-4457-86f2-75fd5e67b1f3", + "FriendlyName": "Laser Shotgun (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "83d7cca0-2c5d-41b2-a5b2-f79fd89f3037", + "FriendlyName": "Laser Shotgun (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "bb36b722-16dc-4172-b61f-237dc1240040", + "FriendlyName": "Laser Shotgun (Jack-O-Lantern)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "cd497ae2-6382-42f2-9f92-93d5141588c3", + "FriendlyName": "Laser Shotgun (Valentine)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "pIRihZheSEW2Ld3VTLkB-g", + "FriendlyName": "Laser Shotgun (Laser Shotgun Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "gjuFgDp-LUWPi1mPCKyqbA", + "FriendlyName": "Laser Shotgun (Shamrock)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "xHEZCBn5D0iUxIDp3swd9g", + "FriendlyName": "Laser Shotgun (Galaxy)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "5wzKTogcskSYvv1HaiU57Q", + "FriendlyName": "Laser Shotgun (Alien Blaster)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "a3a0c2ca-0bd8-4c93-8adb-debf755da05d", + "FriendlyName": "Shotgun Sci Fi Gold", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_Shotgun]", + "ModificationGuid": "40832fe4-1784-4e39-a43f-2103db40edae", + "FriendlyName": "Quest Sci Fi Shotgun Fall Fest", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_RailGun]", + "ModificationGuid": "63a867d7-ccea-4905-b6e0-e6dcfba06f9e", + "FriendlyName": "Laser Railgun (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_RailGun]", + "ModificationGuid": "46977ca7-ee75-4e2a-b8aa-0763879f8a96", + "FriendlyName": "Laser Railgun (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_RailGun]", + "ModificationGuid": "X9gTAbp0Sk6nadpTBseRrg", + "FriendlyName": "Laser Railgun (Lifeguard)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_RailGun]", + "ModificationGuid": "_flhynGnykyZL7F83B7rJQ", + "FriendlyName": "Laser Railgun (Laser Railgun Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_RailGun]", + "ModificationGuid": "uvbozCned0igzscgeBlRpQ", + "FriendlyName": "Laser Railgun (Galaxy)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_SciFi_RailGun]", + "ModificationGuid": "Z9kKHu5I90-pG45NrWntoQ", + "FriendlyName": "Quest Sci Fi Rail Gun Shark Shooter", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "c4d70545-cdf1-4fb1-924b-613de0327faf", + "FriendlyName": "Crossbow (Vitton)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "6bba14be-f207-4933-b09f-f0fd3e28c10c", + "FriendlyName": "Crossbow (Candy Cane)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "1d09c8e4-78c2-49f9-acb7-8f0b3ff6a5ed", + "FriendlyName": "Crossbow (Recasso)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "MsFoMeUZ8Eq2qX9SrOtRPA", + "FriendlyName": "Crossbow (Waves)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "T6PzFfg41UaQ7EAqts6gsw", + "FriendlyName": "Crossbow (Bone)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "jHaoDSAtikyAI82lO53o9g", + "FriendlyName": "Crossbow (Dryad)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "ESQ0qOlgrkCc8MbUXGnF8A", + "FriendlyName": "Crossbow (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "QEwDZYj_OEi5l86LBnuYGw", + "FriendlyName": "Crossbow (Rock)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "VHFKZVhiRkylcxnxzCKUuw", + "FriendlyName": "Crossbow (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "Ys9UQ5b7fEyKxnaek-ihfQ", + "FriendlyName": "Crossbow (Dryad Fall)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "CZAIad-ME0O0iIy77MRxBw", + "FriendlyName": "Crossbow (Winter)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow]", + "ModificationGuid": "ych0ntYcK0eiq67bCLlbew", + "FriendlyName": "Crossbow (Spring)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "f5901f11-b315-4261-9474-2da207e4a229", + "FriendlyName": "Bow (Vitton)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "52c6a138-10c5-4aab-9c3b-04b50c9a2dc5", + "FriendlyName": "Bow (Ghost)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "e738d936-0f75-423b-88bb-f626ffba573a", + "FriendlyName": "Bow (Rock)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "XpxqrY6RYkafs-sd3Z0ZLw", + "FriendlyName": "Bow (Rainbow)", + "Tooltip": "", + "Rarity": 1, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "JBp7CxPhrUC5hnn2EISUFA", + "FriendlyName": "Bow (Dryad)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "T8666vuehkObksBdVbla9g", + "FriendlyName": "Bow (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "YajcGvBcTEaf_MjCbxOciw", + "FriendlyName": "Bow (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "XKFuhM3zikyxTYtTzuPb_A", + "FriendlyName": "Bow (Dryad Fall)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "uPwTBnfWwkWXuyAaAMqeQA", + "FriendlyName": "Bow (Winter)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "gl_tlo_t7U-H308Omy13lw", + "FriendlyName": "Bow (Spring)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Longbow]", + "ModificationGuid": "1c09c79b-cb6e-4296-8609-e0a2bdb9952d", + "FriendlyName": "Longbow Autumn", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "79090a4b-e191-4788-9210-88a7bdf6d828", + "FriendlyName": "Maker Pen (Luxury)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "204f84a2-6ee2-45e1-9168-87f43e893e32", + "FriendlyName": "Maker Pen (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "wultE5yRpkuJREeJw4prKg", + "FriendlyName": "Maker Pen (Dragon)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "HyL83wPtf06DG6nsHofoeA", + "FriendlyName": "Maker Pen (Steampunk)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "juFHwn9UjEiGaWyNnoxbtA", + "FriendlyName": "Maker Pen (Pumpkin)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "Pw1XcUfAck2tNh0fIAu8LA", + "FriendlyName": "Maker Pen (Waves)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "4HJ3wRmSZUuhMRUTA67dpA", + "FriendlyName": "Maker Pen (Professor)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "i3NgZvTwkUSMsXRpqCgSZA", + "FriendlyName": "Maker Pen (Wonderland)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "BGhlN3FKGEy6w-vwc28V4A", + "FriendlyName": "Maker Pen (Beach)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "BwIscV_YnE6iG4brl389Hg", + "FriendlyName": "Maker Pen (Mystery)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "JtMeQRvo00yVwAliICTP0w", + "FriendlyName": "Maker Pen (Movie Magic)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "h4xKGsHcTUy3iH7PVFjb2w", + "FriendlyName": "Maker Pen (Daytime)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "cUBvDvybvEu5eZDPn57ExQ", + "FriendlyName": "Maker Pen (Night)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "VYF9NGZBgU6TOo9ayjgBEQ", + "FriendlyName": "Maker Pen (Green)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "WwtFcRluQkmnCI_qH75L_Q", + "FriendlyName": "Maker Pen (Orange)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "iRtlLKI-X0S9oYUWLenAKQ", + "FriendlyName": "Maker Pen (Purple)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "tvjYGeRdwEqnr9mf84WgcQ", + "FriendlyName": "Maker Pen (Red)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "Q7Tr_0DXAkOdr61UXiocow", + "FriendlyName": "Maker Pen (Yellow)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "c1VRbmDGHUS7KiSgwj5prQ", + "FriendlyName": "Maker Pen (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "MODdVYrjaEy-iy6quALNuA", + "FriendlyName": "Maker Pen (White)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "H91DVbHzR06oie8fLjdE1A", + "FriendlyName": "Maker Pen (Hidden Worlds)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "RPWXRfZwfUeC9PFlvcbmxw", + "FriendlyName": "Maker Pen (Carnival)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "j83FD3aSQ0OUcrp4QIIZsA", + "FriendlyName": "Maker Pen (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "L12C6OfxB0y53bLXZQ4rFg", + "FriendlyName": "Maker Pen (Contest Stranded)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "xDSDJJX2w0ySwtiE6w6ghg", + "FriendlyName": "Maker Pen (Maker Pen Contest Folklore)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "_rlnwZWJrk2eiQi4f_FhWQ", + "FriendlyName": "Maker Pen (Premium)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "ht4zv6ZFi0yrohyBm39o6A", + "FriendlyName": "Maker Pen (Contest Summer Camp)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "TpaRhLqVc02Oyr769imtnQ", + "FriendlyName": "Maker Pen (Diamond Universe)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "Z_mBcmlIHUWE0ox9Dn_kHA", + "FriendlyName": "Maker Pen (Solstice)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "OcixCg4jbUyLb6DFSTjrPg", + "FriendlyName": "Maker Pen (Rhythm And Rooms)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[MakerPen]", + "ModificationGuid": "d7f37e87-cbb4-4f0b-89a6-f4aea971710a", + "FriendlyName": "Maker Pen Makerpen Pattern", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballRifleScoped]", + "ModificationGuid": "6a6b0ecf-286d-4361-92ff-7027c440dc3c", + "FriendlyName": "Paint Scope Rifle (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballRifleScoped]", + "ModificationGuid": "d906709c-e08e-4273-8ba1-e072e0d25957", + "FriendlyName": "Paint Scope Rifle (Candy Cane)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballRifleScoped]", + "ModificationGuid": "3rBevbIk1EusFnFaXAh3qA", + "FriendlyName": "Paint Scope Rifle (Gingerbread)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballRifleScoped]", + "ModificationGuid": "OSrMlhVVSEqb-0AzVzOH9w", + "FriendlyName": "Paint Scope Rifle (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballRifleScoped]", + "ModificationGuid": "0dM2SfqGR0SmtO5ufTWfUQ", + "FriendlyName": "Paint Scope Rifle (Comic)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballRifleScoped]", + "ModificationGuid": "btB2z7ybskSYrn9Nzhfhxg", + "FriendlyName": "Paint Scope Rifle (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballRifleScoped]", + "ModificationGuid": "b_iiMMYayU-an7hOlCzPIA", + "FriendlyName": "Paint Scope Rifle (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow_Hunter]", + "ModificationGuid": "d217ee4c-22f3-4f33-bf7c-9d4ee9c30e29", + "FriendlyName": "Crossbow Hunter (Rock)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow_Hunter]", + "ModificationGuid": "cFMh54GoHEeHZEERSRR8kQ", + "FriendlyName": "Crossbow Hunter (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow_Hunter]", + "ModificationGuid": "RRCvths6eUmAAr9hzGFT0g", + "FriendlyName": "Crossbow Hunter (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow_Hunter]", + "ModificationGuid": "-X_Hm6dIekqoHTY2VMMLeA", + "FriendlyName": "Crossbow Hunter (Shark)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Crossbow_Hunter]", + "ModificationGuid": "BSO9Ahi-2EiwYxE7lkHhlA", + "FriendlyName": "Crossbow Hunter (Valentines)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "dHcxKoPSLUiDSSDPd_07WQ", + "FriendlyName": "Quest Sword Pixel", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "0ee04d78-5a17-4a38-ae67-b576a41fc200", + "FriendlyName": "Sword (Extra)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "431567d2-4df2-43eb-b632-3d79aa7c013c", + "FriendlyName": "Sword (Classic)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "f03dee6a-79b8-4906-9abb-a821748d97a9", + "FriendlyName": "Sword (Red)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "3e46fb2d-bb3f-42ed-83ef-f5716860725c", + "FriendlyName": "Sword (Rock)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "eecdf81c-7ba2-40ba-9d6c-f461708cce16", + "FriendlyName": "Sword (Comic)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "SbEIObEmhkKQniANHqNekg", + "FriendlyName": "Sword (Pumpkin)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "Kitcy2AVB0-OdljiEAI2Pw", + "FriendlyName": "Sword (Recasso)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "K9TzHZPDwU-ewzLYkr_1cg", + "FriendlyName": "Sword (Gingerbread)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "wioj1rR1lkCx7f-oiCHcOw", + "FriendlyName": "Sword (Corn Cob)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "m1JMjJYpSUiXj5897orm2Q", + "FriendlyName": "Sword (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "d97pbQMk9UWpEN0yoOE9Rg", + "FriendlyName": "Sword (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "DRXoMS9phEG2fkasja7sEw", + "FriendlyName": "Sword (Lava)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "uLNdhrrAC0ybxC2XkBnnVQ", + "FriendlyName": "Sword (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "-ybFpUxTsUKWFSIS-8fOOw", + "FriendlyName": "Sword (Electric Bloom)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "X8B2M3hltEKyEnX0dcJi7Q", + "FriendlyName": "Sword (Umbrella)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "SRl7xh9z8kGU2OO5viCykw", + "FriendlyName": "Sword (Clarinet)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[QuestSword]", + "ModificationGuid": "RK4BY5eyZ0aMLhVRCvGi6A", + "FriendlyName": "Quest Sword Rubber Chicken", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Grenade]", + "ModificationGuid": "993f8b81-cb2e-44e4-88a1-b841af1f0e69", + "FriendlyName": "Grenade (Pirate)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Grenade]", + "ModificationGuid": "vlpFc1j820i5n7Y2sTW8Vw", + "FriendlyName": "Grenade (Grenade Chick)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Grenade]", + "ModificationGuid": "0tfDNC0I2kiEVrJttrE0bw", + "FriendlyName": "Grenade (Gum Drop)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Grenade]", + "ModificationGuid": "8Ex4aO2GpEqwvow6bEu_ag", + "FriendlyName": "Grenade (Spring Egg)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "e8e42ef6-9ab6-44f4-84d5-117154a4d13e", + "FriendlyName": "Quest Goblin Wand (Ghost)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "yG6xFxkoS0iIDt7wEj5Hqw", + "FriendlyName": "Quest Goblin Wand (Witch Star)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "3pE7zA-DjkqP1Ch7__-31w", + "FriendlyName": "Quest Goblin Wand (Ice)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "m_otHTEKF0KQljsOc-JENg", + "FriendlyName": "Quest Goblin Wand (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "CyrdBvIbXUq_TsXFliWk1A", + "FriendlyName": "Quest Goblin Wand (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "vfzrRHn24E67BK8Bgy60xA", + "FriendlyName": "Quest Goblin Wand (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "QnLM4Qw-L0ml_mgReFweIw", + "FriendlyName": "Quest Goblin Wand (Trident)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "M6BHcdDZnU6UEDa3wLUUhg", + "FriendlyName": "Quest Goblin Wand (Butterfly)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Quest_Goblin_Wand]", + "ModificationGuid": "7Kdm8AYIkky-N1biw5ljPg", + "FriendlyName": "Quest Goblin Wand (Flowers)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGrenadeLauncher]", + "ModificationGuid": "502f40c3-1c00-4c3f-bfc7-c897df8af37e", + "FriendlyName": "Paint Launcher (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGrenadeLauncher]", + "ModificationGuid": "f34e6270-bcfa-42f2-80d5-69c1bbd34983", + "FriendlyName": "Paint Launcher (Comic)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGrenadeLauncher]", + "ModificationGuid": "8ZAwSDtXlkqlnBLOpfVTVg", + "FriendlyName": "Paint Launcher (Gingerbread)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGrenadeLauncher]", + "ModificationGuid": "3UmIhvqmkU-aWxFDFd4QDg", + "FriendlyName": "Paint Launcher (Honey)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGrenadeLauncher]", + "ModificationGuid": "pPpYY9j0Dku2gtbKoUOCCg", + "FriendlyName": "Paint Launcher (Easter Egg)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGrenadeLauncher]", + "ModificationGuid": "9gBl778RvUeSC8837Gurog", + "FriendlyName": "Paint Launcher (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGrenadeLauncher]", + "ModificationGuid": "tcMMGKcmhkuM82ISCZ-3AQ", + "FriendlyName": "Paint Launcher (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGrenadeLauncher]", + "ModificationGuid": "L-_iZgicHUajydSvUWVJhw", + "FriendlyName": "Paint Launcher (Jumbotron)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "6261a846-e4f8-4e6e-8aa0-68f38cd788d4", + "FriendlyName": "Dodgeball (Comic)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "6ff63f8f-4bcc-4b69-8d89-8b886d19e239", + "FriendlyName": "Dodgeball (Recasso)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "f55dda45-c17e-4237-a638-9f326d306e7d", + "FriendlyName": "Dodgeball (Goblin)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "Jkg-OGXdIUyfEwaQIh6EGA", + "FriendlyName": "Dodgeball (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "lIpyvEMWqUyv41gTp3YZ1A", + "FriendlyName": "Dodgeball (Pumpkin)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "Ms8LD3TqBkiukhUlHxVBfw", + "FriendlyName": "Dodgeball (Billiards)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "dpTV4UmrJkCsz6WXD6YeKQ", + "FriendlyName": "Dodgeball (Cactus)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "g5JrCDryI0-mxlbLMRYzHA", + "FriendlyName": "Dodgeball Ball Beach", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[DodgeballBall]", + "ModificationGuid": "ogArfnzCX0yQ_dqUoa3Yjw", + "FriendlyName": "Dodgeball Ball Coconut", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[SoccerShield]", + "ModificationGuid": "fae1abe3-6bfd-4407-b34e-e2c1b3b03032", + "FriendlyName": "Soccer Shield (Gingerbread)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecRoyale_Backpack]", + "ModificationGuid": "523e3615-4633-41a3-9b2d-17d3207a684b", + "FriendlyName": "Backpack (Camo)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecRoyale_Backpack]", + "ModificationGuid": "6c3b26cd-b38e-492b-a124-759cbcec1396", + "FriendlyName": "Backpack (Camping Pattern)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecRoyale_Backpack]", + "ModificationGuid": "12b54ad2-3847-41db-9ed4-88e8dad63024", + "FriendlyName": "Backpack (Tiger)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecRoyale_Backpack]", + "ModificationGuid": "5c848a04-f528-423b-8be1-4b5cd43a75c2", + "FriendlyName": "Backpack (Denim)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecRoyale_Backpack]", + "ModificationGuid": "e93f5ab7-e9bf-4652-8a7c-2ae120f25f87", + "FriendlyName": "Backpack (Recasso)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecRoyale_Backpack]", + "ModificationGuid": "w2NNBTTCVEah2WlP0JHezQ", + "FriendlyName": "Backpack (Valentine)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecRoyale_Backpack]", + "ModificationGuid": "70uy5UJhhEy1aynKM4MAsQ", + "FriendlyName": "Backpack (Fall Leaves)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecRoyale_Backpack]", + "ModificationGuid": "ujZ4Hl0HO06OysqZcqHhmg", + "FriendlyName": "Backpack (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "d218ae45-8534-4f96-b2ed-1cf281bc3578", + "FriendlyName": "Camera (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "e2844f84-ab44-4141-9a0a-bd5da7caa4f6", + "FriendlyName": "Camera (Disposable)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "1838fa61-de10-4e09-b24a-f5a1bc942600", + "FriendlyName": "Camera (Green)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "fSbzGxsjKE-cwtFFX6dJQw", + "FriendlyName": "Camera (Carpet)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "ATGtNjai20miFwBUOVIMuw", + "FriendlyName": "Camera (Two Tone)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "znrJSCX_SkS-kIPqMERoDg", + "FriendlyName": "Camera (Camping)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "g5u0weNLmkCLeUXFUVn74Q", + "FriendlyName": "Camera (Comic)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "QQALCzCF-0ClDWqmmciHAQ", + "FriendlyName": "Camera (Heart)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "SkTa53OzM0u82YPuZAl9aw", + "FriendlyName": "Camera (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "sE4_-ZVa2EC4MZfGGsExzQ", + "FriendlyName": "Camera (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "5dx423rtM0ygWqMBwJHBOQ", + "FriendlyName": "Camera (Retro Gamer)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "T465YyOavEqTqH3s91uBHQ", + "FriendlyName": "Camera (Video Partner)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "SFz15JbasU-gShl_GHnspw", + "FriendlyName": "Camera (Galaxy)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[ShareCamera]", + "ModificationGuid": "1B8utXzBWUSGJ4Va8Hm1xA", + "FriendlyName": "Camera (Holographic)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D4]", + "ModificationGuid": "KiVWBp5_J0qCLuizAJGcAg", + "FriendlyName": "Sandbox D4 (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D4]", + "ModificationGuid": "x3Cv42hIkkaNIy7fHZiWYg", + "FriendlyName": "Sandbox D4 (Bone White)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D4]", + "ModificationGuid": "_2jxSTYSpEykZ_O91oDAzg", + "FriendlyName": "Sandbox D4 (Turquoise)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D4]", + "ModificationGuid": "WxWaXY61mUuNe_f6SgECnA", + "FriendlyName": "Sandbox D4 (Pewter)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D6]", + "ModificationGuid": "TxIu8CUjykGDMr5RIJcNVQ", + "FriendlyName": "Sandbox D6 (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D6]", + "ModificationGuid": "Fn0gcj17c06tA9s98CKwAg", + "FriendlyName": "Sandbox D6 (Bone White)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D6]", + "ModificationGuid": "HnVo08F-zk-YeikERFa1Yw", + "FriendlyName": "Sandbox D6 (Turquoise)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D6]", + "ModificationGuid": "5U_KiCi_90-eWucQ68VdZQ", + "FriendlyName": "Sandbox D6 (Pewter)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D8]", + "ModificationGuid": "AQXXckjNZ0uzPMTcP1C3Kg", + "FriendlyName": "Sandbox D8 (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D8]", + "ModificationGuid": "d_CbCG3DmUCtWGWYHioghg", + "FriendlyName": "Sandbox D8 (Bone White)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D8]", + "ModificationGuid": "f45tTZp4KUextmAsX8X9Hw", + "FriendlyName": "Sandbox D8 (Turquoise)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D8]", + "ModificationGuid": "77cRA0O4GUqd6nr9QLefQA", + "FriendlyName": "Sandbox D8 (Pewter)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D10]", + "ModificationGuid": "ZM543HEyl0G6n7P5rDMEWw", + "FriendlyName": "Sandbox D10 (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D10]", + "ModificationGuid": "GjQwBmNnK0ukv0hEEHK8tw", + "FriendlyName": "Sandbox D10 (Bone White)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D10]", + "ModificationGuid": "SnSOf-GZ2kastpF5KWxT-Q", + "FriendlyName": "Sandbox D10 (Turquoise)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D10]", + "ModificationGuid": "I161QAym2kqPi5wXIHCt8A", + "FriendlyName": "Sandbox D10 (Pewter)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D12]", + "ModificationGuid": "EoV2ZoLR2UWY898p8j7qGQ", + "FriendlyName": "Sandbox D12 (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D12]", + "ModificationGuid": "heFLfpPNd0-nhLOFpUCnpA", + "FriendlyName": "Sandbox D12 (Bone White)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D12]", + "ModificationGuid": "ub6apg3zEUiCp3k81JjQGw", + "FriendlyName": "Sandbox D12 (Turquoise)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D12]", + "ModificationGuid": "2gYYjbVGqkiOefY2xjOxiQ", + "FriendlyName": "Sandbox D12 (Pewter)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D20]", + "ModificationGuid": "Des7txtX0EO-7NiooR0-ow", + "FriendlyName": "Sandbox D20 (Pink)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D20]", + "ModificationGuid": "gXOOKrQEYE-FmS3uXWj1hA", + "FriendlyName": "Sandbox D20 (Bone White)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D20]", + "ModificationGuid": "DrG0ntsyPUSv8ra0m-Hhaw", + "FriendlyName": "Sandbox D20 (Turquoise)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Sandbox_D20]", + "ModificationGuid": "H3vrSi45AU6lshChLnr19Q", + "FriendlyName": "Sandbox D20 (Pewter)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballAssaultRifle]", + "ModificationGuid": "VOOdhP5qg0WWv2m0qLRVCw", + "FriendlyName": "Paint Burst Gun (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballAssaultRifle]", + "ModificationGuid": "c458a6ae-a1ea-468a-89b8-4c9a85cd3cde", + "FriendlyName": "Paint Burst Gun (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballAssaultRifle]", + "ModificationGuid": "m_qzPvDPF0KPhLd59wzr5A", + "FriendlyName": "Paint Burst Gun (Fall)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballAssaultRifle]", + "ModificationGuid": "996662ad-631d-4a81-8a8b-74ace4833279", + "FriendlyName": "Paint Burst Gun (Pirate)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballAssaultRifle]", + "ModificationGuid": "3aaef60d-142f-4a0a-bea3-f5d99ab62dd5", + "FriendlyName": "Paint Burst Gun (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballAssaultRifle]", + "ModificationGuid": "49d88b73-3c8a-4f4d-8a69-128fa5474e82", + "FriendlyName": "Paint Burst Gun (PSPLUS)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballAssaultRifle]", + "ModificationGuid": "357fe573-fee7-467f-93a7-5e61afb024b8", + "FriendlyName": "Paint Burst Gun (wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballAssaultRifle]", + "ModificationGuid": "2vhCtZjRd0i_nYo04ij__w", + "FriendlyName": "Paint Burst Gun (Ghost)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun] Confetti", + "ModificationGuid": "bfrFOdnHzEaIwHqem2dXkg", + "FriendlyName": "Confetti Gun (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun] Confetti", + "ModificationGuid": "PDyoY2oYKEuzGPxhvk4Lvg", + "FriendlyName": "Confetti Gun (Android)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun] Confetti", + "ModificationGuid": "NNAG0bLnKUGNxeUs4sss2w", + "FriendlyName": "Confetti Gun (Apple)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun] Confetti", + "ModificationGuid": "tlvJnebYeUKMxDGzEM6SKQ", + "FriendlyName": "Confetti Gun (PS5)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun] Confetti", + "ModificationGuid": "3S2KMdBa8EOI4pnZQxQ95Q", + "FriendlyName": "Confetti Gun (Steam)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[PaintballGun] Confetti", + "ModificationGuid": "3o8Wqyxh5UyZ9klvkIp-Pg", + "FriendlyName": "Confetti Gun (Xbox)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Paintball_PaintThrower]", + "ModificationGuid": "y_FesIT5jkmb61JIu7tfsw", + "FriendlyName": "Paint Thrower (Wood)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Paintball_PaintThrower]", + "ModificationGuid": "GbHNkVbVgUCoirMhOGkSTA", + "FriendlyName": "Paint Thrower (Plaid)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Paintball_PaintThrower]", + "ModificationGuid": "qhhhJeHVx0KLnGJrAGIn9g", + "FriendlyName": "Paint Thrower (Caution)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Paintball_PaintThrower]", + "ModificationGuid": "OFEdf4dRC0a3nHugXVOXnA", + "FriendlyName": "Paint Thrower (Fireworks)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Paintball_PaintThrower]", + "ModificationGuid": "VTVy8cweC0K2ca1nTXMu0g", + "FriendlyName": "Paint Thrower (Double Drencher)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Paintball_PaintThrower]", + "ModificationGuid": "0YfCdfAt2kiN0LQcPKSm_Q", + "FriendlyName": "Paint Thrower (Candy Cane)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Paintball_PaintThrower]", + "ModificationGuid": "JZQaa_BwkEuEx-ODac4Dfw", + "FriendlyName": "Paint Thrower (Jack-O-Lantern)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Paintball_PaintThrower]", + "ModificationGuid": "81d59830-e6c8-4433-8d67-61cc4822f09d", + "FriendlyName": "Paintball Paint Thrower Blender", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[BowlingBall]", + "ModificationGuid": "BvUZUamUh0uDbjDOrQrX1A", + "FriendlyName": "Bowling Ball (Gold)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Bucket]", + "ModificationGuid": "VW3WV6oQOkqYshf1xc9n3A", + "FriendlyName": "Bucket (Black Light Swirl)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Bucket]", + "ModificationGuid": "ou23DX__T0qFPjAXC5WrOQ", + "FriendlyName": "Bucket (Fashion)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Bucket]", + "ModificationGuid": "Yy0byohkZEWsiO4q0bQqBQ", + "FriendlyName": "Bucket (Bucket Bat)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Bucket]", + "ModificationGuid": "tFvjv1gAYkaDAlHLTsHelg", + "FriendlyName": "Bucket (Premium)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Bucket]", + "ModificationGuid": "_6HDWw-8-k67ZbGLfJ7cQw", + "FriendlyName": "Bucket (Barrel)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Bucket]", + "ModificationGuid": "MKeaiXxCwUiDDI5c0W6ShA", + "FriendlyName": "Bucket (Festive)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Bucket]", + "ModificationGuid": "U_3PE9Qc10OsVPTXyXsI_w", + "FriendlyName": "Bucket (Tennis Rock)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Arena_Grenade]", + "ModificationGuid": "A3XXaBH1eUuchWVVPdrY_A", + "FriendlyName": "Laser Grenade (Easter Egg)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Arena_Grenade]", + "ModificationGuid": "af3LZrxEgUuB5KAT3q2NLA", + "FriendlyName": "Laser Grenade (Pirate)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Arena_Grenade]", + "ModificationGuid": "kx25AlxB5E-cuIwfOPlqSg", + "FriendlyName": "Laser Grenade (Spring 2)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Vehicle_RallyBuggy]", + "ModificationGuid": "6akn-KjyNEiqBrREYwVg4g", + "FriendlyName": "Rally Buggy (Sport)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Vehicle_RallyBuggy]", + "ModificationGuid": "SQwd0QdyjkOjdIAQdMyiFg", + "FriendlyName": "Rally Buggy (Moon)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[Vehicle_RallyBuggy]", + "ModificationGuid": "H38YZbwl4EK4lpLhWxGP1A", + "FriendlyName": "Vehicle Rally Buggy Glitch", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + }, + { + "PrefabName": "[RecCenter_Cafe_Mug]", + "ModificationGuid": "5p2_8W69NkCTjym9ecWqzA", + "FriendlyName": "Mug (Caution Tape)", + "Tooltip": "", + "Rarity": 0, + "UnlockedLevel": 0, + "Favorited": false, + "PlatformMask": -1 + } +] diff --git a/Server/RadicadeServer-Config/equipment_skins.json b/Server/RadicadeServer-Config/equipment_skins.json new file mode 100644 index 0000000..304b9b5 --- /dev/null +++ b/Server/RadicadeServer-Config/equipment_skins.json @@ -0,0 +1,2013 @@ +{ + "items": [ + { + "prefab_name": "[DiscGolfDisc]", + "modification_guid": "19ef59c7-f74b-4c63-935a-1d4b1abd8518", + "display_name": "Disc (Coop)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DiscGolfDisc]", + "modification_guid": "2b70a76e-6f48-402e-90ff-40b06aa23eb8", + "display_name": "Disc (Coach Select)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DiscGolfDisc]", + "modification_guid": "9d2a3618-12b3-4ca0-be59-ed15d7926d77", + "display_name": "Disc (Laser)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DiscGolfDisc]", + "modification_guid": "7877964e-3a2b-4818-8513-0e26a707bd7c", + "display_name": "Disc (Clear)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DiscGolfDisc]", + "modification_guid": "bd3f6aa5-c6e0-4e06-901d-1176accf1003", + "display_name": "Disc (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DiscGolfDisc]", + "modification_guid": "5d3c3b16-713e-43d8-88e0-7633063091da", + "display_name": "Disc (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "bc4254be-60e0-4cc5-a233-6a0bd4137118", + "display_name": "Paint Pistol (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "33c84c50-7c2e-4a80-ad06-79c4f20f829e", + "display_name": "Paint Pistol (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "7ef5d1d9-24db-428c-85e0-107c1bfcc026", + "display_name": "Paint Pistol (Orange)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "4aff20ee-0b20-434b-a030-d3e1764fe4e7", + "display_name": "Paint Pistol (Purple)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "36751c4a-86a9-40e1-bef2-84f962ab678d", + "display_name": "Paint Pistol (Vitton)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "ec5ef0f3-c072-45ec-bb8f-3781fb16e067", + "display_name": "Paint Pistol (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "b8d5612b-2c6f-46d6-9412-decddac7d4c1", + "display_name": "Paint Pistol (Pirate)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "Dg9PFcg-JUia72PnYJqkQg", + "display_name": "Paint Pistol (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "z0Xw8_BUZUODrGHSZcLHHQ", + "display_name": "Paint Pistol (Valentines)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun]", + "modification_guid": "p70jO7t0OEyaCWGrpUQa0Q", + "display_name": "Paint Pistol (Watermelon)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShotgun]", + "modification_guid": "61e49c13-b414-425f-8e8b-bdcf3d12a5bf", + "display_name": "Paint Shotgun (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShotgun]", + "modification_guid": "f27af5d7-c741-4457-8c98-8e8791f1a41c", + "display_name": "Paint Shotgun (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShotgun]", + "modification_guid": "053c24a0-82a2-43f5-b0e7-e56db05a10ba", + "display_name": "Paint Shotgun (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShotgun]", + "modification_guid": "ZMAx_-B_7kWy6-TLXx8g6Q", + "display_name": "Paint Shotgun (Watermelon)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShotgun]", + "modification_guid": "CwIBKjm3G0-xUGt_9Gf7UQ", + "display_name": "Paint Shotgun (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShotgun]", + "modification_guid": "HSAwF_uBgkC0mBXyLSayWA", + "display_name": "Paint Shotgun (Cactus)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "5dcf8a2e-6fa6-4007-ac1e-73dd53c4c247", + "display_name": "Basketball (Purple)", + "tooltip": "", + "rarity": 1 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "e6899422-ab2f-4bd9-8e98-aef950ec27b6", + "display_name": "Basketball (Blue)", + "tooltip": "", + "rarity": 1 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "d6d22997-fe39-4ffa-b1b3-4955368c829a", + "display_name": "Basketball (Vitton)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "56582acf-c7a0-4aff-b3aa-2fdea4d254a3", + "display_name": "Basketball (Rainbow)", + "tooltip": "", + "rarity": 1 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "56c9a6db-0f54-482c-aba9-e265f899771d", + "display_name": "Basketball (Coach Select)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "WOWwmg7jg0mH2g4LJUp6fA", + "display_name": "Basketball (Cozy)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "bNEcMJOzokeEVKDeXk_cDQ", + "display_name": "Basketball (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "G0b2rKv0iEyXCEUVRcxmTQ", + "display_name": "Basketball (NBA)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "Vit6nO2nHEqvoYLf_6jHQw", + "display_name": "Basketball (NBA 75)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "Q7kLwTIpY0yO_EKYnPWEzA", + "display_name": "Basketball Cute Cat", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Basketball]", + "modification_guid": "mgpkPuEekUK20ZAnR66fOg", + "display_name": "Basketball Cute Cat Gray", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "174adb53-bc6e-4fcf-b03d-a00fc3deeb49", + "display_name": "Paint Shield (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "fd367329-b8b2-4d44-825a-da16092751e3", + "display_name": "Paint Shield (Purple)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "a6c869d9-8f30-4ce6-a7ea-cb7ea9f9d492", + "display_name": "Paint Shield (Pirate)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "jZ7wlwRxgUuWYuLasbnG8w", + "display_name": "Paint Shield (80s Sunset)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "QTyRLpDB3UuReHQqrKxb5A", + "display_name": "Paint Shield (Gingerbread)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "gfGhbEMx_kCRdO-6vPOh3g", + "display_name": "Paint Shield (Paint Shield Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "ClJApV0LC0mxh0UiYhaXGQ", + "display_name": "Paint Shield (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "TleGVT9enUigvfBoWz6kvA", + "display_name": "Paint Shield (Galaxy)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "nb1BXq0jxE2nXihd0RBgLw", + "display_name": "Paint Shield (Halo)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballShield]", + "modification_guid": "a59392cc-8027-43b8-83f5-4a5106bbd4aa", + "display_name": "Paintball Shield Gold", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "a9a3292c-8ccf-4564-aeb1-124c52057ef2", + "display_name": "Shield (Black Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "2389fce4-3ab2-4e05-918c-845eb8538bed", + "display_name": "Shield (Goblin)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "93f5418e-a03d-4074-804c-daa0e374dd9f", + "display_name": "Shield (Purple)", + "tooltip": "", + "rarity": 1 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "735b06c6-1b78-4f60-9291-a414a96cd228", + "display_name": "Shield (wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "affbab2d-3200-4512-a140-685fdc5570c6", + "display_name": "Shield (Yellow)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "a363ba1b-2f70-4c5f-943a-f45cfa59122b", + "display_name": "Shield (Rock)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "eb90210a-6d2d-4429-b1e0-3285333685fc", + "display_name": "Shield (Valentine)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "439be0eb-4d1e-4c80-97f8-b4636d0ce94b", + "display_name": "Shield (Pride)", + "tooltip": "", + "rarity": 1 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "k0BVix46IEmgVrqUmFU3-Q", + "display_name": "Shield (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "RQCgWA3Pf0KTqznUR58-lw", + "display_name": "Shield (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "-rbKD_ztMUq69Nv4tLrrVA", + "display_name": "Shield (Snowflake)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "L9usPszLFkiA6xs32es2Lw", + "display_name": "Shield (Electric Bloom)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "_uaO1FvF5Euw3tMPGfPo2A", + "display_name": "Shield (Sunflower)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "ff08af8b-2f30-4f35-86f5-de1b40ed799e", + "display_name": "Quest Shield Color Crazy", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestShield]", + "modification_guid": "b52450a6-b07f-4e5d-91f7-e982df08eba4", + "display_name": "Shield Gold", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "33a7a3dc-5266-4075-b56f-5eaa2c400e9f", + "display_name": "Laser Pistol (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "db91b61b-ab32-4895-8f5a-0212c1283cbb", + "display_name": "Laser Pistol (Vitton)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "89b1fbbc-e7be-440b-8b22-8fd379c1a568", + "display_name": "Laser Pistol (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "8a2a3219-fed0-4403-9061-451d162307c2", + "display_name": "Laser Pistol (Candy Cane)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "5nQ2kF6h6kunbk5z18Lozg", + "display_name": "Laser Pistol (Frankenblaster)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "0cFcVYCLTU6CKhr9uADrbw", + "display_name": "Laser Pistol (Recasso)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "aByJShU520OzdOXBV8o0Dg", + "display_name": "Laser Pistol (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "dRvcMuM__02iMsRwKEPtXQ", + "display_name": "Laser Pistol (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Pistol]", + "modification_guid": "fdqopuA1qUiK_jg-G5gr3w", + "display_name": "Laser Pistol (Galaxy)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_AutomaticGun]", + "modification_guid": "546b3a57-13d4-4c35-ab3a-bbb7d466f0b8", + "display_name": "Laser Burst Gun (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_AutomaticGun]", + "modification_guid": "bafacc36-02c6-408c-a3f5-8c2aa07a68db", + "display_name": "Laser Burst Gun (Arbor)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_AutomaticGun]", + "modification_guid": "u60IW1Gt10eC-8SlZmQUNQ", + "display_name": "Laser Burst Gun (Recasso)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_AutomaticGun]", + "modification_guid": "dEmh0tniCkeYBY1EdD09jA", + "display_name": "Laser Burst Gun (Zombie)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_AutomaticGun]", + "modification_guid": "jzgz_HrPAUqOeqx3uNXVUA", + "display_name": "Laser Burst Gun (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_AutomaticGun]", + "modification_guid": "TK9ybLf7xUSi6F1bVfyaRg", + "display_name": "Laser Burst Gun (Physics Construction)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "55a9c1e8-07e2-4457-86f2-75fd5e67b1f3", + "display_name": "Laser Shotgun (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "83d7cca0-2c5d-41b2-a5b2-f79fd89f3037", + "display_name": "Laser Shotgun (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "bb36b722-16dc-4172-b61f-237dc1240040", + "display_name": "Laser Shotgun (Jack-O-Lantern)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "cd497ae2-6382-42f2-9f92-93d5141588c3", + "display_name": "Laser Shotgun (Valentine)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "pIRihZheSEW2Ld3VTLkB-g", + "display_name": "Laser Shotgun (Laser Shotgun Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "gjuFgDp-LUWPi1mPCKyqbA", + "display_name": "Laser Shotgun (Shamrock)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "xHEZCBn5D0iUxIDp3swd9g", + "display_name": "Laser Shotgun (Galaxy)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "5wzKTogcskSYvv1HaiU57Q", + "display_name": "Laser Shotgun (Alien Blaster)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "a3a0c2ca-0bd8-4c93-8adb-debf755da05d", + "display_name": "Shotgun Sci Fi Gold", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_Shotgun]", + "modification_guid": "40832fe4-1784-4e39-a43f-2103db40edae", + "display_name": "Quest Sci Fi Shotgun Fall Fest", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_RailGun]", + "modification_guid": "63a867d7-ccea-4905-b6e0-e6dcfba06f9e", + "display_name": "Laser Railgun (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_RailGun]", + "modification_guid": "46977ca7-ee75-4e2a-b8aa-0763879f8a96", + "display_name": "Laser Railgun (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_RailGun]", + "modification_guid": "X9gTAbp0Sk6nadpTBseRrg", + "display_name": "Laser Railgun (Lifeguard)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_RailGun]", + "modification_guid": "_flhynGnykyZL7F83B7rJQ", + "display_name": "Laser Railgun (Laser Railgun Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_RailGun]", + "modification_guid": "uvbozCned0igzscgeBlRpQ", + "display_name": "Laser Railgun (Galaxy)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_SciFi_RailGun]", + "modification_guid": "Z9kKHu5I90-pG45NrWntoQ", + "display_name": "Quest Sci Fi Rail Gun Shark Shooter", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "c4d70545-cdf1-4fb1-924b-613de0327faf", + "display_name": "Crossbow (Vitton)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "6bba14be-f207-4933-b09f-f0fd3e28c10c", + "display_name": "Crossbow (Candy Cane)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "1d09c8e4-78c2-49f9-acb7-8f0b3ff6a5ed", + "display_name": "Crossbow (Recasso)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "MsFoMeUZ8Eq2qX9SrOtRPA", + "display_name": "Crossbow (Waves)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "T6PzFfg41UaQ7EAqts6gsw", + "display_name": "Crossbow (Bone)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "jHaoDSAtikyAI82lO53o9g", + "display_name": "Crossbow (Dryad)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "ESQ0qOlgrkCc8MbUXGnF8A", + "display_name": "Crossbow (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "QEwDZYj_OEi5l86LBnuYGw", + "display_name": "Crossbow (Rock)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "VHFKZVhiRkylcxnxzCKUuw", + "display_name": "Crossbow (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "Ys9UQ5b7fEyKxnaek-ihfQ", + "display_name": "Crossbow (Dryad Fall)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "CZAIad-ME0O0iIy77MRxBw", + "display_name": "Crossbow (Winter)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow]", + "modification_guid": "ych0ntYcK0eiq67bCLlbew", + "display_name": "Crossbow (Spring)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "f5901f11-b315-4261-9474-2da207e4a229", + "display_name": "Bow (Vitton)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "52c6a138-10c5-4aab-9c3b-04b50c9a2dc5", + "display_name": "Bow (Ghost)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "e738d936-0f75-423b-88bb-f626ffba573a", + "display_name": "Bow (Rock)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "XpxqrY6RYkafs-sd3Z0ZLw", + "display_name": "Bow (Rainbow)", + "tooltip": "", + "rarity": 1 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "JBp7CxPhrUC5hnn2EISUFA", + "display_name": "Bow (Dryad)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "T8666vuehkObksBdVbla9g", + "display_name": "Bow (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "YajcGvBcTEaf_MjCbxOciw", + "display_name": "Bow (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "XKFuhM3zikyxTYtTzuPb_A", + "display_name": "Bow (Dryad Fall)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "uPwTBnfWwkWXuyAaAMqeQA", + "display_name": "Bow (Winter)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "gl_tlo_t7U-H308Omy13lw", + "display_name": "Bow (Spring)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Longbow]", + "modification_guid": "1c09c79b-cb6e-4296-8609-e0a2bdb9952d", + "display_name": "Longbow Autumn", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "79090a4b-e191-4788-9210-88a7bdf6d828", + "display_name": "Maker Pen (Luxury)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "204f84a2-6ee2-45e1-9168-87f43e893e32", + "display_name": "Maker Pen (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "wultE5yRpkuJREeJw4prKg", + "display_name": "Maker Pen (Dragon)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "HyL83wPtf06DG6nsHofoeA", + "display_name": "Maker Pen (Steampunk)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "juFHwn9UjEiGaWyNnoxbtA", + "display_name": "Maker Pen (Pumpkin)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "Pw1XcUfAck2tNh0fIAu8LA", + "display_name": "Maker Pen (Waves)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "4HJ3wRmSZUuhMRUTA67dpA", + "display_name": "Maker Pen (Professor)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "i3NgZvTwkUSMsXRpqCgSZA", + "display_name": "Maker Pen (Wonderland)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "BGhlN3FKGEy6w-vwc28V4A", + "display_name": "Maker Pen (Beach)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "BwIscV_YnE6iG4brl389Hg", + "display_name": "Maker Pen (Mystery)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "JtMeQRvo00yVwAliICTP0w", + "display_name": "Maker Pen (Movie Magic)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "h4xKGsHcTUy3iH7PVFjb2w", + "display_name": "Maker Pen (Daytime)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "cUBvDvybvEu5eZDPn57ExQ", + "display_name": "Maker Pen (Night)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "VYF9NGZBgU6TOo9ayjgBEQ", + "display_name": "Maker Pen (Green)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "WwtFcRluQkmnCI_qH75L_Q", + "display_name": "Maker Pen (Orange)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "iRtlLKI-X0S9oYUWLenAKQ", + "display_name": "Maker Pen (Purple)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "tvjYGeRdwEqnr9mf84WgcQ", + "display_name": "Maker Pen (Red)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "Q7Tr_0DXAkOdr61UXiocow", + "display_name": "Maker Pen (Yellow)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "c1VRbmDGHUS7KiSgwj5prQ", + "display_name": "Maker Pen (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "MODdVYrjaEy-iy6quALNuA", + "display_name": "Maker Pen (White)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "H91DVbHzR06oie8fLjdE1A", + "display_name": "Maker Pen (Hidden Worlds)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "RPWXRfZwfUeC9PFlvcbmxw", + "display_name": "Maker Pen (Carnival)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "j83FD3aSQ0OUcrp4QIIZsA", + "display_name": "Maker Pen (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "L12C6OfxB0y53bLXZQ4rFg", + "display_name": "Maker Pen (Contest Stranded)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "xDSDJJX2w0ySwtiE6w6ghg", + "display_name": "Maker Pen (Maker Pen Contest Folklore)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "_rlnwZWJrk2eiQi4f_FhWQ", + "display_name": "Maker Pen (Premium)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "ht4zv6ZFi0yrohyBm39o6A", + "display_name": "Maker Pen (Contest Summer Camp)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "TpaRhLqVc02Oyr769imtnQ", + "display_name": "Maker Pen (Diamond Universe)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "Z_mBcmlIHUWE0ox9Dn_kHA", + "display_name": "Maker Pen (Solstice)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "OcixCg4jbUyLb6DFSTjrPg", + "display_name": "Maker Pen (Rhythm And Rooms)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[MakerPen]", + "modification_guid": "d7f37e87-cbb4-4f0b-89a6-f4aea971710a", + "display_name": "Maker Pen Makerpen Pattern", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballRifleScoped]", + "modification_guid": "6a6b0ecf-286d-4361-92ff-7027c440dc3c", + "display_name": "Paint Scope Rifle (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballRifleScoped]", + "modification_guid": "d906709c-e08e-4273-8ba1-e072e0d25957", + "display_name": "Paint Scope Rifle (Candy Cane)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballRifleScoped]", + "modification_guid": "3rBevbIk1EusFnFaXAh3qA", + "display_name": "Paint Scope Rifle (Gingerbread)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballRifleScoped]", + "modification_guid": "OSrMlhVVSEqb-0AzVzOH9w", + "display_name": "Paint Scope Rifle (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballRifleScoped]", + "modification_guid": "0dM2SfqGR0SmtO5ufTWfUQ", + "display_name": "Paint Scope Rifle (Comic)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballRifleScoped]", + "modification_guid": "btB2z7ybskSYrn9Nzhfhxg", + "display_name": "Paint Scope Rifle (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballRifleScoped]", + "modification_guid": "b_iiMMYayU-an7hOlCzPIA", + "display_name": "Paint Scope Rifle (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow_Hunter]", + "modification_guid": "d217ee4c-22f3-4f33-bf7c-9d4ee9c30e29", + "display_name": "Crossbow Hunter (Rock)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow_Hunter]", + "modification_guid": "cFMh54GoHEeHZEERSRR8kQ", + "display_name": "Crossbow Hunter (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow_Hunter]", + "modification_guid": "RRCvths6eUmAAr9hzGFT0g", + "display_name": "Crossbow Hunter (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow_Hunter]", + "modification_guid": "-X_Hm6dIekqoHTY2VMMLeA", + "display_name": "Crossbow Hunter (Shark)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Crossbow_Hunter]", + "modification_guid": "BSO9Ahi-2EiwYxE7lkHhlA", + "display_name": "Crossbow Hunter (Valentines)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "dHcxKoPSLUiDSSDPd_07WQ", + "display_name": "Quest Sword Pixel", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "0ee04d78-5a17-4a38-ae67-b576a41fc200", + "display_name": "Sword (Extra)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "431567d2-4df2-43eb-b632-3d79aa7c013c", + "display_name": "Sword (Classic)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "f03dee6a-79b8-4906-9abb-a821748d97a9", + "display_name": "Sword (Red)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "3e46fb2d-bb3f-42ed-83ef-f5716860725c", + "display_name": "Sword (Rock)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "eecdf81c-7ba2-40ba-9d6c-f461708cce16", + "display_name": "Sword (Comic)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "SbEIObEmhkKQniANHqNekg", + "display_name": "Sword (Pumpkin)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "Kitcy2AVB0-OdljiEAI2Pw", + "display_name": "Sword (Recasso)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "K9TzHZPDwU-ewzLYkr_1cg", + "display_name": "Sword (Gingerbread)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "wioj1rR1lkCx7f-oiCHcOw", + "display_name": "Sword (Corn Cob)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "m1JMjJYpSUiXj5897orm2Q", + "display_name": "Sword (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "d97pbQMk9UWpEN0yoOE9Rg", + "display_name": "Sword (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "DRXoMS9phEG2fkasja7sEw", + "display_name": "Sword (Lava)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "uLNdhrrAC0ybxC2XkBnnVQ", + "display_name": "Sword (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "-ybFpUxTsUKWFSIS-8fOOw", + "display_name": "Sword (Electric Bloom)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "X8B2M3hltEKyEnX0dcJi7Q", + "display_name": "Sword (Umbrella)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "SRl7xh9z8kGU2OO5viCykw", + "display_name": "Sword (Clarinet)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[QuestSword]", + "modification_guid": "RK4BY5eyZ0aMLhVRCvGi6A", + "display_name": "Quest Sword Rubber Chicken", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Grenade]", + "modification_guid": "993f8b81-cb2e-44e4-88a1-b841af1f0e69", + "display_name": "Grenade (Pirate)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Grenade]", + "modification_guid": "vlpFc1j820i5n7Y2sTW8Vw", + "display_name": "Grenade (Grenade Chick)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Grenade]", + "modification_guid": "0tfDNC0I2kiEVrJttrE0bw", + "display_name": "Grenade (Gum Drop)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Grenade]", + "modification_guid": "8Ex4aO2GpEqwvow6bEu_ag", + "display_name": "Grenade (Spring Egg)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "e8e42ef6-9ab6-44f4-84d5-117154a4d13e", + "display_name": "Quest Goblin Wand (Ghost)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "yG6xFxkoS0iIDt7wEj5Hqw", + "display_name": "Quest Goblin Wand (Witch Star)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "3pE7zA-DjkqP1Ch7__-31w", + "display_name": "Quest Goblin Wand (Ice)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "m_otHTEKF0KQljsOc-JENg", + "display_name": "Quest Goblin Wand (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "CyrdBvIbXUq_TsXFliWk1A", + "display_name": "Quest Goblin Wand (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "vfzrRHn24E67BK8Bgy60xA", + "display_name": "Quest Goblin Wand (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "QnLM4Qw-L0ml_mgReFweIw", + "display_name": "Quest Goblin Wand (Trident)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "M6BHcdDZnU6UEDa3wLUUhg", + "display_name": "Quest Goblin Wand (Butterfly)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Quest_Goblin_Wand]", + "modification_guid": "7Kdm8AYIkky-N1biw5ljPg", + "display_name": "Quest Goblin Wand (Flowers)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGrenadeLauncher]", + "modification_guid": "502f40c3-1c00-4c3f-bfc7-c897df8af37e", + "display_name": "Paint Launcher (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGrenadeLauncher]", + "modification_guid": "f34e6270-bcfa-42f2-80d5-69c1bbd34983", + "display_name": "Paint Launcher (Comic)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGrenadeLauncher]", + "modification_guid": "8ZAwSDtXlkqlnBLOpfVTVg", + "display_name": "Paint Launcher (Gingerbread)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGrenadeLauncher]", + "modification_guid": "3UmIhvqmkU-aWxFDFd4QDg", + "display_name": "Paint Launcher (Honey)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGrenadeLauncher]", + "modification_guid": "pPpYY9j0Dku2gtbKoUOCCg", + "display_name": "Paint Launcher (Easter Egg)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGrenadeLauncher]", + "modification_guid": "9gBl778RvUeSC8837Gurog", + "display_name": "Paint Launcher (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGrenadeLauncher]", + "modification_guid": "tcMMGKcmhkuM82ISCZ-3AQ", + "display_name": "Paint Launcher (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGrenadeLauncher]", + "modification_guid": "L-_iZgicHUajydSvUWVJhw", + "display_name": "Paint Launcher (Jumbotron)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "6261a846-e4f8-4e6e-8aa0-68f38cd788d4", + "display_name": "Dodgeball (Comic)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "6ff63f8f-4bcc-4b69-8d89-8b886d19e239", + "display_name": "Dodgeball (Recasso)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "f55dda45-c17e-4237-a638-9f326d306e7d", + "display_name": "Dodgeball (Goblin)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "Jkg-OGXdIUyfEwaQIh6EGA", + "display_name": "Dodgeball (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "lIpyvEMWqUyv41gTp3YZ1A", + "display_name": "Dodgeball (Pumpkin)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "Ms8LD3TqBkiukhUlHxVBfw", + "display_name": "Dodgeball (Billiards)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "dpTV4UmrJkCsz6WXD6YeKQ", + "display_name": "Dodgeball (Cactus)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "g5JrCDryI0-mxlbLMRYzHA", + "display_name": "Dodgeball Ball Beach", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[DodgeballBall]", + "modification_guid": "ogArfnzCX0yQ_dqUoa3Yjw", + "display_name": "Dodgeball Ball Coconut", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[SoccerShield]", + "modification_guid": "fae1abe3-6bfd-4407-b34e-e2c1b3b03032", + "display_name": "Soccer Shield (Gingerbread)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecRoyale_Backpack]", + "modification_guid": "523e3615-4633-41a3-9b2d-17d3207a684b", + "display_name": "Backpack (Camo)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecRoyale_Backpack]", + "modification_guid": "6c3b26cd-b38e-492b-a124-759cbcec1396", + "display_name": "Backpack (Camping Pattern)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecRoyale_Backpack]", + "modification_guid": "12b54ad2-3847-41db-9ed4-88e8dad63024", + "display_name": "Backpack (Tiger)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecRoyale_Backpack]", + "modification_guid": "5c848a04-f528-423b-8be1-4b5cd43a75c2", + "display_name": "Backpack (Denim)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecRoyale_Backpack]", + "modification_guid": "e93f5ab7-e9bf-4652-8a7c-2ae120f25f87", + "display_name": "Backpack (Recasso)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecRoyale_Backpack]", + "modification_guid": "w2NNBTTCVEah2WlP0JHezQ", + "display_name": "Backpack (Valentine)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecRoyale_Backpack]", + "modification_guid": "70uy5UJhhEy1aynKM4MAsQ", + "display_name": "Backpack (Fall Leaves)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecRoyale_Backpack]", + "modification_guid": "ujZ4Hl0HO06OysqZcqHhmg", + "display_name": "Backpack (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "d218ae45-8534-4f96-b2ed-1cf281bc3578", + "display_name": "Camera (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "e2844f84-ab44-4141-9a0a-bd5da7caa4f6", + "display_name": "Camera (Disposable)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "1838fa61-de10-4e09-b24a-f5a1bc942600", + "display_name": "Camera (Green)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "fSbzGxsjKE-cwtFFX6dJQw", + "display_name": "Camera (Carpet)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "ATGtNjai20miFwBUOVIMuw", + "display_name": "Camera (Two Tone)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "znrJSCX_SkS-kIPqMERoDg", + "display_name": "Camera (Camping)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "g5u0weNLmkCLeUXFUVn74Q", + "display_name": "Camera (Comic)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "QQALCzCF-0ClDWqmmciHAQ", + "display_name": "Camera (Heart)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "SkTa53OzM0u82YPuZAl9aw", + "display_name": "Camera (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "sE4_-ZVa2EC4MZfGGsExzQ", + "display_name": "Camera (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "5dx423rtM0ygWqMBwJHBOQ", + "display_name": "Camera (Retro Gamer)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "T465YyOavEqTqH3s91uBHQ", + "display_name": "Camera (Video Partner)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "SFz15JbasU-gShl_GHnspw", + "display_name": "Camera (Galaxy)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[ShareCamera]", + "modification_guid": "1B8utXzBWUSGJ4Va8Hm1xA", + "display_name": "Camera (Holographic)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D4]", + "modification_guid": "KiVWBp5_J0qCLuizAJGcAg", + "display_name": "Sandbox D4 (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D4]", + "modification_guid": "x3Cv42hIkkaNIy7fHZiWYg", + "display_name": "Sandbox D4 (Bone White)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D4]", + "modification_guid": "_2jxSTYSpEykZ_O91oDAzg", + "display_name": "Sandbox D4 (Turquoise)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D4]", + "modification_guid": "WxWaXY61mUuNe_f6SgECnA", + "display_name": "Sandbox D4 (Pewter)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D6]", + "modification_guid": "TxIu8CUjykGDMr5RIJcNVQ", + "display_name": "Sandbox D6 (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D6]", + "modification_guid": "Fn0gcj17c06tA9s98CKwAg", + "display_name": "Sandbox D6 (Bone White)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D6]", + "modification_guid": "HnVo08F-zk-YeikERFa1Yw", + "display_name": "Sandbox D6 (Turquoise)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D6]", + "modification_guid": "5U_KiCi_90-eWucQ68VdZQ", + "display_name": "Sandbox D6 (Pewter)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D8]", + "modification_guid": "AQXXckjNZ0uzPMTcP1C3Kg", + "display_name": "Sandbox D8 (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D8]", + "modification_guid": "d_CbCG3DmUCtWGWYHioghg", + "display_name": "Sandbox D8 (Bone White)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D8]", + "modification_guid": "f45tTZp4KUextmAsX8X9Hw", + "display_name": "Sandbox D8 (Turquoise)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D8]", + "modification_guid": "77cRA0O4GUqd6nr9QLefQA", + "display_name": "Sandbox D8 (Pewter)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D10]", + "modification_guid": "ZM543HEyl0G6n7P5rDMEWw", + "display_name": "Sandbox D10 (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D10]", + "modification_guid": "GjQwBmNnK0ukv0hEEHK8tw", + "display_name": "Sandbox D10 (Bone White)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D10]", + "modification_guid": "SnSOf-GZ2kastpF5KWxT-Q", + "display_name": "Sandbox D10 (Turquoise)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D10]", + "modification_guid": "I161QAym2kqPi5wXIHCt8A", + "display_name": "Sandbox D10 (Pewter)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D12]", + "modification_guid": "EoV2ZoLR2UWY898p8j7qGQ", + "display_name": "Sandbox D12 (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D12]", + "modification_guid": "heFLfpPNd0-nhLOFpUCnpA", + "display_name": "Sandbox D12 (Bone White)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D12]", + "modification_guid": "ub6apg3zEUiCp3k81JjQGw", + "display_name": "Sandbox D12 (Turquoise)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D12]", + "modification_guid": "2gYYjbVGqkiOefY2xjOxiQ", + "display_name": "Sandbox D12 (Pewter)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D20]", + "modification_guid": "Des7txtX0EO-7NiooR0-ow", + "display_name": "Sandbox D20 (Pink)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D20]", + "modification_guid": "gXOOKrQEYE-FmS3uXWj1hA", + "display_name": "Sandbox D20 (Bone White)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D20]", + "modification_guid": "DrG0ntsyPUSv8ra0m-Hhaw", + "display_name": "Sandbox D20 (Turquoise)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Sandbox_D20]", + "modification_guid": "H3vrSi45AU6lshChLnr19Q", + "display_name": "Sandbox D20 (Pewter)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballAssaultRifle]", + "modification_guid": "VOOdhP5qg0WWv2m0qLRVCw", + "display_name": "Paint Burst Gun (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballAssaultRifle]", + "modification_guid": "c458a6ae-a1ea-468a-89b8-4c9a85cd3cde", + "display_name": "Paint Burst Gun (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballAssaultRifle]", + "modification_guid": "m_qzPvDPF0KPhLd59wzr5A", + "display_name": "Paint Burst Gun (Fall)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballAssaultRifle]", + "modification_guid": "996662ad-631d-4a81-8a8b-74ace4833279", + "display_name": "Paint Burst Gun (Pirate)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballAssaultRifle]", + "modification_guid": "3aaef60d-142f-4a0a-bea3-f5d99ab62dd5", + "display_name": "Paint Burst Gun (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballAssaultRifle]", + "modification_guid": "49d88b73-3c8a-4f4d-8a69-128fa5474e82", + "display_name": "Paint Burst Gun (PSPLUS)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballAssaultRifle]", + "modification_guid": "357fe573-fee7-467f-93a7-5e61afb024b8", + "display_name": "Paint Burst Gun (wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballAssaultRifle]", + "modification_guid": "2vhCtZjRd0i_nYo04ij__w", + "display_name": "Paint Burst Gun (Ghost)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun] Confetti", + "modification_guid": "bfrFOdnHzEaIwHqem2dXkg", + "display_name": "Confetti Gun (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun] Confetti", + "modification_guid": "PDyoY2oYKEuzGPxhvk4Lvg", + "display_name": "Confetti Gun (Android)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun] Confetti", + "modification_guid": "NNAG0bLnKUGNxeUs4sss2w", + "display_name": "Confetti Gun (Apple)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun] Confetti", + "modification_guid": "tlvJnebYeUKMxDGzEM6SKQ", + "display_name": "Confetti Gun (PS5)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun] Confetti", + "modification_guid": "3S2KMdBa8EOI4pnZQxQ95Q", + "display_name": "Confetti Gun (Steam)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[PaintballGun] Confetti", + "modification_guid": "3o8Wqyxh5UyZ9klvkIp-Pg", + "display_name": "Confetti Gun (Xbox)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Paintball_PaintThrower]", + "modification_guid": "y_FesIT5jkmb61JIu7tfsw", + "display_name": "Paint Thrower (Wood)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Paintball_PaintThrower]", + "modification_guid": "GbHNkVbVgUCoirMhOGkSTA", + "display_name": "Paint Thrower (Plaid)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Paintball_PaintThrower]", + "modification_guid": "qhhhJeHVx0KLnGJrAGIn9g", + "display_name": "Paint Thrower (Caution)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Paintball_PaintThrower]", + "modification_guid": "OFEdf4dRC0a3nHugXVOXnA", + "display_name": "Paint Thrower (Fireworks)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Paintball_PaintThrower]", + "modification_guid": "VTVy8cweC0K2ca1nTXMu0g", + "display_name": "Paint Thrower (Double Drencher)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Paintball_PaintThrower]", + "modification_guid": "0YfCdfAt2kiN0LQcPKSm_Q", + "display_name": "Paint Thrower (Candy Cane)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Paintball_PaintThrower]", + "modification_guid": "JZQaa_BwkEuEx-ODac4Dfw", + "display_name": "Paint Thrower (Jack-O-Lantern)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Paintball_PaintThrower]", + "modification_guid": "81d59830-e6c8-4433-8d67-61cc4822f09d", + "display_name": "Paintball Paint Thrower Blender", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[BowlingBall]", + "modification_guid": "BvUZUamUh0uDbjDOrQrX1A", + "display_name": "Bowling Ball (Gold)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Bucket]", + "modification_guid": "VW3WV6oQOkqYshf1xc9n3A", + "display_name": "Bucket (Black Light Swirl)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Bucket]", + "modification_guid": "ou23DX__T0qFPjAXC5WrOQ", + "display_name": "Bucket (Fashion)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Bucket]", + "modification_guid": "Yy0byohkZEWsiO4q0bQqBQ", + "display_name": "Bucket (Bucket Bat)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Bucket]", + "modification_guid": "tFvjv1gAYkaDAlHLTsHelg", + "display_name": "Bucket (Premium)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Bucket]", + "modification_guid": "_6HDWw-8-k67ZbGLfJ7cQw", + "display_name": "Bucket (Barrel)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Bucket]", + "modification_guid": "MKeaiXxCwUiDDI5c0W6ShA", + "display_name": "Bucket (Festive)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Bucket]", + "modification_guid": "U_3PE9Qc10OsVPTXyXsI_w", + "display_name": "Bucket (Tennis Rock)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Arena_Grenade]", + "modification_guid": "A3XXaBH1eUuchWVVPdrY_A", + "display_name": "Laser Grenade (Easter Egg)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Arena_Grenade]", + "modification_guid": "af3LZrxEgUuB5KAT3q2NLA", + "display_name": "Laser Grenade (Pirate)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Arena_Grenade]", + "modification_guid": "kx25AlxB5E-cuIwfOPlqSg", + "display_name": "Laser Grenade (Spring 2)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Vehicle_RallyBuggy]", + "modification_guid": "6akn-KjyNEiqBrREYwVg4g", + "display_name": "Rally Buggy (Sport)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Vehicle_RallyBuggy]", + "modification_guid": "SQwd0QdyjkOjdIAQdMyiFg", + "display_name": "Rally Buggy (Moon)", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[Vehicle_RallyBuggy]", + "modification_guid": "H38YZbwl4EK4lpLhWxGP1A", + "display_name": "Vehicle Rally Buggy Glitch", + "tooltip": "", + "rarity": 0 + }, + { + "prefab_name": "[RecCenter_Cafe_Mug]", + "modification_guid": "5p2_8W69NkCTjym9ecWqzA", + "display_name": "Mug (Caution Tape)", + "tooltip": "", + "rarity": 0 + } + ] +} diff --git a/Server/RadicadeServer-Config/generate-levelup-rewards.py b/Server/RadicadeServer-Config/generate-levelup-rewards.py new file mode 100644 index 0000000..72cd7aa --- /dev/null +++ b/Server/RadicadeServer-Config/generate-levelup-rewards.py @@ -0,0 +1,167 @@ +"""Regenerate and audit levelup-rewards.json from the 2023 catalogue. + +The server enforces two things over a level-up box: the deny-list in +Server/Utils/LevelUpRewardPool.cs, which can never be overridden, and the allow-list in +levelup-rewards.json, which is the reviewed set of items a box may contain. This script rebuilds +that allow-list and prints what it kept and what it refused, so a change to the catalogue or the +deny-list can be reviewed as a diff. + +The patterns below mirror LevelUpRewardPool.DeniedPatterns and DeniedTags and MUST be kept in step +with them. The server's copy is the source of truth - the allow-list is already filtered by it at +pick time, so a pattern missing here only means a candidate that the server would still refuse; +it can never mean an item the server would allow. Run this from anywhere with the repo checked out: + + python Server/RadicadeServer-Config/generate-levelup-rewards.py + +Then read the audit, delete anything from the JSON that should not be there, and remember that an +item with no name, tooltip or tag to judge it by cannot be classified here at all. +""" +import json +import os +import re + +# The script sits beside the files it reads, so it works from any working directory. +CONFIG = os.path.dirname(os.path.abspath(__file__)) + +DENIED = [ + "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", +] +DENIED_TAGS = {"halloween", "holidays", "thanksgiving", "invasion", "elseware", "reccon", "reccon2021"} + + +def denied_name(text): + if not text: + return None + low = text.lower() + for p in DENIED: + if p in low: + return p + return None + + +def denied(item): + for field in ("FriendlyName", "Tooltip"): + hit = denied_name(item.get(field) or "") + if hit: + return field + ":" + hit + tags = (item.get("TagList") or "").lower() + for tag in re.split(r"[,\s]+", tags): + if tag.strip() in DENIED_TAGS: + return "tag:" + tag.strip() + return None + + +def main(): + catalog = json.load(open(CONFIG + r"\2022-avatar.json", encoding="utf-8")) + resolvable = set( + d.strip().lower() + for d in json.load(open(CONFIG + r"\avatar-item-lookup.json", encoding="utf-8"))["descriptions"] + ) + + kept, refused = [], {} + for item in catalog: + desc = (item.get("AvatarItemDesc") or "").strip() + if not desc or item.get("IsBaseAvatarItem"): + continue + rarity = item.get("Rarity", 0) + if rarity not in (10, 20, 30, 50): + refused.setdefault("rarity", []).append(item.get("FriendlyName")) + continue + if desc.lower() not in resolvable: + refused.setdefault("unresolvable", []).append(item.get("FriendlyName")) + continue + why = denied(item) + if why: + refused.setdefault(why.split(":")[0], []).append(item.get("FriendlyName")) + continue + kept.append((rarity, item.get("FriendlyName") or "", desc)) + + kept.sort(key=lambda row: (row[0], row[1].lower())) + per_tier = {} + for rarity, _, _ in kept: + per_tier[rarity] = per_tier.get(rarity, 0) + 1 + + # ---- consumables: the shipped global list minus the deny-list, the premium and the seasonal + consumables = json.load(open(CONFIG + r"\consumables.json", encoding="utf-8")) + # Event, survey, licensed and paid-category consumables that no name pattern can identify. + extra_denied = [ + "hair dye", "coupon", "snowman", "sparkler", + "dracula", "goblin king", "ghost beard", "witch film", + "surve", "survey", "forbidden", "forsaken", "premium", "creator bubbly", + ] + consumable_kept, consumable_refused = [], {} + for item in consumables: + name = item.get("FriendlyName") or "" + why = denied_name(name) + if not why: + for pattern in extra_denied: + if pattern in name.lower(): + why = pattern + break + if why: + consumable_refused.setdefault(why, []).append(name) + else: + consumable_kept.append((name, item["ConsumableItemDesc"])) + + out = { + "AvatarItems": [desc for _, _, desc in kept], + "Consumables": [desc for _, desc in consumable_kept], + } + path = CONFIG + r"\levelup-rewards.json" + with open(path, "w", encoding="utf-8", newline="\r\n") as fh: + json.dump(out, fh, indent=2) + fh.write("\n") + + print("wrote", path) + print("avatar allowed:", len(kept), "per rarity:", per_tier) + print("consumables allowed:", len(consumable_kept)) + print() + print("== refused avatar items by reason ==") + for reason, names in sorted(refused.items(), key=lambda kv: -len(kv[1])): + print(f"{reason:20} {len(names):5} e.g. {names[:4]}") + print() + print("== refused consumables ==") + for reason, names in sorted(consumable_refused.items(), key=lambda kv: -len(kv[1])): + print(f"{reason:20} {len(names):5} {names[:6]}") + print() + print("== kept consumables ==") + for name, _ in consumable_kept: + print(" ", name) + print() + print("== sample of kept avatar items (first 2 per tier) ==") + seen = {} + for rarity, name, _ in kept: + if seen.get(rarity, 0) >= 2: + continue + seen[rarity] = seen.get(rarity, 0) + 1 + print(f" {rarity}: {name}") + print(" ... a further", len(kept) - sum(seen.values()), "omitted") + print() + print("== denied check: items the brief names explicitly ==") + for probe in ["White Hat", "Winged Headphones", "Rec Con Hoodie", "Space Visor", "Royal Cape", + "Top Hat (Gold)", "Hallow's Blazer", "Prom", "MrBeast", "Early Access", + "Alpha Team", "Notes Headphones"]: + hits = [(i.get("FriendlyName"), denied(i)) for i in catalog + if probe.lower() in (i.get("FriendlyName") or "").lower()] + print(f"{probe!r}: " + (", ".join(f"{n}->{w}" for n, w in hits) if hits else "no match")) + + +main() diff --git a/Server/RadicadeServer-Config/keepsake-rooms.json b/Server/RadicadeServer-Config/keepsake-rooms.json new file mode 100644 index 0000000..efec12a --- /dev/null +++ b/Server/RadicadeServer-Config/keepsake-rooms.json @@ -0,0 +1,12 @@ +{ + "_comment": "Keepsakes stay ENABLED for all rooms. Keepsake placement uses the room limit below and the catalog's per-category limits.", + "_comment_enable": "To turn the feature back on, set AllRooms to true (or list room ids in RoomIds) and leave RADICADE_KEEPSAKES unset: a configured keepsake room is what enables the feature. This file is read on every room load, so the edit applies to the next room a player enters - no server restart. RADICADE_KEEPSAKES=off forces the feature off, =on forces it on.", + "_comment_event": "The keepsake PROGRESSION EVENT is OFF: EventEnabled false leaves the room's KeepsakeProgressionEventIds empty, answers null from GET /api/progressionEvents/active and 404s the event routes, while keepsakes themselves keep working. Set EventEnabled back to true to publish the event again - the file is read on every request. When it is on, the room's KeepsakeProgressionEventIds name the event and GET /api/progressionEvents/active reports it, whose KeepsakeRoomLists carry the room list below.", + "AllRooms": true, + "RoomIds": [], + "RoomListId": 1, + "RoomLimit": 10, + "EventId": 1, + "EventEnabled": false, + "EventName": "Test Event!" +} diff --git a/Server/RadicadeServer-Config/levelup-rewards.json b/Server/RadicadeServer-Config/levelup-rewards.json new file mode 100644 index 0000000..07af406 --- /dev/null +++ b/Server/RadicadeServer-Config/levelup-rewards.json @@ -0,0 +1,2152 @@ +{ + "AvatarItems": [ + "03f8c394-28fa-4087-978b-8d108f0bd969,G7p-E7TJHkSojZtj_5pDXw", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,ugpS7o-ZxkCv7HAvhewZgw", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,d5987fee-e3cc-4336-853a-5df57f73df05,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,47da86b9-4e6e-423c-b8c4-ea6bf8472509,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9b35ae52-febe-486a-8557-ec7190a9756e,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,6e152cc7-e93a-4408-8092-236db420b680,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,d1eb3573-672c-4bb4-9ec5-7c81e6a38d3d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,e548124c-d7f3-45de-aaa0-f66130eca576,6094370d-5e28-473a-b422-f3f1c75d5db8", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9b35ae52-febe-486a-8557-ec7190a9756e,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,6e152cc7-e93a-4408-8092-236db420b680,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "t9co2516nkuJO4LIJA1bJA,Wj7I2YiVoE6zp8J_HYs8tw,8kdJkmwNM0WQmMVrb8fK8A", + "QCtqxbvYOkan79YFugH_Cw,LyVf9lnzdku1F4iPBGqk-g,xnxDimLvVUCI9tEJqWDDcA", + "jiBzrzDPkkuL6DqIFKpcCw,OfenZaa1-UGdQ8YVOrXjzw,StxMBEprVkqMa_IoDG-Q9A", + "125a3a62-1005-47dc-9fd8-ee09ea803e9f,97bbb384-b37e-4ea1-902c-88fbd6854ab5,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "125a3a62-1005-47dc-9fd8-ee09ea803e9f,", + "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7nRz9Jmnn066_PzngkhfFA,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7fb8496f-0e6c-4e48-b8fa-5709ebbf4820,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,e13cf33d-7588-4e2e-955e-f0a29d6380b7,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c9589974-c261-485e-b571-cb2f34fb178f,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,d6823e01-69f0-4f85-b94a-74894356a2cf,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,da6d0ced-3d43-4878-a868-925f2ed6fd5b,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c5deba2a-6e35-4b13-8e94-8ba5457f39df,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,X1OFXyDyXES3CgMpIyV3nQ", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6o6sNWyPoUCcKJBHW0KC9w", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "ce81210f-8eb8-4cd2-95d5-046d4da229c8,bb01b51e-a95f-441e-8c52-abc3aeed7145,f7c7d05f-5454-445a-af38-25f110bd204a", + "ce81210f-8eb8-4cd2-95d5-046d4da229c8,", + "14ef6b00-debf-4a85-9755-b4d37df496d3,nzFUAOCAFUCd9S-x4EKRwQ,Qx7nrZH-K06CgIoJYfFCCQ", + "14ef6b00-debf-4a85-9755-b4d37df496d3,M8IYDK6aTUyn5mRhr51GtA,r_GLekt6mkWxXw8zPPR8Mg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,nZor-clxlkm8EO9Gco0Otg,r_GLekt6mkWxXw8zPPR8Mg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,yxWEn-oaYE2YvvnrTVV5WQ,r_GLekt6mkWxXw8zPPR8Mg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,XLYLo5qgW0eociGDgQ16PA,r_GLekt6mkWxXw8zPPR8Mg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,_fBrHCkTKUe82QgxCyI-Kw,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,uDcJZClcFkiNdkEEh9IIyA,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,c24f1DNZjEmqkt-FibKUjQ,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,SeVmHtrMS0y7gKCYkHcrOA,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,fLWfBSEO6U6aHrDm3nKFEQ,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,Iezz6gCFfkKdVcmPXIhunw,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,F5vJzMfNB0Ob6Abjadho7Q,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,58YffnGSUU6umcI1i0RvZw,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,wbFPXIg5gUK3QK6YycZexg,uWXtz0WaO0qJ1W4TfWnLqg", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,vbOUaJsGHEaSfQCivNcMWg,uWXtz0WaO0qJ1W4TfWnLqg", + "f51a7ed5-52ea-485f-a430-303697f6fec9,d320ad9b-ba5c-45ea-b2b8-0d3e409a9db1,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,d61711fd-589a-4669-b991-9408a58fffd9,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,5b58d879-2bc4-47ed-a996-1af2ace16425,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,oJFfs62tR0WVRry2Smt8yA,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,MIfCkcrFOEasJqfDmxThdw,3d971439-41d1-43a2-8434-704f9ec8d906", + "f51a7ed5-52ea-485f-a430-303697f6fec9,", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,7_n11PF4-0KqTZ9az23hqg,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,TKbjEeTWfUyA1u4laoULMg,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,WeP_tPjUNUuGojXrVBOAIA,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,JShcoWWPz0297HeEUJbDCQ,FWL8ZeMe9EerdB0_Fya3AA", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,_psc7WCgOk2JzRQwuFAqNw,FWL8ZeMe9EerdB0_Fya3AA", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,83522c12-9549-4d74-8c87-bae210bcd2bf,0XPd5RzNJEepHztqwAznHQ", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,XNq4Cns4lEGydtktdsXlmQ,0XPd5RzNJEepHztqwAznHQ", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,0XPd5RzNJEepHztqwAznHQ", + "CQuKKNpan0O7rhR1fr14-Q,Gtq8yRlt1UeFW8U-sc7FiA,IWg9wuda4kaCilJk8JQpTQ", + "CQuKKNpan0O7rhR1fr14-Q,ghM-uFq5-0Ci38AOHa9zZA,IWg9wuda4kaCilJk8JQpTQ", + "CQuKKNpan0O7rhR1fr14-Q,XrSL-v2MbkK0md-FMOweOA,IWg9wuda4kaCilJk8JQpTQ", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,-XSM6ZcSjE-vrWL7jyF4fQ,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,t2bByt5oUEG90PMMePCP6A,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,RL6HkufaJ0yubTpCGVI9eQ,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,MVkY4sU5BkGC4QhweRiyIA,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,m7KXVnULz0iHvKfCFzFoyw,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,ux9DQVSRyU-_5GA-t197OA,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,gT0WA7Y1H0my5uMlteIMMg,7nBHNMZhAE-t9rLCYO_STA", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,9tTuREkoQkW0ycqOjWXsiA,7nBHNMZhAE-t9rLCYO_STA", + "c6c08eb5-381a-4193-9722-80da95d62abe,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,3svqbYmGt0uyVWnx7vJNmw,iLo1YhFY-06HNWgM6gkB7g", + "c6c08eb5-381a-4193-9722-80da95d62abe,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "c6c08eb5-381a-4193-9722-80da95d62abe,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "b02053b3-013f-42b0-8881-85f5be2b8cda,qoVZuGa5NkOmyfikHjDhXw,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,faedf215-289b-40f7-90cc-f5b98517d133,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,05K7dmjigkCByO2ufw9jtw,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,", + "b02053b3-013f-42b0-8881-85f5be2b8cda,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,1773d5d8-4714-4721-b30c-97e25d6f2a5a,93987d32-7a6e-45df-af56-76f912969ce7", + "b02053b3-013f-42b0-8881-85f5be2b8cda,yOw1WqEN0EWqR9t53EsQpw,93987d32-7a6e-45df-af56-76f912969ce7", + "BJ2XrGvl40iIl28IbTfbKg,", + "657efe89-620a-46a2-8b58-cafd855c7cd5,", + "657efe89-620a-46a2-8b58-cafd855c7cd5,a0oe68pikUykAgrRo-axkQ,KFq8d1VsO0KDqBWUj4qRFg", + "657efe89-620a-46a2-8b58-cafd855c7cd5,eWf2c62cqEOkRukLzhl_wQ,KFq8d1VsO0KDqBWUj4qRFg", + "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,", + "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,X5SuBHIGBE6wy8N8KDop8Q,5V_1JEDrRUa1UPKhtrSB2w", + "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,6secgNPvmUyFnI3NC5dIYw,5V_1JEDrRUa1UPKhtrSB2w", + "kghEf_iMKUSxPO-h5iRhSQ,", + "a16d59da-caa5-4388-af67-315bfcb16f64,w9I-I0GWRE-3RgOUMMlHjg,WWmx0nBo3k2nXUPPRJVJLQ", + "a16d59da-caa5-4388-af67-315bfcb16f64,", + "a16d59da-caa5-4388-af67-315bfcb16f64,bmi_S166JEGWhf_7woVL0A,WWmx0nBo3k2nXUPPRJVJLQ", + "a16d59da-caa5-4388-af67-315bfcb16f64,dSEiNUAZnUmErO5Vfv51zg,WWmx0nBo3k2nXUPPRJVJLQ", + "a16d59da-caa5-4388-af67-315bfcb16f64,jTG-jnKn0k-Etg_-4Gb9gw,WWmx0nBo3k2nXUPPRJVJLQ", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,ojvIFCTpJkeegestF065wA,10fYSIBytk-yUjveDa407w", + "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,071d1bf3-86dc-4a31-b54a-e6579fab8649,d015cae7-a905-49e4-8823-6dec069689a6", + "8fc7aaef-adec-4534-8f91-445e4e7095d5,", + "8fc7aaef-adec-4534-8f91-445e4e7095d5,5Dbc8qCaY0OXEuwykM4naA,awWjleyNP0akWNYJoehCLA", + "8fc7aaef-adec-4534-8f91-445e4e7095d5,HBt3ToOkUUereoPKwyvzuw,awWjleyNP0akWNYJoehCLA", + "6b337a35-95d9-4b3a-9891-d9fbb06262aa,", + "27e742bf-3081-4274-ba70-301f0edeb4f1,", + "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,8c74ee27-3d56-401b-8a8c-7868a80770e8,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,55b4e661-43e1-4708-82d5-2dd2bebd4f45,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,18b52db5-0261-46a0-9214-ff510d455ed9,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,", + "473c672d-2b79-48a2-a49d-fd68160c5bde,", + "473c672d-2b79-48a2-a49d-fd68160c5bde,30c61e63-5e09-4a6f-8915-1e1fe4d55cf0,0e42108e-3b08-49b4-86c0-7984719e8380", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,0ba92db2-690a-44ce-92f4-130e9503b6c1,73fc2e2a-a036-410b-bafa-3a07658245b5", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,73fc2e2a-a036-410b-bafa-3a07658245b5", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,H-ypJNyVSka9as7rDFRZ_A,73fc2e2a-a036-410b-bafa-3a07658245b5", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,-7uPJmO-wUu3cUhqo0caPg,73fc2e2a-a036-410b-bafa-3a07658245b5", + "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9aYT6E9sd0OSavlmkhtkGw,73fc2e2a-a036-410b-bafa-3a07658245b5", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,_4mk8E4PUEGf_3SPuFUy2g", + "8d703940-224a-4b0c-a04d-f365550071e1,", + "8d703940-224a-4b0c-a04d-f365550071e1,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7c4d447c-6594-4bd8-9510-12ad683ab116", + "8d703940-224a-4b0c-a04d-f365550071e1,qdA34j6p_UqphqChTCHEYQ,7c4d447c-6594-4bd8-9510-12ad683ab116", + "8d703940-224a-4b0c-a04d-f365550071e1,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,7c4d447c-6594-4bd8-9510-12ad683ab116", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,c72911d0-453f-4732-b1bd-dad520220a06,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,90bc2d31-1715-4d1a-813a-7c57e66cb017,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,724c79a3-822c-422f-9462-a5374ee0211c,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,d2b40639-5d34-45b6-904e-6dd29030ff44,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ed134bee-d199-43eb-98bd-206571603f40,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ed134bee-d199-43eb-98bd-206571603f40,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ed134bee-d199-43eb-98bd-206571603f40,fd67ec40-f898-4df6-b846-8bf350f362b8", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ed134bee-d199-43eb-98bd-206571603f40,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,", + "39278302-b52a-48a8-a566-ed756a5fa1a9,e3454bc7-c88d-4958-8b81-db3fe5472a1e,da36bbcc-2a9a-4ba2-9e4a-6f52c9b1ef74", + "39278302-b52a-48a8-a566-ed756a5fa1a9,", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,COZbkLDc20mdUlWSkqMX-w,FwjsTo0nb0uNU6l8XvllNA", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,uwoWPY4YjEmZSQcNCrx1Gg,FwjsTo0nb0uNU6l8XvllNA", + "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,SDbpnjVYmEGQneK5fhZ0Cw,FwjsTo0nb0uNU6l8XvllNA", + "2dFMy4zc7ky_Fd5fzfgInw,Y0e7o0_BKUm7XJSLediN5w,4EHIyZkOOEqnM-PuD_a5xw", + "2dFMy4zc7ky_Fd5fzfgInw,", + "2dFMy4zc7ky_Fd5fzfgInw,UUyjVU4mvECPhI9xTMYR1Q,4EHIyZkOOEqnM-PuD_a5xw", + "2dFMy4zc7ky_Fd5fzfgInw,MS7EPNLJEEyMLC6_yHQYtA,4EHIyZkOOEqnM-PuD_a5xw", + "2dFMy4zc7ky_Fd5fzfgInw,HWc25-d8dUmNN2F3n8mV5A,4EHIyZkOOEqnM-PuD_a5xw", + "745c5110-104e-4b37-aabf-7c690fd54811,8cab8cc6-9a2d-434c-8b72-e66edbdd223c,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "745c5110-104e-4b37-aabf-7c690fd54811,b42d903b-b393-4f49-af5e-38d0fb6c42b0,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "745c5110-104e-4b37-aabf-7c690fd54811,w_aK2K9bHEeU6oSQ7BK0qg,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "745c5110-104e-4b37-aabf-7c690fd54811,", + "745c5110-104e-4b37-aabf-7c690fd54811,657f28d5-672a-4f27-86c6-3cb00f25bb84,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "08d56627-4500-4724-8692-a23c96ddb3ec,531b4297-b38b-4362-9854-4f2139ea43be,c2a9c007-2748-4fa2-ab9b-0081eef792f1", + "08d56627-4500-4724-8692-a23c96ddb3ec,", + "08d56627-4500-4724-8692-a23c96ddb3ec,newGzX_zA0eGUX3Iayq5Ag,QBzMp7MdAUOLIofvddui1A", + "e49d5d78-157e-4bc8-b878-ff7c4963fdc4,", + "2b7f4dae-51da-4af0-b009-12d6005c85c6,", + "64ad8789-1645-45f7-819a-a413ff7c9622,", + "64ad8789-1645-45f7-819a-a413ff7c9622,-Moek0heVkWtKpTw9oZ0EA,FsirX90YM0y5lk7mnqDnEQ", + "64ad8789-1645-45f7-819a-a413ff7c9622,D-XJyw1BPE-m0gXpvcXrQw,FsirX90YM0y5lk7mnqDnEQ", + "8df990d9-141a-4757-b4f2-138d5e68ab74,RROvzwtshUGyRudVIBn8bw,MrrsmY0J0EKduH-f5wUj6g", + "8df990d9-141a-4757-b4f2-138d5e68ab74,AfqXBWLBQEiiag4cEtoEiA,MrrsmY0J0EKduH-f5wUj6g", + "8df990d9-141a-4757-b4f2-138d5e68ab74,69vv21jfTUu1E_LjK5bytg,MrrsmY0J0EKduH-f5wUj6g", + "8df990d9-141a-4757-b4f2-138d5e68ab74,", + "exQz9Zr3HEyOR9nDDrimlQ,", + "exQz9Zr3HEyOR9nDDrimlQ,SmRcrycMZ0qYNsHuQQOORA,S21TW8NwMEWLV9zqHxWfFw", + "exQz9Zr3HEyOR9nDDrimlQ,0svDrGn2NkmhUjC1VJUgRg,S21TW8NwMEWLV9zqHxWfFw", + "exQz9Zr3HEyOR9nDDrimlQ,z-yaXP9ImkysLOGuCQiblw,S21TW8NwMEWLV9zqHxWfFw", + "exQz9Zr3HEyOR9nDDrimlQ,Aq4fjEW5gEyzetXMoG2tdQ,S21TW8NwMEWLV9zqHxWfFw", + "76a73866-5b60-424d-b751-a105e030f46e,", + "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,", + "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,a6796264-9c7e-4605-830c-06db8d0dc780,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,f3b2330a-b981-4c7e-902f-1f57382f2388,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,baffdefe-de21-4551-9330-b50873965bdd,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "3e139dd9-4757-43a8-8546-8da70dc17f0a,", + "49800740-33d6-4280-aefb-3497597c6366,0oMo4q5cLEi9LGpkOFIlkA", + "74365234-cb72-409d-8cf5-9a7209f707e1,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,fa9a3d8e-16c9-4a7e-9166-3226e0d13c39", + "74365234-cb72-409d-8cf5-9a7209f707e1,", + "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,5814a331-6155-4df4-b921-3ffc6059e44c", + "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,", + "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,", + "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,2bb65868-ee12-4871-b50d-64bbc9066f0b,e65530f7-543c-43e9-bf12-b6cda2bc755c", + "c161dc73-6ce3-49c4-8d15-45e6911f84e4,", + "49397e16-1027-4286-9bc4-b59eae565ff0,", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,67fc9269-8dd8-4d6f-b594-c77e716f2482,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,d6823e01-69f0-4f85-b94a-74894356a2cf,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,", + "3dec4865-cf23-4c19-a460-dd2db80ee851,", + "3dec4865-cf23-4c19-a460-dd2db80ee851,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "25d4c652-1730-4925-925e-31a290a2272a,", + "25d4c652-1730-4925-925e-31a290a2272a,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5d1c42a0-7e36-431a-8e78-a8b74046d153,", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,uUOO-k5KS0mU7B5BLaOpnA,adae82eb-a4b9-4541-acf2-76cea839a593", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,6584dada-499e-4d7c-958d-466955b20640,adae82eb-a4b9-4541-acf2-76cea839a593", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,026ca50e-3a81-43cf-87f5-950687b7bbdd,adae82eb-a4b9-4541-acf2-76cea839a593", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,Ijf-j7EgEkGYOKo9SQNLRw,adae82eb-a4b9-4541-acf2-76cea839a593", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,", + "973d84ee-5b88-475c-b579-a76c34a7c533,P9bBSL0YLEyjN8PxMfgbKQ,yBNsYcZXzEiuR0u3tWnnWQ", + "973d84ee-5b88-475c-b579-a76c34a7c533,", + "973d84ee-5b88-475c-b579-a76c34a7c533,bgRKH1wfqkWVHcuSnFYnlQ,sHyNs-dpc0-_Ah-HJVczcw", + "973d84ee-5b88-475c-b579-a76c34a7c533,o2WfRyjJy0CKoUg8YIxNjQ,sHyNs-dpc0-_Ah-HJVczcw", + "973d84ee-5b88-475c-b579-a76c34a7c533,yA0KhVg19kWqG1UkyxQogQ,yBNsYcZXzEiuR0u3tWnnWQ", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,IXWd396grEOWHt32L7fCpQ,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Jt0QqNrAp0qzUR_RQoivVg,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G-VKgepBkkeATcwCaS6Rwg,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,1LonkCbt4kykeAScYKpwyg,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,9b9t8_aKH0-gQXMhZycaOA,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,5mcp7-2ZEE6n3-zN3_CoWw,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,lBePHNL9LkyaAjEFyFE8Sw,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,R0UCacpdhUS906Xf7l142A,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Zz4XatKfUEapReRAQHxKKA,rzoH8P44a0esN-HnYd3aQg", + "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G4HIPUjekUigiU0-Kr9mdA,rzoH8P44a0esN-HnYd3aQg", + "_OWVy3z6iU-M3-zbQgSLig,", + "JWfs9IiQs0Weu8MmyGpqLQ,", + "jiBzrzDPkkuL6DqIFKpcCw,", + "b96426ec-093d-4bfe-95e1-b88489479198,", + "AIpF1QUeMUWytpHYBDBAkg,EbVDqEJQOk6j0UvX3t-8rQ,oSaMpc5rTE-U6yhjtPWQgg", + "AIpF1QUeMUWytpHYBDBAkg,JS7aa4QSUUOpUYPqRZEb5g,oSaMpc5rTE-U6yhjtPWQgg", + "AIpF1QUeMUWytpHYBDBAkg,rwojzpr0E0ylG6_Vv_MVaQ", + "AIpF1QUeMUWytpHYBDBAkg,grutFwJD_0Cb3t6fddNg6w,oSaMpc5rTE-U6yhjtPWQgg", + "AIpF1QUeMUWytpHYBDBAkg,", + "AIpF1QUeMUWytpHYBDBAkg,C3ll7uPfUkeZ1QLX80_BpQ,oSaMpc5rTE-U6yhjtPWQgg", + "52VsI_lKlkSUAOJlfDnyrQ,42ZNGHXMvU29yo9s9tVpOQ,MV1HW2x43kG4rrhyoC6mmw", + "52VsI_lKlkSUAOJlfDnyrQ,FvEjo9let0e_Sr-kWq5AvQ,MV1HW2x43kG4rrhyoC6mmw", + "52VsI_lKlkSUAOJlfDnyrQ,6c-AhDM6K0iHbNMf4BNiUg", + "52VsI_lKlkSUAOJlfDnyrQ,HncURj5yi0ykk2cVyCZ4Sw,MV1HW2x43kG4rrhyoC6mmw", + "52VsI_lKlkSUAOJlfDnyrQ,", + "52VsI_lKlkSUAOJlfDnyrQ,VDo682NvSUa8xYn8zTX-AQ,MV1HW2x43kG4rrhyoC6mmw", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,64545dcc-a8eb-4c69-a539-78ee73a2d327,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "6d36091e-7083-4e3d-8729-94612602c8cb,64545dcc-a8eb-4c69-a539-78ee73a2d327,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "6d36091e-7083-4e3d-8729-94612602c8cb,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "418db0e0-c7af-40c2-9cac-d461ce41e210,64545dcc-a8eb-4c69-a539-78ee73a2d327,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "418db0e0-c7af-40c2-9cac-d461ce41e210,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "27cdecd2-88f1-40a8-80a9-87abd827c3c5,", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,357eb160-360b-4a63-840d-65d11cc8ffc2,47514b9b-0d17-4727-97ed-dab67e7fe031", + "01689baf-405e-4b38-9f09-b624e03865b8,", + "01689baf-405e-4b38-9f09-b624e03865b8,357eb160-360b-4a63-840d-65d11cc8ffc2,abdf525b-b871-41be-a1c8-810f49740ad4", + "97eae086-cfea-45ff-9b77-1d184256d493,", + "97eae086-cfea-45ff-9b77-1d184256d493,357eb160-360b-4a63-840d-65d11cc8ffc2,8768aa33-b014-4062-b323-b146dd7d78d2", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,2qYs9IkE90eppx1DggNlsA,6Y43EyTpiEejGp0ODWTSjA", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,j367nSYyg06rOMZ1GmfmFA,FS7kgc4DpkKuar24J9dW8g", + "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,JV60jZGerUWuzQ2rRnwquQ,gSb8iLTEZ0mi3g5-jZXifQ", + "c70005d5-6276-4a98-acb3-6a77bc19379a,OBcu3bf1NEio_7t9smqGag", + "c70005d5-6276-4a98-acb3-6a77bc19379a,dRQI80Qn4E-kzhPJoW66MA", + "c70005d5-6276-4a98-acb3-6a77bc19379a,FuMUQFSnnU6awB-vPaeuZw", + "c70005d5-6276-4a98-acb3-6a77bc19379a,-OgDdlvL9EyzeiA50YY5kw", + "c70005d5-6276-4a98-acb3-6a77bc19379a,F1IV-zZ22kKtXkr2pHp_YQ", + "c70005d5-6276-4a98-acb3-6a77bc19379a,Sad0HQX_Y0eYTx1qqgxKwQ", + "fbdca48d-4ae6-41ad-8c33-b70a9e33fd77,", + "03f8c394-28fa-4087-978b-8d108f0bd969,d7f0ffd3-ccf9-465e-8afe-4d7efb78dd2c,efeb07ab-6253-436d-bb66-991ddad72e58", + "03f8c394-28fa-4087-978b-8d108f0bd969,", + "03f8c394-28fa-4087-978b-8d108f0bd969,225209dc-3afc-40cf-858e-89f7afa00c0d,efeb07ab-6253-436d-bb66-991ddad72e58", + "03f8c394-28fa-4087-978b-8d108f0bd969,3f6c8bd2-2c1d-4c8c-a9de-16600fee9645,efeb07ab-6253-436d-bb66-991ddad72e58", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,97938f4c-52b8-4c85-8575-856654666316,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,cac27853-e2df-4d77-b3fd-af3bc0813f01,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "eS_or4AnMka9M_bD7YN28g,", + "eS_or4AnMka9M_bD7YN28g,1FcyrVGyGEC9dmOq_51nFQ,ZtCqMnFTQUWv2h0MfZ4m6A", + "eS_or4AnMka9M_bD7YN28g,xpkEQ6yjhU-WfFPSjvPdHg,ZtCqMnFTQUWv2h0MfZ4m6A", + "eS_or4AnMka9M_bD7YN28g,LDjFJtM-WkeYhtwA7cyDOg,ZtCqMnFTQUWv2h0MfZ4m6A", + "eS_or4AnMka9M_bD7YN28g,N9CBuYaMtUOrCks5UcOycg,ZtCqMnFTQUWv2h0MfZ4m6A", + "eS_or4AnMka9M_bD7YN28g,zaSTWOUy_k6cnoVVYzypWg,ZtCqMnFTQUWv2h0MfZ4m6A", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,0c433086-9501-48c4-8d0e-4474eb420c61,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,7221132e-053a-4c58-b719-ed3c8558907d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "t9co2516nkuJO4LIJA1bJA,", + "QCtqxbvYOkan79YFugH_Cw,", + "jiBzrzDPkkuL6DqIFKpcCw,0S7Nqm2kv0qMDTh-5Wy0MQ,StxMBEprVkqMa_IoDG-Q9A", + "c65a94e6-7475-4030-8fcd-9ce25852ff7c,", + "17aed0a0-70ed-49da-ad09-5bc4746f7718,", + "17aed0a0-70ed-49da-ad09-5bc4746f7718,14b43bf9-5ba0-484e-ae5d-55cfeb09a70d,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "17aed0a0-70ed-49da-ad09-5bc4746f7718,5c8f36c5-07da-49f4-af02-d23207a43216,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "02077915-57d3-4e26-b4fe-8e0399cbb293,8c94e45c-6605-4b4f-9ba4-2c6c6e557228,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "02077915-57d3-4e26-b4fe-8e0399cbb293,", + "02077915-57d3-4e26-b4fe-8e0399cbb293,17b16982-871c-476a-860f-f6953c89ee8c,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "42aa7888-f0b9-4584-9cc1-966585c6b36e,da185fbb-ad2b-4f30-87a2-491f24b78ebb,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "42aa7888-f0b9-4584-9cc1-966585c6b36e,", + "42aa7888-f0b9-4584-9cc1-966585c6b36e,2APfqVnnQUaDYKi-fQg8jg,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "42aa7888-f0b9-4584-9cc1-966585c6b36e,bea795cc-aabd-4229-84dd-49ab4f91a445,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "47629ed1-5378-427c-ad61-4b44d2fe90cc,7b82c3c5-fd1f-45ec-b83f-e32668aa8dce,556f876a-70da-4104-b792-fca8ad67be0c", + "47629ed1-5378-427c-ad61-4b44d2fe90cc,", + "47629ed1-5378-427c-ad61-4b44d2fe90cc,qJGX62O_HUiZWN61NAc1Lg,556f876a-70da-4104-b792-fca8ad67be0c", + "47629ed1-5378-427c-ad61-4b44d2fe90cc,23428ca9-be75-4146-b400-df01d1dadac9,556f876a-70da-4104-b792-fca8ad67be0c", + "dcf9012d-f9e9-4f15-872e-f130af3b2efa,631c9015-2a00-47ea-bb35-465a2ddbca5d,83158223-e268-4036-8a6a-218a8430eb2c", + "dcf9012d-f9e9-4f15-872e-f130af3b2efa,7f9cb939-36a4-40da-8337-025e1d7bf8e5,83158223-e268-4036-8a6a-218a8430eb2c", + "dcf9012d-f9e9-4f15-872e-f130af3b2efa,", + "b17591ba-3c2d-4309-ab01-b6743424e07e,", + "293e7ad3-01c9-43af-96e3-06c34145ff12,", + "293e7ad3-01c9-43af-96e3-06c34145ff12,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,FVdDQFOMQ06teQm2ANDPng", + "293e7ad3-01c9-43af-96e3-06c34145ff12,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,FVdDQFOMQ06teQm2ANDPng", + "2f8518fc-c989-40de-98b4-c6f09b304166,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "2f8518fc-c989-40de-98b4-c6f09b304166,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "2f8518fc-c989-40de-98b4-c6f09b304166,", + "2f8518fc-c989-40de-98b4-c6f09b304166,e5de2e45-207c-46af-9a5e-eb01a0c621fe,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "2f8518fc-c989-40de-98b4-c6f09b304166,de0a5a3a-8aff-4799-8269-9efa3aeb383a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "348ac48b-986e-4d42-a0f7-1aea16b88271,59a62fa3-9e15-463e-9bd5-3aecdf604c1b,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "348ac48b-986e-4d42-a0f7-1aea16b88271,", + "348ac48b-986e-4d42-a0f7-1aea16b88271,ojTIShRZHEOV1qQRfhwvTA,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "348ac48b-986e-4d42-a0f7-1aea16b88271,4effd67c-a52f-4b41-a5b0-04287f91a7bc,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "348ac48b-986e-4d42-a0f7-1aea16b88271,se0-2ylLD0u6sqCVPkeP6A,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "348ac48b-986e-4d42-a0f7-1aea16b88271,RU-B71GUF0WPduoSOI1XMg,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "71014872-d879-4858-9d68-58da3c673d8b,83522c12-9549-4d74-8c87-bae210bcd2bf,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "71014872-d879-4858-9d68-58da3c673d8b,", + "71014872-d879-4858-9d68-58da3c673d8b,XNq4Cns4lEGydtktdsXlmQ,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "71014872-d879-4858-9d68-58da3c673d8b,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "843e3364-a743-4dfc-a990-0436e47b8c10,83522c12-9549-4d74-8c87-bae210bcd2bf,1695608b-e9cc-4a03-b56d-0f09d2804379", + "843e3364-a743-4dfc-a990-0436e47b8c10,c5884778-a87f-4612-9717-86fbb65d440f,1695608b-e9cc-4a03-b56d-0f09d2804379", + "843e3364-a743-4dfc-a990-0436e47b8c10,XNq4Cns4lEGydtktdsXlmQ,1695608b-e9cc-4a03-b56d-0f09d2804379", + "843e3364-a743-4dfc-a990-0436e47b8c10,", + "_qXaNbm4yEeyalo372QbvA,", + "308171c3-cfd1-4ff2-8593-856e8de7071a,C9CZUwEZeUiuOue4CK4luA,gUxl7WNXlUqFYHZ9eGq1cg", + "308171c3-cfd1-4ff2-8593-856e8de7071a,46f7Ewe99E6tF2qFaMtQ0g,8IK1U23XlUOV-2ibVznkDQ", + "308171c3-cfd1-4ff2-8593-856e8de7071a,ZsWc8wRafEGl3B7cr6UkzA,ByIfwCtDwU-p9rEfqpjfGg", + "308171c3-cfd1-4ff2-8593-856e8de7071a,imOiYzABkUm9C5EVzvjXgg,0eZVIsv7wUyZ0ohLmF1jGQ", + "308171c3-cfd1-4ff2-8593-856e8de7071a,FgLAEMh1e0GyHO8J8_xj5Q,iM-04HpG6k2N8n-P3XS22w", + "308171c3-cfd1-4ff2-8593-856e8de7071a,adwzLAQEykK8D5hZGQ6yiA,0Nca5nBUpUquFhExd5dxcQ", + "308171c3-cfd1-4ff2-8593-856e8de7071a,", + "308171c3-cfd1-4ff2-8593-856e8de7071a,mM7I9kDTx0ut243UkWKXvw,YKaJjmMHo02FWwem8nw1yw", + "308171c3-cfd1-4ff2-8593-856e8de7071a,R0f3hHGY9UqUEW4aR1fssA,KSqfmVTiGUKOKijwGDSwgQ", + "308171c3-cfd1-4ff2-8593-856e8de7071a,-YsxhNXLAk2qbeXMqwAqng,aNOhXS4nuUeqs0CVw6K7Hg", + "308171c3-cfd1-4ff2-8593-856e8de7071a,ro4sD9txVE29_eT2jK-wEg,0YF5bACMsk2sN4tixbJbKw", + "308171c3-cfd1-4ff2-8593-856e8de7071a,kQD41xK4dU6dI_6pRkP0qA,aSeusgYlPE6vOBu9Ua_dWA", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,sP8lTqgdk0WyuwQczT_ijg", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,Zf0z5Ghkwk-QJJq4u7Fjww", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,dudShaXMYkaIZdZfyleoXw", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rsh2uKli90OoEnJZRFc2Ww", + "2ddd2c2b-596c-4022-a488-655b0db1c265,", + "1e504fee-b593-4037-bd8f-b88025147991,c9589974-c261-485e-b571-cb2f34fb178f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "1e504fee-b593-4037-bd8f-b88025147991,", + "1e504fee-b593-4037-bd8f-b88025147991,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "CQuKKNpan0O7rhR1fr14-Q,-k9_-2onrEG407or8t1RFw,b-IM3eYMJUa40395O5XeLg", + "CQuKKNpan0O7rhR1fr14-Q,GDvaUU10-EKh3pKUQ9tnag,b-IM3eYMJUa40395O5XeLg", + "CQuKKNpan0O7rhR1fr14-Q,kLyM-6otSUy2ql5SEmnF9Q,b-IM3eYMJUa40395O5XeLg", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e69372eb-2fd8-4e17-a448-ee0be61a2c7a,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "b9987d0c-0cbf-41dc-9a9c-8fdb5165800e,", + "c6c08eb5-381a-4193-9722-80da95d62abe,31jelNWFyk6FlWt4HyPAlw,9zGwmle_yk25V-KnC_PJEg", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,bIQKJloWV0SEnBEtoxP_Wg", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,3df5d13b-ba77-4b51-90b2-7a946e4d0be7,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b5d62931-e756-4a8a-8f3b-760beba49319,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,7c1aaa7e-503b-46ef-b1c5-0c61a75b916e,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,6145292a-9002-41c6-8cd5-43679b6f542f,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b9f51861-ad10-4878-a9b1-d849bccf532a,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,857fdcd8-704f-43d0-92b9-bca26619b153,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,27af51bc-8226-4911-8d47-22d28ea8e378,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,", + "07205057-0804-41de-abe4-bd4027fee1df,", + "07205057-0804-41de-abe4-bd4027fee1df,jwXdjDbEmkOFyKZHT_RYOw,YJGsYfwkik2lp7qWqXyXTw", + "07205057-0804-41de-abe4-bd4027fee1df,LdKaVAveokmp3qRwVBxnFA,YJGsYfwkik2lp7qWqXyXTw", + "07205057-0804-41de-abe4-bd4027fee1df,e-oZt-PbKEOfawVammBQTA", + "3bb50a88-09ae-48c7-a42c-df5879680eff,", + "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,", + "e9913b06-58cb-4b90-bd08-981a3f19f352,", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,c4ebfd1a-fcda-4b98-a151-a941b1801867,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,wkqKPZmnoU-8TXSWzi2-Ow,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,17e44296-1cd9-4f8e-9aa7-3837dc69279a,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,tOlpTL8F1Ui_akCjHq5vRg", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,rfIzQjxeRU60VdRQXG9Ccw", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,wat8f6fs80KptZbW39e8tA,8eef8837-9141-4088-835a-b2f47f4126f8", + "9eb7f505-5afd-4b28-9886-7996d38c59c6,c0c33a35-494c-4a1a-aa53-d27d9b9ef5a4,8eef8837-9141-4088-835a-b2f47f4126f8", + "cca7ab3d-5609-4700-92fd-3280fcea9b77,", + "67c0f944-2f70-46d2-8588-2f67028f8bed,", + "5bde836f-9343-4934-b01a-606f707020ef,fEPwvuJCWkGBe4IekXpmLw,axPBGoqorEe77Oqqjfzb5w", + "5bde836f-9343-4934-b01a-606f707020ef,MQOxmUk-mUG-Ee9m9nX31Q,axPBGoqorEe77Oqqjfzb5w", + "5bde836f-9343-4934-b01a-606f707020ef,", + "033e328f-d9d2-4e53-af20-44f18e3ea208,Z1Ib9r_fkUqgoZk0uid_Xw,9ZP617DQk0mZ10J3d5q9oA", + "033e328f-d9d2-4e53-af20-44f18e3ea208,msOFHoFN9UORkeXXtdqBsw,9ZP617DQk0mZ10J3d5q9oA", + "033e328f-d9d2-4e53-af20-44f18e3ea208,", + "587ca2bb-dd42-42bf-833f-4e2a97349866,", + "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,4a85c181-92fe-47a5-925e-05f0ccb25006,944d934d-5e21-42d7-8196-fff9a81ee469", + "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,", + "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,0ba92db2-690a-44ce-92f4-130e9503b6c1,944d934d-5e21-42d7-8196-fff9a81ee469", + "4d533b3a-b5fa-446a-842d-92798ad5b493,fe5288bc-0e02-4ea3-b17d-dee0e576979d,6b38ab79-142b-446e-978c-ad829ade8823", + "4d533b3a-b5fa-446a-842d-92798ad5b493,b444ed0a-eab5-4bd6-af34-d080de74a149,6b38ab79-142b-446e-978c-ad829ade8823", + "4d533b3a-b5fa-446a-842d-92798ad5b493,", + "4d533b3a-b5fa-446a-842d-92798ad5b493,8053b5e5-0f67-4329-a45d-1ee9fc830820,6b38ab79-142b-446e-978c-ad829ade8823", + "34654303-9760-447f-8614-1c26506db994,", + "3dc003c5-428b-47d6-80fb-6520b63119e8,", + "3dc003c5-428b-47d6-80fb-6520b63119e8,67fc9269-8dd8-4d6f-b594-c77e716f2482,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "3dc003c5-428b-47d6-80fb-6520b63119e8,b995b419-1897-4c1d-98be-83d551f7ba71,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "3dc003c5-428b-47d6-80fb-6520b63119e8,5fb2e235-8c67-4335-b432-edb10db2b139,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "3dc003c5-428b-47d6-80fb-6520b63119e8,b539777f-48b2-44a2-9d63-17ac703e4d57,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "a628d62c-fa30-4405-bcbc-3e646e3a3434,", + "a628d62c-fa30-4405-bcbc-3e646e3a3434,55901f12-d5b5-4fa8-b4c8-e479689ee39d,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "a628d62c-fa30-4405-bcbc-3e646e3a3434,4828b50c-95b6-466a-bb25-514891d78202,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "a628d62c-fa30-4405-bcbc-3e646e3a3434,d6823e01-69f0-4f85-b94a-74894356a2cf,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "7553d851-1986-4325-870d-e425dfb7d007,", + "cea5c141-32d8-4dae-80d9-6fb74834a79c,", + "967187b8-df20-4f4f-b48f-10b9ea75cedf,", + "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,7b0d6661-e856-4ec1-ab90-d89c28a70826,1fede91a-434e-49a5-882e-e6db107112a8", + "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,", + "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,8c74ee27-3d56-401b-8a8c-7868a80770e8,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,", + "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,cea53c7f-24a9-4e3b-80b9-fbf95738f973,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "8b065e2a-b106-4f11-b415-4c47ce87993e,eOJkmpSL4EypK-AQz7j8gQ,uZjxYOMa10GzJ9wQkRge2w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,hRxyLsKfsESFAsXAyZQjoA,uZjxYOMa10GzJ9wQkRge2w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,kgCKjtSjFEmFq9ez2llxKQ,uZjxYOMa10GzJ9wQkRge2w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,sscD2O3LEUOAKkqjGpGe8g,uZjxYOMa10GzJ9wQkRge2w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,KaZqNx5VKkarRrOJcTuD9A,uZjxYOMa10GzJ9wQkRge2w", + "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,565c17e0-496d-45bd-a2c3-564e118c06cc,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,", + "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb,", + "49800740-33d6-4280-aefb-3497597c6366,", + "5a38f864-7d1c-4989-8558-40b9668f78ef,61ff7a02-50ff-49ea-b95e-c31b95a30c8e,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,UfhOpLemQ0ypYh7CIC3W1Q,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,d8d490aa-7088-4163-9305-1441367f490c,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,X24EvVokeUqyDNdqevV2EA,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "5a38f864-7d1c-4989-8558-40b9668f78ef,", + "34d45669-b319-4c7d-bd05-078961d70d0b,88c3ed05-ce41-4a9d-a4a7-8a428d240b3f,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,kCqthBeVvUKZA9b76kxuVQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,32dde6dc-e3dc-43ee-9b7c-987bc6778c17,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,vMqhT19x302aYEo6E9HhOA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "34d45669-b319-4c7d-bd05-078961d70d0b,", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,aO5Q4BPpuU-bsNavGii40A", + "cf1a6f8f-1649-423b-b513-b8d40d242b1f,", + "f7764314-c02c-4b46-9c56-8c60351d9b14,", + "eada710d-ea0c-473f-b9c6-23e210607b24,32a5e42d-2b60-40b1-a145-ed96fc91af8e,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "eada710d-ea0c-473f-b9c6-23e210607b24,72a010c0-bddd-4e5d-bca6-9821d82de9a6,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "eada710d-ea0c-473f-b9c6-23e210607b24,", + "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,6584dada-499e-4d7c-958d-466955b20640,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,48c18418-01dd-40ff-9dfe-c4cf5f4e1245,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,", + "f76b709c-b173-426e-81f2-279a67d2fde1,4c845dbb-26c3-48a1-b562-e28a7416b154,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "f76b709c-b173-426e-81f2-279a67d2fde1,96e2b29e-e553-40f1-9eb6-8a9f7e64da39,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "f76b709c-b173-426e-81f2-279a67d2fde1,", + "q7pSsRMNHkqEJmYHuhhG6g,hZKPr4sg10SRj9DLuEgOrw,ojCeJ1VwtUelyVqFnUwK5A", + "q7pSsRMNHkqEJmYHuhhG6g,90qQe0AR3kOou0MsuoTmPA,ojCeJ1VwtUelyVqFnUwK5A", + "q7pSsRMNHkqEJmYHuhhG6g,", + "q7pSsRMNHkqEJmYHuhhG6g,x3Q0vVeG_E6juqGM1o0SOQ,ojCeJ1VwtUelyVqFnUwK5A", + "f09be0ec-19eb-4443-843c-b387b6731e48,", + "fa0e701c-5e80-46f4-8817-6e2dbf904734,", + "fa0e701c-5e80-46f4-8817-6e2dbf904734,oCeStQfVkE2ZwCcQjtV1bg,r94RX2106E2TRK2wQ-u8-w", + "fa0e701c-5e80-46f4-8817-6e2dbf904734,VmHQJIt7HkO4INJEXCBbNA", + "b6d10952-d492-45e5-b54c-de3ab138be0b,2KCpbel8JkaeAGQRgvpt2w", + "b6d10952-d492-45e5-b54c-de3ab138be0b,", + "97253092-90ba-4ff2-b137-6324ab244d11,HQOKcDCvVUCItkAKo2ygGg,RmjxL1O9bEGOuYxY9kzHEA", + "97253092-90ba-4ff2-b137-6324ab244d11,i4Zu9uZuWkWwm8IVGZwkbA,RmjxL1O9bEGOuYxY9kzHEA", + "97253092-90ba-4ff2-b137-6324ab244d11,", + "1014dded-3bbc-479b-a6cb-afa7413de2da,", + "1014dded-3bbc-479b-a6cb-afa7413de2da,hiMYlmk5sEqh-sElOY3A9g,egpzBs77Q0CpDndZHqXOog", + "1014dded-3bbc-479b-a6cb-afa7413de2da,9SAPupKy_EO6YeG0SMkI5A,egpzBs77Q0CpDndZHqXOog", + "1014dded-3bbc-479b-a6cb-afa7413de2da,DtFYrQwaCky5C1ZycqZbiQ,egpzBs77Q0CpDndZHqXOog", + "5f492f8f-ab61-484a-9587-747132b54309,3b2cfc85-4e2a-4e0d-b738-c296ae8692f8,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "5f492f8f-ab61-484a-9587-747132b54309,", + "5f492f8f-ab61-484a-9587-747132b54309,78eb0969-f246-4cac-acd6-295df7c092e4,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,YzJL2s9S0Umv5XH_g5PG_g", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,59866578-d4e0-417b-9483-233ab108b1ac,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,59866578-d4e0-417b-9483-233ab108b1ac,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,59866578-d4e0-417b-9483-233ab108b1ac,fd67ec40-f898-4df6-b846-8bf350f362b8", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,59866578-d4e0-417b-9483-233ab108b1ac,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "7134c60b-0640-463a-8d97-caa9184bef93,e3454bc7-c88d-4958-8b81-db3fe5472a1e,7e688b30-ed13-4790-9ad8-c49885d1b7e8", + "7134c60b-0640-463a-8d97-caa9184bef93,", + "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,", + "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,8NY4MevLp0C2lP178fIZ9w", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,wyKAZSZwT0u9cuSaQ_-IHg,vUgpsaXrO0ak7huhkHqlUA", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,D9U3j_XgSUesfHtSNoAdLw", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,_OM7UNZENEejgO5h7H1ztg,vUgpsaXrO0ak7huhkHqlUA", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,ZXeI9jooq0-dQaX-FlkNuQ,CAqT_iLfQ0qQ-GbIdAsZow", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,nPUQvBsxu0mzhQg9YnWEgw,CAqT_iLfQ0qQ-GbIdAsZow", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,IzdUC8x0F02i3BhPfEUOYw,CAqT_iLfQ0qQ-GbIdAsZow", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,", + "0QOlT9c5xUuinpfZaoEe8Q,B6qNcZQ770eJel13RskuIg,sIP83lqv3kG31TH5Rw4gqg", + "0QOlT9c5xUuinpfZaoEe8Q,p3g8YAOhTUKAiE-Wh-X-aA,sIP83lqv3kG31TH5Rw4gqg", + "0QOlT9c5xUuinpfZaoEe8Q,Mt2a9ygWPkS-DyXj1q-Kvw,znNiHfNdTE-szOIAnIWFQg", + "0QOlT9c5xUuinpfZaoEe8Q,LEUQnDz-k0m-hY-AX3Z9Sg,znNiHfNdTE-szOIAnIWFQg", + "0QOlT9c5xUuinpfZaoEe8Q,cSFt9MWLj06CDzIzATqrTg,znNiHfNdTE-szOIAnIWFQg", + "0QOlT9c5xUuinpfZaoEe8Q,SqHAYcqBXU2PtF2urWGruQ,sIP83lqv3kG31TH5Rw4gqg", + "3eb14fcf-5d20-47d3-b991-97808798a225,J5VsmblfWU2MeUjNo0fz1Q,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,Asfe0ZRi50eryD5wlJzgzA,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,fV4H_1ZXx0-OVtlasI-xGQ,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,yXZO-donI0SHB9-TT0jUdw,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,B9DJPqTeYUuPnPHRbKzEJA,5ekjxLQZwUm7-iXJ0bYSiQ", + "3eb14fcf-5d20-47d3-b991-97808798a225,", + "vDSlMnayzEqWXwT-lkcKSA,4ijeE3LqUUuSplKrMto6Kg,ytfIq0TiYU6EUdd0ruaGAQ", + "vDSlMnayzEqWXwT-lkcKSA,d27e5m1BLUWI42Pxnj6F4A,ytfIq0TiYU6EUdd0ruaGAQ", + "vDSlMnayzEqWXwT-lkcKSA,otQaNOsODUSTfHlCX0IVOA,0WOBGm34H0ySBC10VToa9g", + "vDSlMnayzEqWXwT-lkcKSA,NKf5-r2S_0iP0SaO9bjvaw,0WOBGm34H0ySBC10VToa9g", + "vDSlMnayzEqWXwT-lkcKSA,adJVA26PyUSVvmZgxkCAmQ,0WOBGm34H0ySBC10VToa9g", + "vDSlMnayzEqWXwT-lkcKSA,RZFWcTzxPkOfUuTIQ1mzkg,ytfIq0TiYU6EUdd0ruaGAQ", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,916384ca-e209-42a0-bd64-c64f5a40b56f,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "61ba3c90-c81e-4deb-bc79-50c0f1fe3e83,", + "b537c30c-faef-4649-9a1f-be3985936f93,cKHAetNcgUOxFfDkAPPF9w,lb5tuJOqnUKp9prjjlgHmA", + "b537c30c-faef-4649-9a1f-be3985936f93,7amHTgEv-UOJSGl0ofo4mw,lb5tuJOqnUKp9prjjlgHmA", + "b537c30c-faef-4649-9a1f-be3985936f93,", + "350c5c1e-318b-4233-a7e5-1f49909d8e65,cKHAetNcgUOxFfDkAPPF9w,CUCKsNVnIkavHjcspWAv_A", + "350c5c1e-318b-4233-a7e5-1f49909d8e65,7amHTgEv-UOJSGl0ofo4mw,CUCKsNVnIkavHjcspWAv_A", + "350c5c1e-318b-4233-a7e5-1f49909d8e65,", + "0c537a84-9095-406f-8c87-1464f3a774ba,", + "0c537a84-9095-406f-8c87-1464f3a774ba,LOeHr4xoik6VOv1fOeImjA,3pdsIrhM9EW-y-LNnjbFxg", + "0c537a84-9095-406f-8c87-1464f3a774ba,24zVmiGiaESrM5qcRfF6gA,3pdsIrhM9EW-y-LNnjbFxg", + "0c537a84-9095-406f-8c87-1464f3a774ba,C9cPjOoiCUCHrCBi809CMQ,3pdsIrhM9EW-y-LNnjbFxg", + "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,", + "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,EiGgN7uVukqoRE7UJcMLTw,H3OQDBef1UaacSD52p8Hdw", + "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,nKauYAyWhkm2AMHFyC-fLg,H3OQDBef1UaacSD52p8Hdw", + "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,iokdlBfjuEuAwqIMOQwxdA,H3OQDBef1UaacSD52p8Hdw", + "76369aef-aeda-46d2-996a-cd00594d0543,", + "76369aef-aeda-46d2-996a-cd00594d0543,1421caa9-9a0d-42c2-b847-1500ba2e483a,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "76369aef-aeda-46d2-996a-cd00594d0543,390a476a-5545-48eb-955c-1bf1a16012bf,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "76369aef-aeda-46d2-996a-cd00594d0543,PZx7Iae0pkiDaxT9t3-vgg,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "76369aef-aeda-46d2-996a-cd00594d0543,40283127-2ffc-4989-82f6-29c3381bf9e2,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "ca218759-7eed-440e-b5dc-dba15f81e598,ulOBBvFEJ0ioFA1qmL390A,u9yHtj_tfkuowGp0h_YpFg", + "ca218759-7eed-440e-b5dc-dba15f81e598,V4CVhCB8eU-nrfYgticVOA,u9yHtj_tfkuowGp0h_YpFg", + "ca218759-7eed-440e-b5dc-dba15f81e598,6ZtRAbD-xUufWNLCrlrrCg,u9yHtj_tfkuowGp0h_YpFg", + "ca218759-7eed-440e-b5dc-dba15f81e598,", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,TC79rbThdkCZHGWaxk-qYA", + "35d67043-ca27-4b8a-a893-2d42674e6e68,", + "34144f4c-f6ef-49e5-a286-1781e82e6782,", + "34144f4c-f6ef-49e5-a286-1781e82e6782,kS7jek5tPkSkIgjIF369Vg,_Jx5VqGbVUCPwl_SaUCTRA", + "34144f4c-f6ef-49e5-a286-1781e82e6782,3HAMDzfFO06jtYNXm2Bsjg,_Jx5VqGbVUCPwl_SaUCTRA", + "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,", + "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,fvoaMN4M10WS6jiA6XlOJQ,AZnPWCN41kmNwFwDqWGFWQ", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,uiBklotTUEqRI2F67vyJyA,AZnPWCN41kmNwFwDqWGFWQ", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,jXBi28cRV0CEQgKIrpD0_A,AZnPWCN41kmNwFwDqWGFWQ", + "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,PwsLxCnzOkSW-RGz3tNb1w,AZnPWCN41kmNwFwDqWGFWQ", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,tVbu4hUFHkmEKEz38GG2Nw", + "ad55bb62-671a-4a29-aec8-36a1e3104f05,", + "be75b46c-ce22-4758-8810-c409c5aa1c50,", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,0724b166-9ace-46ff-a191-b13f135b00a8,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,4169bfed-8403-4590-a29d-4941275ac0b6,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,AdJ3ExsUq0ixzaKDHK5RUQ", + "45695fba-5abc-4c5b-b9eb-949d5174d1f5,", + "45695fba-5abc-4c5b-b9eb-949d5174d1f5,oFjt2tSsa0CJ7j0s3P6b4Q,XD9dH8LF_ECmL0TwXJW1zg", + "2bd53190-46f8-4956-9388-87c5a04efc4f,v3af60p1Q02NpdCqIRO9lw,1hVOI7IuY0mKvtr2PZXLww", + "2bd53190-46f8-4956-9388-87c5a04efc4f,", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,GH9q8_0O9EqjGdz7lGWD2g,T7zwcIIc8Ee3ud8a2JCuwQ", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,M-N9vQt0CEefqgmeLp3l4g,T7zwcIIc8Ee3ud8a2JCuwQ", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,q_HcScqHgEG-x70g1wtfLA,T7zwcIIc8Ee3ud8a2JCuwQ", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,", + "74c65554-0d15-409b-b9a8-0b2620a7d7e2,kkvNbVRjcEKtChpy3W1bqQ,nbhzQy_Hdkeohh4S1IezwA", + "74c65554-0d15-409b-b9a8-0b2620a7d7e2,GVoh3mdWf0WNA9lfqL1GEw,w9qG5gHlAUqh8f9ZgnezsQ", + "74c65554-0d15-409b-b9a8-0b2620a7d7e2,x4PPvuhg6USE6GSFgZZnCg,w9qG5gHlAUqh8f9ZgnezsQ", + "74c65554-0d15-409b-b9a8-0b2620a7d7e2,", + "cf1a6f8f-1649-423b-b513-b8d40d242b1f,8lwfuCRnU0291wqwgTwgRQ", + "bc7299ac-be2b-404f-b8e6-402e2c0660f0,", + "bc7299ac-be2b-404f-b8e6-402e2c0660f0,2pnfLqkYZ0-GbpeE8g0vVA,hGtyDIIWckGoRgCaSG6GBw", + "bc7299ac-be2b-404f-b8e6-402e2c0660f0,t_y2xmFYLU2v6Jb6PX6v_g,4J9GPTKjjUOjyLVPmJyxWg", + "bc7299ac-be2b-404f-b8e6-402e2c0660f0,vw2iLomK9keTfEWsqJrmhA,hGtyDIIWckGoRgCaSG6GBw", + "1beaf288-b5a5-475c-8149-c8f3d1146083,", + "1beaf288-b5a5-475c-8149-c8f3d1146083,sX0ZZDcCUkyFSJISfvq0vg,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,TnEDe1QoMUCUdqPL_7CxTA,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,RfZNDPUok0WyQEa3Ko08ow,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,AC3zfm38JEyBZWuXDh4Ukg,rgzG7sfrlkqd9EO13LNmpQ", + "1beaf288-b5a5-475c-8149-c8f3d1146083,dyEaPloE8kazWC7Zblv8uQ,rgzG7sfrlkqd9EO13LNmpQ", + "f47f81e7-fcdd-4687-9b99-013147bef30f,", + "1041d745-c5e9-4218-a3e1-198f5c9c418c,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,84c016ad-133f-45e3-a412-cac35612540b", + "1041d745-c5e9-4218-a3e1-198f5c9c418c,", + "2c679f89-c76e-4cfb-94e9-448c8fd44d55,", + "fe34adaf-47d3-435b-b2fd-b2fee05fceae,KTidYfH0ckWxJGs5qzUBTQ,w07bkcUoQEiNJTVDeSywzQ", + "fe34adaf-47d3-435b-b2fd-b2fee05fceae,", + "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,KTidYfH0ckWxJGs5qzUBTQ,VSZX99mSt0e7gPRM2_XzMw", + "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,", + "11e508a7-fe7d-470e-b270-2dba33e2f7a1,", + "f17a2f9e-1eab-44e8-b10a-1f53c5d0778c,", + "f60e093f-33be-4527-b827-060adc77b1f4,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,464fcf20-361f-4dc9-b074-150c3bc27523", + "f60e093f-33be-4527-b827-060adc77b1f4,", + "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,", + "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,zGOD45zNAk2i6P_zPsNPfg,ORmS9HkfoUeFuO4RhqcW7g", + "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,GQSpXmD0B02_gwzpq6StFA,ORmS9HkfoUeFuO4RhqcW7g", + "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,YvPeCbQS2UOcLjPfoIML_g,ORmS9HkfoUeFuO4RhqcW7g", + "8cc01536-2f43-4de4-ac63-7304d8beb73c,", + "9b62cbca-694f-4a32-b910-14da66bee9bb,", + "3dec4865-cf23-4c19-a460-dd2db80ee851,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "3dec4865-cf23-4c19-a460-dd2db80ee851,002b07bb-4256-44ce-8ca4-25510e70b6b5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,002b07bb-4256-44ce-8ca4-25510e70b6b5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "25d4c652-1730-4925-925e-31a290a2272a,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "25d4c652-1730-4925-925e-31a290a2272a,002b07bb-4256-44ce-8ca4-25510e70b6b5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "94eb7ef3-528f-4f59-92a0-48b0ec287527,whEHo6Rhr0CxsDBsBD32WA,av4PwLz6YkmpBRImQZgn4Q", + "94eb7ef3-528f-4f59-92a0-48b0ec287527,", + "b6610b7b-3978-46fd-9af6-41b4568c9b4d,OnQeVxdstkS2zGxLBAuM3Q,i8bvbGAKREmAEbjKO-geOQ", + "b6610b7b-3978-46fd-9af6-41b4568c9b4d,", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e4IDo-fuH0C9YByzcX5k-A,qCIoeLQw-Uq-SKIetIgIpA", + "250bbb53-1fae-440d-b25a-46a16c271367,ZvUwd0aVfk6SjA7oyouXUQ", + "6427bd57-fcb2-420f-8b3e-d1da43470b84,", + "6427bd57-fcb2-420f-8b3e-d1da43470b84,0Ytm7G4_lkmZWPA_YfaIeQ,KfM7veOVlE-dNZmsbDNZBA", + "6427bd57-fcb2-420f-8b3e-d1da43470b84,dZM1bs6_MU2F6Vz1mH0QKg,KfM7veOVlE-dNZmsbDNZBA", + "a928c6dc-442f-4cbb-b705-cc5a3969b34a,", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,304e5cf1-5c94-481f-8e49-e6a943e6b300,90e0da8b-1941-4ca8-b7e5-b4e31921873c", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,", + "BR0mZKsKaE-t4R8CzEwuyg,B1IotYmwbkiUAfdZiqzq8w", + "cbN5e-t_jEKa4hzir5JAxQ,", + "vDSlMnayzEqWXwT-lkcKSA,", + "vDSlMnayzEqWXwT-lkcKSA,gwdtq3NiEEmMKYtn1VPoiw,M-mi8QUHwEeIewXE8uAlrQ", + "ImwG0snB9ECc_gqRRBoz6A,lAjUeUtTLU2-3bG2OXBukg,LFB_YqTym0-tQmUIj-YcYA", + "ImwG0snB9ECc_gqRRBoz6A,", + "QK6lBlX_wUCiSbJ9RkIwdA,H0sqS2_VPEieVrHSyMNo7g,9HVoTIf90Ea8_czICVg9AA", + "QK6lBlX_wUCiSbJ9RkIwdA,", + "TjrFi6TFhUC5tQXWkZiK9A,3qu7mxSNpkKRrYMhsWHVGg,K5NXVecrqk6Nb6SkBu08ww", + "TjrFi6TFhUC5tQXWkZiK9A,Ij8MXrl1Okip4M7y-gF23Q,K5NXVecrqk6Nb6SkBu08ww", + "TjrFi6TFhUC5tQXWkZiK9A,", + "TjrFi6TFhUC5tQXWkZiK9A,qEojxjSXm06CX52hRDJAew,K5NXVecrqk6Nb6SkBu08ww", + "8b065e2a-b106-4f11-b415-4c47ce87993e,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,", + "8b065e2a-b106-4f11-b415-4c47ce87993e,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "3e3f5ed5-d810-4054-b648-d93167b90033,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "3e3f5ed5-d810-4054-b648-d93167b90033,", + "3e3f5ed5-d810-4054-b648-d93167b90033,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "3e3f5ed5-d810-4054-b648-d93167b90033,4Eck_dNL1UKlIZWsA1YppQ,WeyKw_WS70Cx3NZdOaxGeA", + "3e3f5ed5-d810-4054-b648-d93167b90033,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "8b065e2a-b106-4f11-b415-4c47ce87993e,o2WfRyjJy0CKoUg8YIxNjQ,sxWOOK1RwUOp5YqZ7a36xg", + "8b065e2a-b106-4f11-b415-4c47ce87993e,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "_OWVy3z6iU-M3-zbQgSLig,iJW8XGJfU0G12iXarjxhdQ,N5wN1l0tYEau5np-9PoIiA", + "JWfs9IiQs0Weu8MmyGpqLQ,9QG6qp5FBU-u0-JK-yMMqA,x10FeRBsQU2DIIpG8XIlwg", + "jiBzrzDPkkuL6DqIFKpcCw,lvDIOeSI10SNxnCO_M_9zA,StxMBEprVkqMa_IoDG-Q9A", + "8c51fc1a-b1ad-4002-a13c-4313abd92c7f,", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,G2ncsS9U8E6fyjvliFqdOw", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,m8x6fiLEw0m4pJywntEpzg", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,909ed249-ce09-4304-8b02-56794fa7567d,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "6d36091e-7083-4e3d-8729-94612602c8cb,909ed249-ce09-4304-8b02-56794fa7567d,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "418db0e0-c7af-40c2-9cac-d461ce41e210,909ed249-ce09-4304-8b02-56794fa7567d,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,a38254c0-00e3-429b-9d5d-b9e6a9812e69,47514b9b-0d17-4727-97ed-dab67e7fe031", + "01689baf-405e-4b38-9f09-b624e03865b8,a38254c0-00e3-429b-9d5d-b9e6a9812e69,abdf525b-b871-41be-a1c8-810f49740ad4", + "97eae086-cfea-45ff-9b77-1d184256d493,a38254c0-00e3-429b-9d5d-b9e6a9812e69,8768aa33-b014-4062-b323-b146dd7d78d2", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,MFsWxydybE-dw3KO2hkuZA", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,azbt5pgvrUS4LQwhndFeJg,cHZ4lDnSoUypIXCiRRowzg", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,6dzz5ZcGtE6bEjQHxWmo0A,2cSC290cPkCOrW5vtlVUxw", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,yXVeN9SzQUKE_6u_2KPzgw,sWgRczvpUU6ZHeoMrMD_yw", + "WuShlrJnHUKuscn8e6Rt2g,S9uymuvraUCOM8gmB7bnEA,M5z2wk-GDkOAjvZzqsfcNQ", + "WuShlrJnHUKuscn8e6Rt2g,", + "WuShlrJnHUKuscn8e6Rt2g,i3rD_hnQG0a-5f6Ve4AfWg,M5z2wk-GDkOAjvZzqsfcNQ", + "WuShlrJnHUKuscn8e6Rt2g,_uh3kj6hKUyBKOSR3R8dog,M5z2wk-GDkOAjvZzqsfcNQ", + "WuShlrJnHUKuscn8e6Rt2g,kaRBW1u3dkODGjPMePW6Uw,M5z2wk-GDkOAjvZzqsfcNQ", + "WuShlrJnHUKuscn8e6Rt2g,Ot9EG-FfKEauT0u6pSZuWg,M5z2wk-GDkOAjvZzqsfcNQ", + "1fed823a-22bb-4225-bdc4-491da6eba875,", + "3e307eca-0850-4d85-8cc1-f5c176bfa7e9,", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,1VGZ1QCvcU6rsKVkUDBNjQ,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,8hUBfmq28EixYjv92Xcrgg,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,lgqLGi01o02ZFC9Xs5Ofdg,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,29EZ6r2SsUakWjzuevxeoA,6094370d-5e28-473a-b422-f3f1c75d5db8", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,lgqLGi01o02ZFC9Xs5Ofdg,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,", + "t9co2516nkuJO4LIJA1bJA,i5RKLo-RbEqisnGII1wpaQ,8kdJkmwNM0WQmMVrb8fK8A", + "QCtqxbvYOkan79YFugH_Cw,i5RKLo-RbEqisnGII1wpaQ,xnxDimLvVUCI9tEJqWDDcA", + "jiBzrzDPkkuL6DqIFKpcCw,F7luQ18ZXEaITFiCqqwkpg,StxMBEprVkqMa_IoDG-Q9A", + "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,", + "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,a8de80d3-286a-4fdc-bed2-75d52f8c0964,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,", + "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,FjnZ-D1hcESLOWL4Ukv7kw,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,c6cf096c-f0cb-4500-80a0-bc2916b5a426,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "1a0319e4-24e7-40b3-9976-70bff2c7ff23,", + "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,a5925738-a451-4f25-b622-ccbfa12bd88c,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,c64ac7a9-7fd4-4587-8626-0413a190b706,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,", + "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26,", + "1940c740-695d-4481-a802-bb11b6561635,V-vCW0ICl0Ke3QPAGo9GSg", + "4025e4c4-abaa-4729-a2a3-a7175313e5ed,lBe3yxuE40eBmTrnEHBqQQ,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "4025e4c4-abaa-4729-a2a3-a7175313e5ed,oZ8k0Qv3SkG-DkY0_dP7UA,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "4025e4c4-abaa-4729-a2a3-a7175313e5ed,", + "oYvOo47DJE-DEPlpn5xNUw,", + "JTrfBXhIT02o2-ssP94zew,f_c50saStkaGJuhLjJ9lfA,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,D-SngK7hhkOpoZ2aMWv9EA,mkDVc0Mz4kmuLZ7powBHpQ", + "JTrfBXhIT02o2-ssP94zew,", + "JTrfBXhIT02o2-ssP94zew,FUP4iRnoc0ikNmjrFD06lg,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,8iZO162KNUiMC8l_z3OC_A,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,ZNKGkou_gU-1cxBFy7pDKw,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,GmS0wKwzVEqdf3NoTMHG5A,3DdSzSqV1UG5AtPr8ro3Kg", + "JTrfBXhIT02o2-ssP94zew,-Iqmonry4EKoqAULCKzRPA,3DdSzSqV1UG5AtPr8ro3Kg", + "VQnoLstCyEGDPy1c38-GvA,", + "n52um7A-b0utGxixd1ESvA,", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "de0ac50d-2adb-4114-bd2e-68953b13d706,7WrSbK4Ml0K2z-ig5HIUCw,9skD_xxLq0mpfP13f9n8hQ", + "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,1sxxUbsQMki0sTwxEQIbtw", + "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,", + "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,szDKGCS30UCwXJJcVocFDQ,3v_zUVDTUku_iEf-olDk1w", + "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,", + "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,YBpBFNAFGEunM5GX5lFxHg", + "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,Dvwz4YRu_ES_rl94y6FNBA", + "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,VyqdOGaUnEa8WzccUIn48w", + "CQuKKNpan0O7rhR1fr14-Q,yrQCLXiqRUSwnuQRYX2Rsw,VO_3cgqQqUOTz30TVUxrFA", + "CQuKKNpan0O7rhR1fr14-Q,yZR5aX3gQ0WpRUT6cfK1cA,VO_3cgqQqUOTz30TVUxrFA", + "CQuKKNpan0O7rhR1fr14-Q,KqjQ1xRpokGLKp9GVN1EFA,VO_3cgqQqUOTz30TVUxrFA", + "CQuKKNpan0O7rhR1fr14-Q,", + "7VQvz9sbzUWvGalxd9rISg,", + "sL7iHCnjyEuQKzrTvciEzQ,", + "aeda4e5d-9cfc-44ec-b325-ab8b2d906a8f,", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,70112887-dc8d-49d5-96ed-0845204e0b58,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,2738ab5f-8c94-46b3-bac6-4a8111c41c21,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,f5403c89-9769-479b-8a05-c71607f9b189,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,36e63797-5a42-4779-a5cc-ef5c2e7d17d4,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "f5c9743f-e77c-42f2-ba63-9843342cbf8e,", + "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,PR5dh2jf2kqGLO0lXgA69A,VDn1hO6ahky0GDJt5dRmyg", + "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,", + "3ad53d67-e89e-47eb-94ee-bbd054652815,09zFwIT2AEaQsijLJR2aSw,i0itjI0qz0Cdn-es5NBScw", + "3ad53d67-e89e-47eb-94ee-bbd054652815,g4_xqu9o9US9lIiBsn2BKQ,i0itjI0qz0Cdn-es5NBScw", + "3ad53d67-e89e-47eb-94ee-bbd054652815,RvVVu7gWUEmofuJiZqevlg,i0itjI0qz0Cdn-es5NBScw", + "3ad53d67-e89e-47eb-94ee-bbd054652815,mtIyC_me2EuHSur1ghsKDA,i0itjI0qz0Cdn-es5NBScw", + "3ad53d67-e89e-47eb-94ee-bbd054652815,", + "b1ca1dc4-6867-48b6-a332-4efff06a5db1,", + "TcgHS_0aPkehrBg7ytMOMQ,", + "e60fccda-8711-463d-b8a3-7cd5e76903b2,", + "e60fccda-8711-463d-b8a3-7cd5e76903b2,Chiz8maLoEGDpZOJZEtdQg,cQUquXMk5EGt0VUCuPKBgg", + "e60fccda-8711-463d-b8a3-7cd5e76903b2,2axtF2iOGkez1kfRRGUbYw,cQUquXMk5EGt0VUCuPKBgg", + "618b979e-c6e6-434e-98e5-9b51e117fb69,9lPZEIVbbUK1hdR1aVPLwA", + "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "7VQvz9sbzUWvGalxd9rISg,YTR8qybyVkOhNh4iqtU_UQ,H8LXGRJj8UOuH5dil8L9qw", + "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,Su7kiW24d0KapNns96JoFQ,0p5N9U5qokKdQhW07yYLGQ", + "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,CahBzgN3D0y46AQsvGC14A,0p5N9U5qokKdQhW07yYLGQ", + "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,", + "8d97303c-5138-4da1-a74d-378da19d11cb,Su7kiW24d0KapNns96JoFQ,ZpyoWbO6GkGglXlMuYUARw", + "8d97303c-5138-4da1-a74d-378da19d11cb,CahBzgN3D0y46AQsvGC14A,ZpyoWbO6GkGglXlMuYUARw", + "8d97303c-5138-4da1-a74d-378da19d11cb,", + "6b9e022c-0b68-48fd-8eca-da8573c18900,WWVPdeMMi0S4_Dmn85yEqQ,FRUp0aYZTUmAXsybauOs7g", + "5ac25e5a-8c8e-445c-82a4-7272baec1eb5,", + "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,", + "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,MzNNQ6T1HEqbqcLXZIulLw", + "7df5697f-2f71-47d1-bc27-cfa43792ffd7,", + "ec836d2a-b1b8-4566-b8eb-123ae2845956,8c74ee27-3d56-401b-8a8c-7868a80770e8,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "ec836d2a-b1b8-4566-b8eb-123ae2845956,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "ec836d2a-b1b8-4566-b8eb-123ae2845956,cea53c7f-24a9-4e3b-80b9-fbf95738f973,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "ec836d2a-b1b8-4566-b8eb-123ae2845956,", + "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,", + "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,891d902e-3257-4a8c-9001-858046007997,c8ccb85e-52cd-4b3b-aadf-289819067125", + "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,4c26d1e7-2382-4478-b9c4-a068ed01a607,c8ccb85e-52cd-4b3b-aadf-289819067125", + "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,", + "be75b46c-ce22-4758-8810-c409c5aa1c50,ZLaDizWS7kOmnHVGlLLJ-g,TUZ21v0C9UK9GPsfbRwpsw", + "5f600892-aa67-484a-afc7-14c219d89c52,565c17e0-496d-45bd-a2c3-564e118c06cc,a240d44f-963d-4d35-b22f-980938c7788d", + "5f600892-aa67-484a-afc7-14c219d89c52,", + "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,", + "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,ce465d53-3807-423f-baa0-bdba2a75ceb3,65b89eae-347e-4eff-9ff3-7f25cace0125", + "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,596097a8-d545-4256-959d-1d3cd96fc64c,65b89eae-347e-4eff-9ff3-7f25cace0125", + "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,c5884778-a87f-4612-9717-86fbb65d440f,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,", + "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,827a1538-51bb-4fef-99c1-7f1bebd9866c,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "f41653f9-ae02-443c-aaf4-1c6f4e49035d,5a96ac4d-03c3-4214-9dcb-c2044ff28f2e,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "f41653f9-ae02-443c-aaf4-1c6f4e49035d,27680277-aec1-41be-a476-9f51cf775580,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "f41653f9-ae02-443c-aaf4-1c6f4e49035d,cf515c87-3c0a-4443-9f63-5ea1bdbaa61a,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "f41653f9-ae02-443c-aaf4-1c6f4e49035d,", + "515dfc36-7f7b-47bc-abee-380d68f41be6,1acb1f56-4509-449f-9c6a-065bdeaa9ee3,2d7361d2-31a2-404b-90da-10e89114e1f2", + "515dfc36-7f7b-47bc-abee-380d68f41be6,", + "515dfc36-7f7b-47bc-abee-380d68f41be6,e98c3f98-7844-4d6e-a21c-05027033c18c,2d7361d2-31a2-404b-90da-10e89114e1f2", + "30380607-8b54-4b63-81d3-cb3f08e452c1,rcdwgNvO30Cvx2ycE1GfHw,JmFzNL5v_0q1p0bgoOlBpQ", + "30380607-8b54-4b63-81d3-cb3f08e452c1,", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,m4W5D0Oa9kaWa-8I96YYzA,WF_fg80f30qvHit0vGBwCA", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,ffHKr5EcIU24PbAtWSnalw,bVrkIyADCUiGNYWdmP_HPw", + "77c37481-a185-47ff-af55-65f9fdfcf9e5,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "77c37481-a185-47ff-af55-65f9fdfcf9e5,", + "77c37481-a185-47ff-af55-65f9fdfcf9e5,643b5f2d-0666-4869-b699-03742de34d16,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,", + "14ef6b00-debf-4a85-9755-b4d37df496d3,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "14ef6b00-debf-4a85-9755-b4d37df496d3,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "14ef6b00-debf-4a85-9755-b4d37df496d3,Vk1n941ZgkKrhrL9mh18oA,lxmw7EbzsUuoy0roRy5McQ", + "6QT99hx6DE6t9cKAs61i3w,1_vRe9nJMEWcNX8kAwgh9g", + "6RhwWExXekaYpK0NGHSXGQ,0mwtvRpkmECaT5_4aqCttQ", + "tTKv7eRODka2KC_hsY2O3Q,vek37vRROUKnSJKVP0CH6w,t-ZEWJCgFEyGP2PjNblQMA", + "tTKv7eRODka2KC_hsY2O3Q,eV3Nv6SWS0yl3f-fgjPb-w,t-ZEWJCgFEyGP2PjNblQMA", + "tTKv7eRODka2KC_hsY2O3Q,", + "tTKv7eRODka2KC_hsY2O3Q,Lxx3q6JRnUW_cPG_kGG4ag,t-ZEWJCgFEyGP2PjNblQMA", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,TeSudgm-NEy3DXe3z_VRqg", + "85e03c16-4288-45f4-a579-c911437d27d4,", + "4c52478a-b2c9-4520-8f88-52f2262cc6bf,YR7P-KQLGUKPF7JbW85K7g", + "4c52478a-b2c9-4520-8f88-52f2262cc6bf,", + "ilDgmey2ZUWlFiuEAN7YoQ,", + "ilDgmey2ZUWlFiuEAN7YoQ,K9wPZ9LHo02GPMtf02rANQ,U7jMLMX4_kKIy1K9IcHOXA", + "097a3266-4aab-4b23-8f56-373c9b1bc837,", + "097a3266-4aab-4b23-8f56-373c9b1bc837,wRPoHTsXFESK5pjkxVMqGA,SeA8s77hr0K6DRZyVlRTCg", + "097a3266-4aab-4b23-8f56-373c9b1bc837,bQyNgFWAe0SPttPri6hxyg,SeA8s77hr0K6DRZyVlRTCg", + "097a3266-4aab-4b23-8f56-373c9b1bc837,YB7kXMyLJ0mF_5WidhmH8A,SeA8s77hr0K6DRZyVlRTCg", + "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,", + "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,4RH8_Z1mqU-JIVvx31wOsA,BOB_mAXdA0u2khOH5qjqXQ", + "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,QJSPgq2KXEOWNynY8uqQvA,BOB_mAXdA0u2khOH5qjqXQ", + "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,RBRFHN8_rkuImbyDAfcHWw,BOB_mAXdA0u2khOH5qjqXQ", + "6fccd476-c621-4f3f-b2e3-45c95be912b5,y5O8zRJy4kuhnXzATSWKew,X0_GvGrZGkSP9Ub81CHgqQ", + "6fccd476-c621-4f3f-b2e3-45c95be912b5,v_5qFi-vAUSDd1fLNL7pvA,X0_GvGrZGkSP9Ub81CHgqQ", + "6fccd476-c621-4f3f-b2e3-45c95be912b5,", + "01107a3f-9a4c-4fb9-8918-e6195e346d42,", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,22dc463a-a89b-46ad-9a0d-557c742b4a80,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ITH2OpzjP022vZ5JgxV_iQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,22dc463a-a89b-46ad-9a0d-557c742b4a80,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ITH2OpzjP022vZ5JgxV_iQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,PMa9370LPEig_7pKwCceEQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,22dc463a-a89b-46ad-9a0d-557c742b4a80,fd67ec40-f898-4df6-b846-8bf350f362b8", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ITH2OpzjP022vZ5JgxV_iQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,22dc463a-a89b-46ad-9a0d-557c742b4a80,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ITH2OpzjP022vZ5JgxV_iQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "1940c740-695d-4481-a802-bb11b6561635,g_f9bH8tykGq3yRwWuQPOA", + "665d4fe9-88dd-4fae-97ad-201884a6ca02,", + "C4uuztTiYUe20PACMARksA,Z1F9rjKqCE-Hd2-Ip5g3gA,fz_0gZ6RXEC78NpYsx_eqA", + "C4uuztTiYUe20PACMARksA,", + "C4uuztTiYUe20PACMARksA,apSQiQ1eO0mEzRsbugeu9w,fz_0gZ6RXEC78NpYsx_eqA", + "C4uuztTiYUe20PACMARksA,iufpnteVCEiiZu_m-yPovQ,fz_0gZ6RXEC78NpYsx_eqA", + "C4uuztTiYUe20PACMARksA,VpqHNpPiY0CfRJEjRRM-mw,fz_0gZ6RXEC78NpYsx_eqA", + "rJ9zPPGdlk2i24sxLfkqng,", + "affdc9ad-8312-4421-8f56-e35dfdaeb63a,", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,96b36c98-cba1-4337-91dd-c74c00316f97", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,916384ca-e209-42a0-bd64-c64f5a40b56f,96b36c98-cba1-4337-91dd-c74c00316f97", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,7Jx8mrxoi0aQtZEb03CdRA,-IgxYllxkEyr7V1gosWXUA", + "9c34c2c1-07d7-4180-94a2-be8c9caaae84,TLrw3oncCE2MeuVifXSWTg,-_ViYCHHRUip7oN_AnH0Og", + "6423f23e-dde3-4428-96f5-6091027c92cb,", + "e29d044e-79fb-4280-bca9-aa1e1257e968,", + "25769d1f-4ecb-4d92-b692-322b38c013b5,", + "51b85276-3a40-4c40-93a1-9df307c3e78e,", + "4308da04-af84-40bb-845a-7a9e1a2726ec,4VkPVbBVLE-eJHilwGMAbQ,HTjlzAlWZ0ezY6w3ngSQSg", + "4308da04-af84-40bb-845a-7a9e1a2726ec,531b4297-b38b-4362-9854-4f2139ea43be,33ee376a-586e-4aff-a7e4-4213445a3e92", + "4308da04-af84-40bb-845a-7a9e1a2726ec,", + "4308da04-af84-40bb-845a-7a9e1a2726ec,newGzX_zA0eGUX3Iayq5Ag,6Jv4AJGBYUGP_n4sDcz2Kw", + "936d7126-a3b7-48a2-bceb-6cad9221b267,", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,kKpiR8YM8UG7xcaIgS9aXA", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,oQspLDUDxEaiudqzXY7gbg", + "e670df07-f9c8-42d3-abdc-48b4ca0257aa,W1LdNEXmX0m72Wr7EjJ_gQ", + "eda29bce-5a07-4439-a2a7-cafa11e2ed62,", + "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,ySbPHkP8WEee2V_P8lt_RA", + "3cd52c5d-0c46-41c0-8254-c4542805da31,", + "bb6ee7fa-7a31-4a16-a97e-be6eac42c595,", + "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,clFrpYWFQEmDkE14f3o_Ig,DE1C-RJkuEWIvpR0q1i42g", + "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,", + "pQqNvrhDJ0uuipKHA2qUOA,lq59KZwElEStrviDEQLLcA,olyS074RHU-ojE0GzBBYJw", + "pQqNvrhDJ0uuipKHA2qUOA,", + "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,", + "076e98b6-dc1b-48f8-bb02-aa65d26b33d1,", + "ce250e13-8137-4f62-9e52-2b1cebe7d0ad,", + "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,10nOM4HBRU-e7Bb05SrtLw", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,QBglgj80zkuvUkk55Y5Feg,ACrBLiAIgEur9V6l_B0CvA", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,-HYrY2A5Tk2UWMi4_hmYbg,ACrBLiAIgEur9V6l_B0CvA", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,IjRMopsYlUqSE6327I9TIQ,ACrBLiAIgEur9V6l_B0CvA", + "0ab5790f-a537-4306-ba66-5eb87c02ec7d,zTsJHduQ5kirH9-6mewV4g,ACrBLiAIgEur9V6l_B0CvA", + "7c1552a6-bc36-46e6-884c-1de6b8111a92,", + "7c1552a6-bc36-46e6-884c-1de6b8111a92,926ae943-76cb-46dd-9c66-a6ee44ea9194,f1cd10fb-3d0b-436f-a173-65bc0d88d8bf", + "9fb62d0c-67ff-4675-85d3-a2869c2330aa,", + "9fb62d0c-67ff-4675-85d3-a2869c2330aa,ijCbiOeGD0C4uznegq9CXw,VzyoPsQyCUaW6RSusgVUGQ", + "7adae61e-cd62-46c3-ae42-83ef0bced5c7,jGNpSO25HkO7KbR3v05nkA", + "7adae61e-cd62-46c3-ae42-83ef0bced5c7,", + "7adae61e-cd62-46c3-ae42-83ef0bced5c7,lhqUg1VMNkC8nrsGZF4edQ,uFERkSfSl0i9BPFBVOICeg", + "f277af6a-0807-4716-8948-1bee236ffb74,d0bb468c-b78f-4e80-916d-65fec3ec0010,0272eb19-fc0e-4ad6-8db9-cc0cda2528b0", + "f277af6a-0807-4716-8948-1bee236ffb74,", + "fb907eb2-91c8-45ff-84a5-462bb86ea7de,b415fe60-3e9d-4f4d-a53a-ca15eac6af7d,d21e1b6f-9c9e-42b4-bad5-201ec43161e9", + "fb907eb2-91c8-45ff-84a5-462bb86ea7de,", + "b6rLwzD4NkKV7xKn9ZYVkA,", + "b6rLwzD4NkKV7xKn9ZYVkA,u_Gzj9Nro0WS48xKnH9few,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,ytmSzjhCr0Sm58MSNbwJUA,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,JTgXKeT7w0K2Mw4Jk2HCqg,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,rNhOQoP3XkOBHV28ze2NpA,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,TEgugZRs80iJbhL87RdYwg,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,mvMeyStxGUGlNLSYNeSubQ,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,yM5xRrd9OkmDGyQddwOP6g,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,-Nf7WM2TeEKujB637h3tRA,oHKhT-GTx0iFWrrlz9s6Tw", + "b6rLwzD4NkKV7xKn9ZYVkA,LuYelZY8NkqmOp1pnxPXBg,oHKhT-GTx0iFWrrlz9s6Tw", + "1940c740-695d-4481-a802-bb11b6561635,SV2UmeQDjEaMK_A2Q4tt3w", + "c4622dca-80dc-423b-ae22-2065174e817b,", + "c4622dca-80dc-423b-ae22-2065174e817b,pCEBoSP2pESLl9ih-J-55g,8SzXFoWD8k20cekFe6cLKg", + "ee9a7c8a-7383-4a09-b44c-474b66f9008a,", + "ee9a7c8a-7383-4a09-b44c-474b66f9008a,W-xMu8n2CUuHSXfHvEL4yw,-apO3Y7om0GKhC4V_hVYdQ", + "4b72736e-6fca-4961-98f2-1d4e199eb1e0,", + "155c13b9-3c72-4314-85ac-6fbac19cdc76,", + "e042433a-40a2-4758-8074-3b9d407835ec,", + "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,", + "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,4ve7AWD5pkGjvmtC6JRdiw", + "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,F3m52YaKyEqwbgUyPVfFxQ", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,pjkTGVHkIE-BzCVi1KAesQ,PoMG28QAskagun1LSd4C_A", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,t1MeoroFOUioYmPfT9mCeA,PoMG28QAskagun1LSd4C_A", + "0ab6c95c-e157-48bd-a172-2642896b4ffc,0ps19D7b_U2iejdKQFORyA,PoMG28QAskagun1LSd4C_A", + "e13f6558-d49c-4314-820c-962ef48eb0c6,s2kCppLsN0uY7Hy6qjQc-g,50NUFrDxbUKmXbyAHBenvQ", + "e13f6558-d49c-4314-820c-962ef48eb0c6,", + "e13f6558-d49c-4314-820c-962ef48eb0c6,rCeRk8Pdl0W5M47JAQPhpw,50NUFrDxbUKmXbyAHBenvQ", + "e13f6558-d49c-4314-820c-962ef48eb0c6,5TJPDmvEBEOkP82ZWLFqtg,50NUFrDxbUKmXbyAHBenvQ", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,AC3zfm38JEyBZWuXDh4Ukg,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,RfZNDPUok0WyQEa3Ko08ow,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,TnEDe1QoMUCUdqPL_7CxTA,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,sX0ZZDcCUkyFSJISfvq0vg,ovy_3j_qwE-7mmks1Z47ig", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,", + "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,dyEaPloE8kazWC7Zblv8uQ,ovy_3j_qwE-7mmks1Z47ig", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,JVZHZm6nGEqE2zEEPoHbwA,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Euf1h1qsZ02evDsyoHYbPQ,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,zlqLrGEa8E2WW9ey_szREA,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,lsyYlxY9o0a3TbUrHkxeyQ,PZHohDVX0U2frX13N4KYDQ", + "459ec62c-67db-4b72-9e29-0ad3ecd0b737,L2iFQcL170OajqfW7t6sVw,PZHohDVX0U2frX13N4KYDQ", + "418ed21e-766f-4c69-9d57-6f866dcc7feb,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,0dabda0d-842c-4d39-a6b9-a8ca3ec20825", + "418ed21e-766f-4c69-9d57-6f866dcc7feb,", + "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,", + "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,PNnfMibAUkqetPThlhtBgw", + "d960b47d-f8bd-4c2e-813c-a6a09876ea18,", + "1fcaf4fd-9012-407e-b691-fbc3ccf22421,", + "3c9acb9e-3353-4359-aa5a-ae9512b086b4,", + "bf9dc196-5727-4752-8126-7ad5fcd96d55,67490e3b-0f9d-4bf8-84fb-54957b981c8c,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "bf9dc196-5727-4752-8126-7ad5fcd96d55,639f5e2c-6fbd-4ab4-aa10-7772e0932798,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "bf9dc196-5727-4752-8126-7ad5fcd96d55,", + "bf9dc196-5727-4752-8126-7ad5fcd96d55,b520f3bc-599a-498c-8a4f-97cc25aa741f,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "9c28e450-8e12-455c-a4c8-c0a2160d4190,DRS8xu3AqUOy2L0RKF3f7g,A7Y52vGIN0ijEKsWR_GQLg", + "9c28e450-8e12-455c-a4c8-c0a2160d4190,", + "57552652-d902-43b7-a68e-5c7da87582bc,VqtOYBiKRkaSW4xAlBxQgg,xTqtLPQVyEKjAQZYl6HGow", + "57552652-d902-43b7-a68e-5c7da87582bc,", + "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,HdwZhjrqxE6wuf68MnDhDw,f5z_jhdcqEGjo-1WUmldaw", + "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,Blev_VFjRUysY_nymGWAVg,f5z_jhdcqEGjo-1WUmldaw", + "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,", + "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,b7oobDcDtE-69fhM-5g5AQ,f5z_jhdcqEGjo-1WUmldaw", + "c8c00394-4cd9-4bad-9774-a825ded78f80,", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,zJMD-_HR80WCAUqYGe7_NA,rAzhaLh1nEWMd9v1D-HWcQ", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,sJioq5ARDUyMHFji_6OxFw,bjXO36Nu6EWD2dU1UC4Uwg", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,9P8ARRUYdk6hCoGMKhix4w,o__JORNtYEiJEyyoBIimPA", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,56qchovVVEaVwL9S52kneA,BAjMoTHbD0mSCjoG9jmzjA", + "ae180fb1-3e74-4232-9a9d-388fd488eba1,nu_UGtkJOUK-5MWaW83MUg,pgBFpOtl00K2zsTJcfrefQ", + "6QT99hx6DE6t9cKAs61i3w,KwCEye0nsESFs-yj4h9sjQ,c_t9XDbn-Eqml1O-wC5zJQ", + "6QT99hx6DE6t9cKAs61i3w,m2znGrdH-EOhrbLbFsmyOg", + "6QT99hx6DE6t9cKAs61i3w,", + "5fwjFQoC_Uu9ChvF6uNGZw,fXNkMWFMV0aXalK_y_8SHw,SSY8ZYMgaE2Bl4oFmMjKQA", + "5fwjFQoC_Uu9ChvF6uNGZw,n741vKj02kyt8B5fT6s2og", + "5fwjFQoC_Uu9ChvF6uNGZw,oCB1ZeT0hU6uQGhHNsEvkg", + "5fwjFQoC_Uu9ChvF6uNGZw,", + "6RhwWExXekaYpK0NGHSXGQ,fXNkMWFMV0aXalK_y_8SHw,HQQWccdyakaEmntPhvC6CQ", + "6RhwWExXekaYpK0NGHSXGQ,1BQdC-mTdUqfm4vUf0lx-g", + "6RhwWExXekaYpK0NGHSXGQ,vpxBkYv6LUSVV4NhYAS1Fw", + "6RhwWExXekaYpK0NGHSXGQ,", + "3dec4865-cf23-4c19-a460-dd2db80ee851,90869e86-cbc5-4024-9d08-fcdc41c64eb5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,90869e86-cbc5-4024-9d08-fcdc41c64eb5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "25d4c652-1730-4925-925e-31a290a2272a,90869e86-cbc5-4024-9d08-fcdc41c64eb5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "Wua43ED71UqL71ATtyS0iQ,XFCpIdeDXUqeYCIgh4fGtg,sV_ByCug30G1Gw-77KUe1Q", + "Wua43ED71UqL71ATtyS0iQ,oKxmH-1s2EyIDkZoOQSQQw,sV_ByCug30G1Gw-77KUe1Q", + "Wua43ED71UqL71ATtyS0iQ,", + "Wua43ED71UqL71ATtyS0iQ,MxnAq_XOw0uTzQ8IIxqb_Q,sV_ByCug30G1Gw-77KUe1Q", + "iai6P7dDTUq8S4hqQmY1YA,XFCpIdeDXUqeYCIgh4fGtg,MBuxBAIkpkWI26d-arAKQg", + "iai6P7dDTUq8S4hqQmY1YA,oKxmH-1s2EyIDkZoOQSQQw,MBuxBAIkpkWI26d-arAKQg", + "iai6P7dDTUq8S4hqQmY1YA,", + "iai6P7dDTUq8S4hqQmY1YA,MxnAq_XOw0uTzQ8IIxqb_Q,MBuxBAIkpkWI26d-arAKQg", + "2b325613-84c1-46d8-86b4-a167d79661da,", + "2b325613-84c1-46d8-86b4-a167d79661da,ggpNxFtZMEaHlvWXsDl9Tw", + "b02053b3-013f-42b0-8881-85f5be2b8cda,RwgnKTfMEkWrAp_OMGy__Q,F9vYkdcuGUOSGEg0wpQt2Q", + "b02053b3-013f-42b0-8881-85f5be2b8cda,yDZFr27Zq0-JH-EsEsxG2g,F9vYkdcuGUOSGEg0wpQt2Q", + "b02053b3-013f-42b0-8881-85f5be2b8cda,PLRhiOhhb0KhatFH7f3pwA,F9vYkdcuGUOSGEg0wpQt2Q", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,RwgnKTfMEkWrAp_OMGy__Q,iT7UGMahoEu7BZZqeKXKww", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,NpMkoIK62k63QoLVEXId6g,iT7UGMahoEu7BZZqeKXKww", + "74b91f2b-d4d3-4e41-8946-b1296ab7a773,PLRhiOhhb0KhatFH7f3pwA,iT7UGMahoEu7BZZqeKXKww", + "64fddd4e-3d59-43df-bb39-671e18953c0f,MLB_YJtNtUi2Qw9D4a5i2w", + "qxovV6Vym0ufL2cTCyKjOQ,", + "qxovV6Vym0ufL2cTCyKjOQ,64Hvmzrb3Uu9X_dCA3M3Jw,rPpYm5fIzky4vqVB26guAw", + "CdeM6RJmPUumXtdmdM3RXA,", + "CdeM6RJmPUumXtdmdM3RXA,mZ2ASMrs80axStLp9g1enA,dGzmSibucEe7i1HD7lPcNQ", + "sOREgAGs-06CeGGsG-WIkw,", + "sOREgAGs-06CeGGsG-WIkw,rSzG7s1ocUmx9_VrJLOcHQ,ISycihytUUyoypVb-kXN_Q", + "iBQ-v6xaqkipUlfzkSHEeQ,", + "iBQ-v6xaqkipUlfzkSHEeQ,bH3dgwnwt0KEw8oCDVIcEQ,erPtGSgq_kmqfeSlZWw4VA", + "703f1987-2b31-4b6f-bab7-1939b1105ac0,", + "703f1987-2b31-4b6f-bab7-1939b1105ac0,YS-fXtVLTkG-wl40HPBLOg", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,6PyJEQxyd0K9TPRnvt_86Q,mkNzDvyGfEijbdzAxKYrPQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,OJYpuFw810Sg4bTw0XQf-g,NqU5Mw75BE6kPaD8t11Qow", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,sz-J7RjesE-ke3bKwumzZQ,mkNzDvyGfEijbdzAxKYrPQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,wxvSDMommkenIl5hTY2fdQ,thwvF0DFRk-mV99ruqYFoQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,-sqfCQpzpUyjEShEXH9JYQ,thwvF0DFRk-mV99ruqYFoQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,as9fjqDWiUi1w7yYavKZXA,thwvF0DFRk-mV99ruqYFoQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rGMFdPKmK02_OvIIFYlAxA,NqU5Mw75BE6kPaD8t11Qow", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,QlZt51mdn0WuiqKoJaIRyA,mkNzDvyGfEijbdzAxKYrPQ", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,t4gMAKp4L0WBaTGf_rtqFw,NqU5Mw75BE6kPaD8t11Qow", + "adf2aa89-61c4-4b7a-9307-e39e72db23d3,", + "97253092-90ba-4ff2-b137-6324ab244d11,4oywwjb3-kifr-um9x87Pw,11ZEZTFhy0ONpoXb3kiewQ", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,QzjakcO8tUaPeiI5KBLKtg,Rk18_7icF0Srgh7M1_G-hA", + "FHKZvPFMLkO8iAIPPQzu1A,", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,eb3SmJTIh0628LrZUkI4KQ,Rk18_7icF0Srgh7M1_G-hA", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,Yr0mF1EWzEaIyRMOWFiY6w,Rk18_7icF0Srgh7M1_G-hA", + "FHKZvPFMLkO8iAIPPQzu1A,K17dIz_TRUq5D50NBfLjyw,3drRcfyKkku0qGwCy16m9Q", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,BLgkgLeS-EyazgPQxGcavg,Rk18_7icF0Srgh7M1_G-hA", + "BR0mZKsKaE-t4R8CzEwuyg,Q1knTl-n-EeAUwotFspNew,FHB5XHwNWka7aUt5km3LNg", + "BR0mZKsKaE-t4R8CzEwuyg,", + "0QOlT9c5xUuinpfZaoEe8Q,", + "0QOlT9c5xUuinpfZaoEe8Q,aiBfwCNwpUyJ0rYKCMa7qw,Cjy-bE0PikWLCRrL4U2NGQ", + "t_66r_j5oUSh9awIPmdUKg,YTlnBuEjhUG1hSfAs8qwuw,IcR9Z_MxKkS9uh4uFYuFzQ", + "t_66r_j5oUSh9awIPmdUKg,", + "-SQLtL4rdEWTJdw2IroQCQ,CoSVCyrI-0qg-EkqB2R0hg,JO9lJOQSREWbPyDBZH_cyg", + "-SQLtL4rdEWTJdw2IroQCQ,", + "ReqOfohe6UamzaiHG1x3Ow,lDXambcP60CeY2C8qeGDFw,uiAwVt3HK0mRVtWp9B-gTg", + "ReqOfohe6UamzaiHG1x3Ow,", + "_OWVy3z6iU-M3-zbQgSLig,D0tP58wC30mJ00vNxw8hqA,N5wN1l0tYEau5np-9PoIiA", + "JWfs9IiQs0Weu8MmyGpqLQ,Z8Bfk0MTb0evZehvvgzuBA,x10FeRBsQU2DIIpG8XIlwg", + "jiBzrzDPkkuL6DqIFKpcCw,okpg4ftsak-wc8Zu83uytw,StxMBEprVkqMa_IoDG-Q9A", + "3b792b59-50c0-4c28-b367-fc694b1d65d1,", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,oxi26T5a4U6L73pF-Pgrhg,njEWOjNEQEWCEqmytodpow", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,tXhgU1o_wkaNQ-7P3KXidQ,njEWOjNEQEWCEqmytodpow", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,6547pqf6vUm2L89AG6D91A,njEWOjNEQEWCEqmytodpow", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,nn86ATWxekWdIGlPePPgGQ,njEWOjNEQEWCEqmytodpow", + "24c4404a-839f-46bd-98b6-6848d747a98b,xVwzV9-m6E-8PVpkg4D-AQ,NH0L99bd00OVKMyltuSnHQ", + "24c4404a-839f-46bd-98b6-6848d747a98b,6vjA_vHq70Gclypll_GS_A,nalaCGqtL0SFA4Itwqmp8g", + "24c4404a-839f-46bd-98b6-6848d747a98b,09Cg6dOq2kC2-tI5yyGSYg,YFZimVHKuk--lXS8RCrZTA", + "24c4404a-839f-46bd-98b6-6848d747a98b,3UxfTkPsEkezOXO__wz_mg,nalaCGqtL0SFA4Itwqmp8g", + "3a790be3-2937-44d4-be01-b5d65353bd3d,afWY5VGiF0isGn6qpo4KeQ,_yvss6L59US4-8QYZgcCdA", + "3a790be3-2937-44d4-be01-b5d65353bd3d,", + "3a790be3-2937-44d4-be01-b5d65353bd3d,tyzdtSkR9k-YCnjNvPAT6w,_yvss6L59US4-8QYZgcCdA", + "3a790be3-2937-44d4-be01-b5d65353bd3d,SWvJWvMDpE-it5PFoPgERg,jQjfiz2i6kaPfrkjnBGxBA", + "3a790be3-2937-44d4-be01-b5d65353bd3d,e6hTzFc-fEiJe0L4jDVxRQ", + "3a790be3-2937-44d4-be01-b5d65353bd3d,09Cg6dOq2kC2-tI5yyGSYg,xq7bizx6UEGaMTHB9zI3pw", + "3a790be3-2937-44d4-be01-b5d65353bd3d,83jS7d-cUUC_GNDcGpiDxw,jQjfiz2i6kaPfrkjnBGxBA", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,OrqqGMhK1UKGowFxpJi2mw,6WVXb20giEWyZbW2gWGz_w", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,-tDd0gdxQkuvL2id10AweQ,6WVXb20giEWyZbW2gWGz_w", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,2lD-mjl3pk2wWv24RY2tbA,dUu7b-JFOky9c2lIVCNmcg", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,NJcJw51-xkq0phT-nlJw2A", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,09Cg6dOq2kC2-tI5yyGSYg,eUktNKKK3kqUR8-gA3rNxA", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,cj9OYinX5kyzlhBUQwZKog,AKWH3_SDqkG8SRm5sauROQ", + "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,TDcZZ2ynEU-Mgmju_qIVIg,6WVXb20giEWyZbW2gWGz_w", + "3757149b-f478-4975-81e5-d808a1bed8d5,XOBIPrKCr02xa4tKj8wN9g,Yn1uv3Y8JE6VSxsf0LOBvw", + "3757149b-f478-4975-81e5-d808a1bed8d5,LDqcPkvNXE2gFXxbpNkfhA,Too5jpb1w0iQenkjRRS-QA", + "3757149b-f478-4975-81e5-d808a1bed8d5,09Cg6dOq2kC2-tI5yyGSYg,3fQj_7meDEeQqm_7RfEfxQ", + "3757149b-f478-4975-81e5-d808a1bed8d5,fq0xC9BjLECbsQIZfAvK9A,Yn1uv3Y8JE6VSxsf0LOBvw", + "wy2cfgb8mUqjMrIOwHH2ew,0ZKR_qxrWkiWkjpot8s6Cg,5aX524Lfok-0dC3UFv-OkQ", + "wy2cfgb8mUqjMrIOwHH2ew,", + "7LSm-xCYakmgcAr--Fn-JA,rZ45-lecBESqHaLX0c7NAw,iDxHI0T4Gkifs3_GY5gdjQ", + "7LSm-xCYakmgcAr--Fn-JA,", + "s6mUIqiPCk6e57UYCx3SZA,0cP9EcZRFEuMOvmZ_Kx9dA,smmfK-N2R0uoDiuIHrhImg", + "s6mUIqiPCk6e57UYCx3SZA,", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,aBQ3R9boek-mYeSYSNvDbQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,e02838f8-4029-455a-b35c-dd40f972b05b,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "6d36091e-7083-4e3d-8729-94612602c8cb,e02838f8-4029-455a-b35c-dd40f972b05b,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "418db0e0-c7af-40c2-9cac-d461ce41e210,e02838f8-4029-455a-b35c-dd40f972b05b,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3KmqVuMQkCYK6sVTMOTOA,47514b9b-0d17-4727-97ed-dab67e7fe031", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3a9b576-5433-4305-bd96-b08820c09212,47514b9b-0d17-4727-97ed-dab67e7fe031", + "01689baf-405e-4b38-9f09-b624e03865b8,d3a9b576-5433-4305-bd96-b08820c09212,abdf525b-b871-41be-a1c8-810f49740ad4", + "97eae086-cfea-45ff-9b77-1d184256d493,d3a9b576-5433-4305-bd96-b08820c09212,8768aa33-b014-4062-b323-b146dd7d78d2", + "e149c559-aeb5-4609-9722-52fb6acc70e6,", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,", + "d9c025e1-561b-4997-9f69-c96eb3c00786,", + "6QT99hx6DE6t9cKAs61i3w,ojrXnWkUkEOGM3_oT6V29w", + "5fwjFQoC_Uu9ChvF6uNGZw,iDnij3VaEkSAnpt5QRevig", + "6RhwWExXekaYpK0NGHSXGQ,S7C2A8nga0SlIeOEkeYaUw", + "f5b01307-e495-469c-afac-c10f375cffb0,", + "f5b01307-e495-469c-afac-c10f375cffb0,lIURXMnmn0GcYFUVSXHzWA", + "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,G_fqC7KPaUWFM4Dk3stJYw", + "9117bd15-674e-4f22-9ff8-e7365fa43907,", + "71afc215-3d18-4f36-a114-f11c453e46d2,BPp9SkUpsE2PZlStZrIDIg", + "71afc215-3d18-4f36-a114-f11c453e46d2,O38XfZ3gh0-ofqu36y3mAg", + "9117bd15-674e-4f22-9ff8-e7365fa43907,UdQxfRULDEG6BvJV7HWO_g", + "71afc215-3d18-4f36-a114-f11c453e46d2,", + "b07a3964-7a53-4467-aa85-ec19cc27a57b,", + "7ea796bf-f552-4da8-8f68-a87c4896c8e6,", + "7ea796bf-f552-4da8-8f68-a87c4896c8e6,UcK6aipko0iE4THJUO1TvA", + "7ea796bf-f552-4da8-8f68-a87c4896c8e6,dr6MrV8O30G0i-c7peOoKQ", + "7ea796bf-f552-4da8-8f68-a87c4896c8e6,9yNuA59JO0avaKVA6YxQyA", + "19984948-b6b9-4d3b-8562-d0624930df6e,Owi_JROR80-apNHw-4KNcg", + "19984948-b6b9-4d3b-8562-d0624930df6e,", + "d33c2f3a-6831-4deb-82b2-062d1c4224e4,BoaDgMfzEEycbxVmWneHNA", + "d33c2f3a-6831-4deb-82b2-062d1c4224e4,gIDTSqjSvECgxxd4YtOZIA", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,7qVilU94X0Kldpvku2mcng", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,bS-St6XLHkSwKt41J7BmKw", + "4c032271-a0db-47b0-bfc6-4c330c2c4b21,", + "2d07c0d0-b277-46dd-8980-382bdaa4749e,", + "b7ba6920-e000-4081-a025-7da67c803008,0WW04NMMfU2LLahCq0jQ6Q", + "b7ba6920-e000-4081-a025-7da67c803008,", + "b7ba6920-e000-4081-a025-7da67c803008,4kYWnAwZo0SDZFmiU8jJwg", + "b7ba6920-e000-4081-a025-7da67c803008,YfRfaC49J0yW0wnSJf5DwA", + "MJkDYDH510izAab13ZfXQw,", + "MJkDYDH510izAab13ZfXQw,kTP3xKhzBkuQF3SqvmYFKA,2UNd6eViFUO359xjSiuu2A", + "MJkDYDH510izAab13ZfXQw,hYu9itGe1Ee5Neanhj9VtQ,2UNd6eViFUO359xjSiuu2A", + "MJkDYDH510izAab13ZfXQw,dYPaOkwy3EGAfvl8RyN4pw,2UNd6eViFUO359xjSiuu2A", + "MJkDYDH510izAab13ZfXQw,FE7_gQNBt0CXzMN6di5HWA,2UNd6eViFUO359xjSiuu2A", + "MJkDYDH510izAab13ZfXQw,VQjkyb5aoEKHKo_eZsq9ug,2UNd6eViFUO359xjSiuu2A", + "hT7nSD7Ts06F_1SNlOMLwg,", + "hT7nSD7Ts06F_1SNlOMLwg,kLv_wsbbGUmlfIUO1QLYLw,mG60EPIQIkaJNgOkonVmZg", + "hT7nSD7Ts06F_1SNlOMLwg,f0pjsV0XMUWoE_z2kqOlSA,mG60EPIQIkaJNgOkonVmZg", + "hT7nSD7Ts06F_1SNlOMLwg,EDLxuGjEVEGdOPXDqyN-xA,mG60EPIQIkaJNgOkonVmZg", + "hT7nSD7Ts06F_1SNlOMLwg,lhfOKGDAgEKWucerCmH7Fg,mG60EPIQIkaJNgOkonVmZg", + "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a,", + "23944856-b567-4075-815f-261cb4655cff,", + "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,2be2f040-68e4-44dd-9099-29f11110f3a4,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9ad37173-e640-4b9e-a474-716d482b73d2,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "d2d3264b-fbf6-40b5-9f10-c85ec737d112,61VZkFjB402brzWm4UsftA,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,74b009f4-e128-4c59-8a6a-4c0ba6119bde,6094370d-5e28-473a-b422-f3f1c75d5db8", + "5b6535f0-86cd-417a-bcce-a1ace9a5f260,imDGL6dhrka-KliYlfpdgw,6094370d-5e28-473a-b422-f3f1c75d5db8", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9ad37173-e640-4b9e-a474-716d482b73d2,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,61VZkFjB402brzWm4UsftA,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "t9co2516nkuJO4LIJA1bJA,jwEB6LyEaEekqtPNRc6TVA,8kdJkmwNM0WQmMVrb8fK8A", + "QCtqxbvYOkan79YFugH_Cw,IzkFARlv6EenmrfcNkcOuw,xnxDimLvVUCI9tEJqWDDcA", + "jiBzrzDPkkuL6DqIFKpcCw,poqpmOloPUKotHH9p0YAyw,StxMBEprVkqMa_IoDG-Q9A", + "88739d79-aeb9-471a-b25b-776f46bba9f6,y_gTbnuYS0GKJmnCs2retw,hjk9OO8CW0KNGR7A6_p84A", + "88739d79-aeb9-471a-b25b-776f46bba9f6,", + "f218e2de-46fd-45b9-9e0e-670b6bb905e7,JFalLDUvm0aMm3wh1UVfVg,iQjkS1xYP0S63ltvKpkz5g", + "f218e2de-46fd-45b9-9e0e-670b6bb905e7,", + "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,9GhnX4Pj4E-wxbPlD8GB-g,YSrGwXybGEqaqHNCS64qOA", + "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,", + "d7410e54-5193-48b9-b5c9-06e4439dd924,njOH4bbntk6THrxbd9TDDA", + "d7410e54-5193-48b9-b5c9-06e4439dd924,", + "d7410e54-5193-48b9-b5c9-06e4439dd924,0aNp_2sWHUS_08TMe-_AzQ", + "c2d22328-b41a-4e53-9b1e-814e84b86d15,", + "23b97189-b49f-477b-8a22-0903b3d236f4,rFCKF2ku1UWj-TF-VClPRg", + "23b97189-b49f-477b-8a22-0903b3d236f4,zld80uf3y0uu__Yejms-3Q", + "23b97189-b49f-477b-8a22-0903b3d236f4,", + "23b97189-b49f-477b-8a22-0903b3d236f4,GfC01_o8QkGTbnGNjv_DSw,7mLb5jMJMUug7Nzwxv6D4A", + "5f492f8f-ab61-484a-9587-747132b54309,zwKRET9pP0O5uhPU_yHrlg", + "ed4439c6-edad-4239-934f-79f9cc816c0e,", + "ed4439c6-edad-4239-934f-79f9cc816c0e,7ykIpgU8Tkmmq_zzpjTVrg", + "ed4439c6-edad-4239-934f-79f9cc816c0e,iqYc9qtbZEig8jl_ZqKe6w", + "62e480de-27e9-4719-adac-360698c7a2c1,", + "7d6cf363-76ba-45c8-91f1-cf4434c31b36,", + "764b4c4e-1009-4772-b01f-7b326dd77e8a,", + "764b4c4e-1009-4772-b01f-7b326dd77e8a,93vga8NJQEe5nWdqrroAaw", + "07023692-97e1-4a65-a881-64ca6b4ae08e,", + "e48955ae-50e0-4f55-93a1-611150142331,", + "b17591ba-3c2d-4309-ab01-b6743424e07e,l9cF1s3B8EC6BGlQ4Tx1cg", + "f23e86eb-8b59-4b32-a72a-5d382568e399,", + "8f137b1f-4125-4657-85c4-bcdb74d3b0c2,", + "14ef6b00-debf-4a85-9755-b4d37df496d3,cfiCxhJZ20u07X1x93tNZQ", + "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,", + "cee80307-68cd-4260-a898-1514dd41dd5d,", + "cee80307-68cd-4260-a898-1514dd41dd5d,6WJKUeFNn0amYNvKMxijcw", + "cee80307-68cd-4260-a898-1514dd41dd5d,VPvezA1A8UKt2qNoNRW9dg", + "cee80307-68cd-4260-a898-1514dd41dd5d,u0bLCSxcA0yDHC29XnLZOg", + "cee80307-68cd-4260-a898-1514dd41dd5d,OIfcKSEil0uHgT5baEFuOw", + "cee80307-68cd-4260-a898-1514dd41dd5d,hNFdYhoJYk24MqU2GhQXlw", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,QCOMyeCUK0Gudx4ljBmAdw", + "41bece0e-9b5d-4151-bf5c-d0e786a803b4,CJKB3OO2CkeEVsAK0jeeDA", + "ef7771eb-c5f3-44fd-8c27-f581b035ef10,XKoxnTXYGkSUu52nVS9dnA,TFOAk7kwh0uUgtsyDNxMuQ", + "ef7771eb-c5f3-44fd-8c27-f581b035ef10,", + "2786fc22-3d6c-440d-afcc-15890c061577,", + "bb8602d4-f91f-4e35-9c88-1cdf3a04ff0a,", + "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,", + "df29d81e-851b-40b8-95ae-8461f702b1e0,", + "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,lBe3yxuE40eBmTrnEHBqQQ,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,oZ8k0Qv3SkG-DkY0_dP7UA,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,", + "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,lBe3yxuE40eBmTrnEHBqQQ,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,oZ8k0Qv3SkG-DkY0_dP7UA,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,", + "691b2198-2ee7-425b-b59f-bbadda1253e1,wgIwYsXqiUekieE6pSt1Ng", + "4ee2133d-c419-4fbc-9117-746d756a250d,", + "oYvOo47DJE-DEPlpn5xNUw,ncERd5bELk23jSRxA7NV9g,N-cAmfOgv0eJZMTo18zJHQ", + "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3,", + "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,qspNifi92keI-XssPB5qDA,RVraJOPsOUqZjd8bEiPduQ", + "d0a9262f-5504-46a7-bb10-7507503db58e,922f229c-124f-4f07-b4e3-19138420ca19,2ee3944d-e9fb-4959-973e-570a7fc6cc57,ceb07e4f-50a9-4ed0-b50b-c59d92ba66b5", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,IEnB86oIL0KDa-zzcxhjwg", + "691b2198-2ee7-425b-b59f-bbadda1253e1,SSstW2ZPk0SB47qYlgbZzg", + "9e6397dd-b7b0-4d32-8c86-1910106a8478,kBmmFoqo-0eLSSAhen-VLw", + "75e51889-defa-4689-b3fb-32882e08e5d1,E6md4xTNYkS9geB8zkNRmA", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,FIZdMEUgykOHb7fVlOfa5g", + "34fa5014-e516-42a1-aaa8-91d2b6f59727,", + "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,csLWYuunH0K9Ij1q1FoBsg", + "943d2fc7-a769-4195-b147-a765e6c274c5,", + "943d2fc7-a769-4195-b147-a765e6c274c5,zJKd2ZtIsUGCdHP1M0215Q", + "943d2fc7-a769-4195-b147-a765e6c274c5,BizZfhG8ikOOEvAxN6w48A", + "943d2fc7-a769-4195-b147-a765e6c274c5,KfjLWNUbpEClo_qafEfUEw", + "943d2fc7-a769-4195-b147-a765e6c274c5,Adyl5MkwQEKK_MT5AluYkA", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,lRmNfk8vuEe4Z5ToWvWcTg", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,bgWM6wBe60q2mIHN5dPh4Q", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,cqYUoDpfkEi8Qp2DrpQytA", + "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eQy0qwre-Ui7_sCCNuWCSw", + "691b2198-2ee7-425b-b59f-bbadda1253e1,p5zYbKLGU0m0ZutOicfjsw", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,sA0KRBVMikOo6BBfk0dC1g", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,MVXeErsEE06KenrYaEm7Kg", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,9Z95fSKS00GscDxK8-ARlA", + "618b979e-c6e6-434e-98e5-9b51e117fb69,5Q9E76b580CnMWIg6zbPwA", + "20b424a9-861a-4d39-a583-22d7b7348c14,", + "dedd2e98-2721-4de6-92f2-9d399f34e8fe,", + "4e42e02e-33fd-4ff9-b74e-9bde26253c35,", + "9b636782-47f0-4218-9a1d-be65e550dc44,", + "3b30adc7-a1ad-46ff-96ff-08e656396c23,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,dpxEW-2Un0eeVIsg5_o6Xw", + "9ffa6e06-5200-4e53-ac13-f8491aecc864,", + "e773fddc-ad9f-408c-82c5-2637f75b3214,", + "e773fddc-ad9f-408c-82c5-2637f75b3214,Q3ctsvvDDEy_S1TDiGdf6w,DobTmYhC7EigHmvYnCTm3w", + "e773fddc-ad9f-408c-82c5-2637f75b3214,2N0meQR4GEGu9Yy61C0Lwg", + "2fadddc1-babd-4cb3-8f5a-0e7c889c3785,", + "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,", + "86af3a97-18d8-4dda-a2f2-cfd3b4224254,", + "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,Wm30oV5BtEuFQZFk3dPK_w,0R3Mh96tS0631EZYs03dmw", + "83be5ba4-525a-4781-a5f6-839c22e4d1d3,wF-WrDaY_kmm1AzA20RsZw", + "988e4069-4341-428b-bbf5-023309891385,", + "CQuKKNpan0O7rhR1fr14-Q,032pE8s9Ik-2CImFVDrSqg,5RUG6i2Mx0-55IO-IlhUdQ", + "CQuKKNpan0O7rhR1fr14-Q,M4765UHzsUeNTwH-Pcxp1g,5RUG6i2Mx0-55IO-IlhUdQ", + "CQuKKNpan0O7rhR1fr14-Q,FC8ZFBPt4EyTe7IYbv2Tpg,5RUG6i2Mx0-55IO-IlhUdQ", + "CQuKKNpan0O7rhR1fr14-Q,8xjU4GQ4OkaqGyad-ypwtw,5RUG6i2Mx0-55IO-IlhUdQ", + "b904304b-cce0-466f-8569-c621eeadd4f7,", + "b904304b-cce0-466f-8569-c621eeadd4f7,rzUd1IMTKUmW0HeMPSIOBQ,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "b904304b-cce0-466f-8569-c621eeadd4f7,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,", + "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,rzUd1IMTKUmW0HeMPSIOBQ,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "ff15e25d-d467-47a8-9ecc-e415d5acb90c,", + "ff15e25d-d467-47a8-9ecc-e415d5acb90c,rzUd1IMTKUmW0HeMPSIOBQ,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "ff15e25d-d467-47a8-9ecc-e415d5acb90c,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,3Xlg70ev6UqdcIAmJ9hmlA", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,Kuh-HrhzTUmuGaw7cpLUMg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,4UTuNYISo0OP70FuGzmOsg", + "a35b6774-9245-426d-ac9e-70154679faec,GAWBaCDBzUqdyTGA-HPczQ", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,e8aNxGFlBUWy59Bfrvvv7w", + "35d67043-ca27-4b8a-a893-2d42674e6e68,_3hxdSy9wU6OUPZJA9kGmA", + "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,", + "71fd5353-b439-4a23-b471-bfb99b8154d7,ss931x_Hy0-y1au-fT3zTQ,knKB2ETYmUupbvBwlleZRw", + "71fd5353-b439-4a23-b471-bfb99b8154d7,UQfLaKHzo0WqlRul_O61xQ,knKB2ETYmUupbvBwlleZRw", + "71fd5353-b439-4a23-b471-bfb99b8154d7,yrR3ogjJ50q6ypAnCF-ctQ,knKB2ETYmUupbvBwlleZRw", + "71fd5353-b439-4a23-b471-bfb99b8154d7,wZC46s7Id0CUICN_XOK-Rg,knKB2ETYmUupbvBwlleZRw", + "71fd5353-b439-4a23-b471-bfb99b8154d7,", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,y6NrbfS070WvASkKmPK-9Q", + "05ec4e87-088f-4281-a118-2e63c2585200,", + "05ec4e87-088f-4281-a118-2e63c2585200,Wm30oV5BtEuFQZFk3dPK_w,tfiSfpRez0y_R4QjAhO0bQ", + "0d4e7ebe-efd9-4e68-9900-5645916e7596,", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,lBkOSQNDZUOvoknntnEMew", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,6fp7nDdMvU6Ig-EmvjMpIA", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,UjSxxUK7z0GGdjvI_NsJ3Q", + "c506feae-be73-437b-b18a-1152bb9906e9,PTqpWqqU9k--lNlRrmlVbA,yViVKOLHc0qLiPw9Oy1Obg", + "fc96ea5d-b49b-479b-a963-92f1eab44b2c,", + "ec5f5089-7154-425b-8be2-743954ea2d69,", + "c506feae-be73-437b-b18a-1152bb9906e9,1qHXl55Qn0qSb1gAhr8Wjg,GlB_scM-5kuc4A5TmGe6Yw", + "c506feae-be73-437b-b18a-1152bb9906e9,nODTglESMEC9tBY1k-LrsA,Do__oD53x0-dNtVeHsr4yA", + "c506feae-be73-437b-b18a-1152bb9906e9,", + "27c2eb7f-9fac-4c42-90c7-44e7a33d5303,", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,q9Nganq9nUaV3zuvx9s3nQ", + "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,", + "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,7Ww7mumWXkiyj0R3u_-hrA", + "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,etbo7k9dq0OyjIrNS4sdkQ", + "3d5184e1-0201-4476-bf4b-4eb28190de62,", + "3d5184e1-0201-4476-bf4b-4eb28190de62,713781e5-ce47-41c6-a24f-94d42a565e30,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "3d5184e1-0201-4476-bf4b-4eb28190de62,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "3d5184e1-0201-4476-bf4b-4eb28190de62,c1e430c5-fd8d-4032-9e3f-8d87cc8784da,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,V1M2-FXZSEG782d6D0AhUg", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,wNde5lsob06eQxEH2t-zUw,5KeLrnvaKEWnEiap1xzmjw", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,5p1Hwa8IE0KIuucb7hV16w", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,ZqWSyRnr9ECIvoU5yFyqXQ", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,jCfhcP5ftk6_ruDnFjIhJA", + "9231a9c6-7638-4fe9-bd2f-eac6c943009b,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,yVNoqFjSeU6mPJxEJnzRLg", + "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,PR5dh2jf2kqGLO0lXgA69A,-lYK7LhYyEy6wqL1tIE9cg", + "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,", + "9f2cf07e-f98f-48bc-9c74-784ba4b94527,", + "c2362f96-0a73-451b-9a85-46410eefd2ce,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hE5scNMqJ0igY_jhufvNmg", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,4v1-TIIgnU-sg5ROppnEPQ", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,wl3nqp4FsU2wqEkYFqv3_Q", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,fF9qwnew3Eao2gF0xcOqrw", + "1bd891b1-6ec0-4227-9ed8-73c59af4f819,", + "b73ae23b-c911-4b09-a803-40999bdef6b8,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ibmxOUIFs06gOpRYLe58xg", + "a35b6774-9245-426d-ac9e-70154679faec,", + "a35b6774-9245-426d-ac9e-70154679faec,0EjPIiuYpEGIWup3pKeC8g", + "a35b6774-9245-426d-ac9e-70154679faec,dkJNh15DnEuO1BatzpI_uw", + "a35b6774-9245-426d-ac9e-70154679faec,XMAOwHGXVECoeKoWb_-luw", + "06adb976-ca7f-4b8b-8b72-2e03c8bc10b3,", + "63b5196e-881a-4b96-82c8-9df54de4a506,C64nh9MHv0mlUEP9-s7sJw", + "63b5196e-881a-4b96-82c8-9df54de4a506,WJ27X_UnfUmFKpY6mRk_Rw", + "63b5196e-881a-4b96-82c8-9df54de4a506,ay9ktLnHfUKdj0-q52Gvyg", + "f2db0ec1-e536-4218-8385-dc6913941880,", + "3739a9e1-6dfa-45d2-af54-cc83a58d436a,", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,NnqAnqqLmUypWnNR2s-_sg", + "9d2ea3c2-c199-40ed-a6c3-fab5f8dca7d6,", + "ad0bdec5-f767-42e7-99e0-221473464a85,192ba73d-6990-42da-8fe0-914c2a7181eb,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,K5vPqD3BrEuODRDzFdC6Hw,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,", + "ad0bdec5-f767-42e7-99e0-221473464a85,624c85e5-095a-48b6-85fb-c9bbade19a8d,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,_6LmNp0uX0Ky4jp2j62U2w", + "ad0bdec5-f767-42e7-99e0-221473464a85,cNarx7yecE2gk-7mYaoHwA", + "ad0bdec5-f767-42e7-99e0-221473464a85,teVKVbVEsEyqOSuRdqHQzA,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ad0bdec5-f767-42e7-99e0-221473464a85,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,54778d93-2cff-48db-b25b-65c08b0e5be8", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,192ba73d-6990-42da-8fe0-914c2a7181eb,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,K5vPqD3BrEuODRDzFdC6Hw,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,624c85e5-095a-48b6-85fb-c9bbade19a8d,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,ODOnI3iYoEKrlQfFabbvoQ", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,BW5oLDA36EK9nLzTm7xViQ", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,teVKVbVEsEyqOSuRdqHQzA,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "2a61c844-81c6-4f82-9c75-66cf308ed4e1,", + "ed695ac2-ff70-4ebe-b1db-18d71a4efdd1,", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,nloukujf9kSwK1T2HiwJwg", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,xLcqKXKH-0Gw1lrSSQe-Ag", + "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f,", + "e2ObiQFqCUOdKniKL9a1EA,", + "BqpRbr_T1kO_YJqB1787kA,", + "PryEX4MEvkGKwST6dqJHjA,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,LpUOG6fHz0uQPtmOLtUlKw", + "51e5467f-d310-44f8-80c3-17d7e27450bf,pue_FwobQEiaVubgc5eMUg", + "4885683e-a87e-4d27-990a-c16f1df7a28f,hKBSHovOeUy84yCbvutkOw", + "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,ZYjUekG8JkC9uPkm4xH8ww", + "352b49fc-99a2-4f05-a688-866ade1cd394,PabSuDB_GEejMahhLvLkJg", + "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,EM1x0p66L0Gf6BNBP8-1kA", + "4fcb5b84-13a9-43e6-9680-72b17874ae53,Su7kiW24d0KapNns96JoFQ,a3Er_BCApUiPtBaGkSH9LA", + "4fcb5b84-13a9-43e6-9680-72b17874ae53,CahBzgN3D0y46AQsvGC14A,a3Er_BCApUiPtBaGkSH9LA", + "4fcb5b84-13a9-43e6-9680-72b17874ae53,", + "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,Su7kiW24d0KapNns96JoFQ,EiIow5mS80qpoS10r1ZdvQ", + "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,CahBzgN3D0y46AQsvGC14A,EiIow5mS80qpoS10r1ZdvQ", + "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,sVeZH49mP0yBRKrbVr-0LA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eAq9xK51mE-Nu4RQY0ds4Q", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,rxMCIkLb2UO0Z3tsqtYuSw,x8nPf0xEwUiRdBQU00Bu1g", + "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae,", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,QMScBauef0uOE48-YcDcCg", + "3028ba83-0c85-4e27-a47a-56609fa58b33,", + "40c1f20f-182c-4751-afd1-b572b0fd061e,", + "ab446412-84cd-490d-84e0-2e666633b72f,", + "4095a06c-ccdb-41a0-ac95-5a97372c9a02,", + "a5b747b8-c1a5-4bce-999f-e13dbe77bde6,", + "84992fc3-4801-492b-8a6c-ffa09ae5b593,", + "aaed6385-0ba5-45f3-b0d2-6b0a9ab79628,", + "2cf6ceee-4c45-4fb4-b9df-cb8437742e85,", + "h_To9WQYdEusYQxlbMmMmQ,", + "jUpFzO5MPEGTbS-E0a99Nw,", + "uubY_Cn9A0-Ww4lsIRCF6w,", + "05ab6e79-1198-49f2-945b-528523e306bc,aUsPNgV2c0u15VhGBAIyvg", + "84efabec-fe50-4c92-845f-b99b1d84ca82,", + "84efabec-fe50-4c92-845f-b99b1d84ca82,E_pm-PPj_U-nsfMti-AlCA", + "84efabec-fe50-4c92-845f-b99b1d84ca82,cbk6h_-1Y0exRpGyrQMDMA", + "1a028850-64ee-40ab-b547-ebbcfa1b04b3,5a614f97-ef7f-4b70-afff-9ed837a86407,a8a91216-0fcc-461e-a682-95ee82509cce", + "1a028850-64ee-40ab-b547-ebbcfa1b04b3,b444ed0a-eab5-4bd6-af34-d080de74a149,a8a91216-0fcc-461e-a682-95ee82509cce", + "1a028850-64ee-40ab-b547-ebbcfa1b04b3,", + "1a028850-64ee-40ab-b547-ebbcfa1b04b3,8053b5e5-0f67-4329-a45d-1ee9fc830820,a8a91216-0fcc-461e-a682-95ee82509cce", + "a6f1825e-1647-4d06-85d7-ac2d139612c0,fe5288bc-0e02-4ea3-b17d-dee0e576979d,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "a6f1825e-1647-4d06-85d7-ac2d139612c0,82de1ca3-67d6-454d-84fb-0eeece71fcbc,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "a6f1825e-1647-4d06-85d7-ac2d139612c0,", + "a6f1825e-1647-4d06-85d7-ac2d139612c0,54d3c589-30ba-44c1-a77c-8f9e16eb0b20,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "96ca8d50-0721-47a3-a988-018a1c157677,", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,BYbKbXAgUk6vE7H7w4jSMA", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,AHL9ydUbqEqMw1tluIvyQA,w-vooMawWU6_4spdgBYufA", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,n2pZHS8A3kSgyFUCI_-8TA,w-vooMawWU6_4spdgBYufA", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,", + "8595c7b2-4d1c-4eae-af9a-8180d475d931,_SPKrYIcnUWhTiSrTFLwoQ,w-vooMawWU6_4spdgBYufA", + "GAWURQSqrEW4G5slrPMgZA,", + "25e76b00-4c56-4259-968f-046c14fb36fd,", + "f8514fc6-93f3-4063-bafc-378ddf454836,", + "f641d4f4-e197-4cf4-b477-89069797967d,", + "2af73868-b619-43f7-9b7a-37a8fe2502bf,", + "0058a7c6-04ca-4931-b2ff-03c46f99079f,", + "6a51d8e1-8844-4999-ad8b-06f4d414571e,", + "9eb95934-90f3-4f48-a6b4-d87d75d9bdc4,", + "6a2fa2f4-d335-4349-88a4-49af251454fa,", + "6a2fa2f4-d335-4349-88a4-49af251454fa,liZWUNZih0iakAa7UJkk9A", + "6a2fa2f4-d335-4349-88a4-49af251454fa,zBfCq7T69kqZPTz3P8KKhg", + "485bbda2-37e8-45a7-90db-7c35518dcdd0,", + "485bbda2-37e8-45a7-90db-7c35518dcdd0,M5QwJJRRBUy42HLyp6ghtg", + "a37c3161-1476-44b0-a9d1-d6eb854db2d9,", + "a37c3161-1476-44b0-a9d1-d6eb854db2d9,EmlwkB77pUiiDEmc2pMVaA", + "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,", + "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,z1djgGqy502ha9X_uB3enA", + "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,", + "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,OxO9ivoKB0e-fNIEk33Erw,tmaPvkx3BUOZ3nsl2E0oRw", + "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,M16XTvLPKUWRpiAspgsatQ,tmaPvkx3BUOZ3nsl2E0oRw", + "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,rXhNZtcZCU21dwEHOCgWOA,tmaPvkx3BUOZ3nsl2E0oRw", + "46cde502-86d6-4061-a80c-67458ffd4d60,bTAQ-CkyRUOZoXqXuap2Ig", + "46cde502-86d6-4061-a80c-67458ffd4d60,crl8VlZ59kGxG01noGEWmA,EckOEU0mJkGIVDvYoKBN9g", + "46cde502-86d6-4061-a80c-67458ffd4d60,", + "46cde502-86d6-4061-a80c-67458ffd4d60,l2BbgbHCe0S_SDUbfmpSeQ,XHJ0lW4zYkGcL5UyI1HfEg", + "b6cee099-5840-401a-9d74-0b80ff71ee69,", + "b6cee099-5840-401a-9d74-0b80ff71ee69,an_Z5HMFjkaHyHoZZV4AFA", + "c8a97aeb-e8dc-40ba-b6bc-a7c7be907381,", + "7df5697f-2f71-47d1-bc27-cfa43792ffd7,GCethPzUyUKYxWdfLykbSg", + "9o0gWLJjREG552gxRreT8A,dZyVey0_oU-3u6FLbIdGLA", + "9o0gWLJjREG552gxRreT8A,", + "9o0gWLJjREG552gxRreT8A,rP0LMoFjqUCU1LWA6_wOnw", + "5PUja-WX00Sme9yBwvDq-g,", + "5PUja-WX00Sme9yBwvDq-g,FpvZjltPtU6AKTBphz3ZMA", + "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,uahcQNh3IEiooPFq-SQi5w,c8ccb85e-52cd-4b3b-aadf-289819067125", + "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,0x7a3hV5XUyzyf8cUuzVtQ,vYAcZMymO0m6bCNQIsI8rQ", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,-C1ui-T280GEfW6-vPQ-ig", + "84574356-98ec-415f-8668-82cb52016bd3,ljbO0JAKVky8M97MsjKnPg", + "2565d597-9d17-4c47-a66f-b683583f0cf8,", + "75-HXZBOAEiDBqclnOn8YQ,1w_u4d4AjESxw5dxRkKHrw,CylZksC7KUGXF2VeJhlLSw", + "75-HXZBOAEiDBqclnOn8YQ,", + "EJFGAW-TnUCaeD59parEHg,k3dbqP5z30KU37MVPpsEPQ,GtoHkTpyA0iGIHsxphwivg", + "75-HXZBOAEiDBqclnOn8YQ,LRv1X0ZrjkqO7azGpZ7uMg,sx10-Hsfl0OS62pIFvHH8A", + "efac28ae-ff44-4322-9bc0-61eaaef95399,", + "EJFGAW-TnUCaeD59parEHg,", + "awOFE0_GSECr8GBNBo6a_w,", + "75-HXZBOAEiDBqclnOn8YQ,7BMBl3iA6USKijNCKfCXqA", + "EJFGAW-TnUCaeD59parEHg,hM0OmxjWLESbsJ-M0Z3QvQ,qZzkrGXpPkGq_nnGLZorwA", + "7a98503f-0460-4d25-a0d5-72a32db8f4c2,A8ht7G1iqEeLvP0SW6SwyQ,QppxPdf550-jT7dK7XrTzw", + "75-HXZBOAEiDBqclnOn8YQ,1Z24Obg1MEm6dC_blOy4IA", + "EJFGAW-TnUCaeD59parEHg,9WpjdWJbhkCoEgDljqZkdw,Uyn3QOl1zkGR2_OUOhNWjQ", + "7a98503f-0460-4d25-a0d5-72a32db8f4c2,", + "UGkQa4atNUe1cE5TN79s4Q,", + "75-HXZBOAEiDBqclnOn8YQ,NMmOn0zQZ0mJLxCMnsbIVw,v1ZOE9G9gEWAqNJX1A9f2A", + "84574356-98ec-415f-8668-82cb52016bd3,", + "e149c559-aeb5-4609-9722-52fb6acc70e6,QF05cIuqhEeY8oNnO9CocA", + "e149c559-aeb5-4609-9722-52fb6acc70e6,SoQerU2dOUu9BdRBAsEllA", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,07qFFGmwm0qmIikemncBkQ", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,qvK8xgbyuE-dW3--Ku-mvw", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,hRl4UTGoBUi5dmu0f4s76A", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,F0o3eeWbCUaUAGRUdI0z7A", + "UcPo9SsF3EGTCBB2J-Efdw,pAlEt5sqD0mKgFd_0b7RgA", + "UcPo9SsF3EGTCBB2J-Efdw,sXF1BZr8X06AYnWUrWxy1Q", + "UcPo9SsF3EGTCBB2J-Efdw,PNH0VNS1qk6nWVecYmVDDQ", + "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,rZOgFhv59UKssAH7k-UDyQ,eVl7mLA1VkmnR81YLF-ZNg", + "26f5e849-14b3-4cb3-ab92-673c94b37c51,CwObTyhghkWonhm5nTnAaw", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,", + "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14,", + "73a119ab-d2c8-4da9-98b2-29edbb38e2c2,", + "73c0e2ba-cba1-482a-89d5-1508997aceeb,", + "b95678aa-351a-4929-8a0e-0274a183eb18,", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,9RPbhHJWBkqwpDbMg-QumA,Qzklnrvw0EOiZ1mrj_nAYw", + "1940c740-695d-4481-a802-bb11b6561635,JpaGA1tG6U-VLsHH_vklzA", + "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,", + "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,db-JiDmRhUKX69weDLiHsQ", + "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,UZY81cnsV0meMp2lg3_UaA", + "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,A-xoYdSUF0aEr_XZ9Me-3Q", + "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,MVhTW78G_E6z2mY0Y5ZnAA", + "283e98dd-1ba7-4997-858f-96046e0ac431,", + "283e98dd-1ba7-4997-858f-96046e0ac431,AcLApWMhAU6tK10O7KoOog", + "283e98dd-1ba7-4997-858f-96046e0ac431,dhJm0moskU6urIwG1TXs_A", + "283e98dd-1ba7-4997-858f-96046e0ac431,J8z15LYBak6jHu3ZaaKAXw", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,0wRS4Rs4ykqjJE_5Fk-Flg", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,oIVUmOrjjUysnN4XoWvJSA", + "405f20aa-f888-4e21-b322-34d11017ff16,", + "405f20aa-f888-4e21-b322-34d11017ff16,f-RfHoCWik6QgNKDhCs7Vw", + "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2,", + "c859a371-5d34-4e53-a96f-a6f7f3377aba,aMOwLebcBU6SuPigtovUYg", + "c2df65e0-2e85-4666-bea1-e8efa654b155,", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,RKftA1Fw30-hriuZNMGsoQ", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,P6w6X7-Dak2G6O8HDRwlVg", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,wLpOk1g2rUyDeCKb5uOeSg", + "2e31fd6c-dffa-4571-8e53-fc28fd0c98e0,", + "11cec676-0ec3-4778-abe4-a9d399900b54,", + "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,CEnWMDmcMEyjX5mRdhtXXg", + "ad6e7c98-8056-4e12-92ad-bfa19a00f4af,", + "39dde11e-7aa1-4bd2-8532-62665e8d8dd3,", + "8a26efb4-2116-48ba-84a0-c4fdcf74c191,", + "8a26efb4-2116-48ba-84a0-c4fdcf74c191,tp0jNW7f0Uubr9nTMWuhXw", + "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,", + "1633766e-fa77-48f3-8c6c-a233e56bf320,", + "594a69fb-9f76-4f83-b2aa-94f0e72e996c,", + "c51780fd-9eb5-4bc0-9da0-34ab22d800df,", + "f90773c6-d0f0-417a-9af5-183d1e5a657f,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eCEF5P5N7EC_cEjM9-5iUA", + "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,2Mgx6S9f8k2H1AAYbkzjQg", + "618b979e-c6e6-434e-98e5-9b51e117fb69,N2ofIXeSBECHTBAoP2yQ6w", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,", + "14ef6b00-debf-4a85-9755-b4d37df496d3,mdSSJyipq0KvjDNQ2NyUaw,LQheTa-EvUaAonSyoWNmXg", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,cpTKHXjj0kulivZCKVQqiA,Mrf17aidXEeCF-fFURUUcQ", + "474db08a-d4dc-4625-9f5a-1a6ae0b70827,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,OZ1v-s_XFUyfMA5mfd47FA", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,qwQ8OV3E60WG_VEl9ECHoA", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,cxWuO4qBtUOKYmCXI0D5Jg", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,2r_gYLhQrkS0bjwaxLecfw", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,NaXiRmxgkkGK1U7IQ4yn8w", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,VeZQlPI72E6e57IQqAOq6w", + "a2fc5417-07e1-402c-a80e-5111a7f57287,", + "a2fc5417-07e1-402c-a80e-5111a7f57287,McX_jAOowUmjXNPEHbdS7Q", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,0FlA_T8GsUe9PdiZamlP_g", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,Gct9Vt-L4UiTjAl61hsebQ", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,U8ZZiMEeZ0-qJfcpqBP72A", + "a471995f-1fe7-4b77-a05f-8eea4e0970ab,", + "a471995f-1fe7-4b77-a05f-8eea4e0970ab,YSnoHFmJDUa_XCK6JMMh2g,VZyHWGKNJEGSxDT2GO3ebA", + "a471995f-1fe7-4b77-a05f-8eea4e0970ab,JilOXG1I_Ei65mw6TzFWgg", + "a471995f-1fe7-4b77-a05f-8eea4e0970ab,jXRXO3CVE0-H_iaxbMhFhA,VZyHWGKNJEGSxDT2GO3ebA", + "62a28dea-adcc-40b3-9045-917ae4f38c63,", + "62a28dea-adcc-40b3-9045-917ae4f38c63,EmTrJKm8cUy8_BHAvP8Meg,kxkqq5UA_kCM6lFsFe3dvw", + "62a28dea-adcc-40b3-9045-917ae4f38c63,T670e7nOakqKl3L6LTlTtA", + "62a28dea-adcc-40b3-9045-917ae4f38c63,Gep43Ix44EWybJ80XAyDbg,kxkqq5UA_kCM6lFsFe3dvw", + "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,", + "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,cbn6vE0gDUOuklVhJv1vNA,Ru4HN3stVUutQ9rUh7ro1g", + "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,2eTBbHTkf0uxy9JqA507Hw", + "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,1-_ssucgeEalgr6EDL3S9g,Ru4HN3stVUutQ9rUh7ro1g", + "30380607-8b54-4b63-81d3-cb3f08e452c1,ktnYOVEhtEmuxmjM1TjIOQ,EqNosRGf5U-5P1nKIRV3fw", + "c8d2503e-5d43-4828-8d1d-40c64a577753,", + "28f00eee-d3d1-4cac-ac70-fca064fcbaf0,", + "9ab4ea8a-1b16-4969-9b80-8e36b1f782ca,", + "UcPo9SsF3EGTCBB2J-Efdw,oNtLjorrZ06t4yIdWqKCcQ", + "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,", + "fbf74c15-4b42-4cd4-94dd-d0e86f11541b,", + "5944a66e-b386-43d9-a4b6-7b4a8e987fd8,", + "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,", + "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,SdSjmCkq-ECoboR2PwxJfQ", + "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,-K940hWJXkee1HDrlurepg,ItJCR1utoU6i-syjJpxQqw", + "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,iGhiA5wU8kG3NH6P9NZvGg", + "1988692c-aae3-450b-acd6-e9015b831f5c,", + "1988692c-aae3-450b-acd6-e9015b831f5c,bhYZTPD1nEaEercLIZRucw", + "1988692c-aae3-450b-acd6-e9015b831f5c,zw-evwOmg0q14P040EYYiA,a3I7EyRNVUilLfE6hriYtw", + "1988692c-aae3-450b-acd6-e9015b831f5c,_C4RxcDBQE-mHh2oyL9vGw", + "ffad1160-8383-4b4a-a5b9-223476b49b92,", + "ffad1160-8383-4b4a-a5b9-223476b49b92,W_AVltwf7kSzmftSsGdijw", + "ffad1160-8383-4b4a-a5b9-223476b49b92,JY4zXsTbX0W1sU6g1o4kuw,Y-XQzHOjIkC_-it1j9m9jA", + "ffad1160-8383-4b4a-a5b9-223476b49b92,trFPTiehdUm2Q3bdvBQr7Q", + "fa0e701c-5e80-46f4-8817-6e2dbf904734,nNJ8Tkg57k6pTS_0HSLkUQ", + "c859a371-5d34-4e53-a96f-a6f7f3377aba,", + "3916a22b-ef4b-4ecc-9945-fd57867af597,", + "d52ff2bb-d396-478d-8153-2aa845feb306,", + "32cdcbc0-6a9e-4245-980f-6888f12fe065,", + "c51877eb-efda-4a04-8394-092637e2a9bf,", + "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,", + "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,c3Su0qnh-UK-ymcGaUy4Uw", + "ef7771eb-c5f3-44fd-8c27-f581b035ef10,9nPWmpM4mUmEpHfthqYeng", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,a2mUuKOr-UOM5nPpR8dSMw", + "4a0e91a1-4562-4a82-92be-cb7491f1592b,G_QPd8YBe0-n8ywAGLMKBg", + "84574356-98ec-415f-8668-82cb52016bd3,p379ObEL9Eea0DX9Ld4qnw", + "ac957e1e-676b-45c9-a39d-b238d2329a03,", + "9c4976c1-a0eb-4a1e-b5cd-08d44a4b3011,", + "cd77c8ed-ab60-4f42-9bee-0141c7e3e626,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,OpWc8pumTk6M6x1WSw3a6Q", + "e633d56d-5121-4492-b606-40759108a00e,", + "W7AJ4UYlVkWF5nLbmpmRBg,", + "W7AJ4UYlVkWF5nLbmpmRBg,5Hk7UR60rkquUNxpQ8O0Gw,zMXDVNsKn0GhVd2Tjho0-g", + "3R_zf8xliEiurVU8GwaItg,", + "3R_zf8xliEiurVU8GwaItg,iqSyLD37IUejWYa8ndvqnQ,qrXO8rdEaUqUCXC1QVkhsw", + "45e31b92-0cc5-4841-9c3b-74b3f1830435,KdsZQVCA-E2TiEozsSUv7w", + "45e31b92-0cc5-4841-9c3b-74b3f1830435,", + "45e31b92-0cc5-4841-9c3b-74b3f1830435,ENYQugZRP06SDh6d-oJcjQ", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ow_9g2xNj0G33-zWIhjW-Q", + "105a5100-cb57-410f-a26b-1ee21002f067,", + "105a5100-cb57-410f-a26b-1ee21002f067,prEZHWvOM0ysaNfAe06ywA,q0nBwoIx_UWPe93wHDWTYA", + "e6216087-fb37-46f1-bda4-a88c4640f339,", + "e6216087-fb37-46f1-bda4-a88c4640f339,KwhdQ1deNE6Y6jzO9vaAnQ,byvLynH8x06OQ7GNCXOVYA", + "022ce375-ed02-479d-b87d-9d72d126bbd4,", + "022ce375-ed02-479d-b87d-9d72d126bbd4,H6fcuLxeBUSY3oERFc-plA,oGrNKZLpMU2bLwNEe4MPnw", + "6fccd476-c621-4f3f-b2e3-45c95be912b5,tLbmGIoldE2CC5Qi1oyqyA", + "1940c740-695d-4481-a802-bb11b6561635,Eh1jgOjxpE-AEModcPAFMg", + "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,", + "1631eb78-6718-478c-9b62-abbb0537e09c,", + "1631eb78-6718-478c-9b62-abbb0537e09c,9Yj55hS4gEOB34bggoRnzw", + "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,", + "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,g_jYTusUbU6W_6jHif-CKw", + "936d7126-a3b7-48a2-bceb-6cad9221b267,4PI6XssggEKRxkGlWeo8KQ", + "17e64798-f6ca-47b9-84ed-0c83b8678056,", + "b772a5a9-dd79-43e4-8d95-f965d81bed8d,", + "52ea64e5-f062-4683-a380-4f8a48938686,GCwZMIr720iH3TszOb6SQA", + "52ea64e5-f062-4683-a380-4f8a48938686,ze-eTUX6WUqE-n5nIcH0aA", + "52ea64e5-f062-4683-a380-4f8a48938686,LmMg-y8w8kiXdre4vUJrZA", + "52ea64e5-f062-4683-a380-4f8a48938686,", + "56da4739-f628-4894-95cf-e21a600f2124,eEsEJ78Me02dXnp6gTuZRw", + "56da4739-f628-4894-95cf-e21a600f2124,gYXppJSpDUG9MB0xej4-kQ", + "56da4739-f628-4894-95cf-e21a600f2124,", + "56da4739-f628-4894-95cf-e21a600f2124,VF7QrfNdC0ex5p62bkdt7w", + "5d4b097b-2850-4a13-b26b-280be7ad3f57,", + "5d4b097b-2850-4a13-b26b-280be7ad3f57,T5z4W4H8sU-2NxpepVH0Qg", + "5d4b097b-2850-4a13-b26b-280be7ad3f57,RTrcYpLtukm83lwAfWi5WA", + "5d4b097b-2850-4a13-b26b-280be7ad3f57,3oar1K99PU2OTGaT0i7Hag", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "739fd42a-8a8c-44a5-afa3-e0974802c6ae,PMa9370LPEig_7pKwCceEQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "2e5afc40-7e25-418c-8b1b-316b8ef479a7,PMa9370LPEig_7pKwCceEQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,fd67ec40-f898-4df6-b846-8bf350f362b8", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,PMa9370LPEig_7pKwCceEQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "9176086b-a0b4-4298-a7fb-eb91ea421e89,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,2MUexARJAU-Vc-Qebqqepg", + "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,NpvUosaJTECohTIAR38egg", + "2786fc22-3d6c-440d-afcc-15890c061577,dXz9B9OH2UuLNV35u8KB-A", + "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,USdUyjIzY0q5af3AeTFCOQ", + "32b7a724-1112-476d-9ddc-2d04655c9df0,", + "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,46WaeyRZnUCuLkEpxECRMg", + "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,", + "7b568bd0-ce59-456f-af69-537364a52f69,", + "7b568bd0-ce59-456f-af69-537364a52f69,_fabGqrxrkW2mfx0bejHsA", + "618b979e-c6e6-434e-98e5-9b51e117fb69,rg6sMjE1XkijY4Pq73pjcg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,9bQaIrqI00qtjdtkIkj_3g", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,xNjX_6-aREm0cYZObVLiAg,OE-PtL0-0kWcJPwn1wvuVg", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Dtis2HpVzkelZb6M9mW5TQ,iqGv6iRJ-UGbofv8-aHKVA", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Kcyp8sv0202Puym1Wf86xg,OE-PtL0-0kWcJPwn1wvuVg", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,_VfdBTi7PU276qzsYKayow,qSsEcadcm0-twR1TvVcgYQ", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,", + "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,r_QZLd-Aw0SsdQb_uYroog,eYGWiwqSGUG7Hx1R3I-11A", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,5_UnQ7vZ9E2T-EhJ7M-mQw,jL-LAsd03EeN1J-Zp8opew", + "1596c113-aa3a-4f5c-b398-e5c3575548e7,RA7cizxxYE6J8yV_0XwPxA,VSLcLM0TJU2kJU5gS4KZGw", + "591af332-a01c-454d-89dd-5ed6ae3ed09c,", + "1940c740-695d-4481-a802-bb11b6561635,GU267AQ5T0m1jco3mNw_uw", + "7VQvz9sbzUWvGalxd9rISg,zNBLaOhCAEKWvgLK4P_X7w", + "43354e59-4938-44bc-81e0-4fba47d0ac12,", + "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,", + "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,mkkk2N_2-EKX3pVfk7U5aA,UQe6v62i6ESImdpc7gB_8w", + "43354e59-4938-44bc-81e0-4fba47d0ac12,4xp-xZyNWEi_jJCIhAPI3g,Ulie-RkcJEmblLyoucx-JA", + "281937b2-f994-45e9-a6d3-03d097247ce6,pEWioft3gkOMWS4tZFping,gOw6NOX-QUegt4mxjtfpfA", + "08dfa3f9-d545-4707-be94-6342db90c17c,", + "08dfa3f9-d545-4707-be94-6342db90c17c,eG1Lf1vG60qNQiCBRsAhqA", + "b74ef8c3-405a-4d68-a2bf-01579a9cc397,", + "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,", + "a936c982-28db-4e7e-bc79-ec5eca975beb,", + "a936c982-28db-4e7e-bc79-ec5eca975beb,cS-oBNlLE061SIB40QjiDg", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,pvnZdyL9NEaYYX8T12AgVA", + "16359b51-96c4-4be0-b598-319c7549e18a,", + "fd7774bb-b29c-40b5-84b1-5c2bbe574255,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,clStIXndXk2ayewq-1LKng", + "34fa5014-e516-42a1-aaa8-91d2b6f59727,fWZekjhIxUWhxgVGTWrVsg", + "mfFLpKQRE0anhrmeUvW0bA,p5TStfnDXk-EY7k6UdHxnA", + "30380607-8b54-4b63-81d3-cb3f08e452c1,oM7qz730lke-off_cQw-cA", + "3d9d52cb-6a3d-436b-90e7-e76f238329ee,bJXcVtY7qUK101QUAgr05A", + "a994fc19-974e-444e-ae9e-c2f6c32432bb,", + "745fd5ef-705b-40f0-bfd7-dbf4af5a82e8,", + "7652db86-bc67-4665-8937-f5199d27b5c7,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,oflbr1qvHUyvwUxdFZMcmA", + "9cdb4965-6f00-4304-afed-e301e73a2b3a,08r7prRREUi-Hvp1Xze-wQ", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,j3S5Gk92mEOKlD424seciA", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,y8yTimlV2EWhJMU-Ay9e8A", + "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,f841cdfa-45ba-4f09-a117-503eef34cb4b,9c55b609-b7dc-4070-a3ad-6351a0054a06", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,B5WV1io6fkaAJdm4GbzM_Q", + "5f6a7dee-0b44-4138-b3a7-a079dbc63e83,", + "e21b9dd6-c3d1-42cb-a90d-1b60ffb826fd,", + "3bceb51c-ef29-4942-8b8b-a15ed4a41451,", + "fc96ea5d-b49b-479b-a963-92f1eab44b2c,opDwtb3OdESsA2Zpfx7luQ", + "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,8xqL--ghKUunlS0r4ugCgg", + "f4f372b8-33a1-462e-a9f7-8a0e85b2c0d7,", + "60cc933c-5bc8-49c1-aa6b-9c2adfef088e,", + "23767c66-31de-4e6f-81f5-b17769a0fd5b,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,JBFu1kZ8C0ya5JZLDh0ynw", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,a4r3MYNjT0Ok0uQX9BYH6w", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,", + "745b4725-5bf2-4b78-9847-076f02eab97e,", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,VxmHqppE6UWiXVBXL2QvTA", + "745b4725-5bf2-4b78-9847-076f02eab97e,G9cFrBRvw0S-nDp-Fn494g", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,kjDg5GM1b0iepa52BX5wbg", + "745b4725-5bf2-4b78-9847-076f02eab97e,KcsFLjkhCUmg8HbamtChbg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,WlfHWDcnO0SBT5wTjEPSmw", + "745b4725-5bf2-4b78-9847-076f02eab97e,ye7GYcd6PkObqAsla6BJaA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,_egTT9EA-UK0aLkn_QnWFA", + "745b4725-5bf2-4b78-9847-076f02eab97e,xVPRKfkYG0-u4MFhJYGe9g", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,10nSNj7eRE6XwS9y2OvC2g", + "745b4725-5bf2-4b78-9847-076f02eab97e,84OVMNAiv0mQwX2bpkw2yg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XPBWTcpQ9kaS0mabpdvYhw", + "745b4725-5bf2-4b78-9847-076f02eab97e,ishXZTP3bkeVNk68PBZAiA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FokpgM8R5EuXZwe89T78yA", + "745b4725-5bf2-4b78-9847-076f02eab97e,cLswsDJEmkCqYDGI8DJmPQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,v0nZrzbIMEi3AaGuNjmwDw", + "745b4725-5bf2-4b78-9847-076f02eab97e,fdmR0dt0hEakaNp8mpjGmg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,TtthSwdIq0CuSNOKDLsldg", + "745b4725-5bf2-4b78-9847-076f02eab97e,5mFA4uHLmku8oCc1rDJmXw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,5fjPcf4xQkSEYn3T-f8xLw", + "745b4725-5bf2-4b78-9847-076f02eab97e,ry0fWzcysk2fsr-zIvzjUg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,sjY_XUbXAk6h2TGnjBGDeQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,S9YQnGF2WkaFSG07d-5TUw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,1nFX70Rqo0-qQo2M_Z1KCg", + "745b4725-5bf2-4b78-9847-076f02eab97e,pcEwV_ZPxkSKP39UgPh6fw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,cOyJcjGyp0miaMLAOpz0FA", + "745b4725-5bf2-4b78-9847-076f02eab97e,nHa0WjWfl0-g-sknorb10g", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,nIUjGqY8L0u0ZbX45b4PUQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,w_8Bff5J_Uej5iCAz2WTBw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6JY7l1gtpE631NTmT6xv3A", + "745b4725-5bf2-4b78-9847-076f02eab97e,kdExSysXREOAYqobK_Wzpw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,27Fg--SzD0ertvGEv1Jgsw", + "745b4725-5bf2-4b78-9847-076f02eab97e,XlkkIjP4EEKq0ObjXYGtBA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6EjRVha2i0mCUVLaL_em_w", + "745b4725-5bf2-4b78-9847-076f02eab97e,Rlxn533MdUqMefvUSoZrrQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,EtgbhY9a_EqO4j86Al9Kpw", + "745b4725-5bf2-4b78-9847-076f02eab97e,_BV42pO3n0e0V-eOf9CyGg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,oyvOdKXeGU65V6sKhhb0qg", + "745b4725-5bf2-4b78-9847-076f02eab97e,g6PcjAOH0UinJtrGmxVuyw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,-0DfEFcbNEaNI2uvfHF9qA", + "745b4725-5bf2-4b78-9847-076f02eab97e,i1e3GavcdEu7EJFzwpBXvg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,DVnSVZcj8kyLr1DXRu7kzg", + "745b4725-5bf2-4b78-9847-076f02eab97e,ebjoqQ_pnU2qdFVR1yDIzA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QgECbFDwyk24sI-e_fXy5w", + "745b4725-5bf2-4b78-9847-076f02eab97e,Fr--3nBIUUuD6EYmj1iklA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,zMWcGYnGdk6lVqVNuFwJHw", + "745b4725-5bf2-4b78-9847-076f02eab97e,hrTGnJLb4U21P9PCi7u3xw", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,jYJehFukzUCHKzWQAaM3uw", + "745b4725-5bf2-4b78-9847-076f02eab97e,coRfJXTxhUKsFs1tzSBgUQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,4viU0bMT1UuasZHD8euFKg", + "745b4725-5bf2-4b78-9847-076f02eab97e,Y75a0D5Wj0qmpfxG18LvWQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,YePtDvVnOk6cZkDBVIv-PA", + "745b4725-5bf2-4b78-9847-076f02eab97e,VExSaZXZ9kan-0qjP6YPzQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,q-Gjg0zzI0mTHxjn8wi5HQ", + "745b4725-5bf2-4b78-9847-076f02eab97e,Yec2FLXhpEmrocbVUOkhhQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QD-SRdKijU2RVu60TcU_jw", + "745b4725-5bf2-4b78-9847-076f02eab97e,EziQI39wZUWqIy8zs5I7LA", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,3m5Q5crrHUGZJ9_GuxubQw", + "745b4725-5bf2-4b78-9847-076f02eab97e,PWMkuXHWFEKGKfSiT8rWNg", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XNTUcdaSVEuS3rbzwIwYNg", + "745b4725-5bf2-4b78-9847-076f02eab97e,xdO8lwFuG0e1v2FRmza4dQ", + "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FnezvbcGD0-oyFOTkpUo7g", + "745b4725-5bf2-4b78-9847-076f02eab97e,vtQ3bx6n7kSFdES121M5IQ", + "64fddd4e-3d59-43df-bb39-671e18953c0f,uDJ5ZqU1f0CiOAH8WY22Yw,KOCzRE4a6EKb3_NVOI_lcg", + "64fddd4e-3d59-43df-bb39-671e18953c0f,j66We9SYr0SNQ2ufZqIqhA,KOCzRE4a6EKb3_NVOI_lcg", + "64fddd4e-3d59-43df-bb39-671e18953c0f,_jkPDSXFUkSU_yOfutQ7BA,KOCzRE4a6EKb3_NVOI_lcg", + "64fddd4e-3d59-43df-bb39-671e18953c0f,", + "250bbb53-1fae-440d-b25a-46a16c271367,kIcd-yEnsEqxoeogquvmRw,RR69IGV4j0Sd2xtmyN5wFg", + "250bbb53-1fae-440d-b25a-46a16c271367,qHyKfCsrbUqSWomjLXf7Hw,RR69IGV4j0Sd2xtmyN5wFg", + "250bbb53-1fae-440d-b25a-46a16c271367,", + "250bbb53-1fae-440d-b25a-46a16c271367,2vawoQvJPUegnClmr2RNAQ,RR69IGV4j0Sd2xtmyN5wFg", + "UcPo9SsF3EGTCBB2J-Efdw,", + "UcPo9SsF3EGTCBB2J-Efdw,8ANbOhNnO0yLQOCIBsDwfg,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,4scG_UBlzE-5DWRhSrtm6g,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,thG3Dc1PFEqAvT9xCVshuw,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,nX7krD8e2km_ZlOLWcoxdQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,d9mwowRelE2lDKEA0UQMZQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,3PfP661Fg0CruzRxis9EiA,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,I1J-xY4YNU2dB_od4P_7Dg,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,PQTvQiYu-ESRUldnxgm5BQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,2sf-FU4WckGBDWUkVz4z1g,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,Uz2hrptCa0erjqyPoiZxew,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,JWBK5v78Y0uRVEXlD-Qk6Q,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,hf0gkFVS8ESvceRsC3LAXQ,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,a5nUvJZgTEGBUvdHx0Y6Ng,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,viL4CPch2ECwG-RNaWcyZw,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,k_ZYy0SjtU2TB7IFdq2vDA,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,ns5tIRishUOp-nYqLedcwA,_Yq00H-NWUyQNvGxRunHbg", + "UcPo9SsF3EGTCBB2J-Efdw,OqsbZ8Xls0i8uA6ha7H3qQ,_Yq00H-NWUyQNvGxRunHbg", + "a600fa80-ed4e-4acb-8d6d-72fa87d2bef3,", + "beee4e09-9d93-4023-bc78-c5dc551a0ab8,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ZDsylFpkEUi5PijTLSfYNg", + "c6ae0a86-e590-4097-b77e-ba6fce83a826,Ii2WNQxqtUeoIfGItUkVbQ", + "e149c559-aeb5-4609-9722-52fb6acc70e6,FbedbSonT0izB7Q6cLXCzw", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,B3wjSG3Ui0ipGgN3h7BFiw", + "86e0a4cf-b112-40d2-94b9-7ef35abe5a61,", + "a79e855a-c4fc-4ffa-be2f-9f4fef0ab1d5,", + "cc30f7f7-6c12-4a96-acfd-11aec984c2e4,", + "caf1e229-9660-4abf-8074-f706709f4097,", + "07116e9b-b182-40b4-90c2-99738e7aedd6,", + "8a26efb4-2116-48ba-84a0-c4fdcf74c191,9liduuR6IE-6C4YN_FKcZg", + "8a26efb4-2116-48ba-84a0-c4fdcf74c191,clKD22mme0y8apzUhHmI_A", + "35d67043-ca27-4b8a-a893-2d42674e6e68,J36t8RwGPUqtLBsF-_UtEw", + "35d67043-ca27-4b8a-a893-2d42674e6e68,B5t5RbzKdEObKDlXLcRp7Q", + "35d67043-ca27-4b8a-a893-2d42674e6e68,OaOC278jEE-owjtDP9ZLow", + "7be2eafc-21ea-4266-ae64-8888c06798d6,", + "7be2eafc-21ea-4266-ae64-8888c06798d6,sqPMH0LhLkeG-4XkxpU-sw,URWHffwjGUKgXeV01upePA", + "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,", + "b66cb611-c079-42f0-ae04-6f51384dbe90,", + "a2fc5417-07e1-402c-a80e-5111a7f57287,4YH_0kN1ZUSVEQmCBiTt7Q", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,H_lV3FQG0EC4ebW5vfnRIA", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hU2rt41rvUqulLM1H5NX4g", + "e27d33ca-8a81-4c16-a3c1-ffdbc9104d7f,", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,SiraUm3ZaUOCeXCekunJuA", + "fc6a560a-5ff1-46f6-a593-320c50ee397e,AvIuVibXj0u7UzWFh90_LA", + "98e4c868-df73-4755-84b6-c4d12723b811,", + "cfce35b4-d182-4505-a631-5a8e5011f550,", + "cb061cf3-05fb-4494-be5e-091ddb080271,", + "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,tPAJqFHiX0yUQpDOE0MJ6A", + "11b3413f-2c18-41cb-8ef2-55fb825ed476,", + "11b3413f-2c18-41cb-8ef2-55fb825ed476,DNGqrCGe8kKrNmJaGzjjew", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,49mpP0duKEylQ2v5ZU9SYg", + "7a97838f-24ac-41a0-8390-0c9308644eb1,", + "5b3cae00-88bd-4179-b75a-9444c5a52fa5,", + "5b3cae00-88bd-4179-b75a-9444c5a52fa5,ZM6W70pGt0aAPLYf4DykeA,icHpJch4uUeGdhoLkawyyg", + "3ec19cb7-d0a3-4c35-a7e0-dc1f8609d373,", + "06475461-f3ab-4e92-ad92-243c00b19ff0,", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,9bb0x4qgokyQLbt1jA25Wg", + "e4ba395e-f291-4e7b-8193-45f4422f41ce,YceWQmfB7U-R7OeBIr2Ogw", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,sq5Qv4nVAUqzhMdB7U2MVQ", + "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,bNWm_2_VE0-4fdcF6S0aIA", + "691b2198-2ee7-425b-b59f-bbadda1253e1,pqtwCB_c20yRmOEs6jlH3g", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,0JVWqxXYoUqgI-5ORIr0bA", + "691b2198-2ee7-425b-b59f-bbadda1253e1,", + "24781418-38ad-411f-925e-2321fe616b51,", + "e4888429-6f5b-4f57-9b9b-2559d3a54856,", + "vDSlMnayzEqWXwT-lkcKSA,6taO0KRM_kuKoaTnOIsHRg", + "187569d3-23b4-4118-862a-ace7f50fec45,", + "618b979e-c6e6-434e-98e5-9b51e117fb69,t9-7AjmLgEWawcyWCD4xVw", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,g14i4cTzkUu6RCOELBZEZA", + "9656a307-0c79-4b8f-910f-e67c454faefb,", + "9656a307-0c79-4b8f-910f-e67c454faefb,mG0HW8fJkUOE5u7Whg4kag,3CLmVNB_p0Gd5N8dN74TBw", + "5362d685-fc55-4b43-85a5-cc3d6cad3f69,", + "b6rLwzD4NkKV7xKn9ZYVkA,LovycnPHoUiP_N9w7BzsnA", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,SMeMLRUF-UWrOcXYJzgScw,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,3bqY_T4_HEy406HcXrfLuA,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,WkI_v-Sua0WlhmMf8R4aGA,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,I4lNZy7lR0uyUN5Vk3fabA,8ubY2vSZQEOmhqIg70Gqyw", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,XaNgcjkwP0uxS5_S1JaQrA", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,9ir4Es5d0Ue-8bn2EUcCZQ", + "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,aaaG7Fg8fky2dkrwVsA30g,8ubY2vSZQEOmhqIg70Gqyw", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,SMeMLRUF-UWrOcXYJzgScw,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,3bqY_T4_HEy406HcXrfLuA,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,WkI_v-Sua0WlhmMf8R4aGA,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,I4lNZy7lR0uyUN5Vk3fabA,m-fk_L_e4EiOMUZ_UUjMjg", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,GSO7t0cNs0SIbU-jQjnX5g", + "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,aaaG7Fg8fky2dkrwVsA30g,m-fk_L_e4EiOMUZ_UUjMjg", + "ef937765-d827-45c1-babe-5bcf6d68c08f,SMeMLRUF-UWrOcXYJzgScw,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,3bqY_T4_HEy406HcXrfLuA,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,WkI_v-Sua0WlhmMf8R4aGA,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,I4lNZy7lR0uyUN5Vk3fabA,MjaMSFJlsUisPzveOPrH4w", + "ef937765-d827-45c1-babe-5bcf6d68c08f,", + "ef937765-d827-45c1-babe-5bcf6d68c08f,W7E9KfRTLEK-1EZTNKM3Yw", + "ef937765-d827-45c1-babe-5bcf6d68c08f,d0D2htQZvEKajd0oAesNow", + "ef937765-d827-45c1-babe-5bcf6d68c08f,aaaG7Fg8fky2dkrwVsA30g,MjaMSFJlsUisPzveOPrH4w", + "b3b59410-d3df-464b-a75b-500a9e5ea154,logtSrZSTUqwEmBw9i0qjA", + "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,qWS_znZq2km_MUGvv5QDHg", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,B3t4hjeUeEyT17nfbZgxAQ", + "3eda64ab-2a11-4976-a253-bef7ac36ba5e,dtuR9xLXVUGzSi2G1DMh7A", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,sCj73U9p5E6DQreOsbutqw", + "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4,", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,g8h43tuiN0CCifW9XbnFhQ", + "2f8ba691-4508-4254-adb1-c2b554db4925,HQ1M8kINaEu-0HJtz7f8Gg", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,rmq1AOQvtUGOHvbPUximwg,XFuSiGupZ02Fut1Dxum_Iw", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zljmtj5WqUaRvB3XQTgJXg,XFuSiGupZ02Fut1Dxum_Iw", + "AbYdU7O_9U-Lwg7zFVZzXQ,", + "d4f86b2b-0fa3-4264-9eac-cf15b62463bd,", + "1940c740-695d-4481-a802-bb11b6561635,_aQEMQzp20eMT_Izfc64wA", + "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,", + "51e5467f-d310-44f8-80c3-17d7e27450bf,", + "4885683e-a87e-4d27-990a-c16f1df7a28f,", + "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,", + "352b49fc-99a2-4f05-a688-866ade1cd394,", + "3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686,", + "767684e7-0eec-4f6e-ad58-3d5ae715206e,", + "d33c2f3a-6831-4deb-82b2-062d1c4224e4,", + "e633d56d-5121-4492-b606-40759108a00e,6CFwXqWT50mH1v6WueFsjw", + "63b5196e-881a-4b96-82c8-9df54de4a506,", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,KUSVg33DSUK8aYXxJzOTdA", + "a44766dc-19ad-4476-849c-5113a5faabd0,cKLBS6pbf0SFuavWyeZ7GA,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,MbusQ9bte0i9zirlxtNkSQ,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,_9QlnTPXuEO5OQ3H0CwFkA,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,DZyW-gOG9U-QLGOMFBhOoA,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,mqdncbYcuUSGFvFqEU0z4A,HTU3Wp_ri0GNYGFSezFHSg", + "a44766dc-19ad-4476-849c-5113a5faabd0,", + "40734c4d-e1fe-426c-8921-930f7463d8e8,QkfA71-1z0qOiZAVrbzhtA,hCeaYHy1TE2s6rTPxAQP9A", + "40734c4d-e1fe-426c-8921-930f7463d8e8,wCe2yC2U1kyYvPd8Z1rX2w,hCeaYHy1TE2s6rTPxAQP9A", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hoXpo41nnEOPGq20Tq-t9A", + "02a5b48c-abba-4385-967b-ba900424829e,", + "50c9c6f8-2963-4ef3-95d5-e999a898269f,", + "274cb9b2-2f59-47ea-9a8d-a5b656d148c6,", + "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,", + "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,c7wiHNZ-X0KazEW4cY9PeA,5fVeR6S6R0yo_NcT-TB1lQ", + "76f8f9b8-0793-4d98-962c-9e6a71979540,", + "76f8f9b8-0793-4d98-962c-9e6a71979540,XIluUn9vQEabkEpqyYBG0g,9sSCSLRkYUCs4CCHK8FfAA", + "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,", + "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,tdECtZp1UUuRRKHV60coyA,PIg3ywG2J0CiGUPpm1A6Kw", + "dfcc5a47-5f73-496c-84b4-d44702231f7e,", + "dfcc5a47-5f73-496c-84b4-d44702231f7e,h-uMOz2Hz06ymEjtz4wuzQ,4Ooi0h3SrkObClvS2cGk9Q", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,RddbstjtZ0ClxgY-s66tTw", + "b3b59410-d3df-464b-a75b-500a9e5ea154,P49CJ5imfEuxH1rryrfFRA,nbgwLqKBnUyvieWX0i47dg", + "b3b59410-d3df-464b-a75b-500a9e5ea154,PkMkvHzXek-yKyqJESZ3tw,nbgwLqKBnUyvieWX0i47dg", + "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,V_duN2oWaUmmao1xHhzCJA,Uvarm_CwTUWWQYZvzI8OaQ", + "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,S5IPVluRPkWzapjf7Bdtxg", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,pUHlnnttXUmqP-9Rab8YaA", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,l1cBXE49rkKTDEl1NvmWog", + "fa68bdae-5b0a-4544-a4eb-84b051d4b913,8kAqQHAgnUypHdxnwVSQwQ", + "13e70c80-69a4-4ad5-90e5-4dd5e03da677,", + "13e70c80-69a4-4ad5-90e5-4dd5e03da677,ln5aTG458UOVh4sR820FJQ", + "bu9tb-K7NEiocvZY3o7ukw,", + "bu9tb-K7NEiocvZY3o7ukw,1DQi3Wt8ukK2gWvl0zJnkw", + "bu9tb-K7NEiocvZY3o7ukw,76CA_KCr806y1FA35m-YTg,tqyzKPdDwkWq3jfZ8jZdaA", + "bu9tb-K7NEiocvZY3o7ukw,ZpcM5H2Q1UCt0hh82nSu9w,tqyzKPdDwkWq3jfZ8jZdaA", + "bu9tb-K7NEiocvZY3o7ukw,7B7vpnMTfkGLcMdHCZ_kWA,tqyzKPdDwkWq3jfZ8jZdaA", + "bu9tb-K7NEiocvZY3o7ukw,nO-YacKU1kmLjTFAAqNeYQ,tqyzKPdDwkWq3jfZ8jZdaA", + "37b68987-73d9-4aae-8e3a-edd24ae890a9,UCxz68a5E0WaZkocDfEf5A", + "159aa0f1-38b7-4a31-b1c3-111bcde687c1,7WzGZTf51EWClLTLNhhGVg", + "159aa0f1-38b7-4a31-b1c3-111bcde687c1,", + "159aa0f1-38b7-4a31-b1c3-111bcde687c1,RDkLPQ8OukGIuLgTwnyqfw", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,7IYi1twr6kO3xJiQo5CB4w", + "14ef6b00-debf-4a85-9755-b4d37df496d3,VB_2uEBs70-9MnLn3__upw", + "6936d8e0-9d4a-4e22-99b6-04e5182c1109,", + "ba96458f-84c3-42b7-acc0-e4d96f31ec84,", + "7ee02f33-1135-4adf-a626-44a1b6d1b0cb,", + "6QT99hx6DE6t9cKAs61i3w,sipxCGA1uUu72KV07vAWFA", + "5fwjFQoC_Uu9ChvF6uNGZw,IQMO6E5ofkyC3S-IgrKyIw", + "2f8ba691-4508-4254-adb1-c2b554db4925,OScwXIzJHUKzb-NC6gUnxw", + "2f8ba691-4508-4254-adb1-c2b554db4925,sNtmG8EODkOoiAdOi5Hetg", + "2f8ba691-4508-4254-adb1-c2b554db4925,hPVbRnQq-EW3g0q17N6G5w", + "2f8ba691-4508-4254-adb1-c2b554db4925,QVsGyxlefEeomtJO-dFAOw", + "3dec4865-cf23-4c19-a460-dd2db80ee851,e821aa74-2d47-4388-bcfb-1893b8a83a52,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "3dec4865-cf23-4c19-a460-dd2db80ee851,bzYdPb8GWUS-CgT4wrziIA,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,e821aa74-2d47-4388-bcfb-1893b8a83a52,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "529015c7-3b4a-4a73-b2c3-33103339b6e3,bzYdPb8GWUS-CgT4wrziIA,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "25d4c652-1730-4925-925e-31a290a2272a,bzYdPb8GWUS-CgT4wrziIA,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "25d4c652-1730-4925-925e-31a290a2272a,e821aa74-2d47-4388-bcfb-1893b8a83a52,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "22pyI3Kz-UullhcebC6DLQ,", + "75e51889-defa-4689-b3fb-32882e08e5d1,", + "75e51889-defa-4689-b3fb-32882e08e5d1,PI3_sfTcNEW5TcAmHbsUOw", + "75e51889-defa-4689-b3fb-32882e08e5d1,DSZKCNhXAEiAxMM1Gqfhsw", + "75e51889-defa-4689-b3fb-32882e08e5d1,K1-5YGg2rEiJFRWUKEfEMA", + "75e51889-defa-4689-b3fb-32882e08e5d1,2dGY5MHfxEunzr7jtSFRVw", + "de0ac50d-2adb-4114-bd2e-68953b13d706,YlgQOY1VJUuGzYxoTYAqGg,jhQ23U8Pmk2K2wfBdrEWNA", + "936d7126-a3b7-48a2-bceb-6cad9221b267,eutz2irupECSYGMtru3NMQ,Gruqp7EGM0WJbAM_F8hWbQ", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,8M72ydR_uUSsli43PH2Eyw,wQxO4xmXCUGeC9ZHfDNGTw", + "9441142e-981c-4f35-8f9d-b4b3604da9a4,W83EASFUS06FBVMfL80ruw,Ydd6tdb9dUGooM6WAczNjQ", + "771863b6-70d1-4f48-9c64-c621ce0fea46,", + "771863b6-70d1-4f48-9c64-c621ce0fea46,HWAWBHI3zkGsh06he9CoPg,UAeSz-XeZUaZsk9FNlPX5w", + "771863b6-70d1-4f48-9c64-c621ce0fea46,I_3bHRSG_EmC6novYyy2dA,Cq8BOWcHb0GTDLvXl1iIcg", + "94eb7ef3-528f-4f59-92a0-48b0ec287527,l2g5qO9dokiNepH92oPmug", + "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,RnrXuIRvMkeW5w1lcNiuaQ", + "341dcb4a-818c-4d7c-9ae3-1dabbb79519a,", + "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zu3hTm6JyEa0sE72KtVGXg", + "bc0a1c68-2eb5-4dde-a242-c4e19940699c,", + "88ae6d12-6d39-4770-ac2a-f8c4602a10a5,", + "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,bc-aVFn__E61pyA4SvUoGA", + "657efe89-620a-46a2-8b58-cafd855c7cd5,cFQ1Z7_wp0meS8wdC1hemQ", + "1940c740-695d-4481-a802-bb11b6561635,hbF6ChMyEk-L8O4f6o_l6A", + "2b325613-84c1-46d8-86b4-a167d79661da,Jd_ahlwRskehLggn1T-GdA", + "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,", + "f527ffaf-da03-4519-82ed-46a9cb981dc3,c5884778-a87f-4612-9717-86fbb65d440f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "f527ffaf-da03-4519-82ed-46a9cb981dc3,", + "f527ffaf-da03-4519-82ed-46a9cb981dc3,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "899f1d17-eee1-4b7d-a112-fbbba597e738,", + "899f1d17-eee1-4b7d-a112-fbbba597e738,t8kKXvBU-kSM_MHaqZYpkQ", + "34f45c5b-fea7-40b4-add7-bc7a37000ab5,", + "3ce86805-987e-4958-bcc9-57d89c4a5b1a,", + "b4c93141-f387-4e42-9b04-12b7a97c9416,", + "3ce86805-987e-4958-bcc9-57d89c4a5b1a,PGZihr096kC4XZvHfmVMzA", + "380a5e35-b4d0-4119-9721-8ce4c0840fbd,", + "LK8jt9zCuUGwLRccgOOvjA,", + "LK8jt9zCuUGwLRccgOOvjA,rsUfUnTwbU6zrjrHWps1yA,GJRG8sCM7kSGfUd9gLwpgQ", + "JWanxvOHyESg9F_HlmTOfA,", + "JWanxvOHyESg9F_HlmTOfA,ZUMIXcyOpEGiHgslbHQbwA,Uhw5NBQy-EOIiCWiZFiL7g", + "gwLiwmqBIk6glFxvaTzxeA,", + "gwLiwmqBIk6glFxvaTzxeA,Jy2TN1KBTEeoVehy_x32Ng,fXnd_COSzUeznwe7P4ZYRg", + "E9VTiIVFPEa-OuQRto0e_Q,", + "E9VTiIVFPEa-OuQRto0e_Q,gy-xeYKKREaBlx9fUmuW2w,C8gCmBZ8RUG83vvDw_UYew", + "eacebd1a-070e-4733-a6b6-65811ef7891a,", + "ae7c9d1f-d371-4dfe-a6df-d74662606aae,", + "7aa994d2-6499-4918-ab2a-2531c17e27a7,I8SCbauJX0OLAKWOjy78yQ", + "618b979e-c6e6-434e-98e5-9b51e117fb69,LXR5H_-7JUO7F3JkJh3ouQ", + "42f1a780-1fe1-402e-bf11-176b768e33b9,", + "42f1a780-1fe1-402e-bf11-176b768e33b9,jzGTu3tSB0WM430AiMGAAw", + "e2620c2c-c28e-45ce-a83c-0588329b9c81,", + "87792b6e-1468-45e1-b28c-83f27ada7299,", + "a4c26527-8f75-44da-b150-7aaf8ef739c1,", + "d57cf97e-b5d2-4e47-8231-a226a161f6e3,", + "973dc863-b9a5-40d8-8f15-4096e75bd2cb,", + "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5,", + "17236d76-e3b6-42f6-9179-6fcf0479436f,zu0Jk24WTkan6XWSwGU1Uw,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,6t2rCb6WWEW6pJbcjsAFCw,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,3frH6KxZ9kGUYkjeCDpylQ,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,i1XbjBqTu0CILIuJClWimA,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,Lf4FjROSAEqXedFxpXFQfg,B-sIpWNOfEigMeQF-WVR1g", + "17236d76-e3b6-42f6-9179-6fcf0479436f,", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,TqhSo1hbPE6wdB_6lvLhFw", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,nuR8wvvCXU-EMXjBZuTXJA", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,SExtD3FBk0GwseMZDdGJlw", + "703f1987-2b31-4b6f-bab7-1939b1105ac0,dzH_40F-MUCy_7gATqL4PA", + "e0c4c93d-ea3e-420f-9e8f-7890b5bd4b6f,", + "pQqNvrhDJ0uuipKHA2qUOA,PSpysyn5gEKXCEi7f6gDoQ", + "20eb23e8-ab3b-44fc-8038-3714946c1a09,qzvnj7D1tUuf01HvpiIvqw", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,wCCq14_wc06hkmjg6TTFxw", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,LVuUiGBtn0mp4Wfs-2F5EQ", + "efc10aa4-a203-4465-a2da-eb4590ada8ef,R8VYB2EGPkmQX4dbmGLuRA", + "97c8e2f3-e786-42e5-8852-4abec5dc645d,", + "97c8e2f3-e786-42e5-8852-4abec5dc645d,_8OmgC7c80CsvQN41U6XyA", + "39854685-4793-439b-9c64-c98825456a03,", + "39854685-4793-439b-9c64-c98825456a03,qVeFYveZQEmobBTEMg1RTw", + "M9weaekvIUimiJSVJqZl-w,U2ZbHn8WJEySUXVpzAY6ig,GV8z2dlqGEqIAYaGrF1ZYQ", + "M9weaekvIUimiJSVJqZl-w,L333w3dfIEWDbtt1Yn1gBg,GV8z2dlqGEqIAYaGrF1ZYQ", + "M9weaekvIUimiJSVJqZl-w,GK3yolCNlEm3B_5DtHkE8w,GV8z2dlqGEqIAYaGrF1ZYQ", + "ASHnWS0h9kOJhbOUwxWKFQ,U2ZbHn8WJEySUXVpzAY6ig,6nmHu9zj8k-tEz0tBHblnQ", + "MJQiQ3LATEOymxCC8wfSjw,L333w3dfIEWDbtt1Yn1gBg,lSDrKR69rkSFULy9GA__1w", + "0yFzgVESokC3gFd3hrS8bg,GK3yolCNlEm3B_5DtHkE8w,yKb2A8M_m0GQDhFWCRUHTQ", + "vP9EKTMXP0yni8U7uCz2-g,FsATqzRVekO9hQ3u_q7keg,Q7hqvEo7DUyD9AqIOunWWA", + "O8VqC11xNkSKpDh6Omp_0w,mvj5RDTuh0qoql0OyBFI9g,YQQamy-tQkCkpZPXKepUrQ", + "38e7e182-5166-42d9-9ed3-b525b98579c1,", + "f159e878-7563-46db-b59d-c838612a0a78,", + "1940c740-695d-4481-a802-bb11b6561635,S_dLd3dzvk2hM6ZA2DKTWg", + "281937b2-f994-45e9-a6d3-03d097247ce6,Hae8Pfe8mEWeF8mL9hC9dw,Vat8nL4ZBEK9mLABJiXS6A", + "281937b2-f994-45e9-a6d3-03d097247ce6,GAzH2pThjECghCmCwKJPrQ,4T-xj-xtSEiv1PDYv30URg", + "hTHM3Kjqy0ykqBD8UCRQJw,8HR0DwDl2ECtHf5aEkqSHQ", + "hTHM3Kjqy0ykqBD8UCRQJw,7-lAnMa7M0u0n0nmeIINgw,6GfSjqIb4ECCvIPC8vO8_A", + "hTHM3Kjqy0ykqBD8UCRQJw,", + "J6IQt4G1zUeN8v9Wn50BUw,", + "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,T-JZqXUd3kC1P8L5Pu4ZmA,ao-eeUnb8ECcW2IFGuk76w", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,T9XdU5xAFEqX3QV__SjXYA", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,AtF5be-huUeADedbRurRcQ", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,5GEhQr7Iy0-kB0yCA3uJ6g", + "00f0b914-aa22-4d14-9af1-996e1af61f4d,", + "2f77696c-618c-4fb8-9220-33839cd3bfcd,", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,n8t0gIYFbUaPMhyQeWkPSQ", + "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03,", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,xeK_acj2hE-sYGhsMRwjhw,HwhHShudGkGjXagJxCN2uA", + "102c625b-b988-4bf8-a2aa-a31ad7029cdc,DzgN67HoikexE2rTlZLdYQ,vm1ESK9ls0SaxSwHplNALQ", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,kk271nI1MUu-gZjec9djrQ", + "0QOlT9c5xUuinpfZaoEe8Q,Pu_1I6Zq-0G1HBMHa5MrKg,Cjy-bE0PikWLCRrL4U2NGQ", + "0QOlT9c5xUuinpfZaoEe8Q,7Rw_gt9IUEOYsvVcOTbVBw,Cjy-bE0PikWLCRrL4U2NGQ", + "vDSlMnayzEqWXwT-lkcKSA,XT_txjZfIUOT400UPHLaRA,M-mi8QUHwEeIewXE8uAlrQ", + "vDSlMnayzEqWXwT-lkcKSA,iEvS6I9LjkSaGAmjcxz8uQ,M-mi8QUHwEeIewXE8uAlrQ", + "281937b2-f994-45e9-a6d3-03d097247ce6,", + "281937b2-f994-45e9-a6d3-03d097247ce6,SN8HDRADZ0GvrFgEHKMhWQ", + "59bbb9d4-4fdb-4400-90d5-244643dca4ce,", + "2b7a32da-eda1-45e1-b28a-f9f882cda7fa,", + "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,J7l1YwWjqkutybwykwih7w", + "QK6lBlX_wUCiSbJ9RkIwdA,TjeZwaUp10uhM72q8j4R0A,9HVoTIf90Ea8_czICVg9AA", + "3e3f5ed5-d810-4054-b648-d93167b90033,wMo3Bq1aXkCMFSRJft81BQ", + "618b979e-c6e6-434e-98e5-9b51e117fb69,mKWQMgBuTEq9EwXBTusuJw", + "M9weaekvIUimiJSVJqZl-w,", + "M9weaekvIUimiJSVJqZl-w,wHJnbDOo8ESmUbQDr0k31w,GV8z2dlqGEqIAYaGrF1ZYQ", + "M9weaekvIUimiJSVJqZl-w,gDH1WmW5rk2RdY4wJYYZow,GV8z2dlqGEqIAYaGrF1ZYQ", + "ASHnWS0h9kOJhbOUwxWKFQ,", + "0yFzgVESokC3gFd3hrS8bg,", + "MJQiQ3LATEOymxCC8wfSjw,", + "O8VqC11xNkSKpDh6Omp_0w,", + "vP9EKTMXP0yni8U7uCz2-g,", + "f28aa59c-d6d7-432e-9863-f894dc0e8e32,", + "c6ae0a86-e590-4097-b77e-ba6fce83a826,3L-AGgM50kCCz2V3UJtErA", + "c6ae0a86-e590-4097-b77e-ba6fce83a826,U-KT3nJlW0-JT8GSQuQXhg", + "c6ae0a86-e590-4097-b77e-ba6fce83a826,", + "2caHvVQ1vECrPv-YcdxdyA,FNe9LkWJMUeC3frYJMyEaQ,nW_fBsuZA0OfIjTMXm6ZCA", + "2caHvVQ1vECrPv-YcdxdyA,mudt-Z5-5USdnIq-xaDgOw,nW_fBsuZA0OfIjTMXm6ZCA", + "2caHvVQ1vECrPv-YcdxdyA,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,tkHJl8TQHEiSeD-seDV4jg", + "281937b2-f994-45e9-a6d3-03d097247ce6,kE9vr2bKoUSFm7AkcXSmew,5o-249dUdUOhW28lrJY82A", + "_OWVy3z6iU-M3-zbQgSLig,Vxq6v55a_kGPODzwB5MC1g,N5wN1l0tYEau5np-9PoIiA", + "JWfs9IiQs0Weu8MmyGpqLQ,CavRDqIlmUeTZBtCLn1hZQ,x10FeRBsQU2DIIpG8XIlwg", + "jiBzrzDPkkuL6DqIFKpcCw,WOxMXWKGrUWhnNInttZ-Zw,StxMBEprVkqMa_IoDG-Q9A", + "3b792b59-50c0-4c28-b367-fc694b1d65d1,PXssHhcJsUmUKGA8T4u85A", + "b92f9873-5fbd-4529-803e-80151f5685c2,", + "b92f9873-5fbd-4529-803e-80151f5685c2,EjNmQJBRnUet_ovEQ3HnDA", + "b92f9873-5fbd-4529-803e-80151f5685c2,mO5wjf7_cEWNCHFR6ymGxA", + "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,", + "3b792b59-50c0-4c28-b367-fc694b1d65d1,0Ua_WtxcNEWc_KoRuuHhtg", + "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,zvs7t0P2Q02wIELJzMa2TQ", + "c87239ca-a255-4b13-bbc5-1b38236fb4cc,", + "2ce4ce1e-92a8-4868-8402-c6a1a995292d,M3vXSuDRw0GLNh9A4dF4vA", + "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,ZzJxs2oz6UuXCX5rGvLHFQ", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,P8w3-vcH5kaHaMEsFsKDjw", + "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,wrobkXV-10CD1JJtgyy6Fw", + "05ab6e79-1198-49f2-945b-528523e306bc,", + "6d3be838-85cf-46db-b33d-0985aec9609f,", + "6d3be838-85cf-46db-b33d-0985aec9609f,Vn2gH3yjiE6bwBRgh5ntZQ", + "67d8d599-4611-428f-98cd-98d8a527ace5,", + "67d8d599-4611-428f-98cd-98d8a527ace5,4qSWIO-cy0q5JWHp7tagHA", + "730453b8-a352-4e6b-9573-54294690c2ad,", + "730453b8-a352-4e6b-9573-54294690c2ad,4yMvQ3-hnU2zZQf_XT9r-A", + "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,Rf7CDh2bg0-HdG5n7phGaw,njEWOjNEQEWCEqmytodpow", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,JUOrYcAd7U-ChkFZRiBeKA", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,6JihVI8pm0eJ-wDWuXm0sg", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,wwT8NdC-bk-aAWLoJvef5A", + "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,yxBqgXjlVEeRUOC_6iv77g", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,pY3RdcA2lU6H5rVXp8csfg,Dac47T16Wk6LmmDd2cs1LA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,5JQPQ2JBUkSFyXeYWvr4tA,Dac47T16Wk6LmmDd2cs1LA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,1Cbw9819NE-Fmv1NQIYKow,Dac47T16Wk6LmmDd2cs1LA", + "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,VtsT6sKDYE24bAU-PKkIDQ", + "16707919-756d-45ae-95fc-cfb6715b8ae6,", + "bf13fa1c-f991-4aaa-9402-aac109be9562,iaDL2im4lkCXUo0NWM-F3g,wzt-5C_LjEmIQWUVPcYWhA", + "bf13fa1c-f991-4aaa-9402-aac109be9562,ERnUPsyT6kuEnIRZF8zPwg,wzt-5C_LjEmIQWUVPcYWhA", + "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,pXrZhtzjV02_qvIQoWEFWA,Hj8WnsdidE6rJeNyOmWUfg", + "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,A7l6Q2clsk2XyKf62D3dHA,Hj8WnsdidE6rJeNyOmWUfg", + "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,", + "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,VK4YBLXO10G2_aEl3ZQUGA,Hj8WnsdidE6rJeNyOmWUfg", + "24c4404a-839f-46bd-98b6-6848d747a98b,", + "24c4404a-839f-46bd-98b6-6848d747a98b,mraJrTA4V0eARb_Kq7yYVw,NH0L99bd00OVKMyltuSnHQ", + "24c4404a-839f-46bd-98b6-6848d747a98b,w7ybaJQVo0WCZFDPCJAtWw", + "24c4404a-839f-46bd-98b6-6848d747a98b,u1JNHHTxSU-N53Bs9pRCoQ,0zHD3DaMAUOogqOqyd59SQ", + "3a790be3-2937-44d4-be01-b5d65353bd3d,MmGNESXgOkiZ0xCG41E9ig,Nc6jwNeK_kyajH5xKzCNVw", + "3757149b-f478-4975-81e5-d808a1bed8d5,", + "3757149b-f478-4975-81e5-d808a1bed8d5,i33RmJJCwUem-ADkOwMNQQ,Yn1uv3Y8JE6VSxsf0LOBvw", + "3757149b-f478-4975-81e5-d808a1bed8d5,PG6H6px5HkO4X_xPRd1idA", + "3757149b-f478-4975-81e5-d808a1bed8d5,fTWOc1eEl0aVKXwTYYjA9Q,31nK13dsTkaIf3tGFrZlJQ", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,", + "1974dd48-aa44-434f-879a-eaff0c7c06e6,Tbrc2MlSlE6mKua8qSoluQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "6d36091e-7083-4e3d-8729-94612602c8cb,", + "6d36091e-7083-4e3d-8729-94612602c8cb,aBQ3R9boek-mYeSYSNvDbQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "6d36091e-7083-4e3d-8729-94612602c8cb,Tbrc2MlSlE6mKua8qSoluQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "418db0e0-c7af-40c2-9cac-d461ce41e210,", + "418db0e0-c7af-40c2-9cac-d461ce41e210,aBQ3R9boek-mYeSYSNvDbQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "418db0e0-c7af-40c2-9cac-d461ce41e210,Tbrc2MlSlE6mKua8qSoluQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,47514b9b-0d17-4727-97ed-dab67e7fe031", + "22ef1089-d073-4fe8-b038-6f34adf0b85f,RKmRHk2N_U2gd91nybo6vg,47514b9b-0d17-4727-97ed-dab67e7fe031", + "01689baf-405e-4b38-9f09-b624e03865b8,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,abdf525b-b871-41be-a1c8-810f49740ad4", + "01689baf-405e-4b38-9f09-b624e03865b8,d3KmqVuMQkCYK6sVTMOTOA,abdf525b-b871-41be-a1c8-810f49740ad4", + "01689baf-405e-4b38-9f09-b624e03865b8,RKmRHk2N_U2gd91nybo6vg,abdf525b-b871-41be-a1c8-810f49740ad4", + "97eae086-cfea-45ff-9b77-1d184256d493,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,8768aa33-b014-4062-b323-b146dd7d78d2", + "97eae086-cfea-45ff-9b77-1d184256d493,d3KmqVuMQkCYK6sVTMOTOA,8768aa33-b014-4062-b323-b146dd7d78d2", + "97eae086-cfea-45ff-9b77-1d184256d493,RKmRHk2N_U2gd91nybo6vg,8768aa33-b014-4062-b323-b146dd7d78d2", + "fb2a0e86-3363-4579-9f18-f41138dc85ba,", + "c7c85554-e118-4433-8d6b-b61a2971e045,", + "b4e24678-61d8-40fa-8c27-2342aaf70ac9,Grr0Jm6r9kuiZKvnLPz9tQ", + "283e98dd-1ba7-4997-858f-96046e0ac431,QSXXWZrS6UaoAUn3u7hXFw", + "ad3961ef-4b91-4ed8-971d-4d23c32548a1,HDpCyFn40kOxJ0P_xJr4-A", + "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,T0PMM2iQqkCCMZoznrdkug", + "be1fdff3-ef1a-4be5-9d65-9107488f45d5,", + "459ff0a0-ad68-4337-9399-57e3b03e4bf6,", + "7524b3f3-130b-41b1-83f6-9c241bd92bb4,", + "184368a6-17fa-4cfd-bb57-a2862f6e17bd," + ], + "Consumables": [ + "frOMH6WxDEG1fBqC4_83vg", + "m0bVLwWGj0GuIBSb6wCk6Q", + "Il4VmrnjDkqjmjzddqoIEw", + "A5M-yf9tgUihq1uab3v58g", + "-hy0qD-iUk-v4NHxNzanmg", + "YEfbJTnsR0yT_p7e7tb_kQ", + "xHOwjwpXd0GDkvBz2VqieA", + "VQSgL2pTLkWx4B3kwYG7UA", + "lag2tZyB90W04lQ7ol4vMw", + "xwQWBB_fekmTqRc2LB92cg", + "ZuvkidodzkuOfGLDnTOFyg", + "JxAl1wVJ1UGAjoT3m3ilug", + "iiGTvhOCHkOTNJhb16Zbyw", + "Efd3_dy4ZEqWr7i6j5x0aA", + "EmPvh3I6L0uK_1i8Wy_ylQ", + "Sk_1sm88ZU2zpWn01Lv7hw", + "5hIAZ9wg5EyG1cILf4FS2A", + "BbJb1_0g4EGJP_CeNrjpew", + "mMCGPgK3tki5S_15q2Z81A", + "lWN_g4OcN0S0C4n6cSZq4g", + "K9RbXo-4U0q6NbGu8VL1Sw", + "3Z-9qU4n2EuNRzRNl_pqcA", + "uswFSANaZES0EJMLfOgNSw", + "7OZ5AE3uuUyqa0P-2W1ptg", + "_jnjYGBcyEWY5Ub4OezXcA", + "P15H1ONBhk-5DYYjid1ttg", + "TG_VYTUdvkeT6zG6o1yZlw", + "mq23W-RSP0G8iGNLdrcpUw", + "QRx0aSTT9keMFdAJMQHdTg", + "uMHrUPLYFk2rJOW_uop5Aw", + "LwaotjVEBUir0-w8126n_g", + "JfnVXFmilU6ysv-VbTAe3A", + "InQ25wQMGkG_bvuD5rf2Ag", + "wUCIKdJSvEmiQHYMyx4X4w", + "4My42w1D1Uu3-98pXB-x_Q", + "YpL6eN6sKEquOYIYnbhvew", + "ZtZnYBpKkECJlhHmkj4MiA", + "g3kxdlJv5kO8PuBXveM48w", + "wx--2TPTdEuGAqLCjs9Qag", + "XOZcxx-Klkyhe-MDbTqiwA", + "uGVFydNSokCXFAmXu3aceQ", + "EAhk3ZZdXEmH5wRAXXT24Q", + "Av-wvjXvvkmNVSz7ZZnTiA", + "5AJin8T2iEG7BzOPOgx2HA", + "U38Qe6rhEk6mFvArHfYjng", + "yvqSbK2czkS2sUCRdrGaEw", + "J1WqFNUWo0OBi4LGKPDHWw", + "Tpxqe_lycUelySRHM8B0Vw" + ] +} diff --git a/Server/RadicadeServer-Config/merged-avatar.json b/Server/RadicadeServer-Config/merged-avatar.json new file mode 100644 index 0000000..02c9ade --- /dev/null +++ b/Server/RadicadeServer-Config/merged-avatar.json @@ -0,0 +1,44364 @@ +[ + { + "AvatarItemDesc": "b33dbeee-5bdd-443d-aa6a-761248054e08,", + "FriendlyName": "Team Jersey", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b33dbeee-5bdd-443d-aa6a-761248054e08", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1213992309 + }, + { + "AvatarItemDesc": "6d48c545-22bb-46c1-a29d-0a38af387143,", + "FriendlyName": "Team Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d48c545-22bb-46c1-a29d-0a38af387143", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1037333475 + }, + { + "AvatarItemDesc": "24b8b757-4168-4b3b-ba11-46f29d19044d,", + "FriendlyName": "Glowing Team Jersey", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24b8b757-4168-4b3b-ba11-46f29d19044d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1816296005 + }, + { + "AvatarItemDesc": "e41c32ac-61b7-4e59-9d0e-b231ebdc2a68,", + "FriendlyName": "Glowing Team Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e41c32ac-61b7-4e59-9d0e-b231ebdc2a68", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1140800281 + }, + { + "AvatarItemDesc": "d84993dd-4e15-4481-9450-f88a34cdbc82,FnhttSsCoUCSAW8j-stC5A", + "FriendlyName": "Default Placeholder Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d84993dd-4e15-4481-9450-f88a34cdbc82", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1234016191 + }, + { + "AvatarItemId": 1185405589, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "c5010738-41fa-4eca-aeac-e24adaa29789,", + "FriendlyName": "Helmet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5010738-41fa-4eca-aeac-e24adaa29789", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.489778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1842916691, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "018b39ed-cbef-4fff-862f-f39373b32e2e,", + "FriendlyName": "Curly Helmet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "018b39ed-cbef-4fff-862f-f39373b32e2e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.489778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 986, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "47u2if3fx2rt8p4wj90qo988g.png", + "AvatarItemDesc": "1d27b674-f9e2-4ffc-9d8c-a58a1be06457,", + "FriendlyName": "Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1d27b674-f9e2-4ffc-9d8c-a58a1be06457", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:49.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 556, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9qr2ce4r54kz6w69sxrp0cm74.png", + "AvatarItemDesc": "d84c0ff9-8fbe-4ed8-abf3-7996e81888ab,", + "FriendlyName": "Large Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d84c0ff9-8fbe-4ed8-abf3-7996e81888ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-10-26T01:09:06.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 584, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ctj8umc3ax8se6s476eqty6l0.png", + "AvatarItemDesc": "e5b83dfc-b2e1-4dcb-a4ab-9d3a4c8a34ae,", + "FriendlyName": "Long Wavy Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e5b83dfc-b2e1-4dcb-a4ab-9d3a4c8a34ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:12.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 585, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "85pkqd12j64n3n0av6xlffsxm.png", + "AvatarItemDesc": "7dd6f7b0-7ba0-429f-a04f-e32d3a79ee61,", + "FriendlyName": "Short Wavy Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7dd6f7b0-7ba0-429f-a04f-e32d3a79ee61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T02:21:32.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 587, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eds0e7isjgdyo1d98d51ibdiw.png", + "AvatarItemDesc": "eb9611c6-bb50-41a2-93e9-7f959815a846,", + "FriendlyName": "Dreads Long Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb9611c6-bb50-41a2-93e9-7f959815a846", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T01:07:52.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 998, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8k7za5lr83ybmyz1z2kx03poc.png", + "AvatarItemDesc": "1fd69ef8-0b74-4962-af5a-67f0bf0358f2,", + "FriendlyName": "Ponytail Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fd69ef8-0b74-4962-af5a-67f0bf0358f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:18.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1002, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cy0bdcxljjgonlcc039y6o326.png", + "AvatarItemDesc": "a12f724f-4a73-4ab8-aad4-6bfc662b4dd6,", + "FriendlyName": "Undercut Long Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a12f724f-4a73-4ab8-aad4-6bfc662b4dd6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:24.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 993, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5h63afsihyt4jqw4p15rg5bnh.png", + "AvatarItemDesc": "0753d7a4-8247-4fca-a6fc-359c26086140,", + "FriendlyName": "Fonzie Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0753d7a4-8247-4fca-a6fc-359c26086140", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:12.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1001, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "95n2l3ib6p6i60mz8mpfd18uj.png", + "AvatarItemDesc": "77d3c585-4928-4471-a425-89036efe7299,", + "FriendlyName": "Spiky Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77d3c585-4928-4471-a425-89036efe7299", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:22.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 996, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "epvz80ugc50j724bv8du1ivqo.png", + "AvatarItemDesc": "92302d9d-c527-418c-ac5d-1fa869727505,", + "FriendlyName": "Part Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "92302d9d-c527-418c-ac5d-1fa869727505", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:16.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1003, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4wmgnksu1wj9yabh6v9uxjmtp.png", + "AvatarItemDesc": "f9dd08f8-16d3-4c39-af4f-89f7bb6e80d3,", + "FriendlyName": "Undercut Short Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f9dd08f8-16d3-4c39-af4f-89f7bb6e80d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:26.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 990, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3mpvrp2oqpvbugfnbb57ooj1m.png", + "AvatarItemDesc": "b148cb1e-df81-442f-aea6-ab1727aad00e,", + "FriendlyName": "Chunky Afro Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b148cb1e-df81-442f-aea6-ab1727aad00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:04.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 988, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ddt2ggc02cukv16x74awz2f5s.png", + "AvatarItemDesc": "e36bcd98-7e85-43fa-89f8-57e4ec33823a,", + "FriendlyName": "Bob with Bangs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e36bcd98-7e85-43fa-89f8-57e4ec33823a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:58.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 987, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aj4ljso81bddjrnou7k4r9jvc.png", + "AvatarItemDesc": "880a3cc0-7407-4b61-b759-f9dd890fe9e5,", + "FriendlyName": "Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "880a3cc0-7407-4b61-b759-f9dd890fe9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:55.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 994, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dmg8ywtfbzy57cutb6j4h3vjh.png", + "AvatarItemDesc": "21599b51-c50f-43d8-ac5f-62c30cd02ca5,", + "FriendlyName": "Lori Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21599b51-c50f-43d8-ac5f-62c30cd02ca5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:13.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 991, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "60usmea9f77xjypmnvoptbc9g.png", + "AvatarItemDesc": "193a3bf9-abc0-4d78-8d63-92046908b1c5,", + "FriendlyName": "Emo Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "193a3bf9-abc0-4d78-8d63-92046908b1c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:06.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 997, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9gvfh0l74sodnh53dk03ioc7k.png", + "AvatarItemDesc": "79b90274-6eec-4664-acfb-4a123334661e,", + "FriendlyName": "Pig Tails Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "79b90274-6eec-4664-acfb-4a123334661e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:17.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 989, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5yfm6hc4pnazd6475re6lhwdc.png", + "AvatarItemDesc": "da4e7b34-2095-4a9e-801e-4f409039e0dd,", + "FriendlyName": "Buzz Cut Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da4e7b34-2095-4a9e-801e-4f409039e0dd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:01.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 995, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2k57qatxawouqn71m6owgx9bc.png", + "AvatarItemDesc": "9d9fadb6-97eb-480e-a224-4e0179082071,", + "FriendlyName": "Meatball Buns Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d9fadb6-97eb-480e-a224-4e0179082071", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:15.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1000, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5en879epjtfbbwep4k3qz00wa.png", + "AvatarItemDesc": "d8280c0c-d803-4513-be10-a0ba96d8821e,", + "FriendlyName": "Flowhawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8280c0c-d803-4513-be10-a0ba96d8821e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:21.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 992, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2a7algukgsrdsvd69r2r6nqlw.png", + "AvatarItemDesc": "e286863c-2967-4d00-b837-b49487b9484a,", + "FriendlyName": "Fauxhawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e286863c-2967-4d00-b837-b49487b9484a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:10.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 999, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0v8t308zwge73zqhs45g27w4f.png", + "AvatarItemDesc": "2cb4f372-3372-4583-8b57-c4e3988e3c28,", + "FriendlyName": "Punky Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2cb4f372-3372-4583-8b57-c4e3988e3c28", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:19.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1004, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "814p75pufultq4i0yd07s46mi.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,", + "FriendlyName": "Cat-Eye Glasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:28.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1005, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "56tk3ruae6anx7arnsnyk6k03.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,", + "FriendlyName": "Glasses (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:29.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1006, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "f0wdmxgtyo7xkofqt6gy6s0hn.png", + "AvatarItemDesc": "8d10cc78-6b00-45f3-affb-205e9cc5b03f,", + "FriendlyName": "Beard (Close)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d10cc78-6b00-45f3-affb-205e9cc5b03f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:30.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1007, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "amvkakyjtnfjh0sip52ftw6uk.png", + "AvatarItemDesc": "cc96f8a5-bc5b-4f89-83b7-ecd53905ada7,", + "FriendlyName": "Beard (Thick)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc96f8a5-bc5b-4f89-83b7-ecd53905ada7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:32.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1008, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dfc0t1w1r9z7p33f1562b1u2u.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,", + "FriendlyName": "Business Tie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:33.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1017, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3rr58syq4bpp9v0ot7isaecb3.png", + "AvatarItemDesc": "4d507dfa-4a99-4ac0-8537-229e9dc0eb4a,", + "FriendlyName": "Rec Room Tank Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d507dfa-4a99-4ac0-8537-229e9dc0eb4a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:44.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1019, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2theh2wmoj85n9yoigkwvalhv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,", + "FriendlyName": "Rec Room Shirt (Crew Neck, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:48.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1749, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "67l6cwkqcs2tczds20bgj5rxy.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45", + "FriendlyName": "Rec Room T-Shirt (Crew Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-30T01:35:18.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1009, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aouoxffd4nu47wnb2ngptwmhv.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,", + "FriendlyName": "Collared Shirt (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:34.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1020, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8h9x3f8d3cbsn1mixmjnnxakw.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,", + "FriendlyName": "T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:49.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1018, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9b4c3le7lmjt85hxgjr9xtaz9.png", + "AvatarItemDesc": "8aa79563-ace1-4ba7-ad0c-f3210a78142f,", + "FriendlyName": "Rec Room Shirt (V-Neck, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8aa79563-ace1-4ba7-ad0c-f3210a78142f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:46.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 675, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4n6q6y6u8lbqej4m7zacg6pgv.png", + "AvatarItemDesc": "8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c", + "FriendlyName": "Rec Room T-Shirt (V-Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8aa79563-ace1-4ba7-ad0c-f3210a78142f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:40.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1021, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4hlua7fbqw8nlu7f4buxxbjgy.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,", + "FriendlyName": "Tennis Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:51.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 681, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aqlcnxazbwcmfgx7cp4mpgqpy.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,c5deba2a-6e35-4b13-8e94-8ba5457f39df,b75ef67d-00c3-4ac1-9b72-212032460294", + "FriendlyName": "Tennis Skirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:48.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 682, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bjhjw79pvyn169f35rgbnyhqa.png", + "AvatarItemDesc": "21caa68e-c3fa-474c-af5e-af1e742b7a60,758752bd-db2f-43d2-b580-55b3e1efffd5,b75ef67d-00c3-4ac1-9b72-212032460294", + "FriendlyName": "Tennis Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "21caa68e-c3fa-474c-af5e-af1e742b7a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:50.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1022, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c45s37acw47y54ey191vc8g64.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,", + "FriendlyName": "Zipper Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:52.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1023, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9a4gj5yigfksglnec4np95a2h.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,", + "FriendlyName": "Wristbands (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:53.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1024, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b0nybni7980f1n0fvwjlcpwba.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,", + "FriendlyName": "Wrist Tape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:54.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 674, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0bb1u3you3uny9smblxzcmwyf.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,55901f12-d5b5-4fa8-b4c8-e479689ee39d,f600037d-c9c0-43fa-b45b-02f456f9dd5f", + "FriendlyName": "Collared Shirt (Denim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:38.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 672, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "077ospe8q8kka3lqwcvd2df6w.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,bf82f2f6-9af8-431e-a296-0890dea48ba7,d015cae7-a905-49e4-8823-6dec069689a6", + "FriendlyName": "Collared Shirt (Argyle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:36.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "130079y56zsb2j2jv5bahhp77.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,EfdMcnfHt0mr0PQ_maaYOg,DRJcNhkqvkKFEaZpOguR6w", + "FriendlyName": "Collared Shirt (Flowers, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:43.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 676, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "apumxi9psa3dgu88d82sv7a82.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,6d703981-2734-4c45-8983-cdd5f328902f,a0271cd0-e172-4d3f-aa2f-9806f21a82d2", + "FriendlyName": "Tank Top (Camo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:41.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 679, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cyeqj6jrqz7lap5blce53jyxc.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,9c03f381-7357-4d0f-8cda-8737d4c43d25", + "FriendlyName": "Tank Top (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:45.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 680, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b5581rifqvl56c14u3pjcb613.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:47.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 678, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5pbiviztmh19hus4bp2sifgsl.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tank Top (Dots)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:44.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 677, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4iwbk5q2yccf8huy5ut62aqjm.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,48abd952-214f-48b2-a8f1-1146f6f69aa2,b78008e8-abbd-4ece-be34-9a911f721fcc", + "FriendlyName": "Tank Top (Zebra)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:42.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 669, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3eon8zbs1np0ijovp5ri8gb2h.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:29.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 670, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "drn8bzb22wxe4zkanykjdw7il.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:32.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 664, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6xdunx482y5cr10gt2joszy8u.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:22.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 663, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9x7n4sd9ecxc01cpbo417a6h9.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:16.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 667, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8a4fwzyn75woyrsc9rz1hucpr.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Orange, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:26.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 666, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d1xnxod7dtl3g6htemy60mxxj.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Pink, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:25.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 665, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bt1et6gecchq8vwnxdydsijvs.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:23.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 668, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5j9o29zmheavd8i64j65gc7d5.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,6dd95046-acf8-42fe-ab78-80a334096a9d,56a92c8d-af53-413e-929e-4a9a3cfad780", + "FriendlyName": "Headband (Red, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:27.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 686, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6pf2ftyxdwcm1n73wbvsvqgww.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:56.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 684, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7etdcocmjntbessmyb4q1yrql.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:53.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 685, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dkn9mf26l54mq2qdaq2wxjmmb.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:55.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 687, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8ymxu6vs24zw45qrtx7r3wiir.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:57.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 683, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cbxl5wtnmplfz5vth23g97xve.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-08T05:14:52.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 390, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5n8kq96onh4dhg7u9k46jmucw.png", + "AvatarItemDesc": "fcfcaf63-deb4-45f7-b711-c051c9ea45cb,", + "FriendlyName": "Top Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fcfcaf63-deb4-45f7-b711-c051c9ea45cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:28.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 143, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ap1up0rb8i4a1km9mn9e44dsm.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,", + "FriendlyName": "Blazer (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 147, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00wlb607co30g3d5s8a51rat5.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,6f2e74bf-1e95-463d-97db-d5d1a53b2c28,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Black, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 146, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "crmfr0dn581ysgtbeh5d3m2mr.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,9374bf66-2ee5-493b-8439-efce4b201904,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Grey, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 145, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cugl1bt43o2xnok3ehawcralp.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,272fe8eb-5061-4729-a7a8-414ff667a82f,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Grey, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 148, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0z5rg3veja6t3bw5vpad3759e.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,0ffad843-d6c9-425a-8686-7217009c867e,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Green, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 152, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "28ly2m56wth4ldtr1r5anpadx.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,e0397982-c2c2-4733-9a40-46e18675b5af,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 150, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dw4gnw7y1oclj75acv9nvncv6.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,", + "FriendlyName": "Button Top (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 151, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e1qpklmk4sy92lxlwf1zus9lh.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,49f5864f-9d40-497c-88c8-e87f64d41d74,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 153, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "08g89jon46625gx6apyzgaq7p.png", + "AvatarItemDesc": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352,c5deba2a-6e35-4b13-8e94-8ba5457f39df,dafa658e-753b-46cb-bd85-85c1de5e6ea7", + "FriendlyName": "Button Top (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 194, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9lqg5xqiktt0cxifvy1kkwpa3.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,f750de46-3758-4f7d-9709-0a84b1027009,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 195, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3nhman1tbwhikxyha0tj215xw.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,d66aa400-aa5a-4539-a25d-5f8ce94dc281,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 196, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7nlns9i6osonn2bq8867ceizn.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,2c8924aa-68f8-4912-9759-18992f72f08a", + "FriendlyName": "Collared Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 193, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "02aazblyiy14oivk4h218q67i.png", + "AvatarItemDesc": "6d815b35-6f68-4ed4-817d-70f141e1a571,", + "FriendlyName": "Collared Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d815b35-6f68-4ed4-817d-70f141e1a571", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 417, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3w6yoi9bpa2pl8w7wab8zwodb.png", + "AvatarItemDesc": "241506f6-bf88-4b46-b5fe-513a225421f4,", + "FriendlyName": "Half Up Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "241506f6-bf88-4b46-b5fe-513a225421f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:30:04.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 459, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "08e7y3dct2h7xht6m1vfda9yg.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,d6edbc00-3c1d-4f49-8412-3ef8c7c5f4c2,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:24.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 255, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8aayhgqe518c46fqbtd1x55jk.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,6d703981-2734-4c45-8983-cdd5f328902f,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 254, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6ih1uyez4f5apbi0u4n5szfns.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,830be2fa-60a5-48cc-931f-34b670eae4bd,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 253, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b119rnoqjo60vhf5wcwc9my42.png", + "AvatarItemDesc": "2296ed0d-df56-4d46-b33a-aae9230a47fc,bbfa08e3-8e6b-4e0f-b264-1b398d7cd44a,cfabdefe-0890-436e-b2a3-b5c712e22955", + "FriendlyName": "Zipper Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2296ed0d-df56-4d46-b33a-aae9230a47fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 289, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0r354na62acon4jev863fj1vh.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 294, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1eoaif6mmgo9dolltq50ob9o6.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 287, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "16b0f84wdbvxcp649og13dk8v.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 230, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b1hdewklda06oi0oveqpmi9wc.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 229, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5k1gsvgmixc21tqvaomwh0yft.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 227, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "csvjo09nx4adboqztoamuuq7p.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 228, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7wrlcy8lbaezttb5m6olhzmu2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Tank Top (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 461, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "58gu9l73pm4ez45p70yrkyplt.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,5c4a2b35-0e1c-44de-8c3a-96d4a6458b1b,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:26.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 460, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3akcjd4a37lw3qz88ajis1kcz.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Long Scarf (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:25.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 15, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5kh0jn19pdtrpbc2g180026s5.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 10, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4nxr6u9ei794sh6gu0ucu4ddi.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 7, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0sr1gkccjdo3hx6ruc2ss61p8.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 6, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9tssl8yucapudl7wbupmjwwfw.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,", + "FriendlyName": "Headband (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 16, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5uw2gb9nq7yeqmoplvw1pcsqf.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 582, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7a6q8lttqfwygyoo0xob5drc6.png", + "AvatarItemDesc": "24a240f4-1574-420b-b898-a7e91f170759,", + "FriendlyName": "Back Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24a240f4-1574-420b-b898-a7e91f170759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:09.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 583, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b8pcnjd03lqqw0jzj8ueug6d4.png", + "AvatarItemDesc": "c45ed7b8-99bd-4a4b-a9ff-e16edf5d7a18,", + "FriendlyName": "High Pony Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c45ed7b8-99bd-4a4b-a9ff-e16edf5d7a18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-08T00:39:11.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 29, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "35b472lvh2ejmoj4ppvh8nqvy.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 983, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6u0dzdrw11sr5n7ix18tfuhu1.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:26.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 31, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aghnwd2jnaxd0vtt7oq9ks3pp.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 25, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d7lvri8z5oviwaw0fgktu2pqv.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,", + "FriendlyName": "Baseball Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 30, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9si5yzip1v33nbt5clp7t208l.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 172, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d004gewj67bopyl1n223awz19.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,", + "FriendlyName": "Equestrian Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 173, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9sz04th4t4o0izh811x1j6i3j.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 174, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0ssz8kkzkquzlbsny70teq07u.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,4828b50c-95b6-466a-bb25-514891d78202,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 175, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1ww790yppnpxni1c0om3j7p3e.png", + "AvatarItemDesc": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5,d6823e01-69f0-4f85-b94a-74894356a2cf,d344b8cc-85a8-4ace-9f92-38c84f396e99", + "FriendlyName": "Equestrian Coat (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 81, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4q0530imylzalquyt6zrs4br4.png", + "AvatarItemDesc": "09177621-9ecd-4f6a-b6a5-64490139141d,", + "FriendlyName": "Flat Top Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "09177621-9ecd-4f6a-b6a5-64490139141d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 83, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "63iadlt4uzd2bynj3varsfesy.png", + "AvatarItemDesc": "95ab7a7c-c35d-4da5-9955-0921064470b6,", + "FriendlyName": "Gekko Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "95ab7a7c-c35d-4da5-9955-0921064470b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 11, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cwxdnevuziov7r3meknz7vmkr.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 12, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c4pq7xaobf8onr988cn3t6lzb.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,484b6c13-af22-4ad5-8c43-34c0de095d49,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 13, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "338vomh9pt5m5bf07wih64yjo.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 14, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6onf6k881dne4cqk3bid15ryf.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 20, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1sdg06t7cb2h44qy4zmiv7uz8.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,8377ab96-c908-457f-9fee-b784c9a759f3,018a5c07-e956-457d-a540-a5e2cd68da09", + "FriendlyName": "Headband (Red, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 149, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "22i8ypgdvikndkalc1fn6g0be.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,", + "FriendlyName": "Slim Blazer (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 469, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0no2vbmhmulieokvn9ikhxy23.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,cd5d7285-202d-42d0-b93f-04245875793e,0f36bb97-c61b-4281-929f-ff1d0d11be86", + "FriendlyName": "Slim Blazer (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:37.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 468, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "27ztuh73mvhqa7kbriw9y9jjt.png", + "AvatarItemDesc": "62ce4109-8dee-4895-bf1b-bfa143db4c7e,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,0f36bb97-c61b-4281-929f-ff1d0d11be86", + "FriendlyName": "Slim Blazer (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62ce4109-8dee-4895-bf1b-bfa143db4c7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:36.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 91, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "16t3t321doi4ngo5nknn2kxkt.png", + "AvatarItemDesc": "8b9f1413-e786-4a30-946c-9292f207875a,", + "FriendlyName": "Pulp Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b9f1413-e786-4a30-946c-9292f207875a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 124, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "79i106lkaeghior2bmvlgc8kb.png", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,", + "FriendlyName": "Rec Room Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 95, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7taocf1uwgbysywpo7p7fmcsm.png", + "AvatarItemDesc": "1a71064b-794f-40fa-9109-8ad36602b6e1,", + "FriendlyName": "Shagg Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a71064b-794f-40fa-9109-8ad36602b6e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 200, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "enwc99fhlpla2ltqpv7ug4r1z.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,761a3193-60f0-4190-80c7-285b8192e794,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 201, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "de87szlv72hk7tfhds4c7qowj.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,a819f49b-6c7a-49d3-9e6a-d9d79ef5019f,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 199, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dvdbsnev23t30jm3v75yrn25a.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,64850553-cdfe-455a-ac00-dafbe63d613e,91a451c1-b285-4c48-b14d-59ded8cc006f", + "FriendlyName": "Stoll Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 198, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dqkxn82hm6chx9ysdnrcfvj60.png", + "AvatarItemDesc": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a,", + "FriendlyName": "Stoll Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84cd594c-1cd8-4b4d-8409-85c8fd5fb02a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 297, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9x6rq5lgtswz7z6pcm6ccptiy.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wrist Tape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 296, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6w33yvf6g7oo4h3fayjgbs5gj.png", + "AvatarItemDesc": "71921831-ba6f-408b-a00e-2fd97663636f,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wrist Tape (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71921831-ba6f-408b-a00e-2fd97663636f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 288, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "beff1xr2vo6gnh2vgobzx0hrw.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 292, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "el05p2p32cqdh78wlg2aq7vhl.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,7d8e55fe-3c34-4b4b-9753-0021f6cc6454,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 131, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "74jrfu3nny1joqt8ee5vlxpok.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,", + "FriendlyName": "Long Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1014, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cpcymryww9ol7mfcdpyp63o47.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,0iSsaY-HgkmLaRHCn5vEdw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:41.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1012, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "60nyc11l4xym89g52ynudx3bm.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,jGj28vhq8EGwP2RuM074aQ,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:38.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1011, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0f3qdah3r9o4qv7cqk39g3l7s.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,kmj5zOjcwku_WWKroCeiVQ,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:37.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1015, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4pvbqpmw9ghysmvyr445pupr1.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,FAviMCQ_EE2Mpt6QPo5OEw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:42.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1013, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "13r4rms2vr6r67kp1gm17g388.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,MFrcSQ1DYUm8imvy4ypgvw,PioQ0o3yP0a6szPZ4EKs2A", + "FriendlyName": "Collared Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:40.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 144, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7si3m7no7ulqg9he671mhowgx.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,05ac07e1-67f0-486c-abf5-a62866475abb,be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54", + "FriendlyName": "Blazer (Black, Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 620, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "am6f16pg9zk7wcjm1ks7meivl.png", + "AvatarItemDesc": "0088603e-ec3b-4478-8694-e6fb1989b3f2,", + "FriendlyName": "Angled Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0088603e-ec3b-4478-8694-e6fb1989b3f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-22T19:07:25.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 621, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "91fafagogc6ouzeyn500okchs.png", + "AvatarItemDesc": "ffea7a65-613f-4835-921e-6dd15f357b7e,", + "FriendlyName": "Long Bangs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffea7a65-613f-4835-921e-6dd15f357b7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-22T19:07:26.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 112, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "acy5d2ixrgg8d4qlb09csvhm4.png", + "AvatarItemDesc": "45f5e714-8a5f-4385-a97f-675066167011,", + "FriendlyName": "Seventies Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45f5e714-8a5f-4385-a97f-675066167011", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 361, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1eb87x8cqfkan1tpf4fnxe6e5.png", + "AvatarItemDesc": "9bf5d259-7774-4cbe-a90f-7f188cc0dce7,", + "FriendlyName": "Thick Goatee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf5d259-7774-4cbe-a90f-7f188cc0dce7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:01.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 726, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3pvimigcs9imx8k1sd5juxlf5.png", + "AvatarItemDesc": "a6cbfe76-534a-4655-a8a8-3fed13d001c7,", + "FriendlyName": "Bald Top Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6cbfe76-534a-4655-a8a8-3fed13d001c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T01:29:49.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 727, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6eylc335jw4t1874pjzcwpudy.png", + "AvatarItemDesc": "CTcrvbo3OEepIV4oW8bx4w,", + "FriendlyName": "Receding Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd2b3709-37ba-4738-a921-5e285bc6f1e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T19:25:02.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 728, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bvkq9o8mojgbtpcokr90wuxiv.png", + "AvatarItemDesc": "-twtjyBdQ02EAdOfBGTiEw,", + "FriendlyName": "Van Dyke Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8f2ddcfa-5d20-4d43-8401-d39f0464e213", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-10T22:56:15.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1199, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6xn7zsaqaimo5w5vryw7cyaxt.png", + "AvatarItemDesc": "45eaab67-19c2-4601-8f80-3565a4dceba4,", + "FriendlyName": "Pompadour Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45eaab67-19c2-4601-8f80-3565a4dceba4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:36.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1200, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dpd9st8mzzq7fb7gyrsby9mnh.png", + "AvatarItemDesc": "c855dcc3-96cb-470d-b159-d37a025a47d1,", + "FriendlyName": "Dutch Braid Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c855dcc3-96cb-470d-b159-d37a025a47d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:41.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1477, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3ybnk0r2sa9x5nfulb4n5hfua.png", + "AvatarItemDesc": "d7730a9e-78a1-4356-bc09-6b066615850b,", + "FriendlyName": "Afro Updo Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7730a9e-78a1-4356-bc09-6b066615850b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:09.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1478, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "256bejo1cjwehqhza4ysng9ho.png", + "AvatarItemDesc": "8c35c804-e8d5-49d2-8d5a-ea19fb70bfa6,", + "FriendlyName": "Pencil Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c35c804-e8d5-49d2-8d5a-ea19fb70bfa6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:13.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1480, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eezddupufyjfaxdd5lstfz5gn.png", + "AvatarItemDesc": "5beeb4c4-f276-4eae-87aa-9302e45b05b7,", + "FriendlyName": "Cornrows Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5beeb4c4-f276-4eae-87aa-9302e45b05b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:21.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1457, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dhqnvyh6gae90iy240h1b07t5.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,sxUE0iOSZEmezm54T7xI3Q,tlpa7195x0CkmSjpR1RArQ", + "FriendlyName": "Rec Room Hoodie - Pride (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:18.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1459, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0c78877wqm87gczt3yy70z08e.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,", + "FriendlyName": "Winged Hat - Pride (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:42.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1458, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9lq1a36dbi2qvm0v5g0dci5ne.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,D_Xmo0rOzkS-kgq1CYXt3g,tnCJp2eDI0SwjVfJMhk3LQ", + "FriendlyName": "Rec Room Hoodie - Pride (Trans Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:24.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1460, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7ulpsllq59oaufnjyuw78wn2q.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,knXPidb-Rkayfc3kSHfZeQ,1yMyo6oTjU-VAygoeWaohQ", + "FriendlyName": "Winged Hat - Pride (Trans Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-03T01:07:45.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1621, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2h1h9p8vlyjw7l757o4oin73b.png", + "AvatarItemDesc": "88b6ddeb-a455-460d-91d9-a4569ef6903c,", + "FriendlyName": "Square Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88b6ddeb-a455-460d-91d9-a4569ef6903c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:15.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1620, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3xh3iq78mo5epeepsiol6ghz1.png", + "AvatarItemDesc": "0abb6b08-20ce-444f-879e-0d1344df096c,", + "FriendlyName": "Round Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0abb6b08-20ce-444f-879e-0d1344df096c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:12.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1619, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1jnggaqfe4t82pv8okgvje9e7.png", + "AvatarItemDesc": "9b5bde11-7408-4798-9fcb-c7ec175444df,", + "FriendlyName": "Hoop Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5bde11-7408-4798-9fcb-c7ec175444df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:09.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1742, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4wuzoct4aayihilzg800scbtb.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,", + "FriendlyName": "Leather Necklace (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:33.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1745, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ctkuncvuk0nur9iwwe48eo2ed.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,gsHBUqA1gE2XNTPA3VvBLQ,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:41.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1744, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8umo8jp69be1v9log6wxdhkct.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,JUQn0_PLIkytCrkAg0FflQ,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:38.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1740, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "byk3riscj527wx5q6aq2u1h1y.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,", + "FriendlyName": "Basic Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:20.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1738, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3bofvoeb5yy5y86dljqn9nqyw.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,dBR1uNgk206eVoWntVzVAg,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:14.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1737, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3g3ewvd49267e67sqmpy9df0v.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,zPGCmxM_b0yTt7tqMaomcw,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:10.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1772, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2jsxx5bos2r2jual185q0pgcu.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,", + "FriendlyName": "Pocket Tee (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:07.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1779, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9d8ocr6t26ofb3frswgln8ghp.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,El0s9F6cfkywcKOkqwbJtQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:31.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1775, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9r42wuf4n5jgtidnopmbsho11.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,N0fBxjrCekO-HUqmc3MufQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:24.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1774, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "97wov16x2whj90451paeq5wr9.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,J9cK9BKtHUinG-DwAoPi3g,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Dreen)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:22.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1773, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aj0epqqoal6o3lrbfmlvdjvrw.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,-VF3K4miGE28LxMgqVehRQ,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:21.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1778, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1hb9mgxu2zrau3k0gdj4psvsv.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,pQ_3mZI7yEaxMw9n_Fr4Sg,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:30.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1777, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "avz1q0x4dp2ujj3k9yzfvcufz.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,TpFWTdEW2kqtpvc0sPv-dw,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:28.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1780, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2q0u4aonxjqnyx4hulnj3xvcj.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,vxZVeH2b_E6XlqyqCW0hBA,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:33.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1776, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0u6tuseoo4aelelolic3o693u.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,oNoEl-nDqk-HLsfz1538oA,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:26.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1781, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9yt8aczo24n68e4zolsg3k0cs.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,Aex0Zh1RQ0u9oTDFF1cFIg,_o3mI1mjE02vekPVyJg8Aw", + "FriendlyName": "Pocket Tee (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:35.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1757, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8z6u9864bufzxe9myyoisnl28.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,DIeasJUIj0KNetAzPzuIrg,o3Q2gADhh06dgaipkkdMsA", + "FriendlyName": "Rec Room Hoodie - Pride (Bi Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:15.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1756, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "aa9c59vzcixu27kaoazqzpbsd.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,G6Udm6kMdE28vmRY5KPanA,Fw79pqDPP0C8H07AnTY-Vg", + "FriendlyName": "Rec Room Hoodie - Pride (Lesbian Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:11.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1771, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7px3ud7g3dce8g0qtfmbzy9ar.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,3a1oW3_MY0e4lDN2V1X4ig,za8Mz14Ww0KtC6wavUPXdg", + "FriendlyName": "Rec Room Hoodie - Pride (Nonbinary Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:59.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1791, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "arzqgzmg36st1ri623d76xhhx.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,sf962XJ0tkis0svkMkZCXQ,4bI7QRvdnEansPkwx8Di9A", + "FriendlyName": "Rec Room Hoodie - Pride (Pan Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-08T01:17:55.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1770, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5xxg31ft29j91d3n4nxdkw270.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,tm7Cnd3LzkqWHA1JMrgoeA,nbLr3c3lOkiWhNg9IYbt6Q", + "FriendlyName": "Rec Room Hoodie - Pride (Ace Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:50.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1970, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3jkufkhi2w8i2n3lsvlvf65c1.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,t2fOeVWloUOKDlIaf4Sxnw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:21.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1971, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2izsye42u9sx691sx6b1bpdil.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,3JrW3FFUvEe6HZ9VjOVjMw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:24.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1972, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "955r71z2z3dwqtuuo4h7wtm00.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,OT5APcpbpEiMNNah8n4Txw,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:26.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1973, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bm3lgqlrymyw0u7z8sipii563.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,NjCLn5A5h0-V8jB72etCNQ,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:29.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1974, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bbonh2ov1akhkcgu49ws63rse.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,jeEWPFvzaEGmXaUrxTFvwg,ynPJe7asTk-x_GtcEBQbgA", + "FriendlyName": "Sweat Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:20:35.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1984, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "79dqcbiedmmieho50ebbbddj9.png", + "AvatarItemDesc": "edc7888c-dc42-48ab-8815-884d227760af,", + "FriendlyName": "Short Locs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edc7888c-dc42-48ab-8815-884d227760af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:28.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1978, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2qqensahxq2o8r4avbpt9zoo7.png", + "AvatarItemDesc": "6e48363d-cc62-40a8-87f9-a69fae90c002,", + "FriendlyName": "Box Braid Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6e48363d-cc62-40a8-87f9-a69fae90c002", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:09.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1979, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d45c0mzfbqha85oe0312hkatw.png", + "AvatarItemDesc": "0d1ae26e-aaa4-4914-ad70-c9ab8cfabc61,", + "FriendlyName": "Box Braids with Cuff Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d1ae26e-aaa4-4914-ad70-c9ab8cfabc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:10.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1980, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6vl2aov7qwvmkpf2wux0rctq6.png", + "AvatarItemDesc": "04e348b7-107f-47b4-9331-ae3b9fa184ca,", + "FriendlyName": "Braided Bun Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "04e348b7-107f-47b4-9331-ae3b9fa184ca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:18.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1986, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bathop4rdgdsysv3jg6wi2uua.png", + "AvatarItemDesc": "ec474314-fd79-441d-852b-d8999274fe57,", + "FriendlyName": "Wavy Middle Part Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec474314-fd79-441d-852b-d8999274fe57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:43.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1982, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "abs1vh2rd21swanjh7vd6hi3h.png", + "AvatarItemDesc": "36b2ccf4-4f42-4dc4-aeaf-f3d10b2a6d30,", + "FriendlyName": "Etched Fade Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "36b2ccf4-4f42-4dc4-aeaf-f3d10b2a6d30", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:23.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1983, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "252m5s6mamg4ajchjb98l8me6.png", + "AvatarItemDesc": "37f7b94a-92ba-4f5e-b94c-de00acfa4722,", + "FriendlyName": "Etched Flattop Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37f7b94a-92ba-4f5e-b94c-de00acfa4722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:26.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1977, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1qr2ehkeevot7hvkonb7ygtv6.png", + "AvatarItemDesc": "4eaaad39-aa44-4791-8b67-2eafa8305850,", + "FriendlyName": "Afro Puffs Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4eaaad39-aa44-4791-8b67-2eafa8305850", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:05.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1985, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dzytj60lnm9n8bstqbvcrikss.png", + "AvatarItemDesc": "9bf75b23-84e9-436b-bbb8-4180ccd86b94,", + "FriendlyName": "Wolf Cut Hair)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf75b23-84e9-436b-bbb8-4180ccd86b94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:40.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1981, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e9zj12umhuny6tnh1cdk7tu47.png", + "AvatarItemDesc": "2822aa50-4344-434d-b46f-fd9443f865b9,", + "FriendlyName": "Curly Bob Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2822aa50-4344-434d-b46f-fd9443f865b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:20.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1987, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2fujgcfkoqu654pjz73tner1m.png", + "AvatarItemDesc": "9c20fdd5-acf1-426c-b85a-2220ceee9472,", + "FriendlyName": "Short Curly Twists Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c20fdd5-acf1-426c-b85a-2220ceee9472", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-04T17:04:47.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2059, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mNgon98gpkamzdOKrEwBhQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,PIwziHk1BUuYkAf6kuK6wA", + "FriendlyName": "Rec Room Hoodie - BHM (Black History Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:52.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2192, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3qt9ioRkZ0ClPXKVL2YMvw.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,PRLBrEEZhUumm3VnYVJP6Q", + "FriendlyName": "Rec Room Hoodie - Pride (Aromantic Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:26.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2193, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "pJQjmPlV80Kjht3dBWJjEw.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Xt3Ndm8mC0qsLClPIbgwUw", + "FriendlyName": "Rec Room Hoodie - Pride (Genderfluid Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:29.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2194, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "PO7jrliur0CVizm0SH3TbA.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,MfF0wizDjUa7RUE-AVJpvA", + "FriendlyName": "Rec Room Hoodie - Pride (Intersex Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:33.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2232, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7Z-IZ-6LvUypmMCX8IwHSQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,LovycnPHoUiP_N9w7BzsnA", + "FriendlyName": "Rec Room Hoodie (Juneteenth)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:08.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2337, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "Exs987LYAUOtBgHMuw7Rqw.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,", + "FriendlyName": "Head Scarf (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:08.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2338, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5L_MOzszIUOYUpjCKDoaog.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,B3jdXiOJs0WO2ytm3-hXPg", + "FriendlyName": "Head Scarf (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:11.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2339, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bymnCdtwu0SDAsat4NNB4g.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,VudVdOAjZUibHmtFeNkvSw", + "FriendlyName": "Head Scarf (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:13.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2340, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "lEfY-3T1Lk6G8rEL0ZkSOg.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,5z3jZhYvq0CGmdJ9IcUUsg", + "FriendlyName": "Head Scarf (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:15.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2341, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "J-3USpg66EG6TP6A0OF22g.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,_T4zEki_wk-UIPdhSaBr_Q", + "FriendlyName": "Head Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:17.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2359, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "O9Rxhi7Xp0uMsUE3NL64uQ.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,", + "FriendlyName": "Head Wrap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:55.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2360, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yEzjZ8Usm0289gIRWJ6TqA.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,nPenrNSTE0mDK5cWhxtEug", + "FriendlyName": "Head Wrap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:57.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2361, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mcJSwkCUbUSoHOpUPPSxcw.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,a0A1q_e1q06qquiu2KaZhA", + "FriendlyName": "Head Wrap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:59.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2362, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "TAGSVr0Y6kmZcsvKeNOdxw.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,rwESUxIrQEKA2hdQpcaPaA", + "FriendlyName": "Head Wrap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:01.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2363, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "vlAp24nn10uyws8TDY5Rug.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,M_BhFve7SkC7IJoymLiCLg", + "FriendlyName": "Head Wrap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:04.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2364, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ZBnmV3ahdUa6yCVkJ16fGQ.png", + "AvatarItemDesc": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d,0zldWzi4H0OQwsWPt6gl-w", + "FriendlyName": "Head Wrap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0e6fe4d-991d-4bd1-a1dc-ac4c7375fa4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:34:06.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2342, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "OfSv7-HEQEudAjfpEbowJA.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,EgEoJzG7A0aJ92qfInbX7g", + "FriendlyName": "Head Scarf (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:20.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2343, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "L9nYH9XNvkGX3ZD8c3UfLw.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,OlfQDuz1NUOr88AL2n0jXg", + "FriendlyName": "Head Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:23.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2344, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dBD0grhWnUavKcZsyyx1FQ.png", + "AvatarItemDesc": "071f179a-dde2-407f-92b2-f0c16fcb4e39,NETGR8iysUyxcZM_oGmrwQ", + "FriendlyName": "Head Scarf (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "071f179a-dde2-407f-92b2-f0c16fcb4e39", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:25.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2328, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "EcBeEDeJN0a4mHWBsi4Npg.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,", + "FriendlyName": "Brimless Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:49.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2329, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Eu1IKmX_QkyptaNvKNG26w.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,Rh-RaEHap0e858QeTtCqJg", + "FriendlyName": "Brimless Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:51.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2330, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rUQ5YQ1ymk6xtBaPHA9obw.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,hgLq0WyUAUSl2sboUrpTKQ", + "FriendlyName": "Brimless Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:53.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2331, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "BqkhCnPdjkqPD8KI_tvDBg.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,M5uFQQpoQE-5CKMJnDv_LA", + "FriendlyName": "Brimless Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:55.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2332, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "mWIwHLWbnE2ZVzfBK3Y4CQ.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,JzLPlVNG20G9mxG_4PF8Fg", + "FriendlyName": "Brimless Cap (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:57.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2333, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "jfoJFkvJhEaInI7ytPqonA.png", + "AvatarItemDesc": "9db35412-7a08-49dd-b80d-59c34a4616ea,VuMDnpnK50mzgUMwJ4F6Jw", + "FriendlyName": "Brimless Cap (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9db35412-7a08-49dd-b80d-59c34a4616ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:32:59.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2334, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3PQQmjlqZE2pSQnjsADLfw.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,", + "FriendlyName": "Headdress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:00.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2335, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hgq25v2vbk6EY3TPtz8O8A.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,gLCis5TFxEyRvvZ0PZq_Jw", + "FriendlyName": "Headdress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:02.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2336, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ad1rqlw7oUiEHiwbXRJHuQ.png", + "AvatarItemDesc": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3,1WBsT99QM0anps6ZoWE59Q", + "FriendlyName": "Headdress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "feb8b50b-d3da-4e45-b4d0-69aeeb5cfeb3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:05.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2352, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "taLvR9OtMkuwOHCj9sa8Yw.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,", + "FriendlyName": "Head Scarf Twist (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:39.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2353, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "aTq6lg1RF0KsrotD8g8PVA.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,jcG-vMduWEq14vo2sKYdWw", + "FriendlyName": "Head Scarf Twist (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:42.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2354, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "vkUv2dsvVEqj7J_oudnCnQ.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,s-QTVT5eTECKHL9FnHFCAQ", + "FriendlyName": "Head Scarf Twist (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:44.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2355, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "n71xleEsHEKKy_YJgkOBbQ.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,irw2uj4c2Eil0ndPCTqodQ", + "FriendlyName": "Head Scarf Twist (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:46.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2356, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9EfM-NBnuUud7oDeXW_t_w.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,UnHAtXkd2kCdamzw6B3Mvg", + "FriendlyName": "Head Scarf Twist (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:48.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2357, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8mn7hHezlUePzyx-HN48rg.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,sO8e2epLrUud243Xi9odXg", + "FriendlyName": "Head Scarf Twist (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:50.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2358, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "DSuLZADyxUa5EzpsYky2cg.png", + "AvatarItemDesc": "c414588b-29b3-4a1c-ae9e-7ad251901429,pPZ3sDw3B06mKiYsAiAt9w", + "FriendlyName": "Head Scarf Twist (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c414588b-29b3-4a1c-ae9e-7ad251901429", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:53.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2345, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "j8rw1jLEy0qFDD5N24UOaw.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,", + "FriendlyName": "Head Scarf Halo (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:27.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2346, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e0Y4CS-9IU2km0B3jwPCtg.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,3cPbhpDX-EqDQy6eN1E5Ug", + "FriendlyName": "Head Scarf Halo (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:29.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2347, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "VH7wAcdrW0aDxhWBl3qs3Q.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,iCVglqDJrkKB36za9UiUxg", + "FriendlyName": "Head Scarf Halo (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:31.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2348, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9ftPEgJCtkmrO_fAsxmXxw.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,gEuE-X67b0-q5C0Tv11_nA", + "FriendlyName": "Head Scarf Halo (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:32.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2349, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "UD9QOouEvE2Msgsa8r4Qng.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,gk_mQ-jsakmxhZYYNlRl_w", + "FriendlyName": "Head Scarf Halo (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:34.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2350, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "kHKrIJ-sKU-U7fZBCFu4rg.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,0p_eXn2gXUKrshSEmrQx6Q", + "FriendlyName": "Head Scarf Halo (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:36.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2351, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rcFCLdci0EKcU2Y-zY7EuQ.png", + "AvatarItemDesc": "22a857bb-270a-4278-adbd-2df41181b36f,fAmEEm6ziUO2ZXTscQWtSg", + "FriendlyName": "Head Scarf Halo (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22a857bb-270a-4278-adbd-2df41181b36f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T20:33:38.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2732, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "7Seg-2HnmEuiyQanSBov5A.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,NI8a4j9fl0egSrLbrMTn7Q", + "FriendlyName": "Rabbit Ears (Lunar New Year)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:24:58.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2824, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "iegw0q7VnkCblb2xmJZiRQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,kBaeiw5WG0OHOIsric74Ag", + "FriendlyName": "Rec Room Hoodie - WHM (Women's History Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:58.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262140308, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "k4xHpFAnkEm0feaYPcjMzQ.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,EkA09Hj2m0u3c5STynn3Bw", + "FriendlyName": "Rec Room Hoodie - ANAM (Autism & Neurodiversity Awareness Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.490779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2875, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4lvbe8qhhnk5qhf9gs7iglqbj.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,", + "FriendlyName": "Hearing Aids (BTE - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:42.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2877, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8v81gej0s8m49iztzzyb4eicc.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,xn8Or1kV-EGhpOqnYj6_0A", + "FriendlyName": "Hearing Aids (BTE - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:46.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2876, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b84s2wgn3n5fz4wcz7jmslcez.png", + "AvatarItemDesc": "ea254491-116c-41c4-a9ca-1fa1fca76374,1n-ekJos-EeViiz_zG4jEg", + "FriendlyName": "Hearing Aids (BTE - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ea254491-116c-41c4-a9ca-1fa1fca76374", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:44.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2878, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4hydnfkwj5yum1oq7pdpajh6t.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,", + "FriendlyName": "Hearing Aids (BTE Earmold - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:48.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2880, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9ejntda7ab26w71takrqy1wuc.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,aEsXeTI_SEOclbm82n17tg", + "FriendlyName": "Hearing Aids (BTE Earmold - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:52.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2879, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5kr6spnm1wjdvlki1808o40y6.png", + "AvatarItemDesc": "2e7cbdb0-cf22-4938-964f-acca57d221b7,pZDvYABTN06KdJ5Ck60XLA", + "FriendlyName": "Hearing Aids (BTE Earmold - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e7cbdb0-cf22-4938-964f-acca57d221b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:50.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2881, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6g30dvwaavnpkdqlzdb6czfja.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,", + "FriendlyName": "Hearing Aids (Cochlear Implant - Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:54.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2883, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bmepd5uq48h51oc7xxq9kepf2.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,7ZRNVpxhWk-j8R3oj0oVXg", + "FriendlyName": "Hearing Aids (Cochlear Implant - Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:58.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2882, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3vu86qw9p3ikkjm0hx774naxc.png", + "AvatarItemDesc": "b8e485d4-b5c2-480e-908c-d9b500375759,8WJI7RAuaEy4KvQiQhoKBw", + "FriendlyName": "Hearing Aids (Cochlear Implant - Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b8e485d4-b5c2-480e-908c-d9b500375759", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:56.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "6d55261b-5434-49d4-8057-f4c748f1a238,", + "FriendlyName": "Full Body Clothes Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d55261b-5434-49d4-8057-f4c748f1a238", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1968384766 + }, + { + "AvatarItemDesc": "6f47500a-92d2-42d4-8dfa-42a815c43c02,", + "FriendlyName": "Full Body Clothes Sneakers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f47500a-92d2-42d4-8dfa-42a815c43c02", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1551964329 + }, + { + "AvatarItemDesc": "ff89985c-db80-45f6-9c49-9b7478c38ae0,", + "FriendlyName": "Full Body Clothes Long Pants", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff89985c-db80-45f6-9c49-9b7478c38ae0", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1647504812 + }, + { + "AvatarItemDesc": "8c3885a1-b1b5-407e-98e3-4d909724b6c6,", + "FriendlyName": "Full Body Lumberjack Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c3885a1-b1b5-407e-98e3-4d909724b6c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1416203662 + }, + { + "AvatarItemId": 1275, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eauoml21rxj3iqwoccpn04y5p.png", + "AvatarItemDesc": "2c3773a5-a0b4-41c5-a4df-6572dff516c9,", + "FriendlyName": "2020 Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c3773a5-a0b4-41c5-a4df-6572dff516c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:18.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 967, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0mvz1timijk659hshhzec8jtt.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,", + "FriendlyName": "Swing Dress (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:13:02.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1255, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "holidays", + "ThumbnailImage": "3rls67cl0dxudh9kj7rqgqzlp.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,PEGqjoxP6E2yRx_A-HphrQ,I_OF1c0YWESMwxJr9sXYOA", + "FriendlyName": "Swing Dress (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:48.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1353, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6zl0lixajd47uo5t8r26lce00.png", + "AvatarItemDesc": "FHKZvPFMLkO8iAIPPQzu1A,K17dIz_TRUq5D50NBfLjyw,3drRcfyKkku0qGwCy16m9Q", + "FriendlyName": "Swing Dress (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc997214-4cf1-432e-bc88-020f3d0ceed4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:35.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 757, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1ofk56iwklqug6u3ihb27rfex.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,", + "FriendlyName": "Swing Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:22.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 807, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aje5q2u76zjjzgeehfysg6dik.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,BLgkgLeS-EyazgPQxGcavg,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-10T19:02:35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 790, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "clsb49us68oqy8d5uyq5q31r0.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,ZvPidQ57rUS50Qz89ERmLw,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Halloween)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:31.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 797, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7wagu7eu1akvlpi9036qjkgkx.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,eb3SmJTIh0628LrZUkI4KQ,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 789, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3rko5wknqf4g6gbl016rfci55.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,Yr0mF1EWzEaIyRMOWFiY6w,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:27.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 788, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6sddopu5d9s5sghb2jjycwa4u.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,QzjakcO8tUaPeiI5KBLKtg,Rk18_7icF0Srgh7M1_G-hA", + "FriendlyName": "Swing Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2777, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MGp8JbVCX0OTgJwakQwccA.png", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,NpvUosaJTECohTIAR38egg", + "FriendlyName": "Ladybug Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:36.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TzqM7z7igEq2-tMG1UDu0Q.png", + "AvatarItemDesc": "4b51679e-96e4-4a11-96fe-1d2754d12481,", + "FriendlyName": "Clock Chain (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b51679e-96e4-4a11-96fe-1d2754d12481", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:03.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BktGoS3OckmmBQXz83eY4g.png", + "AvatarItemDesc": "383d4bb7-1ffc-4b40-9b7e-cfeaf9ac5fdd,", + "FriendlyName": "Thick Shades (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "383d4bb7-1ffc-4b40-9b7e-cfeaf9ac5fdd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:05.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2768, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "q3OFxpl3vkyZ2Y6zGTfobg.png", + "AvatarItemDesc": "3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686,", + "FriendlyName": "Retro Layered Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:07.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 755, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8p01g7rfkflo0g5se24b17jny.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,", + "FriendlyName": "Shutter Shades (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:19.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 913, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1md3c8azai7phcdzdwuu83inx.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,ZpcM5H2Q1UCt0hh82nSu9w,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:47.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bo52eq6h1gjw33zb6kj234swy.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,7B7vpnMTfkGLcMdHCZ_kWA,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:46.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ajtgswd058zwm4k13wu9gtw62.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,nO-YacKU1kmLjTFAAqNeYQ,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:45.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c9ilqoetseg0h6s3twpchmkfp.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,76CA_KCr806y1FA35m-YTg,tqyzKPdDwkWq3jfZ8jZdaA", + "FriendlyName": "Shutter Shades (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:43.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1811, + "AvatarItemType": 0, + "Tooltip": "For earning $1000", + "Rarity": 50, + "ThumbnailImage": "3bgeqvxk1ut8bz0ipoy471x1b.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,u_H8YUV4MEGNGh5h9ckm_w,a3OlD7iI_EqurYq_RIQCvw", + "FriendlyName": "Shutter Shades (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:55.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2074, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DePlu75lck-gLWofC8uKmg.png", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,1DQi3Wt8ukK2gWvl0zJnkw", + "FriendlyName": "Shutter Shades (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6f6def6e-bbe2-4834-a872-f658de8eee93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:01:38.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1159, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "daw2bnn661kjxraiwzbj4o13s.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,", + "FriendlyName": "Stunt Singlet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:50.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1158, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0q2tmuvx7l3fxuhgiq8vaftru.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,6t2rCb6WWEW6pJbcjsAFCw,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:46.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1157, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4u9h5rvuhapi02hegh14s9zet.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,zu0Jk24WTkan6XWSwGU1Uw,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:43.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1156, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9jul59tnxdwa6w6bjib681az8.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,i1XbjBqTu0CILIuJClWimA,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:40.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1155, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tpxkoq49mfgt3w5y2vsjnqdm.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,3frH6KxZ9kGUYkjeCDpylQ,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:36.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1154, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "92q8bqs03d33p72ljcvn79lpe.png", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,Lf4FjROSAEqXedFxpXFQfg,B-sIpWNOfEigMeQF-WVR1g", + "FriendlyName": "Stunt Singlet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17236d76-e3b6-42f6-9179-6fcf0479436f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:32.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 758, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6qo0q0lqcx2crqugl2xmhnz4d.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,", + "FriendlyName": "Windbreaker (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:23.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 896, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5rgodebtv8essp0sxgveqp7fd.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,grutFwJD_0Cb3t6fddNg6w,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:25.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 895, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "37ag45u7zn3cqqya03wz5nmgw.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,EbVDqEJQOk6j0UvX3t-8rQ,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:23.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 894, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0kifl6w66mqa2wbhliszoe52l.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,C3ll7uPfUkeZ1QLX80_BpQ,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:22.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 893, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "au0r6ubbl0fyr6xi7sqch0b59.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,JS7aa4QSUUOpUYPqRZEb5g,oSaMpc5rTE-U6yhjtPWQgg", + "FriendlyName": "Windbreaker (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:20.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2065, + "AvatarItemType": 0, + "Tooltip": "Breakdance inspired colorway", + "Rarity": 10, + "ThumbnailImage": "5aqX6fFXKkulI-FwAVm9Kw.png", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,rwojzpr0E0ylG6_Vv_MVaQ", + "FriendlyName": "Windbreaker (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d5458a00-1e05-4531-b2b6-91d804304092", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:18.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 761, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8v6i3ja1btg3mkpog657iixa1.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,", + "FriendlyName": "Windbreaker Cuffs (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:26.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 862, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0xnunm0kmgc2va3zevhooe9ya.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,HncURj5yi0ykk2cVyCZ4Sw,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:15.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 861, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9qf35u5xai7bpf8k7d518b9gz.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,42ZNGHXMvU29yo9s9tVpOQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:13.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 860, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8mq00l9l162o9unwk33egvmsd.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,VDo682NvSUa8xYn8zTX-AQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:02.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 859, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7ndz1tjworge5u01d8cvf815f.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,FvEjo9let0e_Sr-kWq5AvQ,MV1HW2x43kG4rrhyoC6mmw", + "FriendlyName": "Windbreaker Cuffs (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:00.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2071, + "AvatarItemType": 0, + "Tooltip": "Breakdance inspired colorway", + "Rarity": 10, + "ThumbnailImage": "HQaEvdJ2yEKBpIlsj1ChiA.png", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,6c-AhDM6K0iHbNMf4BNiUg", + "FriendlyName": "Windbreaker Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "236c65e7-4af9-4496-9400-e2657c39f2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:31.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2217, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "sVLupklIaUKmgYNatMdcSg.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,", + "FriendlyName": "Acoustic Guitar (Wood)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:02.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2243, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "ElDAaYT06kqpFifv_brl_Q.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,O38XfZ3gh0-ofqu36y3mAg", + "FriendlyName": "Acoustic Guitar (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:54.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2242, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "vpkdcMOxAUylrfe2PrxZfg.png", + "AvatarItemDesc": "71afc215-3d18-4f36-a114-f11c453e46d2,BPp9SkUpsE2PZlStZrIDIg", + "FriendlyName": "Acoustic Guitar (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71afc215-3d18-4f36-a114-f11c453e46d2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:51.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2385, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0n2uuqp3f7gj3z8o0ynlzl3et.png", + "AvatarItemDesc": "b07a3964-7a53-4467-aa85-ec19cc27a57b,", + "FriendlyName": "Alien Head Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b07a3964-7a53-4467-aa85-ec19cc27a57b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:26.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5ed0a62a-19b5-42c0-a255-a2e83f063d04,", + "FriendlyName": "Alien Hunter Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5ed0a62a-19b5-42c0-a255-a2e83f063d04", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1733459499 + }, + { + "AvatarItemDesc": "b841c65f-ed5e-4d2a-803f-48939b909100,", + "FriendlyName": "Alien Hunter Glasses Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b841c65f-ed5e-4d2a-803f-48939b909100", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1626620437 + }, + { + "AvatarItemDesc": "8972c6f8-6e11-49dd-92dc-7b12c417fd40,", + "FriendlyName": "Alien Hunter Tie Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8972c6f8-6e11-49dd-92dc-7b12c417fd40", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1145186950 + }, + { + "AvatarItemId": 363, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6i5j2nqmud9i7j69mikjo4wjl.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,", + "FriendlyName": "Angler Shirt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:03.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 403, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7q4pokzc6588ccgqdjft4wov4.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,cac27853-e2df-4d77-b3fd-af3bc0813f01,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:28.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 404, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8gpdgnc9rmf5fzvsp4sio5zla.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:29.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 405, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4qj5sy46am7e176dh6xnr2ejv.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,97938f4c-52b8-4c85-8575-856654666316,cdbe2cc8-4fd7-451f-9b66-0df0e0e7a85e", + "FriendlyName": "Angler Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:30.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2520, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "Joz7a1NRQES_i5VXRjxiKg.png", + "AvatarItemDesc": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc,ugpS7o-ZxkCv7HAvhewZgw", + "FriendlyName": "Angler Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1bfe0b4-1ff6-420f-acfc-2331d34246dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:31.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1804724359 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,mWw1qw6jAEuZ9ygGYsHNew", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1020115116 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,AmKqwhgKAUSHt9axdxe80g", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1611305998 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,-eEKJlUIbkqHtsPJeHz19Q", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1014853776 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,LA7Kx2pBIEu07CnM-XRLtw", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1426268107 + }, + { + "AvatarItemDesc": "b97fc37e-8f10-48e1-b508-a6a346be896d,MocUXmOH70-h3RxBJNhYYA", + "FriendlyName": "Angler Fish Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b97fc37e-8f10-48e1-b508-a6a346be896d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1429871593 + }, + { + "AvatarItemId": 357, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2taoaui5i9giqj24levmdbst6.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,", + "FriendlyName": "Angler Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:54.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 396, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2i2e0tjhgvflcauuva6tqucvx.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,3f6c8bd2-2c1d-4c8c-a9de-16600fee9645,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:17:57.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 397, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6e94bkakcfscxp997xunr45se.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,d7f0ffd3-ccf9-465e-8afe-4d7efb78dd2c,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:20.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 398, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a791dc940zynp12ezpj4sdavg.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,225209dc-3afc-40cf-858e-89f7afa00c0d,efeb07ab-6253-436d-bb66-991ddad72e58", + "FriendlyName": "Angler Hat (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:22.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2515, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "lRJW1xa-LU2CmI1Uvlzzsw.png", + "AvatarItemDesc": "03f8c394-28fa-4087-978b-8d108f0bd969,G7p-E7TJHkSojZtj_5pDXw", + "FriendlyName": "Angler Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03f8c394-28fa-4087-978b-8d108f0bd969", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:19.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1658, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2i9990m2ddrxscxf3csgnpkm5.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,", + "FriendlyName": "Animal Sweater (Ferret)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:38.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1659, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4say0eirgh8p9gunfvf23skx6.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,azbt5pgvrUS4LQwhndFeJg,cHZ4lDnSoUypIXCiRRowzg", + "FriendlyName": "Animal Sweater (Axolotl)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:46.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1795, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "diqbk46zibrfi814wmivw7o97.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,6dzz5ZcGtE6bEjQHxWmo0A,2cSC290cPkCOrW5vtlVUxw", + "FriendlyName": "Animal Sweater (Penguin)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:19.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1796, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1viyefngw9h64p6bq76mhiaym.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,yXVeN9SzQUKE_6u_2KPzgw,sWgRczvpUU6ZHeoMrMD_yw", + "FriendlyName": "Animal Sweater (Toucan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:21.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2402, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "92nW2XOGOE2VbuJ1_fV_2A.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,qzvnj7D1tUuf01HvpiIvqw", + "FriendlyName": "Sweater (Cow Face)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:43.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2748, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wI9h7dAeEUOvsa1ogCNjiw.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,oIVUmOrjjUysnN4XoWvJSA", + "FriendlyName": "Friendship Sweater (Olivia Otter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:14.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2749, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Q5OR2v7ST0mft6WFh5KyAg.png", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,0wRS4Rs4ykqjJE_5Fk-Flg", + "FriendlyName": "Friendship Sweater (Oliver Otter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20eb23e8-ab3b-44fc-8038-3714946c1a09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:18.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1937, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "emruqyjzh6ox3jrozmu8mooyp.png", + "AvatarItemDesc": "4c032271-a0db-47b0-bfc6-4c330c2c4b21,", + "FriendlyName": "Alpaca Lights Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c032271-a0db-47b0-bfc6-4c330c2c4b21", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:36:34.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 960, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "68fe918b1l9gkogciqeizqf0o.png", + "AvatarItemDesc": "AbYdU7O_9U-Lwg7zFVZzXQ,", + "FriendlyName": "Reindeer Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "531db601-bfb3-4ff5-8bc2-0ef31556735d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T03:04:20.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2085, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X0LbhAoDYk-wQI2tMehxJQ.png", + "AvatarItemDesc": "aaed6385-0ba5-45f3-b0d2-6b0a9ab79628,", + "FriendlyName": "Disguise Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aaed6385-0ba5-45f3-b0d2-6b0a9ab79628", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:03.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 33, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d0ct7ztnb49pz8t2575vjicnw.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,", + "FriendlyName": "Archer Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 34, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "665whx3sukltm50bvhdjb35kb.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9b35ae52-febe-486a-8557-ec7190a9756e,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 35, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "85o1rs3o13bqcy2gu8qjvzgij.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 36, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6neueh6asjwdnysb7fxmpi9ep.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,6e152cc7-e93a-4408-8092-236db420b680,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 37, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ilinnf748ep3x9ag9hht0gds.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9ad37173-e640-4b9e-a474-716d482b73d2,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1123, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6jvwgyrx37n8e3913svxb1s0i.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,lgqLGi01o02ZFC9Xs5Ofdg,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:11.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1301, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wxsli15xh05ark51tui5k2b0.png", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,61VZkFjB402brzWm4UsftA,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + "FriendlyName": "Archer Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2d3264b-fbf6-40b5-9f10-c85ec737d112", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:06.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 216, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vybukcx6j4f40n6enwd6d9mh.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,", + "FriendlyName": "Archer Tunic (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 217, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "azaisqtidm1kgo72k6ahsn245.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9b35ae52-febe-486a-8557-ec7190a9756e,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 218, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5ifgufbqvr5jnazfx4cruhixk.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,6e152cc7-e93a-4408-8092-236db420b680,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 219, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "05hq8v4ju1q79njz45lwlti5v.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,5b5281c9-b795-4bb2-ba7b-ba52047ebe3f,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dkhkp10elyre43sbqzangzn0e.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,9ad37173-e640-4b9e-a474-716d482b73d2,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1115, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "71zptzc6ptipah0zjqi4oj5ks.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,lgqLGi01o02ZFC9Xs5Ofdg,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:23.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1311, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c2wrex5zlcm4q7bek6fuavelp.png", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,61VZkFjB402brzWm4UsftA,f88178b8-b204-4fa5-9dfd-6e438bf1e247", + "FriendlyName": "Archer Tunic (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:02.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bstnvjdujs0tgbj3uwztvija9.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,", + "FriendlyName": "Archer Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 262, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "btgunfy4izmpoj8xglwadngpl.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,47da86b9-4e6e-423c-b8c4-ea6bf8472509,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 263, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "883f69uc5v73zvor8ltld7zdx.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,d5987fee-e3cc-4336-853a-5df57f73df05,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 264, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5a2i66dhqnlvimed4a3fjhz2j.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,0c433086-9501-48c4-8d0e-4474eb420c61,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 265, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78f5tat5pkmywligx33rcvv4k.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,2be2f040-68e4-44dd-9099-29f11110f3a4,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1102, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dclo54kfdapgc9n3yba702l93.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,1VGZ1QCvcU6rsKVkUDBNjQ,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:36.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1317, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1v4tdvaof4avz08bp7oapq1nq.png", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,8hUBfmq28EixYjv92Xcrgg,7b187c06-7ba2-4f3c-bca9-fdf98146f385", + "FriendlyName": "Archer Gloves (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:42.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 125, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "32boa62st14xrnw6alwpdqer3.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,", + "FriendlyName": "Archer Quiver (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 126, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1d7kcdxyqd6vbcvooyqhqiqsv.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,d1eb3573-672c-4bb4-9ec5-7c81e6a38d3d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 127, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "65ob47rf57lzttas5qiw96qkt.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,e548124c-d7f3-45de-aaa0-f66130eca576,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 128, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dakgj68r389mpgtl45ly2nsvo.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,7221132e-053a-4c58-b719-ed3c8558907d,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "46p3j6coqok4330sha7pm7whe.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,74b009f4-e128-4c59-8a6a-4c0ba6119bde,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1116, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "88qgugcpk7v2ra2fi4pz1ehjy.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,29EZ6r2SsUakWjzuevxeoA,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:32.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1307, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a87ioalwyvn2xvaj4bhy5mu0n.png", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,imDGL6dhrka-KliYlfpdgw,6094370d-5e28-473a-b422-f3f1c75d5db8", + "FriendlyName": "Archer Quiver (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b6535f0-86cd-417a-bcce-a1ace9a5f260", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:42.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "xIvzfECzA02itIo1Gx1FnQ.png", + "AvatarItemDesc": "c65a94e6-7475-4030-8fcd-9ce25852ff7c,", + "FriendlyName": "Arrow Through Head Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c65a94e6-7475-4030-8fcd-9ce25852ff7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:44.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 364, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "49e7ejka5p8qmdd0wplyjic3s.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,", + "FriendlyName": "Artist Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:04.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 406, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eiioy82z0xbvbvebsm9vamslv.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,8c94e45c-6605-4b4f-9ba4-2c6c6e557228,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "FriendlyName": "Artist Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:32.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 407, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7e9wiybe217zugyt61b0i6ten.png", + "AvatarItemDesc": "02077915-57d3-4e26-b4fe-8e0399cbb293,17b16982-871c-476a-860f-f6953c89ee8c,5a6db6ba-2a0f-4399-b537-dd78db94bf90", + "FriendlyName": "Artist Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02077915-57d3-4e26-b4fe-8e0399cbb293", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:33.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 358, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "buiszvrl96xas7c9f7b7i84ia.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,", + "FriendlyName": "Artist Beret (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:56.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 428, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c8eo8kzis4cfksqzadwx60q7a.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,14b43bf9-5ba0-484e-ae5d-55cfeb09a70d,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "FriendlyName": "Artist Beret (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:55.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 429, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "er774snrrar0qxts7cl1d2zb0.png", + "AvatarItemDesc": "17aed0a0-70ed-49da-ad09-5bc4746f7718,5c8f36c5-07da-49f4-af02-d23207a43216,3485a30d-27f8-450b-b5aa-1ff3cd227d19", + "FriendlyName": "Artist Beret (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17aed0a0-70ed-49da-ad09-5bc4746f7718", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:56.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a95a1b36-7849-4791-89be-c1a56868bfc5,", + "FriendlyName": "Artsy Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a95a1b36-7849-4791-89be-c1a56868bfc5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1538583493 + }, + { + "AvatarItemId": 1849, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "emuh8dfzl0l4dji5yae6lwwde.png", + "AvatarItemDesc": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,", + "FriendlyName": "Ascot", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:14.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1,ffUVovxlWUCgOxJyUJscIA", + "FriendlyName": "Ascot Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfe276b9-1bdf-4c41-9e86-1a0215c6c5a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1091914702 + }, + { + "AvatarItemId": 134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "60g0w2ayv9lwn8fokpqhccd0m.png", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,", + "FriendlyName": "Astronaut Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4pm9zzbb13116s8257iggug13.png", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,9GhnX4Pj4E-wxbPlD8GB-g,YSrGwXybGEqaqHNCS64qOA", + "FriendlyName": "Astronaut Suit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:30.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1938, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6z6iwsstjeii7vhx905bsn4tj.png", + "AvatarItemDesc": "ae7c9d1f-d371-4dfe-a6df-d74662606aae,", + "FriendlyName": "Stranded Astronaut Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae7c9d1f-d371-4dfe-a6df-d74662606aae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:36:40.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ae7c9d1f-d371-4dfe-a6df-d74662606aae,i9rUo62Tgk-UaM9BsYRGgQ", + "FriendlyName": "Astronaut Stranded Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae7c9d1f-d371-4dfe-a6df-d74662606aae", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1162904659 + }, + { + "AvatarItemId": 266, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h8xjhjeecs5u49k0pqh1txaz.png", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,", + "FriendlyName": "Astronaut Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 801, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ayc9cl76c3xmxyeui2n4vtju.png", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,y_gTbnuYS0GKJmnCs2retw,hjk9OO8CW0KNGR7A6_p84A", + "FriendlyName": "Astronaut Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:43.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,LqBO6GM7F0mGswRqJqCbtg", + "FriendlyName": "Astronaut Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88739d79-aeb9-471a-b25b-776f46bba9f6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1310840186 + }, + { + "AvatarItemId": 38, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3xwp4xtloierefgfubh97k778.png", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,", + "FriendlyName": "Astronaut Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f218e2de-46fd-45b9-9e0e-670b6bb905e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ovfqg649aj942uo3a5rqkbni.png", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,JFalLDUvm0aMm3wh1UVfVg,iQjkS1xYP0S63ltvKpkz5g", + "FriendlyName": "Astronaut Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f218e2de-46fd-45b9-9e0e-670b6bb905e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:47.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "66bfec71-882b-4981-9d02-9c03d8d79789,", + "FriendlyName": "Astronaut Helmet Hat Sglitch", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66bfec71-882b-4981-9d02-9c03d8d79789", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1672693583 + }, + { + "AvatarItemId": 1933, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0w9bwz7zburoq47xylvojlem9.png", + "AvatarItemDesc": "eacebd1a-070e-4733-a6b6-65811ef7891a,", + "FriendlyName": "Stranded Astronaut Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eacebd1a-070e-4733-a6b6-65811ef7891a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:34:19.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2063, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "hItB5Kz2jkyqLdN93apHFQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,", + "FriendlyName": "Women's Athleisure Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:13.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2769, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wKJw4wkhlE2Io6Asemls-Q.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,QF05cIuqhEeY8oNnO9CocA", + "FriendlyName": "Floral Athleisure Crop Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:10.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2770, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Lt9m7wE1U0-91--MzopOoQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,FbedbSonT0izB7Q6cLXCzw", + "FriendlyName": "Orange Floral Athleisure Crop Top", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:17.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2771, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XNVa9yB1X0SSTUTytHozCQ.png", + "AvatarItemDesc": "e149c559-aeb5-4609-9722-52fb6acc70e6,SoQerU2dOUu9BdRBAsEllA", + "FriendlyName": "Floral Athleisure Crop Top (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e149c559-aeb5-4609-9722-52fb6acc70e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:21.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "8ae154ba-8e69-48da-a314-507b419d4338,", + "FriendlyName": "Atlantis Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ae154ba-8e69-48da-a314-507b419d4338", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1123636091 + }, + { + "AvatarItemDesc": "27fa9f11-5368-4f6c-a114-18501478a9fa,", + "FriendlyName": "Atlantis Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27fa9f11-5368-4f6c-a114-18501478a9fa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1068640728 + }, + { + "AvatarItemDesc": "546ca714-8355-4455-be44-d7987974f8af,", + "FriendlyName": "Atlantis Trident Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "546ca714-8355-4455-be44-d7987974f8af", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1015465370 + }, + { + "AvatarItemDesc": "ee835538-058e-43d9-8268-b7d079563799,", + "FriendlyName": "Atlantis Warrior Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee835538-058e-43d9-8268-b7d079563799", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1924464343 + }, + { + "AvatarItemDesc": "f10f2d46-ec03-452b-9b94-0e476ccbb9c6,", + "FriendlyName": "Atlantis Warrior Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f10f2d46-ec03-452b-9b94-0e476ccbb9c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1699140506 + }, + { + "AvatarItemDesc": "a112b8b5-ea0e-4d23-a7eb-2daa4683a87c,", + "FriendlyName": "Atlantis Warrior Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a112b8b5-ea0e-4d23-a7eb-2daa4683a87c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1473012152 + }, + { + "AvatarItemDesc": "05809f10-98b7-4228-8828-e72cb37fe70f,", + "FriendlyName": "Autumn Colors Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05809f10-98b7-4228-8828-e72cb37fe70f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1575197462 + }, + { + "AvatarItemDesc": "2036fbec-e562-43a7-bcf1-c12f0307131a,", + "FriendlyName": "Autumn Colors Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2036fbec-e562-43a7-bcf1-c12f0307131a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1242079628 + }, + { + "AvatarItemDesc": "9d630b9b-7f48-4f34-880c-f52bad628b25,", + "FriendlyName": "Autumn Colors Cuff Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d630b9b-7f48-4f34-880c-f52bad628b25", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1240388911 + }, + { + "AvatarItemId": 379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzqcpgtdl5etxzj1p61vv6r5r.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,", + "FriendlyName": "Aviator Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:17.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 399, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cq2ejdon4vbkzqptgu6lhugwi.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,a8de80d3-286a-4fdc-bed2-75d52f8c0964,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:23.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 400, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ewo7xaztrkgxbdwc8nnspdmtl.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,c6cf096c-f0cb-4500-80a0-bc2916b5a426,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:24.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1606, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vm41bj3a6vlqwhygscxqlcy8.png", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,FjnZ-D1hcESLOWL4Ukv7kw,ff5ae41e-e446-4a96-8c11-49d85556fa81", + "FriendlyName": "Aviator Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:36.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 383, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bqf55zk90gccgpaf57wtsf14z.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,", + "FriendlyName": "Aviator Coat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:23.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 408, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "66bl5lmlw2ayp67bzz7mlitzm.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,da185fbb-ad2b-4f30-87a2-491f24b78ebb,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:34.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 409, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c32bq37y8bertjy6ivb388xbe.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,bea795cc-aabd-4229-84dd-49ab4f91a445,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:35.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1611, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2tmvikftvfs2sv1ll3oaqfq0w.png", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,2APfqVnnQUaDYKi-fQg8jg,9009bcdf-b6f7-4dae-b2ec-895511d118ff", + "FriendlyName": "Aviator Coat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42aa7888-f0b9-4584-9cc1-966585c6b36e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:20.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 384, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dik18b9lkprego4zu9vx7mv75.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,", + "FriendlyName": "Aviator Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:24.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 410, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cfko6vz2smqr4u7q3b5jh5jvh.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,97bbb384-b37e-4ea1-902c-88fbd6854ab5,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:36.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 411, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c6s4wxr4cgcoqmqlgwz47lrnh.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7fb8496f-0e6c-4e48-b8fa-5709ebbf4820,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:37.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1613, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "68cuiknni4ah1844qwlndids2.png", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7nRz9Jmnn066_PzngkhfFA,5659ce2c-9cb0-49da-8c39-4b7df0f8deec", + "FriendlyName": "Aviator Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "125a3a62-1005-47dc-9fd8-ee09ea803e9f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:35.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 381, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xipn9pseh8wykz3t6hpadi3l.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,", + "FriendlyName": "Aviator Goggles (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:21.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 401, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dzmu6jtu8g9jr5k1y002d2m6v.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,7b82c3c5-fd1f-45ec-b83f-e32668aa8dce,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:25.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 402, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "edzgkcu9p46xum15ttgjb0r3p.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,23428ca9-be75-4146-b400-df01d1dadac9,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T23:18:27.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1629, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bx1ejsvvshgpo945a7kst7w1x.png", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,qJGX62O_HUiZWN61NAc1Lg,556f876a-70da-4104-b792-fca8ad67be0c", + "FriendlyName": "Aviator Goggles (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "47629ed1-5378-427c-ad61-4b44d2fe90cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:54.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 135, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6sqmpmy9umgo11ba977kv392b.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,", + "FriendlyName": "Babydoll Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 136, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9qamxgi6spdwdg7gwy4xnedzt.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,d6823e01-69f0-4f85-b94a-74894356a2cf,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 137, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c24u162frxoa2ztfi76luw9ji.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c5deba2a-6e35-4b13-8e94-8ba5457f39df,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 138, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9zwdqgsqcsxt5ced3hkhejxc2.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,e13cf33d-7588-4e2e-955e-f0a29d6380b7,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 514, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6ag9z8xgaq11l42y9relrmux1.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c9589974-c261-485e-b571-cb2f34fb178f,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-20T20:52:13.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 660, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7f7hajiw2luckub1h51u4co33.png", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,da6d0ced-3d43-4878-a868-925f2ed6fd5b,cb9e9e05-c481-4397-86e7-c2111bc0e817", + "FriendlyName": "Babydoll Dress (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:26.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2893, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HsPbJKuTm0WZI6Of5AnXQA.png", + "AvatarItemDesc": "c2d22328-b41a-4e53-9b1e-814e84b86d15,", + "FriendlyName": "Back Kite", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2d22328-b41a-4e53-9b1e-814e84b86d15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-24T17:08:22.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1dbc517c-432b-4c85-8c5e-dfcf22b269cb,", + "FriendlyName": "Back Scythe Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1dbc517c-432b-4c85-8c5e-dfcf22b269cb", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1359892989 + }, + { + "AvatarItemId": 1762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58mihxk3jk5j3t6ag197iz7x8.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,", + "FriendlyName": "Backsword (Leathered Iron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-12T21:10:22.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1886, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8him9qwzellzbybe456axlevp.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,GfC01_o8QkGTbnGNjv_DSw,7mLb5jMJMUug7Nzwxv6D4A", + "FriendlyName": "Backsword (Wolf Steel)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:16.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2114, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "u0lK1AO41UOMSqQHLjVEEw.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,rFCKF2ku1UWj-TF-VClPRg", + "FriendlyName": "Backsword (Cricket)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:23.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2441, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "45ZUSM5vSES1q6qkk-rkFQ.png", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,zld80uf3y0uu__Yejms-3Q", + "FriendlyName": "Backsword (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23b97189-b49f-477b-8a22-0903b3d236f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:15.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1570227863 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,c2e62c10-3120-4991-9aa7-3f3e2cd29054", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1413041026 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,b56bf738-4c41-4169-bee5-b3e36db6d6a9", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1874084400 + }, + { + "AvatarItemDesc": "57e05822-ca63-4726-b3a0-260b6ece7bd7,96585250-512f-4559-9526-1ef77356a72e", + "FriendlyName": "Balloon Snake Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57e05822-ca63-4726-b3a0-260b6ece7bd7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1481339357 + }, + { + "AvatarItemId": 1455, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d93bbdei1z4uj950nvf3c8ai6.png", + "AvatarItemDesc": "49800740-33d6-4280-aefb-3497597c6366,", + "FriendlyName": "Folded Bandana (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49800740-33d6-4280-aefb-3497597c6366", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:22.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2075, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "QBEmq47zQ0ipqcJfw_re5Q.png", + "AvatarItemDesc": "49800740-33d6-4280-aefb-3497597c6366,0oMo4q5cLEi9LGpkOFIlkA", + "FriendlyName": "Riveter Bandana", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49800740-33d6-4280-aefb-3497597c6366", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:10.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2365, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "mrQZPItuWEiqGyXjajov1Q.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,", + "FriendlyName": "Banjo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:05.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2366, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "6Y0DhxPd1UaxX3rZPdXFYQ.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,7ykIpgU8Tkmmq_zzpjTVrg", + "FriendlyName": "Banjo (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:08.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2367, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "I6hIZL0Kp0CyPWnLkxSSMg.png", + "AvatarItemDesc": "ed4439c6-edad-4239-934f-79f9cc816c0e,iqYc9qtbZEig8jl_ZqKe6w", + "FriendlyName": "Banjo (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed4439c6-edad-4239-934f-79f9cc816c0e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:10.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2258, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SBxpBk0gcUG34NYQ8jtNvw.png", + "AvatarItemDesc": "764b4c4e-1009-4772-b01f-7b326dd77e8a,", + "FriendlyName": "Banker Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "764b4c4e-1009-4772-b01f-7b326dd77e8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:00.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2772, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BRPCDGHJ1USSO-gAt69-HQ.png", + "AvatarItemDesc": "764b4c4e-1009-4772-b01f-7b326dd77e8a,93vga8NJQEe5nWdqrroAaw", + "FriendlyName": "Banker Vest (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "764b4c4e-1009-4772-b01f-7b326dd77e8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:24.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2259, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "n1YJ_xf3VE6EzTlwUlW_9g.png", + "AvatarItemDesc": "62e480de-27e9-4719-adac-360698c7a2c1,", + "FriendlyName": "Banker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62e480de-27e9-4719-adac-360698c7a2c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:04.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2815, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "oea3WEkmiU6otRXRPGNm3w.png", + "AvatarItemDesc": "879c43e8-c877-4c8e-a5c9-100123e7a102,", + "FriendlyName": "Banker Hat (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "879c43e8-c877-4c8e-a5c9-100123e7a102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:35.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2260, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9Wd8fqP_uUygfK_VJAt3UA.png", + "AvatarItemDesc": "7d6cf363-76ba-45c8-91f1-cf4434c31b36,", + "FriendlyName": "Banker Monocle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7d6cf363-76ba-45c8-91f1-cf4434c31b36", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:05.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "amQEmFoB-EK_ExgRPUaVoQ.png", + "AvatarItemDesc": "1a0319e4-24e7-40b3-9976-70bff2c7ff23,", + "FriendlyName": "Banker Neck Tie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a0319e4-24e7-40b3-9976-70bff2c7ff23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:07.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1677, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0j5eiu15ludyj18o9vlghi84f.png", + "AvatarItemDesc": "07023692-97e1-4a65-a881-64ca6b4ae08e,", + "FriendlyName": "Barbarian Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07023692-97e1-4a65-a881-64ca6b4ae08e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:28.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1676, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7m1uy36svx5mpua6eq7senkzm.png", + "AvatarItemDesc": "e48955ae-50e0-4f55-93a1-611150142331,", + "FriendlyName": "Barbarian Tunic", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e48955ae-50e0-4f55-93a1-611150142331", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:24.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chch6uszf5qnxlwl6jtw9bftb.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,", + "FriendlyName": "Barbershop Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:28:57.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 412, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d67bfbfq6h2c493xoldetiunp.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,631c9015-2a00-47ea-bb35-465a2ddbca5d,83158223-e268-4036-8a6a-218a8430eb2c", + "FriendlyName": "Barbershop Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:19.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 413, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4d6mxhehvygwsuycquilcwmtt.png", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,7f9cb939-36a4-40da-8337-025e1d7bf8e5,83158223-e268-4036-8a6a-218a8430eb2c", + "FriendlyName": "Barbershop Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcf9012d-f9e9-4f15-872e-f130af3b2efa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:20.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 387, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "224pkbmy07eojqcchbf4djy4z.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,", + "FriendlyName": "Barbershop Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:28:59.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 414, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "93dtf3uavctg3si13zdcnelxe.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,a5925738-a451-4f25-b622-ccbfa12bd88c,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "FriendlyName": "Barbershop Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:22.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 415, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cw4kg51jcq1ugso9kcmngkt04.png", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,c64ac7a9-7fd4-4587-8626-0413a190b706,15edb8d7-2ff6-4170-a7e6-acd9e965cab5", + "FriendlyName": "Barbershop Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:23.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 388, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "24zf9tyok63h84td17n6zo3j5.png", + "AvatarItemDesc": "ce81210f-8eb8-4cd2-95d5-046d4da229c8,", + "FriendlyName": "Barbershop Wrist (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce81210f-8eb8-4cd2-95d5-046d4da229c8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T20:29:00.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 416, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "06z3ng7dlwz8wcsxzadsfgslx.png", + "AvatarItemDesc": "ce81210f-8eb8-4cd2-95d5-046d4da229c8,bb01b51e-a95f-441e-8c52-abc3aeed7145,f7c7d05f-5454-445a-af38-25f110bd204a", + "FriendlyName": "Barbershop Wrist (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce81210f-8eb8-4cd2-95d5-046d4da229c8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T00:16:24.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5687a39e-e5d7-40e4-9b32-190754fc6292,", + "FriendlyName": "Barbie Western Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5687a39e-e5d7-40e4-9b32-190754fc6292", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1670250213 + }, + { + "AvatarItemDesc": "5687a39e-e5d7-40e4-9b32-190754fc6292,km94i3lHJUWfjKgHrV-mHQ", + "FriendlyName": "Barbie Western Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5687a39e-e5d7-40e4-9b32-190754fc6292", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1077124656 + }, + { + "AvatarItemDesc": "7b7574f3-0b0a-4954-a644-6473ddb86424,", + "FriendlyName": "Barbie Western Ken Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b7574f3-0b0a-4954-a644-6473ddb86424", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1076331846 + }, + { + "AvatarItemDesc": "d85c010f-891e-4ce2-a9b8-ed68874da228,", + "FriendlyName": "Barbie Western Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d85c010f-891e-4ce2-a9b8-ed68874da228", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1008995980 + }, + { + "AvatarItemDesc": "0952eb17-9779-490a-98fd-af734a698c76,", + "FriendlyName": "Barbie Western Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0952eb17-9779-490a-98fd-af734a698c76", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1951232994 + }, + { + "AvatarItemDesc": "0952eb17-9779-490a-98fd-af734a698c76,uQcf_scBfU-XAii9HExRfQ", + "FriendlyName": "Barbie Western Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0952eb17-9779-490a-98fd-af734a698c76", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1279100943 + }, + { + "AvatarItemId": 1887, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25kjk74xrqchihponvtrfcese.png", + "AvatarItemDesc": "f23e86eb-8b59-4b32-a72a-5d382568e399,", + "FriendlyName": "Bard's Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f23e86eb-8b59-4b32-a72a-5d382568e399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:20.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2503, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VHA9zLkhZ0uAaJkYQaimwQ.png", + "AvatarItemDesc": "f23e86eb-8b59-4b32-a72a-5d382568e399,wuCrGxBRQUa5BLwx1nASPw", + "FriendlyName": "Bard's Vest (Solstice Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f23e86eb-8b59-4b32-a72a-5d382568e399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:50.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1888, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dub0ri6t7szkccf54a8e3p9ef.png", + "AvatarItemDesc": "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26,", + "FriendlyName": "Bard's Cuffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:06:24.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1885, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7ixk1u407fxtupt3m3sjujxlx.png", + "AvatarItemDesc": "b17591ba-3c2d-4309-ab01-b6743424e07e,", + "FriendlyName": "Bard's Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b17591ba-3c2d-4309-ab01-b6743424e07e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-12T17:05:57.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2794, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "l1uILA2f4EKvZEBtGvSETQ.png", + "AvatarItemDesc": "b17591ba-3c2d-4309-ab01-b6743424e07e,l9cF1s3B8EC6BGlQ4Tx1cg", + "FriendlyName": "Bard's Hat (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b17591ba-3c2d-4309-ab01-b6743424e07e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:13.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2268, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aSaV2nvIFkOp_qOuz0uFBA.png", + "AvatarItemDesc": "8f137b1f-4125-4657-85c4-bcdb74d3b0c2,", + "FriendlyName": "Barrel Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8f137b1f-4125-4657-85c4-bcdb74d3b0c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:08.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 26, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4rmm71n7z0wmlx9lj3u30z4u0.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Baseball Cap (White))", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1217, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "593u0j7z80kmuoamluu1rn0dg.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "FriendlyName": "Graphic Cap (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:03.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1630, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3lp8gu18idyrp1ify9n7g7h8m.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "FriendlyName": "Graphic Cap (Buckaneer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:59.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1216, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "d45mukwdi8abvszf0t0hbk5hr.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,nZor-clxlkm8EO9Gco0Otg,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Red, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:59.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1633, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9x0xbf4dhn0sn8fttdfpdd2k2.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,M8IYDK6aTUyn5mRhr51GtA,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Orange, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:18.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1632, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ajy5peougbbudsm27960iaohq.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,yxWEn-oaYE2YvvnrTVV5WQ,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Turquoise, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:16.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1631, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dospnezxsaqyxcdej4ac9k08b.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,XLYLo5qgW0eociGDgQ16PA,r_GLekt6mkWxXw8zPPR8Mg", + "FriendlyName": "Baseball Cap (White, Yellow, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:14.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1707, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "59nj2htuon9c6gj41ptaat7jk.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,Vk1n941ZgkKrhrL9mh18oA,lxmw7EbzsUuoy0roRy5McQ", + "FriendlyName": "Graphic Cap (Minitron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:17.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2g7h0rh45zr980awo5cvp4si2.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,mdSSJyipq0KvjDNQ2NyUaw,LQheTa-EvUaAonSyoWNmXg", + "FriendlyName": "Graphic Cap (Holographic)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T23:41:16.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2003, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8jcewsfim13hla1ex6nlwucmp.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,nzFUAOCAFUCd9S-x4EKRwQ,Qx7nrZH-K06CgIoJYfFCCQ", + "FriendlyName": "Baseball Cap (Heart)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:53.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2172, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YHvgN-qla0-yF9DyWIkm1g.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,xPs8BvGgdUC1i_luWAfSFA", + "FriendlyName": "NBA 75th Anniversary Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:21.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2773, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-lxSQ88W0UOAP-8tne9rSw.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,cfiCxhJZ20u07X1x93tNZQ", + "FriendlyName": "Baseball Hat (QuinnXCII)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:27.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1186165494, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BJUjvFUapUqyNI-sHp5Plw.png", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,VB_2uEBs70-9MnLn3__upw", + "FriendlyName": "Skater Hat (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "14ef6b00-debf-4a85-9755-b4d37df496d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.492778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1712, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0jjswupors8a1sy83fp1oeipi.png", + "AvatarItemDesc": "ee9a7c8a-7383-4a09-b44c-474b66f9008a,", + "FriendlyName": "Robo Logo Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee9a7c8a-7383-4a09-b44c-474b66f9008a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:08.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1713, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "26f4i1hdx3pb8xotdo0vkhqr9.png", + "AvatarItemDesc": "ee9a7c8a-7383-4a09-b44c-474b66f9008a,W-xMu8n2CUuHSXfHvEL4yw,-apO3Y7om0GKhC4V_hVYdQ", + "FriendlyName": "Robo Logo Cap (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ee9a7c8a-7383-4a09-b44c-474b66f9008a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1218, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9gmjkk497nr0pq75gtfcilfjl.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,", + "FriendlyName": "Graphic Cap (High Fashion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:10.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1219, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bfdnrz82ejhtasrdzh7azcyxx.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg", + "FriendlyName": "Graphic Cap (Sticker, Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:15.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1721, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cllwjm7phajd4j2pjnk1lq87l.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w", + "FriendlyName": "Graphic Cap (Buckaneer, Collector's Edition)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:31.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3cnf1qorsglce2vtvvj1nf4cz.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,cpTKHXjj0kulivZCKVQqiA,Mrf17aidXEeCF-fFURUUcQ", + "FriendlyName": "Graphic Cap (Hyper Ramen)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:07:20.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2221, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IDVmIRptBkSiYWCmC3zeVA.png", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,-C1ui-T280GEfW6-vPQ-ig", + "FriendlyName": "Flat Brim Hat (Basketball)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "474db08a-d4dc-4625-9f5a-1a6ae0b70827", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-24T19:42:09.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 39, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e9shr5go0vsw8gb8dght6710s.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,", + "FriendlyName": "Baseball Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 984, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6at7h1u119kqoj0j8z06fflxl.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,FVdDQFOMQ06teQm2ANDPng", + "FriendlyName": "Baseball Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:44.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 985, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ampb4pvmmtdhc5913do72skwc.png", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,FVdDQFOMQ06teQm2ANDPng", + "FriendlyName": "Baseball Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "293e7ad3-01c9-43af-96e3-06c34145ff12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:46.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 139, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "703se3u45kueznyorg28dx2d4.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,", + "FriendlyName": "Baseball Jersey (White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 492, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b124kxp8adqvgs4bryq7tcrra.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,e5de2e45-207c-46af-9a5e-eb01a0c621fe,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (White, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:15.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 493, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "coknzs3avuwcbyl6z2drrxtd1.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,de0a5a3a-8aff-4799-8269-9efa3aeb383a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:16.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 478, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4ur6eolr90837mh7229goao2z.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-10T16:50:09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 477, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5iy6yfd9qvpfzdo6symnk32xf.png", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,ca37823a-b8f3-4414-b38b-de0dc7fa4295", + "FriendlyName": "Baseball Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8518fc-c989-40de-98b4-c6f09b304166", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-10T16:50:08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1736, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "e9d1k2tttj2ohpp10c8fp9uu1.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,LIgagNf4gk68EYQdlDruxg,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:07.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1739, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a6xlnnqmochthrsk0xmuemrpf.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,6fmUy748hU2dgwgaijSkUA,rL9p5JY83Eyxj00Dieg7oA", + "FriendlyName": "Basic Belt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:17.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1741, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "48ag7b3c5m2vy68rclwrb8km0.png", + "AvatarItemDesc": "12ec292f-0ccd-4c8a-a49c-3598f827659d,buuNFd8sK0eap78TL1gMNQ,-UFHTNvJs0CGDStb0PFkbQ", + "FriendlyName": "Basic Belt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "12ec292f-0ccd-4c8a-a49c-3598f827659d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-22T21:44:23.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1743, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "05r4o0sr1cfy4hmhwsw9aaygk.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,SDbpnjVYmEGQneK5fhZ0Cw,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:36.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1746, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1z2bq777lr7jukwio4h4ahhcr.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,uwoWPY4YjEmZSQcNCrx1Gg,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:42.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1747, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b5f3a10evhggitywma8wlxt69.png", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,COZbkLDc20mdUlWSkqMX-w,FwjsTo0nb0uNU6l8XvllNA", + "FriendlyName": "Leather Necklace (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:42:44.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 140, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dyz7t3e9p724lfy2lqohcvl0m.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,", + "FriendlyName": "Basketball Jersey (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 650, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2z5f34xrfn5i5rko8oh08suh0.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,59a62fa3-9e15-463e-9bd5-3aecdf604c1b,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:35.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 651, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5csh82iknt6zuuty86qfpx32k.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,4effd67c-a52f-4b41-a5b0-04287f91a7bc,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:37.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1728, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4nvt3kwtn2k367794r49ve87e.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,ojTIShRZHEOV1qQRfhwvTA,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:41.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1727, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "24n1sqxebe37pk9ina0smo1nn.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,se0-2ylLD0u6sqCVPkeP6A,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:35.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1726, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "86hv8nip4c5kphcs19ulpi7xv.png", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,RU-B71GUF0WPduoSOI1XMg,54a5c055-f757-4c00-a1e1-8b0b6552c608", + "FriendlyName": "Basketball Jersey (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "348ac48b-986e-4d42-a0f7-1aea16b88271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:25.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1909, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "2nbi9m6b47r4wal4q24bnxfms.png", + "AvatarItemDesc": "0736e71b-a32b-4485-bedf-6fde8fe3b09f,", + "FriendlyName": "Bat Hair Bow", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0736e71b-a32b-4485-bedf-6fde8fe3b09f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:03:59.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1908, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "1snahzhx4gvw2hmv9vx8ves5r.png", + "AvatarItemDesc": "01d0fc12-3ebe-42e1-8caa-5513a40436c1,", + "FriendlyName": "Bat Bowtie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01d0fc12-3ebe-42e1-8caa-5513a40436c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:03:50.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1927, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7s85boxheb6528qy06bkgcocn.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,", + "FriendlyName": "Bat Costume Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:56:23.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2521, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "PImwtIHMIkmPg7xHGM7PXQ.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,GPX92X5n3UWMbh3C13oB5Q", + "FriendlyName": "Bat Costume Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:35.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2522, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6wBcnTssRk-eCxqSkfa3Mw.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,dE_ZWKPV90utoyWtRalh8w", + "FriendlyName": "Green Bat Costume Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:38.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2523, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "ERAg0XXt4EicefSqiFHYcQ.png", + "AvatarItemDesc": "a0a38623-2003-469a-b06f-ce451413727b,jIAMeDkA80GVHYTi0d9LkQ", + "FriendlyName": "White Bat Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a0a38623-2003-469a-b06f-ce451413727b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:41.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1926, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "cheqz81i6ydobk59q4ybrjfks.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,", + "FriendlyName": "Bat Costume Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:55:55.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2516, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "lQoHL1kpXEKcBNWXhquQXw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,mnYzlq3Rw0-Q89gmc5f9XA", + "FriendlyName": "Bat Costume Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:21.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2517, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "kWsDoa_x1UmDw33BY2rMCw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,85pvpo4QqUSrdsbofGkoWg", + "FriendlyName": "Green Bat Costume Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:23.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2518, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "TP2iURkjWE2-TVyQJGWkVw.png", + "AvatarItemDesc": "336aeb24-33a1-4aae-82e6-4fdbd1330452,f6XXbe_mhEuZJ9in5vDRGw", + "FriendlyName": "Bat Costume Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "336aeb24-33a1-4aae-82e6-4fdbd1330452", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:26.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1928, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "5ox8ama5khsmyb0okqnvem5p2.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,", + "FriendlyName": "Bat Costume Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:56:39.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2528, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "R-4c5-nUAkebQ9WzWNLZgQ.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,oO_Ij0Vsvki4Fa6qonrf7A", + "FriendlyName": "Bat Costume Wings (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:59.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2529, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "-KAlFceGmEy5iT39k9-pNQ.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,IgocvfGIn0u_Jq4GP2_fJg", + "FriendlyName": "Green Bat Costume Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:00.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2530, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "DdcTk6htq0eP97IwF_w0mA.png", + "AvatarItemDesc": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf,Q2BNGA-idUyHDPu7m-ZSnA", + "FriendlyName": "White Bat Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1f842c7-4ce5-4bda-8c78-5a75a6c544bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:02.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1819, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f4jxrcx7v17e4u9uskgyxjvje.png", + "AvatarItemDesc": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,", + "FriendlyName": "Sarong Swimsuit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:16.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2238, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "CbY0IoFre0CFmDX2p82E4g.png", + "AvatarItemDesc": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,PNnfMibAUkqetPThlhtBgw", + "FriendlyName": "Sarong Swimsuit (Cactus)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:43.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9cqrtp4vgci8mnuccke5s2itk.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,", + "FriendlyName": "Towel Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:29.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2016, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HTVSEzDsKUSnkvdGvplfvg.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,Hae8Pfe8mEWeF8mL9hC9dw,Vat8nL4ZBEK9mLABJiXS6A", + "FriendlyName": "The Amazings Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:40.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2015, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "S0ChF4sn40ewzPHkvLcNGA.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,kE9vr2bKoUSFm7AkcXSmew,5o-249dUdUOhW28lrJY82A", + "FriendlyName": "Valor Society Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:37.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2014, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2IWuPA8HGU6KrdC4ohARXQ.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,GAzH2pThjECghCmCwKJPrQ,4T-xj-xtSEiv1PDYv30URg", + "FriendlyName": "The Defenders Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:36.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2013, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "avkdszecpez6tb8kq3563bp59.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,pEWioft3gkOMWS4tZFping,gOw6NOX-QUegt4mxjtfpfA", + "FriendlyName": "Masterminds Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:40:34.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2181, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pyxx1jg0F0qzsZMmUcX81w.png", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,SN8HDRADZ0GvrFgEHKMhWQ", + "FriendlyName": "Towel Cape (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:39.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,Im55dSJ4mkmdmHPtX9tYpg", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1727884092 + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,adETWZLIA0O0feRzUjxKQw", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1454267065 + }, + { + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,z7uu8CimMkCjsCGTDPCZLw", + "FriendlyName": "Beach Towel Cape Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "281937b2-f994-45e9-a6d3-03d097247ce6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1018984627 + }, + { + "AvatarItemId": 1400, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2jzqx1rojlzf9q9pado9u0t1o.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,", + "FriendlyName": "Brimmed Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:48.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1401, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "110fo1snzkucd8yr0l83dt96u.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,-XSM6ZcSjE-vrWL7jyF4fQ,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:50.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1402, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5tpxbss93q52nayhe1d5fpao8.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,RL6HkufaJ0yubTpCGVI9eQ,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:51.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1403, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7k8ttzh9wehwhm80g5p4gopa0.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,9tTuREkoQkW0ycqOjWXsiA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:52.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1404, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cijefzd028p1ktkkbndlk0iio.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,MVkY4sU5BkGC4QhweRiyIA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:54.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1405, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9bijtfphf1w416loccnpc2v4m.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,ux9DQVSRyU-_5GA-t197OA,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:55.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1406, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bl1xc0woq0y8j4zowmiyc2wbz.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,t2bByt5oUEG90PMMePCP6A,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:57.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1407, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "340d5n3lde4t69b8l5kc2jupr.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,m7KXVnULz0iHvKfCFzFoyw,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:58.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1408, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7bp85mlgk50a43bna39drnbwj.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,gT0WA7Y1H0my5uMlteIMMg,7nBHNMZhAE-t9rLCYO_STA", + "FriendlyName": "Brimmed Beanie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:59.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2093, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "CCiQ0TXblUORW6PKW2gHkg.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,Hd9xNn-UA0uxwv1axFVqGQ", + "FriendlyName": "Brimmed Beanie (Pastel Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:22.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2094, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "nJX4hvcYAEaOsI07CK_m0w.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,C8b4lTZ-TEOlCVv7Gs219g", + "FriendlyName": "Beanie with Brim Holiday Market", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:28.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2095, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8i3Shpwj70mbaFZ0_YazXg.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,3Xlg70ev6UqdcIAmJ9hmlA", + "FriendlyName": "Brimmed Beanie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:30.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2774, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ODGgLfHg60m9YanIDHhtGA.png", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,QCOMyeCUK0Gudx4ljBmAdw", + "FriendlyName": "Beanie (Musical Notes)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:29.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1389, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3kk89nprwfzgkc47ddr46nppo.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,", + "FriendlyName": "Beanie (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:43:56.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1390, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0si8omfozqsiv0jjns5uz7x3v.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,58YffnGSUU6umcI1i0RvZw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:43:59.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1391, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6go7wf3t618gcga8rjoywcwo1.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,c24f1DNZjEmqkt-FibKUjQ,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:02.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1392, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "clgnc91prro3ozkp7tnx5ld66.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,SeVmHtrMS0y7gKCYkHcrOA,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:04.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1393, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "59xbft3ixffciz9wybly1ufd4.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,uDcJZClcFkiNdkEEh9IIyA,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:06.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1394, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2rnnyghpth3r3etbu364c02mc.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,Iezz6gCFfkKdVcmPXIhunw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:07.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1395, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e4rjntdbfbgxnzzds63z8k3i7.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,fLWfBSEO6U6aHrDm3nKFEQ,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:09.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1396, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2ejkmb3z1znhlpdst7q3jztb2.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,F5vJzMfNB0Ob6Abjadho7Q,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:10.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1397, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7gi71ssytha4oltxzdg3vekb0.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,vbOUaJsGHEaSfQCivNcMWg,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:12.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1398, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5p5se3p4opi1nd646csh6pvsd.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,wbFPXIg5gUK3QK6YycZexg,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:14.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1399, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6w4cwzijj4bww69aju5fi9pzj.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,_fBrHCkTKUe82QgxCyI-Kw,uWXtz0WaO0qJ1W4TfWnLqg", + "FriendlyName": "Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:44:15.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2219, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zsqqOW8x1EqW9fjjiRFwTw.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,CJKB3OO2CkeEVsAK0jeeDA", + "FriendlyName": "Beanie (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:06.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2438, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7kcx60xKr0C5aWqnubk3Og.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,k-WZIhNJAkSpIWLxC2D40Q", + "FriendlyName": "Pumpkin Beanie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:06.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2439, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "j73o6WaQmEubCEKCMVcsLQ.png", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,VE_cHMHKBE2Wuugoqj4rZg", + "FriendlyName": "Pumpkin Beanie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "41bece0e-9b5d-4151-bf5c-d0e786a803b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:10.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 369, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1jj51yv6mj3i8duvbctn67g5j.png", + "AvatarItemDesc": "7553d851-1986-4325-870d-e425dfb7d007,", + "FriendlyName": "Farmer Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7553d851-1986-4325-870d-e425dfb7d007", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:09.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1281, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e8i3fa3rlphdsfofxs7sewwio.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,", + "FriendlyName": "Bear Hug Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:23.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1556, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6rqv598elyp668kqusa9g2dxe.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,OnZujqqU_UqwxmznvBBMfg,_zrnGn1ErEWR8WqrZmKZsA", + "FriendlyName": "Bear Hug Hat (Holiday Panda)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:31.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1557, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ujfxsx62pzx2i549y1zix2uy.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,XKoxnTXYGkSUu52nVS9dnA,TFOAk7kwh0uUgtsyDNxMuQ", + "FriendlyName": "Bear Hug Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:33.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2102, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CneUY-gB-U2whOHJXkONBw.png", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,9nPWmpM4mUmEpHfthqYeng", + "FriendlyName": "Honeybear Hug Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef7771eb-c5f3-44fd-8c27-f581b035ef10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:13.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1187, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ubi3qjigszacxmdn65q4d2nj.png", + "AvatarItemDesc": "2786fc22-3d6c-440d-afcc-15890c061577,", + "FriendlyName": "Bee Antenna", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2786fc22-3d6c-440d-afcc-15890c061577", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:52.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2775, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TgcLGc1A7UK1C6_N3Au8WA.png", + "AvatarItemDesc": "2786fc22-3d6c-440d-afcc-15890c061577,dXz9B9OH2UuLNV35u8KB-A", + "FriendlyName": "Ladybug Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2786fc22-3d6c-440d-afcc-15890c061577", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:31.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1186, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3w6s9ko5eqhqffltdu2xx937j.png", + "AvatarItemDesc": "bb8602d4-f91f-4e35-9c88-1cdf3a04ff0a,", + "FriendlyName": "Bee Body", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb8602d4-f91f-4e35-9c88-1cdf3a04ff0a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:48.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1188, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ex0h92b7vqpk3ao6jhdf8acdf.png", + "AvatarItemDesc": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,", + "FriendlyName": "Bee Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:58.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2776, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NvZjNy5k-k6-IOXr5Mm2TA.png", + "AvatarItemDesc": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,USdUyjIzY0q5af3AeTFCOQ", + "FriendlyName": "Ladybug Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:33.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2816, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GTnJ7Ox8b0SRwSSDw8LESQ.png", + "AvatarItemDesc": "df29d81e-851b-40b8-95ae-8461f702b1e0,", + "FriendlyName": "Beehive Beanie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df29d81e-851b-40b8-95ae-8461f702b1e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:37.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d0s9wj7bjb9h01c8w7xwma53o.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,", + "FriendlyName": "Beekeeper Veil (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:48.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 696, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zg7qszxejhn7ysl2ofksqed8.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,1919d319-6cce-4500-8766-15fed6a13fa8,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:45.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 926, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2zodiht9cu9f4628u37rle1f0.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,lBe3yxuE40eBmTrnEHBqQQ,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:00.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 925, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "28ohmms47updexq8mqh9ey2ih.png", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,oZ8k0Qv3SkG-DkY0_dP7UA,e4b53a9a-ac95-49ae-bf68-615bb23fa075", + "FriendlyName": "Beekeeper Veil (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:59.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "67ho8qppd1uvf14zlw9l7bdmg.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,", + "FriendlyName": "Beekeeper Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:50.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2k4lcmck9k4oohw7zr54f80ke.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,1919d319-6cce-4500-8766-15fed6a13fa8,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:50.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4if4tnkvzu35yc8tyran086ks.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,lBe3yxuE40eBmTrnEHBqQQ,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:19.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 891, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bbjryyglvkoz6f7rpa627np9b.png", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,oZ8k0Qv3SkG-DkY0_dP7UA,afd88a79-7f03-40f6-bcb4-c247dd86edc4", + "FriendlyName": "Beekeeper Suit (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:17.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 710, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "36kccfcoypwfay2raguim9kb9.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,", + "FriendlyName": "Beekeeper Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-06T19:33:52.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 703, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dqln9eqsu4l7dk5wpe8ad2i8c.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,1919d319-6cce-4500-8766-15fed6a13fa8,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:56.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 927, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "82jnl4qpioioo5e84i408z96v.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,lBe3yxuE40eBmTrnEHBqQQ,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:03.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 928, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "egoh9oarmlgl1e2wh5az6t7vu.png", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,oZ8k0Qv3SkG-DkY0_dP7UA,1f18670d-df52-4d2b-8009-a32cf7c67b40", + "FriendlyName": "Beekeeper Gloves (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4025e4c4-abaa-4729-a2a3-a7175313e5ed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:04.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 40, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8zfbgqf2y36bwmt9v0iq15tuj.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,", + "FriendlyName": "Beret (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1601, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cpgewpuv8enuu5r81mj51rdpb.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,7_n11PF4-0KqTZ9az23hqg,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:21.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1602, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5sln9xpcozzr4gxw3l8dxk5cs.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,TKbjEeTWfUyA1u4laoULMg,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:23.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1603, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "61ormf3tp9m7rfze5jen89ywn.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,JShcoWWPz0297HeEUJbDCQ,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:27.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1604, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "16amrlrlhliiat2ld7vswaurs.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,_psc7WCgOk2JzRQwuFAqNw,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:30.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1605, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4b809ml4l1gchsyq9ftgm08bj.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,WeP_tPjUNUuGojXrVBOAIA,FWL8ZeMe9EerdB0_Fya3AA", + "FriendlyName": "Beret (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:32.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2519, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r4KMHNh_x0-32HKgY1U3sQ.png", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,8xqL--ghKUunlS0r4ugCgg", + "FriendlyName": "Mushroom Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:29.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 41, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6bwvu5xxyxigc6heyrh7sw2bi.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,", + "FriendlyName": "Bike Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 483, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2mdqjp0pan01n0jy9wmab9gka.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,83522c12-9549-4d74-8c87-bae210bcd2bf,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:02.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 484, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bfdtco5996aiaxyo9h38yw0b7.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:03.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 850, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d8wwf7fmiptl4zd7nj52mqemx.png", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,XNq4Cns4lEGydtktdsXlmQ,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd", + "FriendlyName": "Bike Helmet (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71014872-d879-4858-9d68-58da3c673d8b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-05T20:03:02.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 267, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "28jeraj9pdlhoxmjt0mhkqvj7.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,", + "FriendlyName": "Bike Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 929, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "30u7o3e8jtfekidt2dw5uia5v.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:05.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 930, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "53455700060fde41p5z5thud2.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,XNq4Cns4lEGydtktdsXlmQ,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:06.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 931, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5tnpmoxpb4gujzn1apm2ywi3z.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,83522c12-9549-4d74-8c87-bae210bcd2bf,0XPd5RzNJEepHztqwAznHQ", + "FriendlyName": "Bike Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:07.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2024, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X4W5cZITW06S0a_Bere_PQ.png", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,qspNifi92keI-XssPB5qDA,RVraJOPsOUqZjd8bEiPduQ", + "FriendlyName": "Bike Gloves (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:11.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 141, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "do070rgyuzlvn3h08jelsszce.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,", + "FriendlyName": "Bike Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 494, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1yuq0u2hdapelj37471lkep1i.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,c5884778-a87f-4612-9717-86fbb65d440f,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:17.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 495, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4guodx87w200pz0ev441o50w0.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,83522c12-9549-4d74-8c87-bae210bcd2bf,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:18.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 799, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "13zz3iuzmt1r8kfhefj6kqj9h.png", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,XNq4Cns4lEGydtktdsXlmQ,1695608b-e9cc-4a03-b56d-0f09d2804379", + "FriendlyName": "Bike Shirt (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "843e3364-a743-4dfc-a990-0436e47b8c10", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:38.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 751, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3m8jywcsatm5rq2um5dbimatv.png", + "AvatarItemDesc": "n52um7A-b0utGxixd1ESvA,", + "FriendlyName": "Biker Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bae9d9f-3eb0-4b6f-ad1b-18b1775112bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:22.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 749, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dyz4zg15sdjewdf5zjojjk36p.png", + "AvatarItemDesc": "_qXaNbm4yEeyalo372QbvA,", + "FriendlyName": "Biker Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35daa5fe-b8b9-47c8-b26a-5a37ef641bbc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:19.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 746, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1shya0fxztg1hrivkr6kz5t5s.png", + "AvatarItemDesc": "VQnoLstCyEGDPy1c38-GvA,", + "FriendlyName": "Biker Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ee80955-42cb-41c8-833f-2d5cdfcf86bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:07.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dfffe31f-9f82-4622-a920-ea5f5bd26042,", + "FriendlyName": "Binoculars Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfffe31f-9f82-4622-a920-ea5f5bd26042", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1059720796 + }, + { + "AvatarItemId": 1682, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9gv2f7uy1uo763i2urf7qdtys.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,", + "FriendlyName": "Birthstone Earrings (June Pearl)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:24.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1683, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "awvrj98971hzwbeeg8i9zxo3l.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,mM7I9kDTx0ut243UkWKXvw,YKaJjmMHo02FWwem8nw1yw", + "FriendlyName": "Birthstone Earrings (March Aquamarine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:25.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1684, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ekdcofkpc4pqcwpk0rwxxdu21.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,46f7Ewe99E6tF2qFaMtQ0g,8IK1U23XlUOV-2ibVznkDQ", + "FriendlyName": "Birthstone Earrings (April Diamond)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:27.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1685, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4xaaxp8lgcjr1lrwq0jnqoxbt.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,R0f3hHGY9UqUEW4aR1fssA,KSqfmVTiGUKOKijwGDSwgQ", + "FriendlyName": "Birthstone Earrings (May Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:28.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1686, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2qnbcqo4a9vldg7eer5ofwli9.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,adwzLAQEykK8D5hZGQ6yiA,0Nca5nBUpUquFhExd5dxcQ", + "FriendlyName": "Birthstone Earrings (July Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:33.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1687, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3ap2lnn1cayd34hkw01ody958.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,ZsWc8wRafEGl3B7cr6UkzA,ByIfwCtDwU-p9rEfqpjfGg", + "FriendlyName": "Birthstone Earrings (August Peridot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:38.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1688, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4z8ff179kc1znimkl5hhclu3c.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,kQD41xK4dU6dI_6pRkP0qA,aSeusgYlPE6vOBu9Ua_dWA", + "FriendlyName": "Birthstone Earrings (September Sapphire)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:40.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1689, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "62x43h2lnvgu2pa5ubjuz4yuk.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,ro4sD9txVE29_eT2jK-wEg,0YF5bACMsk2sN4tixbJbKw", + "FriendlyName": "Birthstone Earrings (October Opal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:45.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1690, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "17kngr9n3ywkzhzibzp9gckn0.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,-YsxhNXLAk2qbeXMqwAqng,aNOhXS4nuUeqs0CVw6K7Hg", + "FriendlyName": "Birthstone Earrings (November Citrine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:34:51.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1691, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1o20o1igl0kxcykasd3bf7drr.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,imOiYzABkUm9C5EVzvjXgg,0eZVIsv7wUyZ0ohLmF1jGQ", + "FriendlyName": "Birthstone Earrings (December Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:19.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1692, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b4372447x75hrt7zmp36xexi3.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,FgLAEMh1e0GyHO8J8_xj5Q,iM-04HpG6k2N8n-P3XS22w", + "FriendlyName": "Birthstone Earrings (January Garnet)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:22.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1693, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "51ybcms7jhdsyx03odmfrhned.png", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,C9CZUwEZeUiuOue4CK4luA,gUxl7WNXlUqFYHZ9eGq1cg", + "FriendlyName": "Birthstone Earrings (February Amethyst)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "308171c3-cfd1-4ff2-8593-856e8de7071a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:26.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 74, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "a1hubxyh2x781ngkvrnl6q4r2.png", + "AvatarItemDesc": "b861e5f3-fc6d-43b3-9861-c1b45cb493a8,", + "FriendlyName": "awesome sauce", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b861e5f3-fc6d-43b3-9861-c1b45cb493a8", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 221, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "c7r9nusyu8a3jy7ydrv4shwx2.png", + "AvatarItemDesc": "6930ce13-4be4-4ab9-9817-667bd261ffc3,", + "FriendlyName": "aaaaa", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6930ce13-4be4-4ab9-9817-667bd261ffc3", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 268, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "d5ga9td4yt4xjgrj7q9m03toz.png", + "AvatarItemDesc": "abc25091-ed5f-4c72-9364-fffeef1bc239,", + "FriendlyName": "STOP", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "abc25091-ed5f-4c72-9364-fffeef1bc239", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RWXXh3Ixg0KSe82fQIeKiw.png", + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,", + "FriendlyName": "Bladed Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:39.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2552, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "te1b6n09g0WjqI3yH5n3mg.png", + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,fWZekjhIxUWhxgVGTWrVsg", + "FriendlyName": "Midnight Gala Bladed Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:38.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "34fa5014-e516-42a1-aaa8-91d2b6f59727,a7477071-b04f-4b9e-9fcd-bd44991e5f0c", + "FriendlyName": "Bladed Wings Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34fa5014-e516-42a1-aaa8-91d2b6f59727", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1516632647 + }, + { + "AvatarItemId": 2447, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uF47LVvccUucyESgIuygpw.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,", + "FriendlyName": "Blaster Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:09.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2471, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "oLjvlMCC2kuFc3_lVEfvEA.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,BizZfhG8ikOOEvAxN6w48A", + "FriendlyName": "Blaster Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:07.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2450, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6JCWD1J9U-qP46fezaoXQ.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,KfjLWNUbpEClo_qafEfUEw", + "FriendlyName": "Blaster Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:23.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2448, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KX8r_kEiAU2h5vaiH70O2g.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,zJKd2ZtIsUGCdHP1M0215Q", + "FriendlyName": "Blaster Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:16.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2451, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "P9xsKr4uU0mPWfnYZEGItQ.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,Adyl5MkwQEKK_MT5AluYkA", + "FriendlyName": "Blaster Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:27.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2449, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "JYKGsy8zbE-uVRh5F0aELw.png", + "AvatarItemDesc": "943d2fc7-a769-4195-b147-a765e6c274c5,xtgcJ8klHUyAYRntKhwCzw", + "FriendlyName": "Blaster Suit Hat) Blaster Suit Hat Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "943d2fc7-a769-4195-b147-a765e6c274c5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:21:20.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2452, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZQvc0FZMVUKVsARripsYXA.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,", + "FriendlyName": "Blaster Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:31.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2454, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HE3yVvkZXE294hudT6aXLQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,bgWM6wBe60q2mIHN5dPh4Q", + "FriendlyName": "Blaster Suit (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:39.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2425, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QVu4sV34w0mJ62ZfIhLifQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,cqYUoDpfkEi8Qp2DrpQytA", + "FriendlyName": "Blaster Suit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:21.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2453, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CY_yVMmPd0SsiIdZ5xLZfw.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,lRmNfk8vuEe4Z5ToWvWcTg", + "FriendlyName": "Blaster Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:34.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2455, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wa-FDCaLx0er4dEZFzygbw.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eQy0qwre-Ui7_sCCNuWCSw", + "FriendlyName": "Blaster Suit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:42.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2472, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yD1_cAEdh0a6Uf6vIWKVyQ.png", + "AvatarItemDesc": "9ab1518c-1781-44c9-884f-686bcfc9d8a6,eU2ePFNmbUCPoQUp1WCbzQ", + "FriendlyName": "Blaster Suit Shirt) Blaster Suit Shirt Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab1518c-1781-44c9-884f-686bcfc9d8a6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:23:11.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2456, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "zmUtvtALhEyonXMMbmhMLQ.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,", + "FriendlyName": "Blaster Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:46.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2458, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OlimAxzdbEWXp8C2t0UdRw.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,csLWYuunH0K9Ij1q1FoBsg", + "FriendlyName": "Blaster Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:51.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2460, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "C3a5a-EHuUeFBNVj8c4TcA.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,Zf0z5Ghkwk-QJJq4u7Fjww", + "FriendlyName": "Blaster Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:56.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2457, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a8n5oQkGBU6RKd7IECdwjA.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,sP8lTqgdk0WyuwQczT_ijg", + "FriendlyName": "Blaster Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:49.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2461, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0aPPOEDh1EqRvNey2G6Buw.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,dudShaXMYkaIZdZfyleoXw", + "FriendlyName": "Blaster Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:21:58.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2459, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "swrVud60vU-52WTaynUCqQ.png", + "AvatarItemDesc": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01,3EOUuzxLSkSopjIohgQ-zQ", + "FriendlyName": "Blaster Suit Wrist) Blaster Suit Wrist Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ddfe493a-9a30-45e0-9e67-e6a3a7632f01", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:21:53.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1254, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "holidays", + "ThumbnailImage": "d57lxs90awzdwpbei5f4j6nz8.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,_ycDQ6RcyEGAntVlSDpPeA,6UBZuZQBG0eZtNmty2IGdA", + "FriendlyName": "Blazer (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:42.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1352, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "10hg6w8e7ui5q5yod1bb5dhab.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,7WrSbK4Ml0K2z-ig5HIUCw,9skD_xxLq0mpfP13f9n8hQ", + "FriendlyName": "Blazer (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:21.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1918, + "AvatarItemType": 0, + "Tooltip": "Available until 10/25", + "Rarity": 50, + "ThumbnailImage": "dwz0pxz74xy4zmr653hor487a.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,uCA4tnv9Mk2mX-NnFlj2Mg,NvlKJFz4IkKDliVOJKJYhA", + "FriendlyName": "Hallow's Blazer (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:20:50.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1929, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dswjk6ox1i8gqtdq0td0c43qy.png", + "AvatarItemDesc": "de0ac50d-2adb-4114-bd2e-68953b13d706,YlgQOY1VJUuGzYxoTYAqGg,jhQ23U8Pmk2K2wfBdrEWNA", + "FriendlyName": "Snowflake Blazer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de0ac50d-2adb-4114-bd2e-68953b13d706", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T18:59:25.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1338, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5g3kf0itlpuocmwiedgwzxbrn.png", + "AvatarItemDesc": "20b424a9-861a-4d39-a583-22d7b7348c14,", + "FriendlyName": "Bog Monster Hood", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20b424a9-861a-4d39-a583-22d7b7348c14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:59.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1654, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9uu4lgs1wp05xxg31w3ixcj1t.png", + "AvatarItemDesc": "20b424a9-861a-4d39-a583-22d7b7348c14,afnIqin4lkKKqWrvBvVagA,bCuW1YadE06p6v1dBHsx4w", + "FriendlyName": "Bog Monster Hood (Movie Magic Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "20b424a9-861a-4d39-a583-22d7b7348c14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:50.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1336, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cdcrail4xl68zz5i57rfmgvcm.png", + "AvatarItemDesc": "dedd2e98-2721-4de6-92f2-9d399f34e8fe,", + "FriendlyName": "Bog Monster Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dedd2e98-2721-4de6-92f2-9d399f34e8fe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:46.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1653, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7jabhuhr3ufptld365gkfrn58.png", + "AvatarItemDesc": "dedd2e98-2721-4de6-92f2-9d399f34e8fe,2jj5lxx6Gka5U6j0cWjN5w,HQnd9m0Wi0upaXRNsE5HPQ", + "FriendlyName": "Bog Monster Suit (Movie Magic Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dedd2e98-2721-4de6-92f2-9d399f34e8fe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:45.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1337, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "15ql8y6860hwqib0t4k041550.png", + "AvatarItemDesc": "4e42e02e-33fd-4ff9-b74e-9bde26253c35,", + "FriendlyName": "Bog Monster Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4e42e02e-33fd-4ff9-b74e-9bde26253c35", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:52.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2462, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "QQLvWQYc7Uuml1bMBoD0UQ.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,", + "FriendlyName": "Book of Elseware Vol I* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:00.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2463, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "UV1LMH8or0KUAZAidj0x-Q.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,bpzKQ-B4_0KpMPf4ixTumw", + "FriendlyName": "Book of Elseware Vol III (Cursed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:03.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2466, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "vHLg9EajA02BxJcYdCE1Iw.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,14Gft1zCQECXPxPmiUc5VQ", + "FriendlyName": "Cosmic Cultist Book", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:11.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2464, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "kXsMoNdwF0e5eg7JqgZnMA.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,7ziZgKKqXkuyo9v77chIlA", + "FriendlyName": "Cipher of Elseware (Uncursed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:05.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2465, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "DFFMheRvREahgpIp_KezuA.png", + "AvatarItemDesc": "5f2992a0-08a2-4638-83a2-11658c51681c,hKkdXrgEQ0SStVnZsXu0uQ", + "FriendlyName": "Book of Elseware Vol II* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f2992a0-08a2-4638-83a2-11658c51681c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:08.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2734, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "BwziYgoOMkSILcJf4wBllg.png", + "AvatarItemDesc": "3b30adc7-a1ad-46ff-96ff-08e656396c23,", + "FriendlyName": "Boombox*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b30adc7-a1ad-46ff-96ff-08e656396c23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:35.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1549, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0rwlmd8pei5yizua0rge6be77.png", + "AvatarItemDesc": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,", + "FriendlyName": "Bounty Hunter Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:00.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1932, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7hb7tjten2kt2wn72jc6b3djj.png", + "AvatarItemDesc": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,szDKGCS30UCwXJJcVocFDQ,3v_zUVDTUku_iEf-olDk1w", + "FriendlyName": "Bounty Hunter Belt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-01T21:38:20.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1550, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wi7442sqsy6n251dpxoai2q1.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,", + "FriendlyName": "Bounty Hunter Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:03.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1976, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e82j0afggi0d70h9zv4jq8z5q.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,Q3ctsvvDDEy_S1TDiGdf6w,DobTmYhC7EigHmvYnCTm3w", + "FriendlyName": "Bounty Hunter Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-29T17:07:39.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ayapiP7cAE2QSEcl72wYQw.png", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,2N0meQR4GEGu9Yy61C0Lwg", + "FriendlyName": "Bounty Hunter Cape (Western)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e773fddc-ad9f-408c-82c5-2637f75b3214", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:06.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1552, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1kkukibngh4tv7hnld1z6zmzk.png", + "AvatarItemDesc": "2ddd2c2b-596c-4022-a488-655b0db1c265,", + "FriendlyName": "Bounty Hunter Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ddd2c2b-596c-4022-a488-655b0db1c265", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:09.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1548, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3j8vqf9bf8mtlc1esp7pjg52y.png", + "AvatarItemDesc": "2fadddc1-babd-4cb3-8f5a-0e7c889c3785,", + "FriendlyName": "Bounty Hunter Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2fadddc1-babd-4cb3-8f5a-0e7c889c3785", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:39:57.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1551, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9sotdnybus40og7ig3za9imke.png", + "AvatarItemDesc": "9ffa6e06-5200-4e53-ac13-f8491aecc864,", + "FriendlyName": "Bounty Hunter Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ffa6e06-5200-4e53-ac13-f8491aecc864", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:07.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 42, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73yfl2xb8qwuws91gzirri5dd.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,", + "FriendlyName": "Bow (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 629, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3suvedjn7frrhme4rxyn56qtt.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,c9589974-c261-485e-b571-cb2f34fb178f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Bow (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:50.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 630, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8hxvd0blvnfweb21vtd3vyy7o.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:51.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 631, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2d53cleqqzz1la8h0m130s58q.png", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Bow (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1e504fee-b593-4037-bd8f-b88025147991", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:52.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1192, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ftlnff1zjo948sndiuh1x636.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,", + "FriendlyName": "Bowler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:16.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2206, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Z8BRM2D_akKVUKgLncneKA.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,YBpBFNAFGEunM5GX5lFxHg", + "FriendlyName": "Bowler Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:19.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2208, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "wztK_Auwy0SJicMfhG1M-w.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,VyqdOGaUnEa8WzccUIn48w", + "FriendlyName": "Bowler Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:24.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2207, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "w97a0JIhSkGDeHj7x9Tx8Q.png", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,Dvwz4YRu_ES_rl94y6FNBA", + "FriendlyName": "Bowler Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:22.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 812, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9mi7d2raqzh1s4hk8ya5hwblu.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,", + "FriendlyName": "Bowling Shirt (Turkey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:17.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 811, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5qcfg0698s80wf9u8vhxu0ykx.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,-k9_-2onrEG407or8t1RFw,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Hambone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:13.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 890, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uwainjpb5gco3zqlgd0ofvkm.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,GDvaUU10-EKh3pKUQ9tnag,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Runway)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:16.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 889, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73mnfpvyaf58qnzfmjq9gdmfp.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,kLyM-6otSUy2ql5SEmnF9Q,b-IM3eYMJUa40395O5XeLg", + "FriendlyName": "Bowling Shirt (Spared)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:15.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ewgjupwpbyyvetn7svdbg3pka.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,8xjU4GQ4OkaqGyad-ypwtw,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:20.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 888, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ib04jli4dwo8vm3pm3qmk46c.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,032pE8s9Ik-2CImFVDrSqg,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:13.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 887, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a8ua2rvt9gw4r4necvamkx7sz.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,M4765UHzsUeNTwH-Pcxp1g,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:12.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1161, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ddse4tp99yydkypotqc8d4hpg.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,FC8ZFBPt4EyTe7IYbv2Tpg,5RUG6i2Mx0-55IO-IlhUdQ", + "FriendlyName": "Bowling Shirt (Flames, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:03:06.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 814, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2sj414tpbb69wiylcanfba7e7.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,ghM-uFq5-0Ci38AOHa9zZA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Deadwood)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-04T00:45:22.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 886, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e46hwhpywl6eqefwj0lqe1mah.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,Gtq8yRlt1UeFW8U-sc7FiA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Brooklyn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:11.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 885, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0t1wg2mro2gwc3d4flp6re5pk.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,XrSL-v2MbkK0md-FMOweOA,IWg9wuda4kaCilJk8JQpTQ", + "FriendlyName": "Bowling Shirt (Sleeper)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:09.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 884, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "apdshw5awntmir8e59mptvuwj.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,KqjQ1xRpokGLKp9GVN1EFA,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Striker)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:07.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 883, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3lw7exr10ocq54wlzytnktw7h.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yrQCLXiqRUSwnuQRYX2Rsw,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:06.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 882, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f20xxqkrb5z55ft9ia6vtt5u4.png", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yZR5aX3gQ0WpRUT6cfK1cA,VO_3cgqQqUOTz30TVUxrFA", + "FriendlyName": "Bowling Shirt (Split)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "288a0b09-5ada-439f-bbae-14757ebd78f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:04.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 825, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2rglb359vpaaizusjkmzxzqiu.png", + "AvatarItemDesc": "GAWURQSqrEW4G5slrPMgZA,", + "FriendlyName": "Dude Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45940518-aa04-45ac-b81b-9b25acf32064", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:20.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 113, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0ua96avoonx4qcwxsrbdl8lqb.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,", + "FriendlyName": "Bowtie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 442, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1ipmq038lb20nmgpf0k7p3lj8.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:06.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 443, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ec965jtympxjh0juzh6uea66i.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:07.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 444, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ey39vqepkupw80nkatijbvxm6.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e69372eb-2fd8-4e17-a448-ee0be61a2c7a,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Bowtie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:08.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 648, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "7lwepk9pha13ajtddc8yjp6i5.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Bowtie (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:32.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1064, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "88tf66ieaq5s0d6ezi59mgoni.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e4IDo-fuH0C9YByzcX5k-A,qCIoeLQw-Uq-SKIetIgIpA", + "FriendlyName": "Soda Clerk Bow Tie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:35.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1250, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "holidays", + "ThumbnailImage": "6ypl01cto10al3qpkqm95fhe3.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,W4a144so90eRzCojUnDowA,rcuK1wZcXkCe0_LGX7E-Tw", + "FriendlyName": "Bow Tie (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:24.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2789, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DIaJORsIX0GaMSsklR3SxQ.png", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,wF-WrDaY_kmm1AzA20RsZw", + "FriendlyName": "Bow Tie (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:00.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,Q9TGwLgrgk2t8ba8WlpM1w", + "FriendlyName": "Bowtie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83be5ba4-525a-4781-a5f6-839c22e4d1d3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1726152636 + }, + { + "AvatarItemId": 1268, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bdv0ee6pbzi0i8z8f5n5zfc2n.png", + "AvatarItemDesc": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,", + "FriendlyName": "Bow Tie (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:47.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1615, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9oo75aubpgwd257zfkh94c7y8.png", + "AvatarItemDesc": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,Wm30oV5BtEuFQZFk3dPK_w,0R3Mh96tS0631EZYs03dmw", + "FriendlyName": "Bow Tie (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:50.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1354, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8z118afr4npaz1lqtevl4cq9f.png", + "AvatarItemDesc": "86af3a97-18d8-4dda-a2f2-cfd3b4224254,", + "FriendlyName": "Bow Tie (Green Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86af3a97-18d8-4dda-a2f2-cfd3b4224254", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:42.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1266, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8nu7ynhikwf10a7dnyr1bqmi7.png", + "AvatarItemDesc": "988e4069-4341-428b-bbf5-023309891385,", + "FriendlyName": "Bow Tie (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "988e4069-4341-428b-bbf5-023309891385", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:39.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0mfqwinkpsyr6llxo7yocofz3.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,", + "FriendlyName": "Boxing Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 437, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dwa5wuhnhdrvml7z2kour4vi.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "FriendlyName": "Boxing Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:05.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 881, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a9oypi8hweb517wrupg36vi1w.png", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,rzUd1IMTKUmW0HeMPSIOBQ,0ec6628c-a106-4452-af82-105a3e3a9f2e", + "FriendlyName": "Boxing Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff15e25d-d467-47a8-9ecc-e415d5acb90c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:03.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a39t7yom90yxeiwitw6n8iohp.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,", + "FriendlyName": "Boxing Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 438, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3tg5nlsq7otflej53cfartbv3.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "FriendlyName": "Boxing Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:07.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 932, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0to9e0lty6offdt6cnxeq1qpk.png", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,rzUd1IMTKUmW0HeMPSIOBQ,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b", + "FriendlyName": "Boxing Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b904304b-cce0-466f-8569-c621eeadd4f7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:08.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 43, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2c8gnq9h5a6jrn81ru4497q6i.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,", + "FriendlyName": "Boxing Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 430, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cwp28pmovv17mrz6o4w62ejpa.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "FriendlyName": "Boxing Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:57.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 918, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0oaynqpwbmosrcxdikxyfnnm7.png", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,rzUd1IMTKUmW0HeMPSIOBQ,92714eac-cc07-4f74-a6f5-cfbbadd07e06", + "FriendlyName": "Boxing Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:52.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2829, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "l6JiqgrjoU6Pe9nqG_A2Yw.png", + "AvatarItemDesc": "b9987d0c-0cbf-41dc-9a9c-8fdb5165800e,", + "FriendlyName": "Braided Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9987d0c-0cbf-41dc-9a9c-8fdb5165800e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-28T20:16:50.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 823, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9fef5u01k6k4fcki20zg26pey.png", + "AvatarItemDesc": "sL7iHCnjyEuQKzrTvciEzQ,", + "FriendlyName": "Bride of Frank Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1ce2beb0-e329-4bc8-902b-3ad3bdc884cd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:52:39.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 824, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "239w9hcx69t1gd8xuj8wathwz.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,", + "FriendlyName": "Bride of Frank Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:17.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1528, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6jn69aku0kec2denwy9axd25m.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,YTR8qybyVkOhNh4iqtU_UQ,H8LXGRJj8UOuH5dil8L9qw", + "FriendlyName": "Crimson Bride Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:15.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6sLaNK31602XAlV-PLUz2Q.png", + "AvatarItemDesc": "7VQvz9sbzUWvGalxd9rISg,zNBLaOhCAEKWvgLK4P_X7w", + "FriendlyName": "Mardi Gras Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf2f54ed-1bdb-45cd-af19-a97177dac84a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:10.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e7zfauqbveb04wiazlcnt3nna.png", + "AvatarItemDesc": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,", + "FriendlyName": "Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T20:59:28.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc,97f3e262-8bac-4238-9d85-3872f293f204", + "FriendlyName": "Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b16768f0-ce8b-4844-96f1-e38d63f9c1fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1497412622 + }, + { + "AvatarItemId": 2290, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "AlSw_umA9E-_tWoN6PsFsg.png", + "AvatarItemDesc": "aeda4e5d-9cfc-44ec-b325-ab8b2d906a8f,", + "FriendlyName": "Bullwhip Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aeda4e5d-9cfc-44ec-b325-ab8b2d906a8f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:35.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 622, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1m40flxsxhcbfatcc8mhbk1kx.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,", + "FriendlyName": "Bunny Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-03-27T21:42:46.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1950, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4o8g6zqfewfnr504cvcbano60.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,wZC46s7Id0CUICN_XOK-Rg,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:30.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1951, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a0g5v9gzs5d8fpdvoeh8rvqbm.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,yrR3ogjJ50q6ypAnCF-ctQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:34.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1952, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b45xlo71b6cuovl9no7k2cacs.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,ss931x_Hy0-y1au-fT3zTQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:41.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1953, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "95hz7svaey618hpsf9c4or7xo.png", + "AvatarItemDesc": "71fd5353-b439-4a23-b471-bfb99b8154d7,UQfLaKHzo0WqlRul_O61xQ,knKB2ETYmUupbvBwlleZRw", + "FriendlyName": "Bunny Ears (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "71fd5353-b439-4a23-b471-bfb99b8154d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:14:44.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 454, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e0ix17ip3n8ghjd6zp8tpdsz1.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:19.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 455, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "47orvw4rgb2dcq2uysth7k5kz.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:20.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 456, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9govgvnlf9gabvjvh5rckwqt2.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:21.503Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 457, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1eq0w8wx46ey92gsw096z68mj.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:22.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 458, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "863juzzox17unyjhagvnbslfh.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Business Tie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:23.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 649, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dftbch1qkdqq457hq33rhhz9h.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,0809a5cf-a0f3-4614-9f98-71518524518a,8212812b-e7e7-4f6d-aa27-d1112af45473", + "FriendlyName": "Business Tie (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:33.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 968, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7x53ntzd6jkkh7xk2h9lwypz0.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,31jelNWFyk6FlWt4HyPAlw,9zGwmle_yk25V-KnC_PJEg", + "FriendlyName": "Business Tie (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:13:07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1251, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "holidays", + "ThumbnailImage": "22ppyftjkt057kgxnmn84jhax.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,W4a144so90eRzCojUnDowA,pHw_MghIYUazSFve6L237w", + "FriendlyName": "Business Tie (Candy Cane)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:28.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1902, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "f3b04h0f6kp8xvajy1fhvj8se.png", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,3svqbYmGt0uyVWnx7vJNmw,iLo1YhFY-06HNWgM6gkB7g", + "FriendlyName": "Business Tie (Neighborly)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6c08eb5-381a-4193-9722-80da95d62abe", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:30:59.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1362, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7l8qp4zd4ankofzt9s3j5k8c6.png", + "AvatarItemDesc": "fc96ea5d-b49b-479b-a963-92f1eab44b2c,", + "FriendlyName": "Butterfly Wings (Cool Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc96ea5d-b49b-479b-a963-92f1eab44b2c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:10.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2105, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ypuue6OM0Si-gMkXnm_LA.png", + "AvatarItemDesc": "fc96ea5d-b49b-479b-a963-92f1eab44b2c,opDwtb3OdESsA2Zpfx7luQ", + "FriendlyName": "Moth Wings (Pepper)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc96ea5d-b49b-479b-a963-92f1eab44b2c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:17.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1717, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "345iaeu3y6o2ptwgo6fuggdrw.png", + "AvatarItemDesc": "ec5f5089-7154-425b-8be2-743954ea2d69,", + "FriendlyName": "Butterfly Wings (Crystal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec5f5089-7154-425b-8be2-743954ea2d69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:55.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1364, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "99uel8r3j0b7wyrporms60tok.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,", + "FriendlyName": "Butterfly Wings (Monarch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:36.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1591, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1kwmi22q1osg0qzg3f3truukf.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,PTqpWqqU9k--lNlRrmlVbA,yViVKOLHc0qLiPw9Oy1Obg", + "FriendlyName": "Butterfly Wings (Candy Agrias)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:47.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1593, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4rdnvl3uo6e2xmx16jac16cg2.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,nODTglESMEC9tBY1k-LrsA,Do__oD53x0-dNtVeHsr4yA", + "FriendlyName": "Butterfly Wings (Midnight Gossamer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:15:33.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3kupnkadfcqst9wcpz79lvelg.png", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,1qHXl55Qn0qSb1gAhr8Wjg,GlB_scM-5kuc4A5TmGe6Yw", + "FriendlyName": "Butterfly Wings (Frontier Bluebottle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:15:25.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,10f0c6ce-319d-4231-ae88-446d33eead54", + "FriendlyName": "Butterfly Wings Monarch Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c506feae-be73-437b-b18a-1152bb9906e9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1092022807 + }, + { + "AvatarItemId": 1363, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4mbd5lxxwtmjvp5enggelazu8.png", + "AvatarItemDesc": "27c2eb7f-9fac-4c42-90c7-44e7a33d5303,", + "FriendlyName": "Butterfly Wings (The Blue One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27c2eb7f-9fac-4c42-90c7-44e7a33d5303", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:30:31.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "620934cd-aa73-42ec-97cd-65dd9eed1226,", + "FriendlyName": "Camera Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "620934cd-aa73-42ec-97cd-65dd9eed1226", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1929411673 + }, + { + "AvatarItemId": 2055, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "oROtFcbPVUKgu5jyR4cmxQ.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,", + "FriendlyName": "Oversized Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:43.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "rf7qGqus3keA_hz_CqguUw.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,bIQKJloWV0SEnBEtoxP_Wg", + "FriendlyName": "Candy Corn Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:28.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2387, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4wbSOraJ9UqV8L-NG1fvjA.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,kKpiR8YM8UG7xcaIgS9aXA", + "FriendlyName": "Oversized Cardigan (Forbidden)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:32.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2586, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2-BK_tv4akuy0tNJWv2PRA.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,W1LdNEXmX0m72Wr7EjJ_gQ", + "FriendlyName": "Oversized Cardigan (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:43:52.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1802535203, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pkxpJOu830etcGZdkiTlFw.png", + "AvatarItemDesc": "e670df07-f9c8-42d3-abdc-48b4ca0257aa,oQspLDUDxEaiudqzXY7gbg", + "FriendlyName": "Oversized Cardigan (Painted Egg)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e670df07-f9c8-42d3-abdc-48b4ca0257aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1320636581 + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,7d025ccf-f3b9-4601-9dba-9f25b135d11e", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1033755188 + }, + { + "AvatarItemDesc": "264fd0c2-88f9-4780-ad72-52351e31b13f,2d035c24-8437-4403-a024-4c3bfd0d80a9", + "FriendlyName": "Casual Cardigan Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "264fd0c2-88f9-4780-ad72-52351e31b13f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1774245390 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1665457670 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,e5172c0a-3099-4e61-a181-f6b114b0a608", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1632878111 + }, + { + "AvatarItemDesc": "86694be4-17da-40bc-945b-044d0e735bd8,bab8193c-f67b-4022-834a-597cbf5dc7dc", + "FriendlyName": "Casual Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86694be4-17da-40bc-945b-044d0e735bd8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1186975696 + }, + { + "AvatarItemDesc": "5929bc1e-7ead-4818-a9ef-8e6d3bc885b3,", + "FriendlyName": "Casual Necklaces Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5929bc1e-7ead-4818-a9ef-8e6d3bc885b3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1738196188 + }, + { + "AvatarItemId": 2371, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "ZImIysD_H0S8AXDFmb2-vw.png", + "AvatarItemDesc": "1fab9cd8-257e-4825-8b7d-bd8bd281d362,", + "FriendlyName": "Science Shovel (Invasion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fab9cd8-257e-4825-8b7d-bd8bd281d362", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:26.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2700, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "n4mGv9jTj0GUDOgF6nFXVA.png", + "AvatarItemDesc": "1fab9cd8-257e-4825-8b7d-bd8bd281d362,q9hshPgwT0ijshnbKEBKWw", + "FriendlyName": "Science Shovel (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fab9cd8-257e-4825-8b7d-bd8bd281d362", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:31.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2086, + "AvatarItemType": 0, + "Tooltip": "This item may come in handy at concerts, afterparties, or in dark, forbidden lairs...", + "Rarity": 50, + "ThumbnailImage": "TU5LjIU2P0WNnRai4I1qHQ.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,", + "FriendlyName": "Cat Ear Headphones (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:05.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2098, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QRxUhTvrak6Pv3s0nde-8w.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,etbo7k9dq0OyjIrNS4sdkQ", + "FriendlyName": "Cat Ear Headphones (Neon Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:02.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2097, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ov9bwV4XmEuoyMkR3zIVgg.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,7Ww7mumWXkiyj0R3u_-hrA", + "FriendlyName": "Cat Ear Headphones (Glitch Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:02.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1207385977, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Cr0h8X1hoUuexTHM501mLA.png", + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,8br3f77_CEiE2Oetiof1Pw", + "FriendlyName": "Cat Ear Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012,8606473d-8d58-4e93-8123-3d6cd1d47ad4", + "FriendlyName": "Cat Ear Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c9df7cc-2ccf-4d5b-8116-2d8d6c839012", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1214875831 + }, + { + "AvatarItemId": 44, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a78j3mr78eeqcg6enr7t2usio.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,", + "FriendlyName": "Cat Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 712, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ee3wf3szcjj03dklm3r2yu3qx.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,37f8378b-b98c-477a-b8c9-9340772800b5,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:11.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 713, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e4o0sq9fto4a26lfver3ezwyq.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,713781e5-ce47-41c6-a24f-94d42a565e30,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:12.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 714, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cs7qiwunx61p8789gg5w4alhp.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:14.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 715, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9k2zja3ouictarzennr87c6lx.png", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,c1e430c5-fd8d-4032-9e3f-8d87cc8784da,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69", + "FriendlyName": "Cat Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d5184e1-0201-4476-bf4b-4eb28190de62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:15.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1750, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "18xiejb6mr7ln2d5ix9xk4i7f.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,", + "FriendlyName": "Alley Cat Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:28.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2174, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "319gh5kstwntozibq3ylve24n.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,dr6MrV8O30G0i-c7peOoKQ", + "FriendlyName": "Alley Cat Ears (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:25.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2173, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "432bfm0lzuskgxgwj4id7tebe.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,UcK6aipko0iE4THJUO1TvA", + "FriendlyName": "Alley Cat Ears (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:23.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2175, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "10khixt04h6x1ktssnlbwt88h.png", + "AvatarItemDesc": "7ea796bf-f552-4da8-8f68-a87c4896c8e6,9yNuA59JO0avaKVA6YxQyA", + "FriendlyName": "Alley Cat Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ea796bf-f552-4da8-8f68-a87c4896c8e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:27.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1716, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7c9vakaf9hozc3kfkfgvgows1.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,", + "FriendlyName": "Cat Hat (Calico)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:20.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1715, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "825i9txrb63ysexwqx6avbebh.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,V1M2-FXZSEG782d6D0AhUg", + "FriendlyName": "Cat Hat (Burmese)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1714, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4c5dywgjhkaf3fk8o08x91iz8.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,wNde5lsob06eQxEH2t-zUw,5KeLrnvaKEWnEiap1xzmjw", + "FriendlyName": "Cat Hat (Tabby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:33:14.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1942, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5xbgkkfn1legwnjtvtt5uvco3.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,T-JZqXUd3kC1P8L5Pu4ZmA,ao-eeUnb8ECcW2IFGuk76w", + "FriendlyName": "Tiger Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:27:29.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2479, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GsDtJeTdtUqMwfH0lH8bfg.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,NaXiRmxgkkGK1U7IQ4yn8w", + "FriendlyName": "Grinning Cat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:24.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2480, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HbIV9ocBIE6QOVO0wQaSrA.png", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,VeZQlPI72E6e57IQqAOq6w", + "FriendlyName": "Grinning Cat Hat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:27.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2467, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ufrfw4K9xkuYsOmmzZdOVg.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,", + "FriendlyName": "Chainsaw", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:13.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2470, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U51KTR3VHEuTJ810dMgdVA.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,qWS_znZq2km_MUGvv5QDHg", + "FriendlyName": "Red Chainsaw", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:22.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2469, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01j_eNlSvEmV2Czfp-GEwA.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,jCfhcP5ftk6_ruDnFjIhJA", + "FriendlyName": "Chainsaw (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:18.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2426, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DB_j3Vtx0U-xhO9yu4xlIw.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,5p1Hwa8IE0KIuucb7hV16w", + "FriendlyName": "Chainsaw (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:27.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2468, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T6Ig5zJQmkWoduzZdbHTKw.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,ZqWSyRnr9ECIvoU5yFyqXQ", + "FriendlyName": "Chainsaw (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:22:16.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1852235451, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "6uJcUEMxpEG8ljDFGcByMg.png", + "AvatarItemDesc": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739,odtEahcf10isGbVQCyc2bg", + "FriendlyName": "Science Chainsaw (Restoration)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6ffb2d7e-d4ee-4de0-9ab8-b5eb093b9739", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1982845644, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "a03e9576-acfc-48d1-82e4-4d320d151a50,", + "FriendlyName": "Chameleon Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a03e9576-acfc-48d1-82e4-4d320d151a50", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 142, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3josy4gpalnfb4bgbv15krqjb.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,", + "FriendlyName": "Cheerful Dress (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 431, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bwdc3w9brd906m9tlr3iwesz3.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:39:59.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 432, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1hw2ja5gln6hu18squbodnpaa.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,faedf215-289b-40f7-90cc-f5b98517d133,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:00.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 433, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aseuozf59hxlehklnyniqbre9.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,1773d5d8-4714-4721-b30c-97e25d6f2a5a,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:01.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1598, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8z3e22ju1mrd78t30pzbm5z5l.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,05K7dmjigkCByO2ufw9jtw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:05.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1599, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "56blq2erf400cehs8x4zcnwrf.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,yOw1WqEN0EWqR9t53EsQpw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:08.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1600, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cs5izn6ddok4ol512yitiw3cs.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,qoVZuGa5NkOmyfikHjDhXw,93987d32-7a6e-45df-af56-76f912969ce7", + "FriendlyName": "Cheerful Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:12.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1366, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eum5bolzoy8k5w31k9j0rtzcj.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,yDZFr27Zq0-JH-EsEsxG2g,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:44.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1367, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eppwkjhvfbuczv31sgbty26v0.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,PLRhiOhhb0KhatFH7f3pwA,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:50.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1368, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8q1o0y04vs1ve8bpxz6iqkwui.png", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,RwgnKTfMEkWrAp_OMGy__Q,F9vYkdcuGUOSGEg0wpQt2Q", + "FriendlyName": "Spring Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b02053b3-013f-42b0-8881-85f5be2b8cda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:09:56.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1137, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e5wnr991h4k0p24z7l5fzcchr.png", + "AvatarItemDesc": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,", + "FriendlyName": "Chef Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1573, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6da55yu9ap4a2qw81ndsgake8.png", + "AvatarItemDesc": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,PR5dh2jf2kqGLO0lXgA69A,-lYK7LhYyEy6wqL1tIE9cg", + "FriendlyName": "Chef Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:48.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1141, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5bz83i3j02o47w4zlz433lomz.png", + "AvatarItemDesc": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,", + "FriendlyName": "Chef Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:35.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1574, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9lxxqrqcchmxcnzz3uf7de4kv.png", + "AvatarItemDesc": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,PR5dh2jf2kqGLO0lXgA69A,VDn1hO6ahky0GDJt5dRmyg", + "FriendlyName": "Chef Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:55.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ogxBM3YvhUu3E19PxYAYwQ.png", + "AvatarItemDesc": "9f2cf07e-f98f-48bc-9c74-784ba4b94527,", + "FriendlyName": "Chess Knight Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9f2cf07e-f98f-48bc-9c74-784ba4b94527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:15:33.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ZkEpc01eUGaW4hzohQxlw.png", + "AvatarItemDesc": "c2362f96-0a73-451b-9a85-46410eefd2ce,", + "FriendlyName": "Chess Knight Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2362f96-0a73-451b-9a85-46410eefd2ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:15:35.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1751, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "az5ok7syap6nxgjegr2yu68bu.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,", + "FriendlyName": "Chicken Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:41.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2500, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_inbDi877kyEOl5VH0d_AQ.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,wl3nqp4FsU2wqEkYFqv3_Q", + "FriendlyName": "Chicken Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:44.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2499, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PO9FzkR9AkerfmOa9FPB6w.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,4v1-TIIgnU-sg5ROppnEPQ", + "FriendlyName": "Chicken Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:42.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2501, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Gh8G1oJ4Zkmt6qsBqaj_XA.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,AvIuVibXj0u7UzWFh90_LA", + "FriendlyName": "Pink Chicken Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:46.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2502, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aRh-bOy1702NIh5iAUUksg.png", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,fF9qwnew3Eao2gF0xcOqrw", + "FriendlyName": "Chicken Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fc6a560a-5ff1-46f6-a593-320c50ee397e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:48.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1299568996, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PPwy0yri-EypHFSutVm7-A.png", + "AvatarItemDesc": "0058a7c6-04ca-4931-b2ff-03c46f99079f,", + "FriendlyName": "Eggshell Chick Beanie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0058a7c6-04ca-4931-b2ff-03c46f99079f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.497778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2870, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DI6H0HT2tUqy7jNzWn9WDg.png", + "AvatarItemDesc": "6a51d8e1-8844-4999-ad8b-06f4d414571e,", + "FriendlyName": "Eggshell Chick Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a51d8e1-8844-4999-ad8b-06f4d414571e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:30.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2871, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "iF4tjDkdYEe3NUEZrY7znA.png", + "AvatarItemDesc": "9eb95934-90f3-4f48-a6b4-d87d75d9bdc4,", + "FriendlyName": "Eggshell Chick Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb95934-90f3-4f48-a6b4-d87d75d9bdc4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:34.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1965, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5gq339wje3p8y6ppfyku4gwyt.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,", + "FriendlyName": "Chunky Scarf (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:14.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1966, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "015zn2g9spl2iit7pplji0g62.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,RvVVu7gWUEmofuJiZqevlg,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:19.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1967, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a3r79cjrswbxqhnvo4sy9xfav.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,g4_xqu9o9US9lIiBsn2BKQ,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Olive)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:22.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1968, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6cyfecuvuiz6xxc0mom9o8a12.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,09zFwIT2AEaQsijLJR2aSw,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Beige)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:25.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1969, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2wll5s9pcyyfdicg3i1sgsbf4.png", + "AvatarItemDesc": "3ad53d67-e89e-47eb-94ee-bbd054652815,mtIyC_me2EuHSur1ghsKDA,i0itjI0qz0Cdn-es5NBScw", + "FriendlyName": "Chunky Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ad53d67-e89e-47eb-94ee-bbd054652815", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:19:27.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1891, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6qgeth2im91d0g6kce2zm8k7h.png", + "AvatarItemDesc": "b1ca1dc4-6867-48b6-a332-4efff06a5db1,", + "FriendlyName": "Cleric Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ca1dc4-6867-48b6-a332-4efff06a5db1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:10.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2yzkfucefna1jy3vuypzb7b3y.png", + "AvatarItemDesc": "b73ae23b-c911-4b09-a803-40999bdef6b8,", + "FriendlyName": "Cleric Headpiece", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b73ae23b-c911-4b09-a803-40999bdef6b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:15.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1893, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ugmcn0xr6htk34ah2hhep52p.png", + "AvatarItemDesc": "1bd891b1-6ec0-4227-9ed8-73c59af4f819,", + "FriendlyName": "Cleric Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1bd891b1-6ec0-4227-9ed8-73c59af4f819", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:25.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 45, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2q5g72y38c6wxtl9ob1g6osjo.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,", + "FriendlyName": "Cloche (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1634, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3ft3mh0blr3stuvbs8p0kyll2.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,w9I-I0GWRE-3RgOUMMlHjg,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:43.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1635, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8dwxff103s58nebb8y2kbn194.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,bmi_S166JEGWhf_7woVL0A,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:45.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1636, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "b8f53f91dy30khotxt5f1ktnl.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,dSEiNUAZnUmErO5Vfv51zg,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:44:54.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1637, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0sm192b6zl94b0y64i6sh5wpn.png", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,jTG-jnKn0k-Etg_-4Gb9gw,WWmx0nBo3k2nXUPPRJVJLQ", + "FriendlyName": "Cloche (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a16d59da-caa5-4388-af67-315bfcb16f64", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:01.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2473, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zNy5rjlaD06F8Uq9ifQuGg.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,", + "FriendlyName": "Clockwork Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:15.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2474, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0LdcFFwAQkSh1FSiaDwp6Q.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,0EjPIiuYpEGIWup3pKeC8g", + "FriendlyName": "Clockwork Shoulder Bag (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:17.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2476, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vd7QP1vfykeRmhB2_5D88g.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,dkJNh15DnEuO1BatzpI_uw", + "FriendlyName": "Clockwork Shoulder Bag (Rose/Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:24.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2427, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XOVkcyCEQUWRJU73kORscw.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,XMAOwHGXVECoeKoWb_-luw", + "FriendlyName": "Clockwork Shoulder Bag (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:32.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2475, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "A6d5UocVhE-gR4dNG396UA.png", + "AvatarItemDesc": "a35b6774-9245-426d-ac9e-70154679faec,GAWBaCDBzUqdyTGA-HPczQ", + "FriendlyName": "Brown Clockwork Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a35b6774-9245-426d-ac9e-70154679faec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:23:22.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2025, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ES_b9-XkfEGzWShw1v1EMg.png", + "AvatarItemDesc": "06adb976-ca7f-4b8b-8b72-2e03c8bc10b3,", + "FriendlyName": "Clover Head Bopper", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06adb976-ca7f-4b8b-8b72-2e03c8bc10b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:18.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "aeadad3b-b83a-442c-829a-3eaccd9726fd,", + "FriendlyName": "Clown Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "aeadad3b-b83a-442c-829a-3eaccd9726fd", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1199232272 + }, + { + "AvatarItemDesc": "6dcc5a53-621e-4f8c-97ed-e7fe42fc86c2,", + "FriendlyName": "Clown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6dcc5a53-621e-4f8c-97ed-e7fe42fc86c2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1491263048 + }, + { + "AvatarItemId": 729, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bjg8u844kgyz1lqasebmtopqf.png", + "AvatarItemDesc": "TcgHS_0aPkehrBg7ytMOMQ,", + "FriendlyName": "Clubmaster Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b07c84d-1afd-473e-a1ac-183bcad30e31", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-11T22:30:20.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1748, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6rf1m7pd44oq6kn8h6yf9xeqj.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,", + "FriendlyName": "Overcoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-29T20:43:55.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2213, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fZkRsxu0GEq0HJicKi0Gxg.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,_3hxdSy9wU6OUPZJA9kGmA", + "FriendlyName": "Brown Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:32.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1096982540, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FSw9DUpPzECdOB8T9OOx4g.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,B5t5RbzKdEObKDlXLcRp7Q", + "FriendlyName": "Pastel Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1199022146, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QqL58lbLCkSvxjPln1rEsQ.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,OaOC278jEE-owjtDP9ZLow", + "FriendlyName": "Pastel Yellow Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "G1k3m0JjFkWpKsbWb3D4eQ.png", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,J36t8RwGPUqtLBsF-_UtEw", + "FriendlyName": "Pastel Purple Turtleneck Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35d67043-ca27-4b8a-a893-2d42674e6e68", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1010, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4t0t0evfwd04sjvdwb35fvj5n.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,ojvIFCTpJkeegestF065wA,10fYSIBytk-yUjveDa407w", + "FriendlyName": "Collared Shirt (Plaid)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:16:36.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 158, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7k5gf8gh1bklveyos3l1ydl68.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,071d1bf3-86dc-4a31-b54a-e6579fab8649,d015cae7-a905-49e4-8823-6dec069689a6", + "FriendlyName": "Collared Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 166, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "b3stfwojthp7css2c8xqi5n5y.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,48abd952-214f-48b2-a8f1-1146f6f69aa2,c2a91a10-2f5b-4ab0-a79f-51408cf4b047", + "FriendlyName": "Referee Shirt (White Collar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1042, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "29urcstxxxjo9a30k66pvi43j.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,48abd952-214f-48b2-a8f1-1146f6f69aa2,RNN0ejjlm0yqGDcEuUkLjw", + "FriendlyName": "Referee Shirt (Black Collar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-03-11T21:45:58.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2026, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cjm3isfczhcb0g2sceven3lwz.png", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,n4SdiVms4kaqdjXdbjRBbw,YEMLBlOWsEO4tELHseZ7LA", + "FriendlyName": "Referee Shirt (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:22.573Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 656, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "57y04gj7sxv3ffgs9mbnww3wk.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,", + "FriendlyName": "Conductor Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:17.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1553, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "augnnz7p6ycztoyhc5w3sitrn.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,jwXdjDbEmkOFyKZHT_RYOw,YJGsYfwkik2lp7qWqXyXTw", + "FriendlyName": "Conductor Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:48.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1562, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f11gnls9uil36r3thydrn8hk1.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,LdKaVAveokmp3qRwVBxnFA,YJGsYfwkik2lp7qWqXyXTw", + "FriendlyName": "Conductor Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:34.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2263, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6-G1yAklOU-Vz5BRJzhaAw.png", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,e-oZt-PbKEOfawVammBQTA", + "FriendlyName": "Conductor Hat (Striped)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07205057-0804-41de-abe4-bd4027fee1df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:12.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 657, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1xwbdqjl5uws8zmxy544nzuk6.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,", + "FriendlyName": "Conductor Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:19.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1554, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6hvy6jdqfz4iasmq7xqwcfpgr.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,Chiz8maLoEGDpZOJZEtdQg,cQUquXMk5EGt0VUCuPKBgg", + "FriendlyName": "Conductor Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:40:56.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1561, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "16dvdrgwodptixm96hemhraul.png", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,2axtF2iOGkez1kfRRGUbYw,cQUquXMk5EGt0VUCuPKBgg", + "FriendlyName": "Conductor Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e60fccda-8711-463d-b8a3-7cd5e76903b2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:21.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1373, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ptrsoi2msj3q9zp0nv3opjho.png", + "AvatarItemDesc": "5154be09-bb8a-4917-95a0-51c91b6a4e09,", + "FriendlyName": "Hatter Suit (Wonderland Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5154be09-bb8a-4917-95a0-51c91b6a4e09", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:11:11.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 658, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3jwqqbdhd5j68gzti0f3ov9d8.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,", + "FriendlyName": "Conductor Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-02T20:48:21.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1555, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "btsztyyman7ydhbo8ch7havr5.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,5Dbc8qCaY0OXEuwykM4naA,awWjleyNP0akWNYJoehCLA", + "FriendlyName": "Conductor Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-20T23:41:02.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1560, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1rj6j38s3tlgfuz9dlk7rlcbs.png", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,HBt3ToOkUUereoPKwyvzuw,awWjleyNP0akWNYJoehCLA", + "FriendlyName": "Conductor Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fc7aaef-adec-4534-8f91-445e4e7095d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:56:18.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "asrlf9nb0mfzjx6vbj87jqc2z.png", + "AvatarItemDesc": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,", + "FriendlyName": "Construction Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1914259171, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "2F2USYfWgkewvbwK6oa1WA.png", + "AvatarItemDesc": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a,uym5X1GZgUSzVb8U-pwlIw", + "FriendlyName": "Science Hard Hat (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8263a9ca-8c57-4e81-b3e3-24f2ca19a70a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 223, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7qj8vsrrhy3lfmq74f8h4kp4t.png", + "AvatarItemDesc": "e9913b06-58cb-4b90-bd08-981a3f19f352,", + "FriendlyName": "Construction Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e9913b06-58cb-4b90-bd08-981a3f19f352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1651838298, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "pfFTo8CNSEWIT3J-S7fQHg.png", + "AvatarItemDesc": "e9913b06-58cb-4b90-bd08-981a3f19f352,QXtBrjnRckKJGiRtsy4AVQ", + "FriendlyName": "Science Hi-Viz Vest (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e9913b06-58cb-4b90-bd08-981a3f19f352", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 270, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4w02bnkzhi1ylgnq3yy60xyag.png", + "AvatarItemDesc": "3bb50a88-09ae-48c7-a42c-df5879680eff,", + "FriendlyName": "Construction Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bb50a88-09ae-48c7-a42c-df5879680eff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 109, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5xt4rvf95k2jm6bpmmwurfs07.png", + "AvatarItemDesc": "3e139dd9-4757-43a8-8546-8da70dc17f0a,", + "FriendlyName": "Ranger Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e139dd9-4757-43a8-8546-8da70dc17f0a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1921, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b8r15kxxhpkjnd8grlinjcsqd.png", + "AvatarItemDesc": "f2db0ec1-e536-4218-8385-dc6913941880,", + "FriendlyName": "Cosmic Hero Gauntlet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f2db0ec1-e536-4218-8385-dc6913941880", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:06.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1923, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5m4wvktjw7xe4tdyw0cs3p4wk.png", + "AvatarItemDesc": "3739a9e1-6dfa-45d2-af54-cc83a58d436a,", + "FriendlyName": "Cosmic Hero Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3739a9e1-6dfa-45d2-af54-cc83a58d436a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:15.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2743, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6daz2-CA9keE0A25Vugkvw.png", + "AvatarItemDesc": "3739a9e1-6dfa-45d2-af54-cc83a58d436a,GpYghupLTUK9cuiL-0uHYg", + "FriendlyName": "Cosmic Hero Vest (Rhythm and Rooms Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3739a9e1-6dfa-45d2-af54-cc83a58d436a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:55.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2005, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ews3jw7aorj2ii9jvvnd7rr81.png", + "AvatarItemDesc": "19984948-b6b9-4d3b-8562-d0624930df6e,", + "FriendlyName": "Alloy Armor (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19984948-b6b9-4d3b-8562-d0624930df6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:12.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2099, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "o9dqbqFcv0SWG6AF4UzpWA.png", + "AvatarItemDesc": "19984948-b6b9-4d3b-8562-d0624930df6e,Owi_JROR80-apNHw-4KNcg", + "FriendlyName": "Alloy Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19984948-b6b9-4d3b-8562-d0624930df6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:05.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202152364, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "11462b96-dd62-4057-a187-49d8322c009a,", + "FriendlyName": "Costume Tank Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11462b96-dd62-4057-a187-49d8322c009a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.498777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 832, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "clebbebesxjzf8wnllqxmejvg.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,", + "FriendlyName": "Aristocrat Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:47.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 841, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "80vyoqh3t9fmtu13lcn8r3h87.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,i5RKLo-RbEqisnGII1wpaQ,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:46.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 842, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "40s4vjnqn68azxmiparci9gtm.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,IzkFARlv6EenmrfcNkcOuw,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:47.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 853, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3ky2r4usf4c9jpd0da9kxr9az.png", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,LyVf9lnzdku1F4iPBGqk-g,xnxDimLvVUCI9tEJqWDDcA", + "FriendlyName": "Aristocrat Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c56a2b40-d8bb-463a-a7ef-d605ba01ff0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:58.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 834, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "axv14g3ocxvf01h82ybehl3rg.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,", + "FriendlyName": "Aristocrat Cuff (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:49.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 845, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9zr3klyo948zva87e3ynyoij8.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,i5RKLo-RbEqisnGII1wpaQ,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:51.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 846, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01xxmh8nnuu5xc4oom2f1r1qj.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,jwEB6LyEaEekqtPNRc6TVA,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:52.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 854, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "due9stf9zpfkmdoblb2wlfcuk.png", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,Wj7I2YiVoE6zp8J_HYs8tw,8kdJkmwNM0WQmMVrb8fK8A", + "FriendlyName": "Aristocrat Cuff (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db28d7b7-7a9d-4b9e-893b-82c8240d5b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:59.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 46, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4jywgtwnb8sydqwmy9qd6aszg.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,", + "FriendlyName": "Cowboy Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 47, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0sgisiuk6yb2x3qtliioxly31.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,624c85e5-095a-48b6-85fb-c9bbade19a8d,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 48, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d9rwabvsu3fo05lmoaqnv6udq.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,192ba73d-6990-42da-8fe0-914c2a7181eb,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 49, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ioaonefjjjvtyvufi87rptrp.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 917, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5p88yvfhaz0xqx9u944krzm90.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,teVKVbVEsEyqOSuRdqHQzA,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:51.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 916, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "55ecravwda31t7zdi490k0t6u.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,K5vPqD3BrEuODRDzFdC6Hw,54778d93-2cff-48db-b25b-65c08b0e5be8", + "FriendlyName": "Cowboy Hat (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:50.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2229, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hFGaKfD6BkyXWpL9Z29Htw.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,_6LmNp0uX0Ky4jp2j62U2w", + "FriendlyName": "Cowboy Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:54.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2230, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2nwp59be1lvbayv88iqb1rr4k.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,cNarx7yecE2gk-7mYaoHwA", + "FriendlyName": "Cowboy Hat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:00.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2308, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YisZ-46Na02k8dv191Spew.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,AEqCasEqOUCgetoTwuMTug", + "FriendlyName": "Science Hat (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:59.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2701, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0b-BFgiGEEer1cPxNaI-YQ.png", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,zoUmQIcOrU6jgBu_0urrGA", + "FriendlyName": "Science Hat (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad0bdec5-f767-42e7-99e0-221473464a85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:32.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 168, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7p83fzhqxxbxo8vh3t9otc9e1.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,", + "FriendlyName": "Cowboy Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 169, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ev3mq9lpdefdivq6ayo76ak58.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,192ba73d-6990-42da-8fe0-914c2a7181eb,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2l9uggmozvea8gtpv9ytjfglp.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "digjcfruws97b6pe7orkqjunq.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,624c85e5-095a-48b6-85fb-c9bbade19a8d,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 898, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dg66aywux3nkmv6q4rwlf4uwy.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,teVKVbVEsEyqOSuRdqHQzA,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:27.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 897, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7bsroab0vzswtlgqzzd5mijnk.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,K5vPqD3BrEuODRDzFdC6Hw,98553538-cafd-4df9-8f37-bb0a4bb478a5", + "FriendlyName": "Cowboy Vest (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:26.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2225, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RxR8AFtkNky4E7NUQNbCCQ.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,ODOnI3iYoEKrlQfFabbvoQ", + "FriendlyName": "Cowboy Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:11.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2226, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kxjJxDcwh0u3MPgS6XdCzA.png", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,BW5oLDA36EK9nLzTm7xViQ", + "FriendlyName": "Cowboy Vest (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:25.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 271, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "aq9fxwenux4k6wi50q7r0yqm9.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,", + "FriendlyName": "Cowboy Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 272, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "408o1nfsbf7uwx8ji3868l7j4.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,17e44296-1cd9-4f8e-9aa7-3837dc69279a,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 273, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "74jyglxoijr47llzzcuyhnhe6.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,c4ebfd1a-fcda-4b98-a151-a941b1801867,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 274, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2abrokbv6i5wmyx0dj57x43zo.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,c0c33a35-494c-4a1a-aa53-d27d9b9ef5a4,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 933, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5lr5c7ooc5e1x5c8f29zbk048.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,wat8f6fs80KptZbW39e8tA,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:09.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 934, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b8w1fmsevjv9eaqf2yjooy8ta.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,wkqKPZmnoU-8TXSWzi2-Ow,8eef8837-9141-4088-835a-b2f47f4126f8", + "FriendlyName": "Cowboy Gloves (Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:10.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2234, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "fGnMN9gdtUWyx2MEQZbOrQ.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,tOlpTL8F1Ui_akCjHq5vRg", + "FriendlyName": "Cowboy Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:26.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2235, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "qy6K5v706kGJUmNb4oGH0A.png", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,rfIzQjxeRU60VdRQXG9Ccw", + "FriendlyName": "Cowboy Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9eb7f505-5afd-4b28-9886-7996d38c59c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:36.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 957, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6ck4di8cgqlprwg40uowlar1a.png", + "AvatarItemDesc": "vpP2ZvnL0Uy3n-I_Jv3eeQ,", + "FriendlyName": "Cowboy Hat (Hairy - Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66f693be-cbf9-4cd1-b79f-e23f26fdde79", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:09.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 115, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "21rygk1c1r8ccciu1jlzdsan4.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,", + "FriendlyName": "Bandana (Flowers, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 445, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "72m3n2l0f0v5ryummqatwer5t.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:10.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 446, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8pn1j2wbx9ivldclxa1ubgufx.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:11.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 447, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1prcefvfnwi7yewdgbbeapboi.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:12.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1575, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7756o1qqkod0e7anndh2zr867.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Cowboy Scarf Mask (Plaid/Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:45.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1576, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "37j4pw9ip3kccqd36l4be3uw6.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Black Polka Dot Cowboy Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:49.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1577, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5en0adt83zfaj5j1bemmxhldj.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Bandana (Dots, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:04:54.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 448, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9xy3gwxnxiy9pqpf90r0hq3kc.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:13.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 449, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5653hakuvc2f7zkqwh2cln0lx.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:14.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 450, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dh0x7lyldgb793zodhe1sscg4.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:15.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1578, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "exyzdp83a10yb68pl68bc1t70.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:25.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1579, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e7ru80lk5fp2s35uvhj2a8m45.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:28.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1580, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "18r0xqjozrmq3c6iyz17zdqhv.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,27a5df72-4095-4837-bf24-cdd3d95a43e9", + "FriendlyName": "Bandana (Flowers, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:30.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 451, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4as6ol0a5z9gfyofshfg9ed39.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:16.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 452, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "59fpaqmmt45bv0ojtuamhjir0.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:17.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 453, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2mmw0lzovs5bx1dwz8tid3cze.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:18.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1581, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "a0g2oy7wel38y6w7abixqi1zi.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:33.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1582, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bv2g4hsg5h2czu7ouxhuzozj9.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Black Plaid Cowboy Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:35.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1583, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "99zusz6icmt0hmbe2pk2wulet.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Bandana (Plaid, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:05:37.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2224, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "MUhkmM6xxkKXejZd4iDh6A.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6o6sNWyPoUCcKJBHW0KC9w", + "FriendlyName": "Bandana (Flowers, Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:59.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2223, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "U-3blhOO3kWIzeZFNKXgTA.png", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,X1OFXyDyXES3CgMpIyV3nQ", + "FriendlyName": "Bandana (Flowers, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:33:46.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 110, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e01eh3p4tl9606u818mhsjsx1.png", + "AvatarItemDesc": "cca7ab3d-5609-4700-92fd-3280fcea9b77,", + "FriendlyName": "Cowboy Stache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cca7ab3d-5609-4700-92fd-3280fcea9b77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2227, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gpHXKk3jR061QowF7qKdGw.png", + "AvatarItemDesc": "9d2ea3c2-c199-40ed-a6c3-fab5f8dca7d6,", + "FriendlyName": "Cow Ears Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d2ea3c2-c199-40ed-a6c3-fab5f8dca7d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:40.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2254, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d837uciWekyImW4Ny0wliQ.png", + "AvatarItemDesc": "2a61c844-81c6-4f82-9c75-66cf308ed4e1,", + "FriendlyName": "Cowgirl Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2a61c844-81c6-4f82-9c75-66cf308ed4e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:38.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2255, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "USrRub60Gk6zTcni3MGCjQ.png", + "AvatarItemDesc": "ed695ac2-ff70-4ebe-b1db-18d71a4efdd1,", + "FriendlyName": "Cowgirl Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ed695ac2-ff70-4ebe-b1db-18d71a4efdd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:46.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2256, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "sH2WnqiB8E6lk4hZ9s70RA.png", + "AvatarItemDesc": "67c0f944-2f70-46d2-8588-2f67028f8bed,", + "FriendlyName": "Cowgirl Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67c0f944-2f70-46d2-8588-2f67028f8bed", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:49.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2587, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "J2d0ZFyToECidRm1EHVhLA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,", + "FriendlyName": "Cozy Winter Coat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:43:57.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2591, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PhvdPQnEikmC4BSDxeHfeA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,B3t4hjeUeEyT17nfbZgxAQ", + "FriendlyName": "Red Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:09.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2588, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tWrlF7BJxU-nPUjBYNlJxw.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,IEnB86oIL0KDa-zzcxhjwg", + "FriendlyName": "Black Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:00.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "m7LntT7DgUy0q0ALmr_4sA.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,ZzJxs2oz6UuXCX5rGvLHFQ", + "FriendlyName": "White Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:12.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2590, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "t0ntXj7xqUqXyp7YAxwnGg.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,xLcqKXKH-0Gw1lrSSQe-Ag", + "FriendlyName": "Cozy Winter Coat (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:06.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2589, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "doAF6sFqkEWtgY2AjjfQKg.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,nloukujf9kSwK1T2HiwJwg", + "FriendlyName": "Cozy Winter Coat (Pastel Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:03.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2593, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5_Is3y0bw0-_CDD5izOqfQ.png", + "AvatarItemDesc": "b4e24678-61d8-40fa-8c27-2342aaf70ac9,Grr0Jm6r9kuiZKvnLPz9tQ", + "FriendlyName": "Yellow Cozy Winter Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e24678-61d8-40fa-8c27-2342aaf70ac9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:18.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1472, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6l2rw2xqkm2h6er2jcsouoohm.png", + "AvatarItemDesc": "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f,", + "FriendlyName": "Crab Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:29.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2477, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "spUKGpKq3keVBvBjMRkRIQ.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,", + "FriendlyName": "Grinning Cat Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:15.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2478, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4yiLN6Z-r0msFhufhO36Mg.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,McX_jAOowUmjXNPEHbdS7Q", + "FriendlyName": "Grinning Cat Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:20.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2594, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-iEX4DOt8Ems8CPrv9vFPg.png", + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,4YH_0kN1ZUSVEQmCBiTt7Q", + "FriendlyName": "Penguin Onesie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:21.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a2fc5417-07e1-402c-a80e-5111a7f57287,kTyKeivEskaxQNLhfPWo1A", + "FriendlyName": "Crazy Cat Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a2fc5417-07e1-402c-a80e-5111a7f57287", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1791659539 + }, + { + "AvatarItemId": 1625, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2vvcu1ehg70m9a5l4pccyi05l.png", + "AvatarItemDesc": "6423f23e-dde3-4428-96f5-6091027c92cb,", + "FriendlyName": "Martial Arts Gi (Cunning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6423f23e-dde3-4428-96f5-6091027c92cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:38.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 360, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dn6xgp8b6mjoilolmwxfn8tom.png", + "AvatarItemDesc": "6b337a35-95d9-4b3a-9891-d9fbb06262aa,", + "FriendlyName": "Curly Mustache", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b337a35-95d9-4b3a-9891-d9fbb06262aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:00.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2300, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "jhTnA-cti0mo7tgXVYQI3Q.png", + "AvatarItemDesc": "cf360f73-b349-44e7-b5c1-f175a0f5e70d,", + "FriendlyName": "Belt of Elseware*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf360f73-b349-44e7-b5c1-f175a0f5e70d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:41.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2562, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "bIvb2VlYREWBdsNT4kX_2Q.png", + "AvatarItemDesc": "cf360f73-b349-44e7-b5c1-f175a0f5e70d,zQgy7h3CO0a02zJfhA9wXA", + "FriendlyName": "Belt of Elseware (Shadowy Illusion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf360f73-b349-44e7-b5c1-f175a0f5e70d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:55.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2301, + "AvatarItemType": 0, + "Tooltip": "A mysterious and powerful object from an unknown place. No one knows what it does yet but it seems to respond when worn... (Catalyst)", + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "7YFPA4lmf0iQcexgqXhxaA.png", + "AvatarItemDesc": "f6eee46c-084b-473e-954b-a0c8aa99a02d,", + "FriendlyName": "Amulet of Elseware* (Cursed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6eee46c-084b-473e-954b-a0c8aa99a02d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:43.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2563, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "egJ0Guweb06skCFq1ExhQw.png", + "AvatarItemDesc": "f6eee46c-084b-473e-954b-a0c8aa99a02d,qbEYPChG5EmnFDUfzwAakg", + "FriendlyName": "Amulet of Elseware (Cleansed)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6eee46c-084b-473e-954b-a0c8aa99a02d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:57.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2302, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "hyRSoiE5ZUGnAC91bFgIIg.png", + "AvatarItemDesc": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d,", + "FriendlyName": "Bindings of Elseware*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:44.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2564, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "elseware", + "ThumbnailImage": "BA2wuggUAkyxEev61GWUYg.png", + "AvatarItemDesc": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d,cfAfhzaNvkG7wGx0gkaRUg", + "FriendlyName": "Bindings of Elseware (Shadowy Illusion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b35aae1f-f5fb-4417-bb6c-4ddce4ec499d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:05:58.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 659, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2qs4c3hd1jm20l964lznj7hj8.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,", + "FriendlyName": "Deep Sea Diver Helmet (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:21.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 924, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "970zmxcbriwvube7waktj8lib.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,Su7kiW24d0KapNns96JoFQ,a3Er_BCApUiPtBaGkSH9LA", + "FriendlyName": "Deep Sea Diver Helmet (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:58.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 923, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d47yw12jvdnmgznyxtsb774ss.png", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,CahBzgN3D0y46AQsvGC14A,a3Er_BCApUiPtBaGkSH9LA", + "FriendlyName": "Deep Sea Diver Helmet (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fcb5b84-13a9-43e6-9680-72b17874ae53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:57.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2j7gd6uoyus6lyn8g1yvji91e.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,", + "FriendlyName": "Deep Sea Diver Suit (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:28.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 880, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "clcdiuwdbo9i9gcuvogdg36me.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,Su7kiW24d0KapNns96JoFQ,EiIow5mS80qpoS10r1ZdvQ", + "FriendlyName": "Deep Sea Diver Suit (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:00.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 879, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ejlghhpy20pr22d1be9zamyak.png", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,CahBzgN3D0y46AQsvGC14A,EiIow5mS80qpoS10r1ZdvQ", + "FriendlyName": "Deep Sea Diver Suit (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:56.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 691, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9rtsv91ax2euh05felzvtd1w7.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,", + "FriendlyName": "Deep Sea Diver Tank (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:15.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 905, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c2brbedhjhfswczcszbb6vdn5.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,Su7kiW24d0KapNns96JoFQ,ZpyoWbO6GkGglXlMuYUARw", + "FriendlyName": "Deep Sea Diver Tank (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:37.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 904, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8qnxcx64rh4jdr41v292zqnvl.png", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,CahBzgN3D0y46AQsvGC14A,ZpyoWbO6GkGglXlMuYUARw", + "FriendlyName": "Deep Sea Diver Tank (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d97303c-5138-4da1-a74d-378da19d11cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:35.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 662, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8yxna4s3e3firsdmmowshealh.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,", + "FriendlyName": "Deep Sea Diver Gloves (Triton)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-04T22:39:29.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 935, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4isq4kmfnatlmnrbk9ylp4ur2.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,Su7kiW24d0KapNns96JoFQ,0p5N9U5qokKdQhW07yYLGQ", + "FriendlyName": "Deep Sea Diver Gloves (Kraken)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:11.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 936, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ael0gchmqiiy4xijypkebw5gy.png", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,CahBzgN3D0y46AQsvGC14A,0p5N9U5qokKdQhW07yYLGQ", + "FriendlyName": "Deep Sea Diver Gloves (Leviathan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:12.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1527, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "a2skv7f3dcpvd1rrpejoce5q8.png", + "AvatarItemDesc": "1a742ff9-c4f7-45d8-8e24-8f24aa021db2,", + "FriendlyName": "Demon Horns", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a742ff9-c4f7-45d8-8e24-8f24aa021db2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:51.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1525, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "badoqabz6dkrchkzj2vkx5fxy.png", + "AvatarItemDesc": "b84f5e9e-11fc-424d-ad39-86901edb9edc,", + "FriendlyName": "Demon Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b84f5e9e-11fc-424d-ad39-86901edb9edc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:43.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1526, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6fwxedm1vpp6m7vdyj1mjush3.png", + "AvatarItemDesc": "26f5e849-14b3-4cb3-ab92-673c94b37c51,", + "FriendlyName": "Demon Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26f5e849-14b3-4cb3-ab92-673c94b37c51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:48.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2179, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZrqDYptat0GjjSq9O6O2TQ.png", + "AvatarItemDesc": "26f5e849-14b3-4cb3-ab92-673c94b37c51,CwObTyhghkWonhm5nTnAaw", + "FriendlyName": "Flying Machine Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26f5e849-14b3-4cb3-ab92-673c94b37c51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:35.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1376, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1vas1a6all7s8fowlh0nithtn.png", + "AvatarItemDesc": "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae,", + "FriendlyName": "Dino Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:37.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1259, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2za37ica0cvevp732s8h5llq0.png", + "AvatarItemDesc": "a5b747b8-c1a5-4bce-999f-e13dbe77bde6,", + "FriendlyName": "Disco Ball Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a5b747b8-c1a5-4bce-999f-e13dbe77bde6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:13.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1956, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b70wx32unnc05ssstrdjeqdl9.png", + "AvatarItemDesc": "4095a06c-ccdb-41a0-ac95-5a97372c9a02,", + "FriendlyName": "Disco Ball Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4095a06c-ccdb-41a0-ac95-5a97372c9a02", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:57:55.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1957, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4qarl3kkz504i1mf21lhb6dhg.png", + "AvatarItemDesc": "84992fc3-4801-492b-8a6c-ffa09ae5b593,", + "FriendlyName": "Disco Ball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84992fc3-4801-492b-8a6c-ffa09ae5b593", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:57:59.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "adbd2eaa-0dd0-4955-be4c-0d5a66d64ed2,", + "FriendlyName": "DJ Headphones Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adbd2eaa-0dd0-4955-be4c-0d5a66d64ed2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1329276406 + }, + { + "AvatarItemId": 2, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "egsz5bap153w61sfwpsx9tgjw.png", + "AvatarItemDesc": "5ac25e5a-8c8e-445c-82a4-7272baec1eb5,", + "FriendlyName": "Doctor's Head Mirror", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5ac25e5a-8c8e-445c-82a4-7272baec1eb5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 224, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2chr298lunemuuurc2ka18725.png", + "AvatarItemDesc": "27e742bf-3081-4274-ba70-301f0edeb4f1,", + "FriendlyName": "Doctor's Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27e742bf-3081-4274-ba70-301f0edeb4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 123, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8lo669z9xca0upv4z2ocdcde1.png", + "AvatarItemDesc": "587ca2bb-dd42-42bf-833f-4e2a97349866,", + "FriendlyName": "Doctor's Stethoscope", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587ca2bb-dd42-42bf-833f-4e2a97349866", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 365, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0czsyf22bw9er1mbnytxi0ytj.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,", + "FriendlyName": "Dodgeball Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:05.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 439, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5dzmypownxu9mz6fttdzw0cjp.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,4a85c181-92fe-47a5-925e-05f0ccb25006,944d934d-5e21-42d7-8196-fff9a81ee469", + "FriendlyName": "Dodgeball Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:09.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 440, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "08j0sgwi31ma53zy69e9h1cvs.png", + "AvatarItemDesc": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59,0ba92db2-690a-44ce-92f4-130e9503b6c1,944d934d-5e21-42d7-8196-fff9a81ee469", + "FriendlyName": "Dodgeball Shirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:10.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 851, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8nnw6vhiwxlys6lvncv55q2xd.png", + "AvatarItemDesc": "h_To9WQYdEusYQxlbMmMmQ,", + "FriendlyName": "Dracula Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8f487-1864-4b74-ac61-0c656cc98c99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:53.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 857, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2l8ozzohqd9a0k2deuxxucq9c.png", + "AvatarItemDesc": "uubY_Cn9A0-Ww4lsIRCF6w,", + "FriendlyName": "Dracula Medal", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fcd8e6ba-fd29-4f03-96c3-896c211085eb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-14T01:26:24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2388, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "k1-iNuM_jUadcrVNAvy83Q.png", + "AvatarItemDesc": "05ab6e79-1198-49f2-945b-528523e306bc,", + "FriendlyName": "Wicked Sorceress Amulet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ab6e79-1198-49f2-945b-528523e306bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:35.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2389, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pR-mOlAIOEiskisbb6ZQUw.png", + "AvatarItemDesc": "05ab6e79-1198-49f2-945b-528523e306bc,aUsPNgV2c0u15VhGBAIyvg", + "FriendlyName": "Dragon Necklace (Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ab6e79-1198-49f2-945b-528523e306bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:37.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1678, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7i6yh1qwt3e3zrrb77zwz3wta.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,", + "FriendlyName": "Dragon Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:06:49.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2201, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B0mOl7jCckGx4VBglg52ww.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,cbk6h_-1Y0exRpGyrQMDMA", + "FriendlyName": "Dragon Wings (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:59.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2200, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gN3tkjocB0iH14KDkjDACg.png", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,E_pm-PPj_U-nsfMti-AlCA", + "FriendlyName": "Dragon Wings (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84efabec-fe50-4c92-845f-b99b1d84ca82", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:57.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 3, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a3yzsuscvgxw37qx4iplxyqvr.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,", + "FriendlyName": "Drum Major Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 418, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "81kgcsyk5xpfnseux5kk365d6.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,5a614f97-ef7f-4b70-afff-9ed837a86407,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:20.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 419, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "06rlbrw6875digtc4y8c0l3i8.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,b444ed0a-eab5-4bd6-af34-d080de74a149,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:21.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 420, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4visb1djbytbo881ltks1hzo.png", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,8053b5e5-0f67-4329-a45d-1ee9fc830820,a8a91216-0fcc-461e-a682-95ee82509cce", + "FriendlyName": "Drum Major Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:22.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 225, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2hyiw1ost44xvt2vreduatn4w.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,", + "FriendlyName": "Drum Major Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 422, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d1ickhvbj03efs8x35tvbakq2.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,fe5288bc-0e02-4ea3-b17d-dee0e576979d,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:25.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 423, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "93qrv8cwm8k71rmnk1o22mzpq.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,82de1ca3-67d6-454d-84fb-0eeece71fcbc,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:26.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 424, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5la3vym26ex06q0rm9ykg3w1f.png", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,54d3c589-30ba-44c1-a77c-8f9e16eb0b20,b0298c45-0dd1-453e-bad4-41e4656bb38e", + "FriendlyName": "Drum Major Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a6f1825e-1647-4d06-85d7-ac2d139612c0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:27.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 275, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0rq8z5vxkj1jdvbjqezvbckyx.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,", + "FriendlyName": "Drum Major Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 425, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "817bm8rsomfrxcjwad8033u20.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,fe5288bc-0e02-4ea3-b17d-dee0e576979d,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:28.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 426, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3kkimmvnludjevzzo56uhvuk6.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,b444ed0a-eab5-4bd6-af34-d080de74a149,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:29.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 427, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73tjv2ussx3904ivxkhezp2yr.png", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,8053b5e5-0f67-4329-a45d-1ee9fc830820,6b38ab79-142b-446e-978c-ad829ade8823", + "FriendlyName": "Drum Major Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4d533b3a-b5fa-446a-842d-92798ad5b493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:30.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2735, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "J_F4me5Ad06VJ52Xr7npMw.png", + "AvatarItemDesc": "162a3442-e6d0-4895-be09-40d78af16dc8,", + "FriendlyName": "aaa", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "162a3442-e6d0-4895-be09-40d78af16dc8", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:40.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1439, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7fx9lc7nedtemokvl10u71bvo.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,", + "FriendlyName": "Dryad Dress (Summer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:09.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1440, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ae92fjqai6ajms6tff2mfv81j.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,AHL9ydUbqEqMw1tluIvyQA,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Fall)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:11.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1441, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b660cr3jdsc9mlg5dejtru1o.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,_SPKrYIcnUWhTiSrTFLwoQ,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:12.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1442, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7amwyi3juehdo73arc4mepxm5.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,n2pZHS8A3kSgyFUCI_-8TA,w-vooMawWU6_4spdgBYufA", + "FriendlyName": "Dryad Dress (Spring)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:14.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zg42szCjeUGZC18WPpdf6Q.png", + "AvatarItemDesc": "8595c7b2-4d1c-4eae-af9a-8180d475d931,BYbKbXAgUk6vE7H7w4jSMA", + "FriendlyName": "Dryad Dress (Cherry Blossom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8595c7b2-4d1c-4eae-af9a-8180d475d931", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:41.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1443, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bzjuuf0r1ocwg6am5t2mtrvxg.png", + "AvatarItemDesc": "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb,", + "FriendlyName": "Flowing Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:50:19.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1436, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "65ofog1r81v83c1zkimz5dc7n.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,", + "FriendlyName": "Fae Horns (Fall)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:49.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1437, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aom69vkzej29cjh1jcqgxddom.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,M16XTvLPKUWRpiAspgsatQ,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Summer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:57.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1438, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8s3arpmce6wx552qttsj9og6n.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,rXhNZtcZCU21dwEHOCgWOA,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:49:59.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1752, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bq7qytcrpeczoqxkplbewz1uf.png", + "AvatarItemDesc": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a,OxO9ivoKB0e-fNIEk33Erw,tmaPvkx3BUOZ3nsl2E0oRw", + "FriendlyName": "Fae Horns (Spring)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f15d81c8-3d19-4bbe-87c0-1144aaa4138a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:39:49.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1506, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bvjmdwvfu70toj5uhdjc9bmyn.png", + "AvatarItemDesc": "2cf6ceee-4c45-4fb4-b9df-cb8437742e85,", + "FriendlyName": "Doubloon Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2cf6ceee-4c45-4fb4-b9df-cb8437742e85", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:56.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2717, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4jhq0ihf9i0h357l82r1f4ugm.png", + "AvatarItemDesc": "f8514fc6-93f3-4063-bafc-378ddf454836,", + "FriendlyName": "Edgy Earrings (Dangle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f8514fc6-93f3-4063-bafc-378ddf454836", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:54.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2718, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9ter0n7sprtghgk2o4clgjugr.png", + "AvatarItemDesc": "f641d4f4-e197-4cf4-b477-89069797967d,", + "FriendlyName": "Edgy Earrings (Gauge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f641d4f4-e197-4cf4-b477-89069797967d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:56.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "02dj9jh2t6iwcatksesgdp9v5.png", + "AvatarItemDesc": "2af73868-b619-43f7-9b7a-37a8fe2502bf,", + "FriendlyName": "Edgy Earrings (Spiked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2af73868-b619-43f7-9b7a-37a8fe2502bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:13:58.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2681, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "GVvKZUlMlU-oAczCFnnkPA.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,", + "FriendlyName": "Electric Guitar (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:48.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2683, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "K9ApHkO1I0WYcDiKg_7bMw.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,zBfCq7T69kqZPTz3P8KKhg", + "FriendlyName": "Electric Guitar (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:54.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2682, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "uZtpsMTIe0WfPlipjC6sfQ.png", + "AvatarItemDesc": "6a2fa2f4-d335-4349-88a4-49af251454fa,liZWUNZih0iakAa7UJkk9A", + "FriendlyName": "Electric Guitar (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a2fa2f4-d335-4349-88a4-49af251454fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 356, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8o02lqyi0dx2nwz9l24ei0511.png", + "AvatarItemDesc": "34654303-9760-447f-8614-1c26506db994,", + "FriendlyName": "Electrician Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34654303-9760-447f-8614-1c26506db994", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:51.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 351, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7mhhpensvbg501x8mtatplhsh.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,", + "FriendlyName": "Electrician Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:31.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 352, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73egrs7lt4bztq4mkimehdlwx.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,b995b419-1897-4c1d-98be-83d551f7ba71,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:34.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 353, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a4ha61zluoi17g7smiinsv5pp.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,67fc9269-8dd8-4d6f-b594-c77e716f2482,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:35.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 354, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ef0ejvn9b4x628qk2dfjie7xo.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,b539777f-48b2-44a2-9d63-17ac703e4d57,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:37.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 355, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "40dmxzroivdmghm7ola3zu9u1.png", + "AvatarItemDesc": "3dc003c5-428b-47d6-80fb-6520b63119e8,5fb2e235-8c67-4335-b432-edb10db2b139,daa38a31-fb7b-4e26-a2f5-728d6dcec81a", + "FriendlyName": "Electrician Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dc003c5-428b-47d6-80fb-6520b63119e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T22:31:40.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1622, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "7m2mi3d8ga6cj0snk6kbhszx8.png", + "AvatarItemDesc": "686ac516-630f-4989-9c99-0b8d055a00f9,", + "FriendlyName": "Holiday Elf Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "686ac516-630f-4989-9c99-0b8d055a00f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:20.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1623, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "av5quwapam4mgd2prq9f64nkf.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,", + "FriendlyName": "Holiday Elf Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:23.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2526, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "tRXo9yhf00enptfGUMyqHQ.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,Hd5IIMd7VUqndR_og0zxPg", + "FriendlyName": "Elf Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:52.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2524, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "QWw5Yklbyk2cF85WzqFl-g.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,8k3q_1d_-kSBzNLCl1HQhw", + "FriendlyName": "Elf Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:45.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2527, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "lNu1DktQikamJcxDfb6XpQ.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,YwossyXZNEm3OPWwXsyTAg", + "FriendlyName": "Elf Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:55.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2525, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "A9c3qAoffkKGpt3NGDQj7Q.png", + "AvatarItemDesc": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff,i3pwJQoNDkO_68pQ_3Y0tw", + "FriendlyName": "Elf Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3ba47d5-a165-4bd6-8479-e5f8d709f4ff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:48.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1624, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "6m4lkkcj4tnqk0xc8aqctsa6v.png", + "AvatarItemDesc": "61969200-2790-41e0-bccb-d9feff8c7b7d,", + "FriendlyName": "Holiday Elf Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "61969200-2790-41e0-bccb-d9feff8c7b7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:30.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "2e50d8d2-b7fc-44e9-a882-a58bd3b6f099,", + "FriendlyName": "Elseware Cloak Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e50d8d2-b7fc-44e9-a882-a58bd3b6f099", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1829511188 + }, + { + "AvatarItemDesc": "a73e1a56-2710-4448-a594-ecc80571e999,", + "FriendlyName": "Elseware Crown Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a73e1a56-2710-4448-a594-ecc80571e999", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1208807230 + }, + { + "AvatarItemDesc": "c7ea70bd-7681-4b90-b4e3-10a100e4652e,", + "FriendlyName": "Elseware Earrings Ear", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7ea70bd-7681-4b90-b4e3-10a100e4652e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1175771749 + }, + { + "AvatarItemId": 1792, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "26o0a59b3xk9bngu5q6rjwddq.png", + "AvatarItemDesc": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,", + "FriendlyName": "Elven Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:04:59.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2186, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E6CVDPt2mUWUQ6WM1GzI2g.png", + "AvatarItemDesc": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,z1djgGqy502ha9X_uB3enA", + "FriendlyName": "Elven Crown (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:10.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1797, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ss3ohgl1cgeoknp1gcpackva.png", + "AvatarItemDesc": "485bbda2-37e8-45a7-90db-7c35518dcdd0,", + "FriendlyName": "Elven Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "485bbda2-37e8-45a7-90db-7c35518dcdd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:26.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2191, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sR5683LqRUiQsI8U2LXZVA.png", + "AvatarItemDesc": "485bbda2-37e8-45a7-90db-7c35518dcdd0,M5QwJJRRBUy42HLyp6ghtg", + "FriendlyName": "Elven Armor (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "485bbda2-37e8-45a7-90db-7c35518dcdd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:23.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1793, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9pskxjn1i5vvqw267s8jfq4cn.png", + "AvatarItemDesc": "a37c3161-1476-44b0-a9d1-d6eb854db2d9,", + "FriendlyName": "Elven Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a37c3161-1476-44b0-a9d1-d6eb854db2d9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:05.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2189, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DRy_t5nuCUqcj18J33q3Hg.png", + "AvatarItemDesc": "a37c3161-1476-44b0-a9d1-d6eb854db2d9,EmlwkB77pUiiDEmc2pMVaA", + "FriendlyName": "Elven Cape (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a37c3161-1476-44b0-a9d1-d6eb854db2d9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:18.477Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1799, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "275lkom5he8sdivhtrbsz9ioj.png", + "AvatarItemDesc": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,", + "FriendlyName": "Elven Bracer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:33.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2196, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "42hTRTPSNkmQRqAiceQwVA.png", + "AvatarItemDesc": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,MzNNQ6T1HEqbqcLXZIulLw", + "FriendlyName": "Elven Bracer (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:38.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2493, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vskg3C6Z10uF_tdfqjarlA.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,", + "FriendlyName": "Beam Sword (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:12.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2494, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OodKnoLtC0KlwWUbUQQJjQ.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,u0bLCSxcA0yDHC29XnLZOg", + "FriendlyName": "Beam Sword (Science Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:14.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2496, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7NNMQvKd_02w8L9dd1zvxA.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,6WJKUeFNn0amYNvKMxijcw", + "FriendlyName": "Beam Sword (Forbidden Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:20.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2497, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ObiBqarizkuy3DPPXfx4dw.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,VPvezA1A8UKt2qNoNRW9dg", + "FriendlyName": "Beam Sword (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:38.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2498, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RuBjPNvqok6wRw6kkXH2GQ.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,OIfcKSEil0uHgT5baEFuOw", + "FriendlyName": "Beam Sword (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:40.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2495, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OCi7E50f4UqQMDGTuaWxCg.png", + "AvatarItemDesc": "cee80307-68cd-4260-a898-1514dd41dd5d,hNFdYhoJYk24MqU2GhQXlw", + "FriendlyName": "Beam Sword (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cee80307-68cd-4260-a898-1514dd41dd5d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:16.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 50, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6gyrqvu42014zf0m05yueqgws.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,", + "FriendlyName": "Equestrian Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 632, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0dobf9ialtu2o3i6ia3w9whbc.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,55901f12-d5b5-4fa8-b4c8-e479689ee39d,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:53.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 633, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ebxhd6rhrlk3qem5ly6uri8od.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,4828b50c-95b6-466a-bb25-514891d78202,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:55.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 634, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7w9g6a5fxuoeblfcf3ahgvbs7.png", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,d6823e01-69f0-4f85-b94a-74894356a2cf,be6b3fa1-3388-48ac-8957-d7b8c85ff968", + "FriendlyName": "Equestrian Helmet (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a628d62c-fa30-4405-bcbc-3e646e3a3434", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:56.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1409, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "avhll17max3g5tbutzgkr4j3d.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,", + "FriendlyName": "Night Coat (Zzz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:01.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1410, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "10ks6xzccejyj65uikvyl23xf.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,uDJ5ZqU1f0CiOAH8WY22Yw,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Dozy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:03.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1411, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4g0dvwsd3dz5sthxkhgq6scu0.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,j66We9SYr0SNQ2ufZqIqhA,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Sandman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:05.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1412, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6lrk2hle92tc35cdc3fs9nqhp.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,_jkPDSXFUkSU_yOfutQ7BA,KOCzRE4a6EKb3_NVOI_lcg", + "FriendlyName": "Night Coat (Zonked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:06.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2066, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "m9a7PytQ6kK8OBPmwOadSw.png", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,MLB_YJtNtUi2Qw9D4a5i2w", + "FriendlyName": "Starry Night Robe", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64fddd4e-3d59-43df-bb39-671e18953c0f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:21.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1584, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bbiqjubciywrt9n52sthcrv57.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,", + "FriendlyName": "Fancy Cape (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:06:59.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1585, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "appu9oibb8u3du39aint01ugz.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,crl8VlZ59kGxG01noGEWmA,EckOEU0mJkGIVDvYoKBN9g", + "FriendlyName": "Fancy Cape (Gilded)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:07:02.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1925, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8eli5j86ufvn0nzgy2emp66xo.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,WqX8pAB7LkyPKEzIUCtnNg,8Y2F-cVStkSn9YB47VCoKA", + "FriendlyName": "Fancy Cape (Holiday)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:47.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2027, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-FiTuUGn-kGGXgQjYaAC7Q.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,l2BbgbHCe0S_SDUbfmpSeQ,XHJ0lW4zYkGcL5UyI1HfEg", + "FriendlyName": "Fancy Cape (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:28.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2161, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GadkXWWyHEOkWhpEtq2mrw.png", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,bTAQ-CkyRUOZoXqXuap2Ig", + "FriendlyName": "Fancy Cape (Basketball)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "46cde502-86d6-4061-a80c-67458ffd4d60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:28.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 973, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2so63kdd99rxf93sphtoxpre7.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,", + "FriendlyName": "Belt Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:48.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1450, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1n94yhtw4pxw2vp1k9nf43brr.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,whs9olNz9Uani7Gou31mvw,y6aASPUwDkas0W5Yl27ydQ", + "FriendlyName": "Belt Bag (Rainbow Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:29:03.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2028, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7iYxky7EC0WBmVm1yt7YDQ.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,ncERd5bELk23jSRxA7NV9g,N-cAmfOgv0eJZMTo18zJHQ", + "FriendlyName": "Belt Bag (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:32.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2282, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "w2agGc4e5E2zgiHvAsFu7Q.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,eaRDMCA4fE6KJHi639QFvQ", + "FriendlyName": "Rec Con Belt Bag (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:27.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2281, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "UnP3rBcwpEmBR6ED8vVH0A.png", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,zIIQkllHFEyjEvAGYC0cPQ", + "FriendlyName": "Rec Con Belt Bag (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3ce8ba1-c38e-4f24-8310-f9699f9c4d53", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:22.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1420, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0x4ghshpxazgbb0zpcyu0g22v.png", + "AvatarItemDesc": "4ee2133d-c419-4fbc-9117-746d756a250d,", + "FriendlyName": "Belt Bag (Cosmic)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ee2133d-c419-4fbc-9117-746d756a250d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:26.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 367, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dd3ww7dcaihkg0ags2cxuiyuz.png", + "AvatarItemDesc": "cea5c141-32d8-4dae-80d9-6fb74834a79c,", + "FriendlyName": "Farmer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cea5c141-32d8-4dae-80d9-6fb74834a79c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:05.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 371, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9iudjbzlizfmrgxtwdwwsj6hp.png", + "AvatarItemDesc": "967187b8-df20-4f4f-b48f-10b9ea75cedf,", + "FriendlyName": "Farmer Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "967187b8-df20-4f4f-b48f-10b9ea75cedf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:11.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2023, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kv11sGN1q0uGTiTIyHPeTA.png", + "AvatarItemDesc": "b6cee099-5840-401a-9d74-0b80ff71ee69,", + "FriendlyName": "Feather Boa (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6cee099-5840-401a-9d74-0b80ff71ee69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-02T18:10:57.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rwu9Nb6pTU6Pjd3orGYWOQ.png", + "AvatarItemDesc": "b6cee099-5840-401a-9d74-0b80ff71ee69,an_Z5HMFjkaHyHoZZV4AFA", + "FriendlyName": "Feather Boa (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6cee099-5840-401a-9d74-0b80ff71ee69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1547, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cl2yzoi5qm9yxxli0m8dx34ey.png", + "AvatarItemDesc": "16707919-756d-45ae-95fc-cfb6715b8ae6,", + "FriendlyName": "Winged Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16707919-756d-45ae-95fc-cfb6715b8ae6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:28:04.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1765, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ceggd7cookytj7l34cfoxzeuw.png", + "AvatarItemDesc": "7df5697f-2f71-47d1-bc27-cfa43792ffd7,", + "FriendlyName": "Felted Shawl", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7df5697f-2f71-47d1-bc27-cfa43792ffd7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:36.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2220, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_jqJN6iJzUuEi-MlHpRpyg.png", + "AvatarItemDesc": "7df5697f-2f71-47d1-bc27-cfa43792ffd7,GCethPzUyUKYxWdfLykbSg", + "FriendlyName": "Felted Shawl (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7df5697f-2f71-47d1-bc27-cfa43792ffd7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:08.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 975, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dj7x2xk5ttme8r1jpcmeebmjj.png", + "AvatarItemDesc": "BJ2XrGvl40iIl28IbTfbKg,", + "FriendlyName": "Cheerful Ponytail", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ac979d04-e56b-48e3-8897-6f086d37db2a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T02:00:33.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 176, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "41wwwt83ors7iub44o0ftgtzs.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,", + "FriendlyName": "Cheerleader Skirt (Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1626, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4o1fhoua48d5fb5rbrs2ijloi.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,eWf2c62cqEOkRukLzhl_wQ,KFq8d1VsO0KDqBWUj4qRFg", + "FriendlyName": "Cheerleader Skirt (Victory)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:07.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1627, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "edpmi0dav6ft7rwfn86f1km9l.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,a0oe68pikUykAgrRo-axkQ,KFq8d1VsO0KDqBWUj4qRFg", + "FriendlyName": "Cheerleader Skirt (Rally)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:12.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2422, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Hhv0MwY_0EitaME8aQhP_A.png", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,cFQ1Z7_wp0meS8wdC1hemQ", + "FriendlyName": "Space Suit Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "657efe89-620a-46a2-8b58-cafd855c7cd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:46.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 132, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5rmads8tgd0mb71na01q6g4l4.png", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,", + "FriendlyName": "Rec Room Sash (Small)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1359256033, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,IodiruapIEqjGym-YcXzXA,LjQ1UKN2xkqJkok40Dxaew", + "FriendlyName": "Female Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.502777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1597463395, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "60067e91-18b8-43ab-ae20-a8ea74c757bf,KUAMuM41hk-YLZoqTiKncA,LjQ1UKN2xkqJkok40Dxaew", + "FriendlyName": "Green Cheer Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60067e91-18b8-43ab-ae20-a8ea74c757bf", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.502777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2514, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "F2FsXvPi5UqyVCDKuPzs0Q.png", + "AvatarItemDesc": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b,", + "FriendlyName": "Festive Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:12.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2542, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "fNauUoIJ10uT7PJQCp9Agg.png", + "AvatarItemDesc": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b,1LtCxaCDjUmg62fYCjNUSQ", + "FriendlyName": "Festive Antlers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1c89e162-fbce-45e2-a158-6bf0c4f5e52b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:58:27.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 179, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "39n1fu8uzgykuoibgylda8en0.png", + "AvatarItemDesc": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,", + "FriendlyName": "Field Hockey Uniform (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "akd6dknn22mbqs5rpgcphqekv.png", + "AvatarItemDesc": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,7b0d6661-e856-4ec1-ab90-d89c28a70826,1fede91a-434e-49a5-882e-e6db107112a8", + "FriendlyName": "Field Hockey Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T18:31:24.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 347, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cz08dx8wptcrolonzvozwn4w1.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,", + "FriendlyName": "Fighter Pilot Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:03.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 338, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ef7205mj0de2hjfkm2shgsjo3.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,cea53c7f-24a9-4e3b-80b9-fbf95738f973,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:20.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 339, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5f87fvdl1iouxqppbtsu50ic0.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:21.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 340, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f54hknoj615yks00svpyjvttz.png", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,8c74ee27-3d56-401b-8a8c-7868a80770e8,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff", + "FriendlyName": "Fighter Pilot Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ec836d2a-b1b8-4566-b8eb-123ae2845956", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:24.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 349, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1wcxmgwhypyd2evqqigqmes19.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,", + "FriendlyName": "Fighter Pilot Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:07.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 341, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1d9tx3eid2kmtnq3wejxoceub.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,cea53c7f-24a9-4e3b-80b9-fbf95738f973,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:25.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 342, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bfv2kddhqvn7dw1f648zhjf63.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:27.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 343, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8ko7qcft9b1b9aubbimm8cjfb.png", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,8c74ee27-3d56-401b-8a8c-7868a80770e8,435eba0d-d8f9-43cc-91aa-2959d63c6fa7", + "FriendlyName": "Fighter Pilot Uniform (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:29.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 350, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0tlurbm4ftp2t94h35qrfzqtm.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,", + "FriendlyName": "Fighter Pilot Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:08.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 344, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7q0ythybtrbsgg7c2njbi9c68.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,18b52db5-0261-46a0-9214-ff510d455ed9,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:30.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 345, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6328cpp2tizf50t3nsq5yrbz9.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,55b4e661-43e1-4708-82d5-2dd2bebd4f45,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:31.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 346, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7l7k238jg65h9nok7guj535ix.png", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,8c74ee27-3d56-401b-8a8c-7868a80770e8,7869b2a5-cd89-4033-b9f7-4f1451ecb47b", + "FriendlyName": "Fighter Pilot Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T18:21:35.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 955, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "99bp2zxrcuohv18hmmh26iuao.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,", + "FriendlyName": "Figure Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:15:20.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2163, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ls7NQ0Vos0SREhMD3UGSJg.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,dZyVey0_oU-3u6FLbIdGLA", + "FriendlyName": "Figure Skater Collar", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:34.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2164, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kO-X2uWxJEGueWZ1XNqQfA.png", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,rP0LMoFjqUCU1LWA6_wOnw", + "FriendlyName": "Figure Skater Shirt (Burgundy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58208df6-63b2-4144-b9e7-683146b793f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:36.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 956, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "arq4ixmsd54yjuazkfe5xglft.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,", + "FriendlyName": "Figure Skater Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:15:24.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2166, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "_J7s68Cf2kKhS_I47a_7Cg.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,ngF6e9k3wkme3HdodiB03w", + "FriendlyName": "Figure Skater Dress) Figure Skater Dress ng F6e9k3 Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-04-13T01:18:42.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2165, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c_ldiPCEC0uVSLKrER_-tQ.png", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,FpvZjltPtU6AKTBphz3ZMA", + "FriendlyName": "Figure Skater Skirt (Burgundy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:39.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,z7bASCi-JEKuLiOmFx3EFQ", + "FriendlyName": "Figure Skater Dress Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b23f5e4-97e5-44d3-a67b-dc81c2f0eafa", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1092505797 + }, + { + "AvatarItemId": 51, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "agf9y9srlnxo896ku56vbgb2e.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,", + "FriendlyName": "Firefighter Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 635, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4jk6yhqfbgqan7m5g4uz672n.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,891d902e-3257-4a8c-9001-858046007997,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:58.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 636, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8mvts9qqf1v3kmd2l4mgbqjkx.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,4c26d1e7-2382-4478-b9c4-a068ed01a607,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:59.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2006, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bnhtf2lj2d5khggvf485zqnxb.png", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,uahcQNh3IEiooPFq-SQi5w,c8ccb85e-52cd-4b3b-aadf-289819067125", + "FriendlyName": "Firefighter Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:26.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 180, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "573itausg418c5prdz6bv3dpo.png", + "AvatarItemDesc": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,", + "FriendlyName": "Firefighter Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2007, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8gwu9vgybc4jmwudmwkep73uk.png", + "AvatarItemDesc": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,0x7a3hV5XUyzyf8cUuzVtQ,vYAcZMymO0m6bCNQIsI8rQ", + "FriendlyName": "Firefighter Jacket (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:06:31.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "7ed81aea-0e77-4b38-a87c-729048c6fe41,", + "FriendlyName": "Fireworks Antennae Headband Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ed81aea-0e77-4b38-a87c-729048c6fe41", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1920491330 + }, + { + "AvatarItemDesc": "b734172c-e405-4fc7-88c3-c859309bb4ac,", + "FriendlyName": "Flamingo Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b734172c-e405-4fc7-88c3-c859309bb4ac", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1246829493 + }, + { + "AvatarItemId": 588, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a1vs7a8qu2iy5kaxke59mq5sa.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,", + "FriendlyName": "Flapper Dress (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T18:57:31.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 641, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cu5pah7uuv614frsr65tvynut.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,565c17e0-496d-45bd-a2c3-564e118c06cc,a240d44f-963d-4d35-b22f-980938c7788d", + "FriendlyName": "Flapper Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:19.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 642, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a1844ejer8zz68n1qyajbci5m.png", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,a240d44f-963d-4d35-b22f-980938c7788d", + "FriendlyName": "Flapper Dress (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f600892-aa67-484a-afc7-14c219d89c52", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:22.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 589, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "acmwuunndj6ex6zr3885jbewo.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,", + "FriendlyName": "Flapper Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T18:57:33.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 643, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5b73p030rhcezdz56gzz5ke13.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,30c61e63-5e09-4a6f-8915-1e1fe4d55cf0,0e42108e-3b08-49b4-86c0-7984719e8380", + "FriendlyName": "Flapper Gloves (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:23.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 644, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3v8m5i8t4wkf4669wvp55t2ex.png", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,2cf2b644-3fa6-449c-9fe8-d0ab917f3106,0e42108e-3b08-49b4-86c0-7984719e8380", + "FriendlyName": "Flapper Gloves (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "473c672d-2b79-48a2-a49d-fd68160c5bde", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:25.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 586, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2q0dg92w8rd2uzk57ib0y1cj0.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,", + "FriendlyName": "Flapper Headband (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-14T01:07:49.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 624, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e7tt4jrd0jwhdkeastwunm8bb.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,565c17e0-496d-45bd-a2c3-564e118c06cc,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "FriendlyName": "Flapper Headband (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:41.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 625, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uukseprpubic1ed6c0zapb86.png", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e", + "FriendlyName": "Flapper Headband (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:43.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 52, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6s2gbnnr5jsigj562mzity2yp.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,", + "FriendlyName": "Flat Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 637, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7iezcsxl2wujed3zpx0sad4pp.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:06.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 638, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e688283ak00jg806f3cw5gc0k.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,0ba92db2-690a-44ce-92f4-130e9503b6c1,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:09.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1638, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "24i3534uix45v9rp0lwimi7sf.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9aYT6E9sd0OSavlmkhtkGw,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:20.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1639, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5w6vsff1wv2psbj5naflpxebd.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,H-ypJNyVSka9as7rDFRZ_A,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:22.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1640, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0xi16q5h0z36akxhihlw7prh3.png", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,-7uPJmO-wUu3cUhqo0caPg,73fc2e2a-a036-410b-bafa-3a07658245b5", + "FriendlyName": "Flat Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e3929d3-d291-4e91-a0ba-2ef203e407d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:45:24.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "f567f792-e5bf-41d9-a8a9-e3af7aaddaf7,", + "FriendlyName": "Flipped Up Shades Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f567f792-e5bf-41d9-a8a9-e3af7aaddaf7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1877445964 + }, + { + "AvatarItemId": 2218, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TgzWE1dBGkqvgHpdtFU2Uw.png", + "AvatarItemDesc": "c7c85554-e118-4433-8d6b-b61a2971e045,", + "FriendlyName": "Wrist Floaties", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7c85554-e118-4433-8d6b-b61a2971e045", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-18T17:40:04.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c7c85554-e118-4433-8d6b-b61a2971e045,MS0Mglf3qEmzKzbCa6uOAg", + "FriendlyName": "Floaties Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c7c85554-e118-4433-8d6b-b61a2971e045", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1483238024 + }, + { + "AvatarItemId": 22, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "20q0kpiy9pbsmzz8qgeyhz0kt.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,", + "FriendlyName": "Flower Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 479, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "548zksu11edcwj6jg6aoag1s7.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,c5884778-a87f-4612-9717-86fbb65d440f,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "FriendlyName": "Flower Headband (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:34:47.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 480, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c1g1jmqtrkrhxhm8c2puubehx.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,827a1538-51bb-4fef-99c1-7f1bebd9866c,7d040391-03f7-4b3d-bcfd-a1278ff459c9", + "FriendlyName": "Flower Headband (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:34:50.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1875, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bmw4n7qs969ehy3whhp8d7x2q.png", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,rZOgFhv59UKssAH7k-UDyQ,eVl7mLA1VkmnR81YLF-ZNg", + "FriendlyName": "Flower Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:39.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 181, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5bmu6mglbinvcbqu05awxhls9.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,", + "FriendlyName": "Flower Swimsuit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 434, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3oz7p1t6fe7pqfgq0hoilmiey.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,cf515c87-3c0a-4443-9f63-5ea1bdbaa61a,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:02.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 435, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3bd0st7c55qfn1i4j4cuevmu6.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,27680277-aec1-41be-a476-9f51cf775580,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:03.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 436, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7kp3fr2rwvgorhc9uaqpahhmb.png", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,5a96ac4d-03c3-4214-9dcb-c2044ff28f2e,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622", + "FriendlyName": "Flower Swimsuit (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:04.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 53, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b9vsrdat9b9pm7myqavsy8mbw.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,", + "FriendlyName": "Football Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 54, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6sd4dcoqxuhvhrf3casbx3080.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,d8d490aa-7088-4163-9305-1441367f490c,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 55, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ekahaz7vcxq5ejy0zaxrfx28a.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,61ff7a02-50ff-49ea-b95e-c31b95a30c8e,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1662, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d2vzz38oww35dcnjvsxmoj4yd.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,X24EvVokeUqyDNdqevV2EA,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:25:59.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1504587764, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3jI4Pafh9k2B-YTxx1gQOg.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,UQKS4u4SO0umKOHncLqKpw,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Black Yellow Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1720424941, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "T0KyAJMATUqP3TLksvoBxA.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,Q3bhpoWbVkyUBTKUn5A5bg,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Blue Green Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1663, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7t42ep2l7eezztyekwigomtjj.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,UfhOpLemQ0ypYh7CIC3W1Q,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Football Helmet (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:07.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1155274878, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "mYOYiE057EOkRmW2vXuTdw.png", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,_MS-R9DB5kinLBszWle8hg,7660e11a-7ef0-43d3-bc02-76b3a4aab064", + "FriendlyName": "Foot Ball) Football Helmet Hat Black Gold Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5a38f864-7d1c-4989-8558-40b9668f78ef", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 182, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ewgz9ts3guc8ukhtofeng0haj.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,", + "FriendlyName": "Football Shoulder Pads (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 183, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "e8nojpjvm8xk8hxnuivjumxe8.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,32dde6dc-e3dc-43ee-9b7c-987bc6778c17,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 184, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5wkaquhpl7lseclkpi4mgjdkp.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,88c3ed05-ce41-4a9d-a4a7-8a428d240b3f,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1664, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d34fyy9i80gyrjrhwyh2coy4s.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,vMqhT19x302aYEo6E9HhOA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:23.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1192138115, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6FJtINu9eEeqW-_S2Cd5Rw.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,G7B-u9qn5EGqSNskBCVsAA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Black Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1242284106, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Y_q7sebeeUuwLnOb_qMjtw.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,jV1TGp1dHUWCJzVMbJskyQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Blue Green Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1665, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2oj4qcrvtpo7yivbcpx3f2voa.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,kCqthBeVvUKZA9b76kxuVQ,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Football Shoulder Pads (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-25T19:26:34.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016992135, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cJAAR0vVREamuQT8FfpdHQ.png", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,dzCc_QmTYUGQ67r4yy3XpA,e17481fc-b2b6-47a5-91e5-7205f45a3247", + "FriendlyName": "Foot Ball Shoulder Pads) Football Shoulder Pads Shirt Black Gold Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34d45669-b319-4c7d-bd05-078961d70d0b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.503777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1260, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1wfcf6b158y11zvf8rsj18asi.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,", + "FriendlyName": "Formal Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:17.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1261, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1hodrkppvhvmxs18btxdu979c.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,rcdwgNvO30Cvx2ycE1GfHw,JmFzNL5v_0q1p0bgoOlBpQ", + "FriendlyName": "Formal Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:21.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1924, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dhy0nd82sozehq400v5il9pjj.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,ktnYOVEhtEmuxmjM1TjIOQ,EqNosRGf5U-5P1nKIRV3fw", + "FriendlyName": "Hallow's Dress (Patchwork)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:26.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2553, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sWwqVZYnhEuU3MEH8_419g.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,oM7qz730lke-off_cQw-cA", + "FriendlyName": "Midnight Gala Fancy Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:41.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2819, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "abYMjMnf3UyRf8ys43UA1A.png", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,HzTJsuYdLkC_NY3ClszRpQ", + "FriendlyName": "Formal Dress (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "30380607-8b54-4b63-81d3-cb3f08e452c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:45.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d2403tow1ivxsp7dq769lkahh.png", + "AvatarItemDesc": "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14,", + "FriendlyName": "Formal Dress (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:25.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1264, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3o9n9ip2a7y91q3v2kwe6jpnj.png", + "AvatarItemDesc": "73a119ab-d2c8-4da9-98b2-29edbb38e2c2,", + "FriendlyName": "Formal Dress (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73a119ab-d2c8-4da9-98b2-29edbb38e2c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:32.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1263, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8op0aqlcnxdsbinjfpuqdslgn.png", + "AvatarItemDesc": "73c0e2ba-cba1-482a-89d5-1508997aceeb,", + "FriendlyName": "Formal Dress (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73c0e2ba-cba1-482a-89d5-1508997aceeb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:28.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "80f68c67-2df6-4b58-bf2a-dfd76653a781,", + "FriendlyName": "Fortune Teller Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "80f68c67-2df6-4b58-bf2a-dfd76653a781", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1199731224 + }, + { + "AvatarItemDesc": "d536cf31-5649-4d09-94b0-995c4d6f89df,", + "FriendlyName": "Fortune Teller Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d536cf31-5649-4d09-94b0-995c4d6f89df", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1077066165 + }, + { + "AvatarItemDesc": "a98251fe-3eb9-41cb-9982-6ce44e454d85,", + "FriendlyName": "Fortune Teller Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a98251fe-3eb9-41cb-9982-6ce44e454d85", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1777474640 + }, + { + "AvatarItemId": 1454, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dmas6u15o98831skwq4yiijbp.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,", + "FriendlyName": "Fox Ears", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:17.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2178, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2Q7b4md8JE2YQ8VUXFaioQ.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,A-xoYdSUF0aEr_XZ9Me-3Q", + "FriendlyName": "Fox Ears (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:32.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2176, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "oTmagc6WVkCO9huhLC83Lw.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,db-JiDmRhUKX69weDLiHsQ", + "FriendlyName": "Fox Ears (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:28.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2177, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "puLgwYOR7UaDsU2V90_S0A.png", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,UZY81cnsV0meMp2lg3_UaA", + "FriendlyName": "Fox Ears (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:30.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 808, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "9w8alcxm82aptgu5w3qz5fmq0.png", + "AvatarItemDesc": "OTnfT-4NQU6Gd_iVPDx8eg,", + "FriendlyName": "Frankenstein Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fdf3939-0dee-4e41-8677-f8953c3c7c7a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:33.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 809, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "3yzcedaeybzym9rpwv7nr1pkg.png", + "AvatarItemDesc": "ZsggwOJ8XUOGfaJltM418g,", + "FriendlyName": "Frankenstein Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c020c866-7ce2-435d-867d-a265b4ce35f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:35.743Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 810, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "8rb3glamh1kq36xnt8gupn6np.png", + "AvatarItemDesc": "tkbHKZ9wu0W00VGeUbIaaw,", + "FriendlyName": "Frankenstein Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "29c746b6-709f-45bb-b4d1-519e51b21a6b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-20T18:25:37.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2428, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YAStiIqV1kOk0QSNHUcb4w.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,", + "FriendlyName": "Frantic Rabbit Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:34.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2431, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mjpFdsz4lkijguGsysSzgQ.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,J8z15LYBak6jHu3ZaaKAXw", + "FriendlyName": "Frantic Rabbit Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:44.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2432, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dTQ_d68ca0Ct3Gh71yHo4Q.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,QSXXWZrS6UaoAUn3u7hXFw", + "FriendlyName": "Yellow Frantic Rabbit Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:49.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2429, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PKZOkAkm-EK4joFEnKiyWg.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,AcLApWMhAU6tK10O7KoOog", + "FriendlyName": "Frantic Rabbit Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:37.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2430, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SsUBkL6SS0OyRRh-yZ886g.png", + "AvatarItemDesc": "283e98dd-1ba7-4997-858f-96046e0ac431,dhJm0moskU6urIwG1TXs_A", + "FriendlyName": "Frantic Rabbit Vest (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "283e98dd-1ba7-4997-858f-96046e0ac431", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:41.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2433, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "JmX7fBYzcUyHQStd9MQjHQ.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,", + "FriendlyName": "Frantic Rabbit Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:52.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2436, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XOxmltIN6kSzCdXiXACw6w.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,MVhTW78G_E6z2mY0Y5ZnAA", + "FriendlyName": "Frantic Rabbit Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:00.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2437, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NAR-63A3vUSNmcNgu2Jhyw.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,Adiw7dQqyk6pS0q3T0lcSg", + "FriendlyName": "Frantic Rabbit Wrist) Frantic Rabbit Wrist Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-28T00:20:03.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2434, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "Ny4bGecU1kqDWzGnr1B1Uw.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,_4mk8E4PUEGf_3SPuFUy2g", + "FriendlyName": "Frantic Rabbit Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:55.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2435, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "VqjRqu51m0mkMRhHinsJ9w.png", + "AvatarItemDesc": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c,aO5Q4BPpuU-bsNavGii40A", + "FriendlyName": "Frantic Rabbit Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d898dc4b-d6a9-41af-8e6d-f22097e8a01c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:19:57.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2228, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b4ixc77euj1q8qnhp0oronjic.png", + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,", + "FriendlyName": "Frog Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:34:47.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2690, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6s01rm1gm99uf83uib3fvl2fe.png", + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,f-RfHoCWik6QgNKDhCs7Vw", + "FriendlyName": "Frog Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:13.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "405f20aa-f888-4e21-b322-34d11017ff16,f8100fb5-92fc-4520-9755-2deaa5430ddb", + "FriendlyName": "Froggy Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "405f20aa-f888-4e21-b322-34d11017ff16", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1783947886 + }, + { + "AvatarItemDesc": "2b1f0884-c887-4ec9-9984-426405c50f93,", + "FriendlyName": "Froggy Floatie Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b1f0884-c887-4ec9-9984-426405c50f93", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1878911014 + }, + { + "AvatarItemId": 1286, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8xe1257ru45z0b0867roht3eu.png", + "AvatarItemDesc": "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2,", + "FriendlyName": "Froggy Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:57.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2675, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "bU_6Nph8n0KifAktXPcZww.png", + "AvatarItemDesc": "9117bd15-674e-4f22-9ff8-e7365fa43907,", + "FriendlyName": "Acoustic Guitar", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9117bd15-674e-4f22-9ff8-e7365fa43907", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:12.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2676, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "RhxHB33yikq68wkkfgieQA.png", + "AvatarItemDesc": "9117bd15-674e-4f22-9ff8-e7365fa43907,UdQxfRULDEG6BvJV7HWO_g", + "FriendlyName": "Acoustic Guitar (Dark Oak)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9117bd15-674e-4f22-9ff8-e7365fa43907", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:18.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2691, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "63a52r43lcwxjmtic35ksma5j.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,", + "FriendlyName": "Keytar (Teal/Pink)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:16.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2694, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "exgelbk5b8wyv6c26lb3m0bpe.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,GCwZMIr720iH3TszOb6SQA", + "FriendlyName": "Keytar (Black/Red)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:21.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2692, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "9xylg3wbs72hs1ygy34nllz47.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,LmMg-y8w8kiXdre4vUJrZA", + "FriendlyName": "Keytar (Silver)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:18.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2693, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "28bnag6bw350qkhpaeyn6qxbi.png", + "AvatarItemDesc": "52ea64e5-f062-4683-a380-4f8a48938686,ze-eTUX6WUqE-n5nIcH0aA", + "FriendlyName": "Keytar (Lime)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52ea64e5-f062-4683-a380-4f8a48938686", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:20.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "309f3ab1-d2a4-4d63-b248-93d9795a8a23,", + "FriendlyName": "Fruit Salad Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "309f3ab1-d2a4-4d63-b248-93d9795a8a23", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1227548947 + }, + { + "AvatarItemDesc": "58d3f945-510f-4f92-87e3-a57e29611e4f,", + "FriendlyName": "Fruit Salad Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "58d3f945-510f-4f92-87e3-a57e29611e4f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1528201612 + }, + { + "AvatarItemDesc": "c9badd8f-3f81-4c05-8a31-24abd8839872,", + "FriendlyName": "Fruit Salad Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c9badd8f-3f81-4c05-8a31-24abd8839872", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1617129328 + }, + { + "AvatarItemDesc": "788baa71-6ffb-42f5-8dc8-d0f117b04a8b,", + "FriendlyName": "Full Body Beard Close", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "788baa71-6ffb-42f5-8dc8-d0f117b04a8b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1033187763 + }, + { + "AvatarItemDesc": "83ff0105-37a7-4c71-902c-20f473223d1e,", + "FriendlyName": "Full Body Clothes Boots", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "83ff0105-37a7-4c71-902c-20f473223d1e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1780430972 + }, + { + "AvatarItemDesc": "ccae1771-434a-48b7-9b4e-1dcf7b70e0da,", + "FriendlyName": "Full Body Clothes Shorts", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ccae1771-434a-48b7-9b4e-1dcf7b70e0da", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1326708353 + }, + { + "AvatarItemDesc": "a3648455-51cb-4ed4-85c5-9a11f96ad3e7,", + "FriendlyName": "Full Body Hair Emo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a3648455-51cb-4ed4-85c5-9a11f96ad3e7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1690082234 + }, + { + "AvatarItemDesc": "c5760078-15bc-4ea6-acb9-0ea295a1894b,", + "FriendlyName": "Full Body Hair Flat Top Etched", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5760078-15bc-4ea6-acb9-0ea295a1894b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1377927688 + }, + { + "AvatarItemDesc": "fb64a2ef-ae83-48a9-b021-0b3f0de2d7a5,", + "FriendlyName": "Full Body Hair Fonzie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb64a2ef-ae83-48a9-b021-0b3f0de2d7a5", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1598195373 + }, + { + "AvatarItemDesc": "0d58f3a0-7165-41a4-8d77-f88f393d0003,", + "FriendlyName": "Full Body Hair Wavy Middle Part2", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d58f3a0-7165-41a4-8d77-f88f393d0003", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1183373744 + }, + { + "AvatarItemId": 185, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3tc8n5oesvirh7gtgk7ky4kbe.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,", + "FriendlyName": "Fur Coat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 470, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7yjxqxjcdtwsuu11rr9ijfijs.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "FriendlyName": "Fur Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:38.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 471, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ekhbvef0wyu4wu32a5dvhlqqq.png", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,643b5f2d-0666-4869-b699-03742de34d16,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6", + "FriendlyName": "Fur Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "77c37481-a185-47ff-af55-65f9fdfcf9e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:39.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 555, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bw125vxk818kcf1j36i5htzc4.png", + "AvatarItemDesc": "e042433a-40a2-4758-8074-3b9d407835ec,", + "FriendlyName": "Romper (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e042433a-40a2-4758-8074-3b9d407835ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-10-26T00:08:07.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2790, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zR88MAKiPkimNE_ZJTgJ5w.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,", + "FriendlyName": "Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:03.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2791, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ih-SEYO0UGR8D_71mKT7A.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,1sxxUbsQMki0sTwxEQIbtw", + "FriendlyName": "Blue Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:06.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2792, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1Ud6HT3jLUC49RkE6B4aIA.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,CEnWMDmcMEyjX5mRdhtXXg", + "FriendlyName": "Gardener Gloves (Floral Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:08.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2793, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "vyZN5MKfwEqqAG1yJQQsbw.png", + "AvatarItemDesc": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f,ySbPHkP8WEee2V_P8lt_RA", + "FriendlyName": "Pink Gardener Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a7aae41-ae4e-42a2-a1df-2bc89ab11d3f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:11.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2820, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uc9Y0cmhYUi-BL1OyWYdxQ.png", + "AvatarItemDesc": "2e31fd6c-dffa-4571-8e53-fc28fd0c98e0,", + "FriendlyName": "Gardener Apron", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e31fd6c-dffa-4571-8e53-fc28fd0c98e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:47.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2821, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BC8BhDnhAUWqYwj2dvuT2g.png", + "AvatarItemDesc": "11cec676-0ec3-4778-abe4-a9d399900b54,", + "FriendlyName": "Gardener Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11cec676-0ec3-4778-abe4-a9d399900b54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:50.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2822, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Jr88nPGi9EOu05WlVylKgg.png", + "AvatarItemDesc": "ad6e7c98-8056-4e12-92ad-bfa19a00f4af,", + "FriendlyName": "Gardener Shoulder Basket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad6e7c98-8056-4e12-92ad-bfa19a00f4af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:52.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1452, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7z1qr4njpz9qbv9yoq0metngg.png", + "AvatarItemDesc": "cf1a6f8f-1649-423b-b513-b8d40d242b1f,", + "FriendlyName": "Gathered Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1a6f8f-1649-423b-b513-b8d40d242b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:06.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2083, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "sWYoOZIiY0KBXKZYOQ38zw.png", + "AvatarItemDesc": "cf1a6f8f-1649-423b-b513-b8d40d242b1f,8lwfuCRnU0291wqwgTwgRQ", + "FriendlyName": "Riveter Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1a6f8f-1649-423b-b513-b8d40d242b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:26.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2372, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HpB5RkqKYkWc9i_l8Ul66Q.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,", + "FriendlyName": "Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:28.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1343124314, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c6waJLUmskK9zsdjD-IYZw.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,tp0jNW7f0Uubr9nTMWuhXw", + "FriendlyName": "Gentlelady Hat (Pastel Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1842821000, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "H2YS04F1YEKwQ4HAk1GV9A.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,clKD22mme0y8apzUhHmI_A", + "FriendlyName": "Pastel Purple Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1605291822, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NKtxE0Ix9Emzu3RF2XLuxg.png", + "AvatarItemDesc": "8a26efb4-2116-48ba-84a0-c4fdcf74c191,9liduuR6IE-6C4YN_FKcZg", + "FriendlyName": "Pastel Green Gentlelady Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8a26efb4-2116-48ba-84a0-c4fdcf74c191", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.504777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2373, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wUMF4BFbWkqp6MofjZVxOw.png", + "AvatarItemDesc": "39dde11e-7aa1-4bd2-8532-62665e8d8dd3,", + "FriendlyName": "Gentlelady Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39dde11e-7aa1-4bd2-8532-62665e8d8dd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:30.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2374, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67o7qPb1O0q7OC60gJfjew.png", + "AvatarItemDesc": "f7764314-c02c-4b46-9c56-8c60351d9b14,", + "FriendlyName": "Gentlelady Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f7764314-c02c-4b46-9c56-8c60351d9b14", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:33.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1845, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4wnr9uzel6d0lekcez3cw73vw.png", + "AvatarItemDesc": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,", + "FriendlyName": "Gladiator Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:42.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2058, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjmkJTDtvkWsVfz6feF-eQ.png", + "AvatarItemDesc": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,RjX1VTHHoEiNBfXkkniDgw", + "FriendlyName": "Gladiator Torso (Folklore Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:50.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1846, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "azdd85r2w7xl4v8s9ahpnhll3.png", + "AvatarItemDesc": "1633766e-fa77-48f3-8c6c-a233e56bf320,", + "FriendlyName": "Gladiator Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1633766e-fa77-48f3-8c6c-a233e56bf320", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:54.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1847, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ds517zev16c5r1kkrizriku7.png", + "AvatarItemDesc": "594a69fb-9f76-4f83-b2aa-94f0e72e996c,", + "FriendlyName": "Gladiator Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "594a69fb-9f76-4f83-b2aa-94f0e72e996c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-13T16:23:59.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2056, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "JohKrR2o7kqMPVnZzCfTyA.png", + "AvatarItemDesc": "594a69fb-9f76-4f83-b2aa-94f0e72e996c,GL6frGGo2E25VLcw61otXA", + "FriendlyName": "Gladiator Helmet (Folklore Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "594a69fb-9f76-4f83-b2aa-94f0e72e996c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:46.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1880, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6s6sp9bunolpihm8u9g9a538k.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,BqNgpjP7a0qmm3I3DNbKLA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:54.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1879, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "9hfdcf1titio5gbw8o07wtk2x.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,5H0I1anlkkuUYIM28_ZOlA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:52.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1878, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "ax893cneyhf2wbm98vxenyxxl.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,9tZq7Xvfj0WhhLBAuAacxA,l_iJLV14q0eNgOsOI1u0pw", + "FriendlyName": "Glasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:49.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2092, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "tD99sEOpYUyYZ-MFVKK3lQ.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,dRQI80Qn4E-kzhPJoW66MA", + "FriendlyName": "Acetate Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:19.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2087, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "SYNkbSO4106ID65XGWzI2A.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,FuMUQFSnnU6awB-vPaeuZw", + "FriendlyName": "Acetate Glasses (Bubble Gum)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:06.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2089, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1lmEHPGbEUmTeG7d_lJ6Uw.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,OBcu3bf1NEio_7t9smqGag", + "FriendlyName": "Acetate Glasses (Amber)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:08.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2090, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "h-G0dl2uBEKQJ4YBI0Sw4w.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,F1IV-zZ22kKtXkr2pHp_YQ", + "FriendlyName": "Acetate Glasses (Seafoam)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:12.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2091, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bV0WFKjHdUGPdgeJ9k_BZA.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,Sad0HQX_Y0eYTx1qqgxKwQ", + "FriendlyName": "Acetate Glasses (Sunrise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:18.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2088, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "yt7jg1pEiUuJZMJuLmP55A.png", + "AvatarItemDesc": "c70005d5-6276-4a98-acb3-6a77bc19379a,-OgDdlvL9EyzeiA50YY5kw", + "FriendlyName": "Acetate Glasses (Charcoal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c70005d5-6276-4a98-acb3-6a77bc19379a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-09T20:24:07.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "0b38899f-6429-49aa-b626-2696359dc1cf,", + "FriendlyName": "Glitch Wings Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b38899f-6429-49aa-b626-2696359dc1cf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1805065453 + }, + { + "AvatarItemId": 2831, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GenNVM5HVEKt_DZK0VQsQw.png", + "AvatarItemDesc": "f90773c6-d0f0-417a-9af5-183d1e5a657f,", + "FriendlyName": "Glowstick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f90773c6-d0f0-417a-9af5-183d1e5a657f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:37.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2832, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "veozF1-boEi1gm3EPXGN2Q.png", + "AvatarItemDesc": "c51780fd-9eb5-4bc0-9da0-34ab22d800df,", + "FriendlyName": "Glowstick Bracelet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51780fd-9eb5-4bc0-9da0-34ab22d800df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:39.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1916, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "084qi740gtrdi1jyi3yjt5q6r.png", + "AvatarItemDesc": "b772a5a9-dd79-43e4-8d95-f965d81bed8d,", + "FriendlyName": "Kevin the Goblin Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b772a5a9-dd79-43e4-8d95-f965d81bed8d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:19:16.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 116, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "04xgdtlhrslfn0o41so5fnjoi.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,", + "FriendlyName": "Winner's Medal (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8xn26bulod52740rbedbiwok2.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,ERnUPsyT6kuEnIRZF8zPwg,wzt-5C_LjEmIQWUVPcYWhA", + "FriendlyName": "Winner's Medal (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:40.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 906, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4rm8hpw76k9han4s5k58jc9gc.png", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,iaDL2im4lkCXUo0NWM-F3g,wzt-5C_LjEmIQWUVPcYWhA", + "FriendlyName": "Winner's Medal (Bronze)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf13fa1c-f991-4aaa-9402-aac109be9562", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:38.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 503, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3dzbcs7n1malcbemyi60hae9g.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,", + "FriendlyName": "Golfer Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:30.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 504, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bwzoz4dtuwlngxk9f0tu4ubak.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,48c18418-01dd-40ff-9dfe-c4cf5f4e1245,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "FriendlyName": "Golfer Hat (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:31.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 505, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1iqnljjvv128u9d8mlok69amn.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,6584dada-499e-4d7c-958d-466955b20640,ddf429fd-a883-48a5-869b-bc12e44d30bf", + "FriendlyName": "Golfer Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:32.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2309, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "V_xCN9YtNEqAF6pAvOkE9w.png", + "AvatarItemDesc": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54,2Mgx6S9f8k2H1AAYbkzjQg", + "FriendlyName": "Golfer Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:01.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 506, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67p6qo2ys9s1k0dg867wt3hlo.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,", + "FriendlyName": "Golfer Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:33.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 507, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "arl9g7pg2z44m7uqebdjk9ool.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,4c845dbb-26c3-48a1-b562-e28a7416b154,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "FriendlyName": "Golfer Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:34.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 508, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "01ylknc03jnajh84tyy03knwg.png", + "AvatarItemDesc": "f76b709c-b173-426e-81f2-279a67d2fde1,96e2b29e-e553-40f1-9eb6-8a9f7e64da39,6ce21c65-f4e9-41ac-be94-8eb1a08d5c76", + "FriendlyName": "Golfer Shirt (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f76b709c-b173-426e-81f2-279a67d2fde1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:34.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 510, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eqstzzls6wlgjfdkwtcpj53gp.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,", + "FriendlyName": "Golfer Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:36.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 511, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6etin2gx8z6mi85m8niz96so2.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,72a010c0-bddd-4e5d-bca6-9821d82de9a6,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "FriendlyName": "Golfer Gloves (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:37.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 512, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bi4tmrffhhhfm5xmt03sfyvnu.png", + "AvatarItemDesc": "eada710d-ea0c-473f-b9c6-23e210607b24,32a5e42d-2b60-40b1-a145-ed96fc91af8e,f4cea991-fc3d-4715-bf82-8a8fb60e8799", + "FriendlyName": "Golfer Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eada710d-ea0c-473f-b9c6-23e210607b24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:38.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1085, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "02907rdlfgdak15kydocegpp7.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,", + "FriendlyName": "Graduation Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:19:58.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1163, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ll333kyucn9aqhff96o0txse.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,FaWMLkJJgEKFIO493fzGxg,yzL0M_N5Q0eAa8IxACHPAQ", + "FriendlyName": "Graduation Cap (Maker Pen Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:05:53.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1803, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2lx5rfp2pcfj2z8z1wr7f1uuu.png", + "AvatarItemDesc": "KPmC_wyX_ECe7fdKwoLMGg,nCPVMrWXGkeeIN-969xzuQ,yzL0M_N5Q0eAa8IxACHPAQ", + "FriendlyName": "Graduation Cap (Video Creation Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ff82f928-970c-40fc-9eed-f74ac282cc1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:53.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1114, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bm925en2c6cq90rczyqx8jtek.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,", + "FriendlyName": "Graduation Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:20.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1162, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "44739pgupptu8ki1u1i0efr2i.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,iZy7RmfAz0603oMO-jD-WA,ica0LZyM_kOox8gxW7CaMw", + "FriendlyName": "Graduation Gown (Maker Pen Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:05:43.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1810, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "91oh3z6dug9f7buh2555h4ggn.png", + "AvatarItemDesc": "U_H976qYRUmg9e2clsAflg,4D4ge1PYokaj_g96k4oasA,ica0LZyM_kOox8gxW7CaMw", + "FriendlyName": "Graduation Gown (Video Creation Class)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "effdf153-98aa-4945-a0f5-ed9c96c01f96", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:36.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1065, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "98c5cktigvhw45kiu5db444wj.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,", + "FriendlyName": "Grillmaster Apron (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:36.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1066, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4z1alsikbw52tg9t7o4b0tmf8.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,eV3Nv6SWS0yl3f-fgjPb-w,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (Denim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:45.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1067, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f08vx9xbl5n8d0cdwaqunjhs5.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,Lxx3q6JRnUW_cPG_kGG4ag,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:48.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1768, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4ulmpmgcp45eu79inqrlhz2d.png", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,vek37vRROUKnSJKVP0CH6w,t-ZEWJCgFEyGP2PjNblQMA", + "FriendlyName": "Grillmaster Apron (Charcoal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "edaf32b5-4ee4-460e-b628-2fe1b18d8edd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:22.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1076, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5gw83joytwxpoarjjrzas61ip.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,", + "FriendlyName": "Grillmaster Mitt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:09.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1077, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "10xu47yb16kjm62v7fasn937i.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,90qQe0AR3kOou0MsuoTmPA,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:11.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1078, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1uhjnjl997sivm0uih15zk8ug.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,hZKPr4sg10SRj9DLuEgOrw,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:12.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1782, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4oo9dvqgiu2yehkvk6dey3jdc.png", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,x3Q0vVeG_E6juqGM1o0SOQ,ojCeJ1VwtUelyVqFnUwK5A", + "FriendlyName": "Grillmaster Mitt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b152baab-0d13-4a1e-8426-6607ba1846ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:38.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 961, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2i4t0ybjtnnq1pyn2a2zmo32b.png", + "AvatarItemDesc": "m5Plrduzr0yj_9mTn1x3Vw,", + "FriendlyName": "1988 Jacket (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ade5939b-b3db-4caf-a3ff-d9939f5c7757", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T22:07:36.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2736, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PEHKs2wQe0SFdV5ekTdSkA.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,", + "FriendlyName": "Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:42.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2741, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "h6jjmTpeBkGLdb3Sbt_-0Q.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,dtuR9xLXVUGzSi2G1DMh7A", + "FriendlyName": "Red Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:50.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2737, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GjQ7GjOgmUSLmyl9DH0hjA.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,0FlA_T8GsUe9PdiZamlP_g", + "FriendlyName": "Guitar Pick Necklace (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:43.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2740, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-IEuy3vB4kK_xxTTWTsQXw.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,U8ZZiMEeZ0-qJfcpqBP72A", + "FriendlyName": "Guitar Pick Necklace (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:48.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2739, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rVsH83vYHkO80KaIwEVyyw.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,Gct9Vt-L4UiTjAl61hsebQ", + "FriendlyName": "Guitar Pick Necklace (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:47.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2738, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EfUdI1XtV0eRy8PsZBG2dg.png", + "AvatarItemDesc": "3eda64ab-2a11-4976-a253-bef7ac36ba5e,2r_gYLhQrkS0bjwaxLecfw", + "FriendlyName": "Grey Guitar Pick Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eda64ab-2a11-4976-a253-bef7ac36ba5e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:45.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2185, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NboCAaiehkigdPqWgZxRoA.png", + "AvatarItemDesc": "2d07c0d0-b277-46dd-8980-382bdaa4749e,", + "FriendlyName": "Angel Halo", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2d07c0d0-b277-46dd-8980-382bdaa4749e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:07.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2554, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "z5qNLmnhd0aHhDdIxO-wsg.png", + "AvatarItemDesc": "2d07c0d0-b277-46dd-8980-382bdaa4749e,f6O5cw0vc0ineEptRysocg", + "FriendlyName": "Science Halo (Invasion)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2d07c0d0-b277-46dd-8980-382bdaa4749e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:44.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2742, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "i6sQmmZpgEWvZNkWaYpaWg.png", + "AvatarItemDesc": "fe16e054-9cae-4a97-a0c0-256b51cdb0e5,", + "FriendlyName": "awesome sauce", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe16e054-9cae-4a97-a0c0-256b51cdb0e5", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:52.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1147, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2y48o7b7lqi2zpho283lmy51n.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,", + "FriendlyName": "Runner Harness (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:00.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1146, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6xgus9q1g8stsgzfpsinr3li4.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,sX0ZZDcCUkyFSJISfvq0vg,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:56.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1145, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5fi6lrfurv47a5jv8vcz3fmxv.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,TnEDe1QoMUCUdqPL_7CxTA,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:51.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1144, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "axtrgokadupvoy6xhgle28790.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,AC3zfm38JEyBZWuXDh4Ukg,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:47.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1143, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ke3ipxnfopjshcyjl3kc7mhu.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,RfZNDPUok0WyQEa3Ko08ow,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:43.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1142, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c7vhxic6odjal3d4elovf6li9.png", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,dyEaPloE8kazWC7Zblv8uQ,ovy_3j_qwE-7mmks1Z47ig", + "FriendlyName": "Runner Harness (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:38.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,E7zTfb8zbUKuld0rxNt7fg", + "FriendlyName": "Harness Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1003112581 + }, + { + "AvatarItemId": 1164, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "435egcfj9uj17vp4n7ey9u4rv.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,", + "FriendlyName": "Runner Wrist Guard (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:29.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1165, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bwo3vsz07ltq6bsv2c8bszrgs.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,sX0ZZDcCUkyFSJISfvq0vg,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:32.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1166, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "aiygi6wtiasforjki3lnkg1ab.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,TnEDe1QoMUCUdqPL_7CxTA,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Lavender)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:34.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1167, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4poast86ikgyo8gx5xw0v6tl8.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,AC3zfm38JEyBZWuXDh4Ukg,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:35.887Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1168, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3qn2bl4qw76u5zafxwim8hslp.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,RfZNDPUok0WyQEa3Ko08ow,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:37.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1169, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9ny1060mulzukvbnj8r8jsk77.png", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,dyEaPloE8kazWC7Zblv8uQ,rgzG7sfrlkqd9EO13LNmpQ", + "FriendlyName": "Runner Wrist Guard (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-28T06:02:38.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,MvC6bRHxmkaXvWS6Q0cIFg", + "FriendlyName": "Harness Gloves Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1beaf288-b5a5-475c-8149-c8f3d1146083", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1884541935 + }, + { + "AvatarItemId": 1153, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ac4yc0p8960f0pcub6ys93vk1.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,", + "FriendlyName": "Runner Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:28.913Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1152, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "diwahrdx0ag6e8a70yal6uos9.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Euf1h1qsZ02evDsyoHYbPQ,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:19.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1151, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8i6nmv636c2mf1lxl17ox1d4i.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,lsyYlxY9o0a3TbUrHkxeyQ,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:14.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1150, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6urhwi4bkj3jxkd4pq9lxzapp.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,JVZHZm6nGEqE2zEEPoHbwA,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:10.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1149, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6m4gs8ysz42vgc0rn06moyk71.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,zlqLrGEa8E2WW9ey_szREA,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:06.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1148, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a22f3ny3x4czh17wi8o7cxnp9.png", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,L2iFQcL170OajqfW7t6sVw,PZHohDVX0U2frX13N4KYDQ", + "FriendlyName": "Runner Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:03.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Iz6cWpY2B0uOQ9BnY3fSbA", + "FriendlyName": "Harness Helmet Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ec62c-67db-4b72-9e29-0ad3ecd0b737", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1073107630 + }, + { + "AvatarItemDesc": "8705d28f-49f4-48d9-9946-13f18aabfe4b,", + "FriendlyName": "Hammer Head Shark Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8705d28f-49f4-48d9-9946-13f18aabfe4b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1285875244 + }, + { + "AvatarItemId": 1088, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6t7adsd6rb8v7w110axmy2fpk.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,", + "FriendlyName": "Big Head Bow (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:16.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1089, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e6lw7lptzoy6kedqecx8jk0ob.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,8iZO162KNUiMC8l_z3OC_A,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:21.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1090, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzilh25e7rcim8kgspfpj95ba.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,f_c50saStkaGJuhLjJ9lfA,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:25.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1091, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "18wsz87rejkgdxqq1wc65icjt.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,FUP4iRnoc0ikNmjrFD06lg,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:28.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1092, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d40z3ipsfgbe9hux480cvd5a2.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,GmS0wKwzVEqdf3NoTMHG5A,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Teal, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:32.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1093, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3bok5be6yuo76i8jwnvzr1kf2.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,ZNKGkou_gU-1cxBFy7pDKw,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (Red, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1094, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bo0hnyxq2ym7xxw3uicmaydgj.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,-Iqmonry4EKoqAULCKzRPA,3DdSzSqV1UG5AtPr8ro3Kg", + "FriendlyName": "Big Head Bow (White, Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:38.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1294, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9y2nffs1iso5dhjr38gstlhpq.png", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,D-SngK7hhkOpoZ2aMWv9EA,mkDVc0Mz4kmuLZ7powBHpQ", + "FriendlyName": "Big Head Bow (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05df3a25-4878-4d4f-a8db-eb2c3fde337b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:35.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1355, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "824gaa1rig6e7wnf2tax1j1j9.png", + "AvatarItemDesc": "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3,", + "FriendlyName": "Big Head Bow (Green Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:45.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 563, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ckopqewba1tguffkue6s78rc3.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Headband (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-13T20:56:57.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2076, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "JGqSITx8dk6mMtS4gTrMaQ.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,TeSudgm-NEy3DXe3z_VRqg", + "FriendlyName": "Headband (Retro)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:11.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2077, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dl4fJip8EEukov2aRWgT7A.png", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,RnrXuIRvMkeW5w1lcNiuaQ", + "FriendlyName": "Soccer Print Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:14.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1095, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3cc5bj2gxpsblgaruo3t9egwm.png", + "AvatarItemDesc": "J6IQt4G1zUeN8v9Wn50BUw,", + "FriendlyName": "Tied Headband (YouTuber, SoulFox)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b710a227-b581-47cd-8df2-ff569f9d0153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:57:43.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dn3d4707ue1k2osfrlcvavlzo.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,", + "FriendlyName": "Notes Headphones (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:43.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1045, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6gdfkv6dgyb7aup991rfx4ns8.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,viL4CPch2ECwG-RNaWcyZw,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:47.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1046, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ftwtrq2ong4idyrsil0vdmek.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,8ANbOhNnO0yLQOCIBsDwfg,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:48.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1047, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cph7oedpx7hgjd38qq4qtgoal.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,a5nUvJZgTEGBUvdHx0Y6Ng,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:49.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1048, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "60mqpyf61x722pn9if4oa70qi.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,JWBK5v78Y0uRVEXlD-Qk6Q,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:50.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1502, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ecbfwjrpe9e79d5vrdubmh2c4.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,thG3Dc1PFEqAvT9xCVshuw,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:44.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1049, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6k8vpwyhep8o2yv8j2grqd8h5.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,hf0gkFVS8ESvceRsC3LAXQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Psy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:51.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1050, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bhaum9si19ovo5u6f0ik7fcyv.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,4scG_UBlzE-5DWRhSrtm6g,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Chillout)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:52.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1051, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f012aaez937gwxfcp6amwbh1a.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,OqsbZ8Xls0i8uA6ha7H3qQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Trance)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:54.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1052, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0qoenhowtvhtxia42kmo22j5v.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,nX7krD8e2km_ZlOLWcoxdQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Lounge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:55.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1053, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "75w0v1yh3q3o0dfoelib0un32.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,ns5tIRishUOp-nYqLedcwA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Techno)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:56.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1501, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1czj45wblivuiu4m5plehpt8f.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,k_ZYy0SjtU2TB7IFdq2vDA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Synth)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:40.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1054, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "48dwhvntvbcgfw5d6yly1wr9y.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,I1J-xY4YNU2dB_od4P_7Dg,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Heat)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:57.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1055, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2jwdp3bzhm7xcqru8enpkveu7.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,2sf-FU4WckGBDWUkVz4z1g,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Sky)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:58.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1056, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7yuwkss6may3hvmhg1sepbsvw.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,3PfP661Fg0CruzRxis9EiA,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Dreams)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:24:59.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1057, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "19ts2iqffk8o10duwieerytd8.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,d9mwowRelE2lDKEA0UQMZQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Dawn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:01.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1058, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3lnjdd8bye13tx2sv12v4bisy.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,Uz2hrptCa0erjqyPoiZxew,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Void)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:02.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1500, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4dh2u6nja9oh06bc83r9s4pqb.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,PQTvQiYu-ESRUldnxgm5BQ,_Yq00H-NWUyQNvGxRunHbg", + "FriendlyName": "Notes Headphones (Neon Jungle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:35.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2209, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8thOMYf1lUybc3FN_bY8WQ.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,oNtLjorrZ06t4yIdWqKCcQ", + "FriendlyName": "Headphones (Wooden)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:26.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2795, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SbQkirL3sUidVCVTEkiS0A.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,pAlEt5sqD0mKgFd_0b7RgA", + "FriendlyName": "Floral Headphones (Blossom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:16.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2796, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OjJJcLC_l0SxYuXsaT4blg.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,PNH0VNS1qk6nWVecYmVDDQ", + "FriendlyName": "Floral Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:22.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2797, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "snOakm9_R0upwnPGZDeEuw.png", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,sXF1BZr8X06AYnWUrWxy1Q", + "FriendlyName": "Floral Headphones (Lavender)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:25.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,e5954339-a379-4510-a54e-18f64e29ffe5", + "FriendlyName": "Headphones Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5e8c351-052b-41dc-9308-107627e11f77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1452629529 + }, + { + "AvatarItemId": 1083, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c0aetz7tzn1vg6rqcopztwq0i.png", + "AvatarItemDesc": "akAhnN0cFkecj8q3spixkw,", + "FriendlyName": "Notes Headphones (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c21406a-1cdd-4716-9c8f-cab7b298b193", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-12T01:24:56.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2375, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ONlwGgbS3EG8In6YslOpGw.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,", + "FriendlyName": "Headphones Mic Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-30T17:42:35.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2376, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NY7YBusIgkCX1gRBRn_KDg.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,M9ZKWX59e02037Qy4rgmAA", + "FriendlyName": "Headphones Mic Hat) Headphones Mic Video Contest Orange Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-30T17:42:37.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2565, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "46_hhqT9P0isgoM6SBDAVw.png", + "AvatarItemDesc": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95,u2nh8EAZT02TxPTWVl59UA", + "FriendlyName": "Science Headset (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1c4f1aa-3acb-42ca-bfe7-5e2a1e863f95", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:01.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2151, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "730oGQopMUGTrJNdUPTkeA.png", + "AvatarItemDesc": "1db08f02-d559-40ea-9bae-dd11046694a7,", + "FriendlyName": "Video Partner Headset", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1db08f02-d559-40ea-9bae-dd11046694a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:18.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r0K8LjSj0U2lRyRwptpOoQ.png", + "AvatarItemDesc": "fbf74c15-4b42-4cd4-94dd-d0e86f11541b,", + "FriendlyName": "Heart Antennae", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fbf74c15-4b42-4cd4-94dd-d0e86f11541b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:00.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1955, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1uytf301ssh2nyytxcnm0r2v1.png", + "AvatarItemDesc": "5944a66e-b386-43d9-a4b6-7b4a8e987fd8,", + "FriendlyName": "Heart Sunglasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5944a66e-b386-43d9-a4b6-7b4a8e987fd8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:15:27.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2239, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6Wo-wGbct0y9F6OdVvsZog.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,", + "FriendlyName": "Anime Hair Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:26.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2557, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5VAj7sHGqkWRLz4ktf2ibQ.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,YfRfaC49J0yW0wnSJf5DwA", + "FriendlyName": "Anime Hair Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:50.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2555, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gK3kKpiWpkeCTN3muHzInw.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,0WW04NMMfU2LLahCq0jQ6Q", + "FriendlyName": "Anime Hair Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:45.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2556, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HPGiZzkfbUagfHZSMVdpIQ.png", + "AvatarItemDesc": "b7ba6920-e000-4081-a025-7da67c803008,4kYWnAwZo0SDZFmiU8jJwg", + "FriendlyName": "Anime Hair Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b7ba6920-e000-4081-a025-7da67c803008", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:48.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2576, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d7RSXQeGNE2FITMNS4x9lA.png", + "AvatarItemDesc": "f09be0ec-19eb-4443-843c-b387b6731e48,", + "FriendlyName": "He-Man Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f09be0ec-19eb-4443-843c-b387b6731e48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:32.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2577, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KeJx2mtu3UWduFaoj3_lzQ.png", + "AvatarItemDesc": "28f00eee-d3d1-4cac-ac70-fca064fcbaf0,", + "FriendlyName": "He-Man Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28f00eee-d3d1-4cac-ac70-fca064fcbaf0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:34.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2578, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YgFKySGtSUOhM_WsMBYbNQ.png", + "AvatarItemDesc": "9ab4ea8a-1b16-4969-9b80-8e36b1f782ca,", + "FriendlyName": "He-Man Bracers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ab4ea8a-1b16-4969-9b80-8e36b1f782ca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:37.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 56, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "37a5o4unxmplzke25isjr0n4n.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,", + "FriendlyName": "Flower Hair Clip (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 485, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "chwhfus6b1dkb3vtb59wuymig.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,ce465d53-3807-423f-baa0-bdba2a75ceb3,65b89eae-347e-4eff-9ff3-7f25cace0125", + "FriendlyName": "Flower Hair Clip (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:05.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 486, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "23uhduy56xwn0r399ed4oscrw.png", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,596097a8-d545-4256-959d-1d3cd96fc64c,65b89eae-347e-4eff-9ff3-7f25cace0125", + "FriendlyName": "Flower Hair Clip (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:07.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1763, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0lhij515w2a2pi1bjg8pqzztr.png", + "AvatarItemDesc": "d15139de-5e57-423b-b8da-335ad7a4df5a,", + "FriendlyName": "Fae Horns (Hidden Worlds Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d15139de-5e57-423b-b8da-335ad7a4df5a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:29.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ehfh8lkiq0cqwio8dxk9kdg0y.png", + "AvatarItemDesc": "f51abeae-7c5d-4065-aa2a-b2e7025b3fcd,", + "FriendlyName": "Fae Tunic (Hidden Worlds Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51abeae-7c5d-4065-aa2a-b2e7025b3fcd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-19T20:29:33.157Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2251, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CHOox5OrjECPbDwbhliATw.png", + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:05.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,N-C_xBTZeUq1YsdELln53Q", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1435331028 + }, + { + "AvatarItemDesc": "d52ff2bb-d396-478d-8153-2aa845feb306,G4MuMrpiKEWN0uEBcGlX4w", + "FriendlyName": "Hiker Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d52ff2bb-d396-478d-8153-2aa845feb306", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1797238548 + }, + { + "AvatarItemId": 2252, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nonED7GaH0qPJ67vtt7QxQ.png", + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:07.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,VfSI4zfHbECwcrQmcbelTg", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1842931803 + }, + { + "AvatarItemDesc": "32cdcbc0-6a9e-4245-980f-6888f12fe065,qRIbIelHBUOU71eoK2XQeg", + "FriendlyName": "Hiker Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32cdcbc0-6a9e-4245-980f-6888f12fe065", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1992229999 + }, + { + "AvatarItemId": 2253, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwCEXZw8HkeluG48ThKdoQ.png", + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,", + "FriendlyName": "Hiker Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-28T17:06:11.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,IcgjR69rYUuNhsb3s9qjYA", + "FriendlyName": "Hiker Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1494615195 + }, + { + "AvatarItemDesc": "3916a22b-ef4b-4ecc-9945-fd57867af597,yMkN0om7l0-_47gDrBWxiw", + "FriendlyName": "Hiker Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3916a22b-ef4b-4ecc-9945-fd57867af597", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1664824131 + }, + { + "AvatarItemId": 57, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0ky7ceo16g76hoi2vudcp4u2d.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,", + "FriendlyName": "Hockey Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 639, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7rm6qzjybqpssdvxptzb3cq12.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:11.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 640, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ap6srpnsg5qje8yjd0k6xoq5v.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:12.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1718, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "08ly4egzp8znsf9rr74dqyzom.png", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,qdA34j6p_UqphqChTCHEYQ,7c4d447c-6594-4bd8-9510-12ad683ab116", + "FriendlyName": "Hockey Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d703940-224a-4b0c-a04d-f365550071e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:09:30.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2078, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HfG5sUm4_EC1LqHmdMhF4Q.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,", + "FriendlyName": "Hockey Helmet (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:16.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2715, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yVFjcpLMhUGC2rFphSKjxg.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,Z6o5ySTsmkmwAGslDH7k4w", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:06.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2716, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "l33ddE1N5kSh2m4vYm-lhw.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,cLKDeDSwd0OvF27bW_wBWA", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat Yellow Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:08.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2714, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hGYlm15Q8kS3Qwi0ugLJdw.png", + "AvatarItemDesc": "c51877eb-efda-4a04-8394-092637e2a9bf,m4t_QKkV4EWTVEH8biQuEA", + "FriendlyName": "Hockey Set Hat) Hockey Set Hat Purple Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c51877eb-efda-4a04-8394-092637e2a9bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-02T20:39:03.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2079, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DLbcAnoh6kOcI8vlUfbceg.png", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,", + "FriendlyName": "Hockey Jersey (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:18.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2702, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "R1XZo-34Wkm75mdVSeCFPw.png", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,c3Su0qnh-UK-ymcGaUy4Uw", + "FriendlyName": "Hockey Jersey (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:34.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1547381390, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2,zhgbWHad00aE2KNFM3FNWQ", + "FriendlyName": "Hockey Set Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ebdf9a1b-46d2-4960-8da5-0cfa0c0adef2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.507777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 186, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3c1iifcjs873h8qwb9pekkmpu.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,", + "FriendlyName": "Hockey Shirt (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 496, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8j3mq83a7m2bcd1ue5tzv6r2o.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,fcbdfbfd-254a-4538-b119-43e23404b8b5,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:19.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 497, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bkzswe4hfhdvak21tbjhe6icc.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,b1ea158d-1663-4db1-bb09-9847f6e9d1e6,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:20.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 498, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "91cioy61054580v8ruloocj3l.png", + "AvatarItemDesc": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,e5635772-4e84-4b30-b77f-4a1196cb599b", + "FriendlyName": "Hockey Shirt (Red, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:21.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 752, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "964z64p0djst7okp5yy4qnb5n.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,", + "FriendlyName": "Rec Room Hoodie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:24.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 787, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9d2j5ikkdbngspuc7tki5lecu.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,mvMeyStxGUGlNLSYNeSubQ,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:22.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 786, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4jf3ogeghh4h207uwsgs53ay.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,u_Gzj9Nro0WS48xKnH9few,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:19.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 796, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "14fpnws4q76d3svlzzp3ogr29.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,JTgXKeT7w0K2Mw4Jk2HCqg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:26.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 793, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ccdrg5p5ytm15zierfd33d677.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,rNhOQoP3XkOBHV28ze2NpA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:50.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 878, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "89cwe9sw6lx2jml1bc48d9io1.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,TEgugZRs80iJbhL87RdYwg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:54.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 877, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "81kofnq5cb3fvgyozz15k1ro0.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,ytmSzjhCr0Sm58MSNbwJUA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:39.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 876, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6w9zh5x30uv01aj0rnudzyicy.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,yM5xRrd9OkmDGyQddwOP6g,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:37.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 875, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2xo1n1x3mq2iry1ptnih904dt.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,-Nf7WM2TeEKujB637h3tRA,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (White - Starter Pack)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1612, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7ngrs0bphj486q6105peoygyq.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,LuYelZY8NkqmOp1pnxPXBg,oHKhT-GTx0iFWrrlz9s6Tw", + "FriendlyName": "Rec Room Hoodie (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:23.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1809, + "AvatarItemType": 0, + "Tooltip": "For earning $100", + "Rarity": 50, + "ThumbnailImage": "5yo24l49i1ul66r6i2yec4ggz.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Z10TTlOeW0mumezbqXKGzA,Ht0WRvpguUaLZ1tigCzCMA", + "FriendlyName": "Rec Room Hoodie (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:33.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2153, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9ZVUjjO21U2ujrZzoNX9SA.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Ist3SXFwKkCCvxirWLLjng", + "FriendlyName": "Video Partner Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:21.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2202, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6TEq-Xvb8kirly5K2g96vg.png", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,9jB8U3yxjE6hanEZgMdvtA", + "FriendlyName": "Rec Room Hoodie - AAPI (AAPI Heritage Month)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:02.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,Ebi3A88h2U2yiygzWBBL_w", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1692692324 + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,gPPg0QrOlEmVmvEc_OheXA", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1976989717 + }, + { + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,d022fcd7-850e-490b-893e-eb713e5848b5", + "FriendlyName": "Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c3cbaa6f-f830-4236-95ef-12a7f5961590", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1155409075 + }, + { + "AvatarItemId": 1375, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aojr333s0451ovrb3sccpajtm.png", + "AvatarItemDesc": "5362d685-fc55-4b43-85a5-cc3d6cad3f69,", + "FriendlyName": "Rec Room Hoodie (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5362d685-fc55-4b43-85a5-cc3d6cad3f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:10.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2245, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9EjzWesDb0qc78bYLR4gXw.png", + "AvatarItemDesc": "ac957e1e-676b-45c9-a39d-b238d2329a03,", + "FriendlyName": "Horseshoe Bracelet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ac957e1e-676b-45c9-a39d-b238d2329a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:58.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2246, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ewhdfsr2ocppmfgs5a2ddeb9h.png", + "AvatarItemDesc": "9c4976c1-a0eb-4a1e-b5cd-08d44a4b3011,", + "FriendlyName": "Horseshoe Earring", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c4976c1-a0eb-4a1e-b5cd-08d44a4b3011", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:00.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2247, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5vzoo1Ope0ayRkVlKiJDZg.png", + "AvatarItemDesc": "cd77c8ed-ab60-4f42-9bee-0141c7e3e626,", + "FriendlyName": "Horseshoe Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cd77c8ed-ab60-4f42-9bee-0141c7e3e626", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:02.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2377, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "Vv4sv_u_EU2qi4FLQuoCmg.png", + "AvatarItemDesc": "b6d10952-d492-45e5-b54c-de3ab138be0b,", + "FriendlyName": "Hot Dog Hat (Mustard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6d10952-d492-45e5-b54c-de3ab138be0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:39.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2378, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "N2RqYRINKU-RJEE5tp5BNg.png", + "AvatarItemDesc": "b6d10952-d492-45e5-b54c-de3ab138be0b,2KCpbel8JkaeAGQRgvpt2w", + "FriendlyName": "Hot Dog Hat (Ketchup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6d10952-d492-45e5-b54c-de3ab138be0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:41.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pR32NwZrJUWfiKEdw3CYeg.png", + "AvatarItemDesc": "4c52478a-b2c9-4520-8f88-52f2262cc6bf,", + "FriendlyName": "Hot Dog Suit (Mustard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c52478a-b2c9-4520-8f88-52f2262cc6bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:43.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Oy7uq9Y6VEi4oYPxwG-aMA.png", + "AvatarItemDesc": "4c52478a-b2c9-4520-8f88-52f2262cc6bf,YR7P-KQLGUKPF7JbW85K7g", + "FriendlyName": "Hot Dog Suit (Ketchup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c52478a-b2c9-4520-8f88-52f2262cc6bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:46.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b5221aee-22ca-4855-81d9-0b4491229060,", + "FriendlyName": "Hoverboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b5221aee-22ca-4855-81d9-0b4491229060", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1868649629 + }, + { + "AvatarItemDesc": "2c09d6b4-039e-4475-b013-0630740d820d,", + "FriendlyName": "Ice Cream Truck Box Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c09d6b4-039e-4475-b013-0630740d820d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1514562461 + }, + { + "AvatarItemId": 952, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9zxq5okpcuze5r8vwhknknnfd.png", + "AvatarItemDesc": "W7AJ4UYlVkWF5nLbmpmRBg,", + "FriendlyName": "Ice Queen Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e109b05b-2546-4556-85e6-72db9a999106", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:39.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1939, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8tqs6ckiohne11n3sui6d7dvq.png", + "AvatarItemDesc": "W7AJ4UYlVkWF5nLbmpmRBg,5Hk7UR60rkquUNxpQ8O0Gw,zMXDVNsKn0GhVd2Tjho0-g", + "FriendlyName": "Ice Queen Crown (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e109b05b-2546-4556-85e6-72db9a999106", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:31.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 954, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "da9hnrgqj5nbv2mz69que9ixy.png", + "AvatarItemDesc": "ilDgmey2ZUWlFiuEAN7YoQ,", + "FriendlyName": "Ice Queen Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "99e0508a-b6ec-4565-a516-2b8400ded8a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:50.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1941, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4n3khnydgbrm5og1sy02ak2rp.png", + "AvatarItemDesc": "ilDgmey2ZUWlFiuEAN7YoQ,K9wPZ9LHo02GPMtf02rANQ,U7jMLMX4_kKIy1K9IcHOXA", + "FriendlyName": "Ice Queen Gloves (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "99e0508a-b6ec-4565-a516-2b8400ded8a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:55.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 953, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7lqx31eaisz33b3nvpajuxr47.png", + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,", + "FriendlyName": "Ice Queen Gown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-11T23:14:48.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1940, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "edfuldgko8416a06hqug8ckhi.png", + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,iqSyLD37IUejWYa8ndvqnQ,qrXO8rdEaUqUCXC1QVkhsw", + "FriendlyName": "Ice Queen Gown (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-09T18:26:48.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "3R_zf8xliEiurVU8GwaItg,TRpbSTiI9Ua2E6H7XiT0lQ", + "FriendlyName": "Ice Queen Gown Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ff31fdd-65cc-4888-aead-553c1b0688b6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1042519908 + }, + { + "AvatarItemId": 2695, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5YfnXd6wCEGAI8JfuYuzDQ.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,", + "FriendlyName": "Ice Skates on Neck (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:23.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2698, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vZa1r1ZS0CNjfN_JffQ5w.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,ENYQugZRP06SDh6d-oJcjQ", + "FriendlyName": "Ice Skates on Neck (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:29.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2697, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VLP_xmNQBkWJGvDaXrvrQQ.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,KdsZQVCA-E2TiEozsSUv7w", + "FriendlyName": "Ice Skates on Neck (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:38:27.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2696, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cdkQ8Fl7h0-dBHorHAVq7g.png", + "AvatarItemDesc": "45e31b92-0cc5-4841-9c3b-74b3f1830435,uQjh0DySZE6M6DzD2VUPgA", + "FriendlyName": "Ice Skates Neck) Ice Skates Neck Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e31b92-0cc5-4841-9c3b-74b3f1830435", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-21T19:38:25.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1505, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "21f1vt9sfsmvu7i0m9fa5v25j.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,", + "FriendlyName": "I Heart Rec Room Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:52.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1586, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e2e7acypymae4je7p6sgzp5cj.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,4oywwjb3-kifr-um9x87Pw,11ZEZTFhy0ONpoXb3kiewQ", + "FriendlyName": "Suspicious Goblin Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-09T22:38:26.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1695, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dpd8v8j4srhnyyau92pk9e677.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,i4Zu9uZuWkWwm8IVGZwkbA,RmjxL1O9bEGOuYxY9kzHEA", + "FriendlyName": "I Heart Rec Room Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:32.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1694, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a4crcmj7ke4jvau32zw1jzrfl.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,HQOKcDCvVUCItkAKo2ygGg,RmjxL1O9bEGOuYxY9kzHEA", + "FriendlyName": "I Heart Rec Room Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:29.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1922, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "dygizmsl47ijdpie6x1q5tspr.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,Rd-iQjCjUUW8_o2kIb1rFw,Ez3OIIGl0Eqys2JGnSnQHw", + "FriendlyName": "Pumpkin Pattern Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-05T22:17:09.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1988, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5nmucss74wx9jq66yxgs5bstt.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,lYsxZfxd0EadrnjliEj4Ug,xnZj7ipl60WO8e_BXK-mlw", + "FriendlyName": "Lunar New Year Sweater (Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:43:34.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2778, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GVwVHoCBbEqlEsScL_SXfQ.png", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,ymRNNF0h3UOv-yH0ILxhiA", + "FriendlyName": "Rec Rocks Sweater (Ethan Bortnick)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97253092-90ba-4ff2-b137-6324ab244d11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:39.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1514, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "36ltpv6ah0ck4rjqp3q1ypw3k.png", + "AvatarItemDesc": "5873814e-c9a1-4042-b96d-2aa0e0d52e2a,", + "FriendlyName": "International Thief Coat (Mystery Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5873814e-c9a1-4042-b96d-2aa0e0d52e2a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:57.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1385, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ev03rmx75zjo4wrysupitq82o.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,", + "FriendlyName": "International Thief Gloves (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:15.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1386, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3hl7xtr4md2vxqqi3d3z5zgxe.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,DtFYrQwaCky5C1ZycqZbiQ,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:17.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1387, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3gmgmoqsor5tij57yfuqtj9i0.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,9SAPupKy_EO6YeG0SMkI5A,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:19.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1388, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "29vqlnj8n33qtfh0u4ka6qbtz.png", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,hiMYlmk5sEqh-sElOY3A9g,egpzBs77Q0CpDndZHqXOog", + "FriendlyName": "International Thief Gloves (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1014dded-3bbc-479b-a6cb-afa7413de2da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:21.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1381, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7oh8ujd818cghfg99enk8x1d8.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,", + "FriendlyName": "International Thief Hat (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:54.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1382, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d0flbz3gdxvalp78ggvuj2x8p.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,RBRFHN8_rkuImbyDAfcHWw,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:58.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1383, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2jtzah4nvld7knjcyuxcgml69.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,QJSPgq2KXEOWNynY8uqQvA,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:01.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1384, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0bjo5i6bxmha24uhgq15xq71t.png", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,4RH8_Z1mqU-JIVvx31wOsA,BOB_mAXdA0u2khOH5qjqXQ", + "FriendlyName": "International Thief Hat (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:55:05.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1515, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "87ayi0litn96xap15w1n2iq85.png", + "AvatarItemDesc": "d12e5738-00c7-47e7-9334-8399eec5aa89,", + "FriendlyName": "International Thief Hat (Mystery Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d12e5738-00c7-47e7-9334-8399eec5aa89", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:19:01.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1377, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "alszurr07un4fg5ta4aw0z10o.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,", + "FriendlyName": "International Thief Coat (Mastermind)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:44.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1378, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzgwcv1v83e9lql441lco77up.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,YB7kXMyLJ0mF_5WidhmH8A,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Spy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:47.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1379, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8r2mn0ynnf5yr8p2yifhlpskz.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,bQyNgFWAe0SPttPri6hxyg,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Rogue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:49.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5lo58vo2p2eebv8s1aunra4xy.png", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,wRPoHTsXFESK5pjkxVMqGA,SeA8s77hr0K6DRZyVlRTCg", + "FriendlyName": "International Thief Coat (Outlaw)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "097a3266-4aab-4b23-8f56-373c9b1bc837", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-14T23:54:51.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1588, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0h4tnqaliavr973k1vxic8j21.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,", + "FriendlyName": "Jacket Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:15.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1589, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "688mit0sflv394soqbkisg5u9.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,v_5qFi-vAUSDd1fLNL7pvA,X0_GvGrZGkSP9Ub81CHgqQ", + "FriendlyName": "Jacket Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:21.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1590, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e1v40txlz8i12cmhyjswztu53.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,y5O8zRJy4kuhnXzATSWKew,X0_GvGrZGkSP9Ub81CHgqQ", + "FriendlyName": "Jacket Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:12:33.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2492, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4DTZdxI0zUWUUrv5xARVCA.png", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,tLbmGIoldE2CC5Qi1oyqyA", + "FriendlyName": "Jacket Dress (Winter)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6fccd476-c621-4f3f-b2e3-45c95be912b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:08.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1656, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b8nie0lojas6v6embnj3tteiq.png", + "AvatarItemDesc": "85e03c16-4288-45f4-a579-c911437d27d4,", + "FriendlyName": "Hoodie Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "85e03c16-4288-45f4-a579-c911437d27d4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:20.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1284, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7b7sjloem429tv2xpco7r45jw.png", + "AvatarItemDesc": "105a5100-cb57-410f-a26b-1ee21002f067,", + "FriendlyName": "Jack Frost Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "105a5100-cb57-410f-a26b-1ee21002f067", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:48.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1754, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "36iyxpz2ym6qs0k43ssehyv05.png", + "AvatarItemDesc": "105a5100-cb57-410f-a26b-1ee21002f067,prEZHWvOM0ysaNfAe06ywA,q0nBwoIx_UWPe93wHDWTYA", + "FriendlyName": "Jack Frost Coat (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "105a5100-cb57-410f-a26b-1ee21002f067", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:03.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1283, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xvu0kz1tmmaoifwn7fvdtl6o.png", + "AvatarItemDesc": "e6216087-fb37-46f1-bda4-a88c4640f339,", + "FriendlyName": "Jack Frost Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6216087-fb37-46f1-bda4-a88c4640f339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:44.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1755, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6swgo4j116b5mqtoe4dl3pyh1.png", + "AvatarItemDesc": "e6216087-fb37-46f1-bda4-a88c4640f339,KwhdQ1deNE6Y6jzO9vaAnQ,byvLynH8x06OQ7GNCXOVYA", + "FriendlyName": "Jack Frost Hair (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6216087-fb37-46f1-bda4-a88c4640f339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:06.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1282, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7e934oy5eg0xzkuoeyk090u8w.png", + "AvatarItemDesc": "fbdca48d-4ae6-41ad-8c33-b70a9e33fd77,", + "FriendlyName": "Action Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fbdca48d-4ae6-41ad-8c33-b70a9e33fd77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:40.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1285, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "extmqwfwviflhb677k7y0j5vw.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,", + "FriendlyName": "Jack Frost Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:52.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1753, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f5anyvib4zotf2hlzp6twhrpq.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,H6fcuLxeBUSY3oERFc-plA,oGrNKZLpMU2bLwNEe4MPnw", + "FriendlyName": "Jack Frost Wings (Lilac)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:02.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1851, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "23wddlluexp562bjt32f4humv.png", + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,dpfTnOvsrUePTVs0XZD9qQ,gOt7BYXvw0uUS-U53KThAA", + "FriendlyName": "Carnivale Wings (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:59.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "022ce375-ed02-479d-b87d-9d72d126bbd4,3XVjOeAlF0K23sB1krWo7g", + "FriendlyName": "Jack Frost Wings Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "022ce375-ed02-479d-b87d-9d72d126bbd4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1956877502 + }, + { + "AvatarItemId": 84, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "en7nrykz2qqj8buybdl5qjmo7.png", + "AvatarItemDesc": "49397e16-1027-4286-9bc4-b59eae565ff0,", + "FriendlyName": "Side Pony Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "49397e16-1027-4286-9bc4-b59eae565ff0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 187, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cbcx8aneit1a0djh7mi7a9pd1.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,", + "FriendlyName": "Jazzercise Shirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 652, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ln4084il57kp9glk7af3u8u0.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,3b2cfc85-4e2a-4e0d-b738-c296ae8692f8,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "FriendlyName": "Jazzercise Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:39.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 653, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "axojfsb5b2u9dmz817cfa6h7z.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,78eb0969-f246-4cac-acd6-295df7c092e4,f6f26e2b-657b-454a-8663-55d6b9b6e50c", + "FriendlyName": "Jazzercise Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:40.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1850, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dhyvjxkfgg2fqqwac0tztvlo1.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,oF_wUv-ShU6GP34Ch0MnlQ,YTd0lIQLcU2Y2trwuLhJTg", + "FriendlyName": "Carnivale Shirt (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:45:46.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2159, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HIXUSEOjTEymO-YYuEyRWw.png", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,zwKRET9pP0O5uhPU_yHrlg", + "FriendlyName": "Ballerina Leotard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f492f8f-ab61-484a-9587-747132b54309", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 236, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4tf3fmonukbqbhbj46tlqwovk.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 237, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6e2rsyvdnxb0lqz1t1x527ji2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 546, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6yhybki2x1u4frp9d3mj5057z.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:33:21.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 548, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3404czywuocyttg1yeivg4l6d.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:29.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 549, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bjb4do6miad7c1uhkxlvmvtdu.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,d2b40639-5d34-45b6-904e-6dd29030ff44,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:31.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 550, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "68baneyontwji279r6ya132th.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,724c79a3-822c-422f-9462-a5374ee0211c,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:33.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 551, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "88z32iu1vqx5t79u8rbzxb2jh.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,c72911d0-453f-4732-b1bd-dad520220a06,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:34.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 552, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e8kd5l3itqg0vfla6oyfwyug2.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,90bc2d31-1715-4d1a-813a-7c57e66cb017,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:35.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 553, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "00krwbc9mtk0c5c3rczo0ba1i.png", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8", + "FriendlyName": "Jersey (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-12T23:38:36.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1666, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "elfjxtuklom8p26dxh4behbx3.png", + "AvatarItemDesc": "1631eb78-6718-478c-9b62-abbb0537e09c,", + "FriendlyName": "Jester Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1631eb78-6718-478c-9b62-abbb0537e09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:30.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2745, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pRrGRfj8XE-CVSQQ4C_hmg.png", + "AvatarItemDesc": "1631eb78-6718-478c-9b62-abbb0537e09c,9Yj55hS4gEOB34bggoRnzw", + "FriendlyName": "Jester Hat (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1631eb78-6718-478c-9b62-abbb0537e09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:03.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2029, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0sChD_Bp9Eq36jZRpwRRUw.png", + "AvatarItemDesc": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,", + "FriendlyName": "Jester Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:50.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2747, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YNqKRBfsS0CFwf6ZTMjyvw.png", + "AvatarItemDesc": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1,g_jYTusUbU6W_6jHif-CKw", + "FriendlyName": "Jester Shirt (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b1ece775-b1c4-4c5b-b2bd-cd0542794dd1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:11.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2030, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "v__U3oLXhEWjGpKh_L8MoA.png", + "AvatarItemDesc": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,", + "FriendlyName": "Jester Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:08:53.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2746, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "FhANeW3-fUqUVfzO1F7Ujg.png", + "AvatarItemDesc": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5,YzJL2s9S0Umv5XH_g5PG_g", + "FriendlyName": "Jester Gloves (Mardi Gras)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e6d49fa3-db6c-4e5a-938c-8cd9b6f977e5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:06.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "96781359-a8e3-4cfb-be5e-79456ead88c6,", + "FriendlyName": "Jetpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96781359-a8e3-4cfb-be5e-79456ead88c6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1129156756 + }, + { + "AvatarItemId": 2187, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wsQ3uMHglEWj0o4orNrwvg.png", + "AvatarItemDesc": "17e64798-f6ca-47b9-84ed-0c83b8678056,", + "FriendlyName": "Karate Gi", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17e64798-f6ca-47b9-84ed-0c83b8678056", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:12.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2188, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "oPruDc0j2EGwzv9I9FWS5g.png", + "AvatarItemDesc": "01107a3f-9a4c-4fb9-8918-e6195e346d42,", + "FriendlyName": "Karate Wrist Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01107a3f-9a4c-4fb9-8918-e6195e346d42", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:15.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2276, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "abgo5oixia5zua1xh9pr9knb6.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,", + "FriendlyName": "Back Keytar (Pink)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:08.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2277, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "8jjzm1iwi6v9h63b4saad9462.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,njOH4bbntk6THrxbd9TDDA", + "FriendlyName": "Back Keytar (Orange)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:11.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2278, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "aiaec0zo6aruhuugqrw4xr6p2.png", + "AvatarItemDesc": "d7410e54-5193-48b9-b5c9-06e4439dd924,0aNp_2sWHUS_08TMe-_AzQ", + "FriendlyName": "Back Keytar (Purple)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d7410e54-5193-48b9-b5c9-06e4439dd924", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:13.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 238, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7fmex9oqxz42zd52m3gbxb3l6.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,", + "FriendlyName": "Knight Armor (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 239, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2p5x227st2nelqfya7ywpcppx.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ed134bee-d199-43eb-98bd-206571603f40,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 240, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7hajn9eszpqh00q654d0iqds1.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,59866578-d4e0-417b-9483-233ab108b1ac,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 241, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4d9p7yhef4c8w1jrj3cd61gbp.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,22dc463a-a89b-46ad-9a0d-557c742b4a80,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 242, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ce1v4ja422emiw67599tvacqd.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1113, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e00pks3p8szzbwf082p7udhb7.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,ITH2OpzjP022vZ5JgxV_iQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:17.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1312, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58uqy0e4o8nv424g5aory3woy.png", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,PMa9370LPEig_7pKwCceEQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "FriendlyName": "Knight Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "739fd42a-8a8c-44a5-afa3-e0974802c6ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:08.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 117, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "23zm48ii99k5gviu6wd03hbo7.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,", + "FriendlyName": "Knight Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 118, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6zfzza7fqr56kicscf9e2lil6.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ed134bee-d199-43eb-98bd-206571603f40,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 119, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7rnan20ml6o94ezwuvfv1cpqu.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,59866578-d4e0-417b-9483-233ab108b1ac,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 120, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1r4tyyoovhhxpcatrb3ixmuwl.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,22dc463a-a89b-46ad-9a0d-557c742b4a80,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 121, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d1fd8ubrjvta346f7rqtixtxy.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1119, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3dqqaqnta15d1edl9k3vlzu5f.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,ITH2OpzjP022vZ5JgxV_iQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:56.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1308, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6hsc92hhcj6jhxh9avyxdqvj.png", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,PMa9370LPEig_7pKwCceEQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2", + "FriendlyName": "Knight Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2e5afc40-7e25-418c-8b1b-316b8ef479a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:46.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 276, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "85cm7w6an4z42wlzdgr9k9d3m.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,", + "FriendlyName": "Knight Gauntlet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 277, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bohc8ayxvepselhmkomofmhb4.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ed134bee-d199-43eb-98bd-206571603f40,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 278, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "37g3go1ko7zez3ua49s4tjpbf.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,59866578-d4e0-417b-9483-233ab108b1ac,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 279, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1q79g6q9p9to9xi0qtaj9a9s3.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,22dc463a-a89b-46ad-9a0d-557c742b4a80,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7fy3687o43lmb87xgdmjslpcy.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1109, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bvdx04lp2rrweg7mjd5hl2bnk.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,ITH2OpzjP022vZ5JgxV_iQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlets (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:07.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1318, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9tsbb3bjtxr44158yh7ncrn5n.png", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,PMa9370LPEig_7pKwCceEQ,fd67ec40-f898-4df6-b846-8bf350f362b8", + "FriendlyName": "Knight Gauntlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:47.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 58, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0gmqaftgdlsoph92078ffbhpq.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,", + "FriendlyName": "Knight Helmet (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 59, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "acqok07g1d8homim9mnjivxuf.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ed134bee-d199-43eb-98bd-206571603f40,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 60, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0r1snvqxftybe0m39s41incq1.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,59866578-d4e0-417b-9483-233ab108b1ac,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 61, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a4l2zz1yv4o8nlxgjjcyczfza.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,22dc463a-a89b-46ad-9a0d-557c742b4a80,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 62, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c2t0mn4y8uzp3mnvpdd9z0unb.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,e1f49b74-b071-4bd1-9c4b-af36d24d45c5,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1122, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "15u1a4mvdkbujoe41brggozje.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,ITH2OpzjP022vZ5JgxV_iQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:07.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1302, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eb444rmgn7q2ygy1kaxe6tsh2.png", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,PMa9370LPEig_7pKwCceEQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d", + "FriendlyName": "Knight Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1917, + "AvatarItemType": 0, + "Tooltip": "Survive any game with the Squid King watching your back. Go Krakens!", + "Rarity": 50, + "ThumbnailImage": "5gf9eq3qeqfuxeh3zi34kmtt1.png", + "AvatarItemDesc": "9176086b-a0b4-4298-a7fb-eb91ea421e89,", + "FriendlyName": "Kraken Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9176086b-a0b4-4298-a7fb-eb91ea421e89", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-27T20:19:29.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 243, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5zxnu4wl529tzcip7nwk8im0o.png", + "AvatarItemDesc": "39278302-b52a-48a8-a566-ed756a5fa1a9,", + "FriendlyName": "Lacrosse Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39278302-b52a-48a8-a566-ed756a5fa1a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 244, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0pmiv1zb13od6nl9giu03eizj.png", + "AvatarItemDesc": "39278302-b52a-48a8-a566-ed756a5fa1a9,e3454bc7-c88d-4958-8b81-db3fe5472a1e,da36bbcc-2a9a-4ba2-9e4a-6f52c9b1ef74", + "FriendlyName": "Lacrosse Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39278302-b52a-48a8-a566-ed756a5fa1a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 4, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0eekw2et1by72nmm01x4haan4.png", + "AvatarItemDesc": "7134c60b-0640-463a-8d97-caa9184bef93,", + "FriendlyName": "Lacrosse Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7134c60b-0640-463a-8d97-caa9184bef93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 5, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6cznt9a4cfpxfot00vquk2gb5.png", + "AvatarItemDesc": "7134c60b-0640-463a-8d97-caa9184bef93,e3454bc7-c88d-4958-8b81-db3fe5472a1e,7e688b30-ed13-4790-9ad8-c49885d1b7e8", + "FriendlyName": "Lacrosse Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7134c60b-0640-463a-8d97-caa9184bef93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aqhhqebivzflcpyzyf70m1608.png", + "AvatarItemDesc": "c859a371-5d34-4e53-a96f-a6f7f3377aba,", + "FriendlyName": "Hero's Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c859a371-5d34-4e53-a96f-a6f7f3377aba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-29T23:24:59.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2155, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kNf3tlW1kEGHxU4EDJABMQ.png", + "AvatarItemDesc": "c859a371-5d34-4e53-a96f-a6f7f3377aba,aMOwLebcBU6SuPigtovUYg", + "FriendlyName": "Future Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c859a371-5d34-4e53-a96f-a6f7f3377aba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:24.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 576, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "500ajcxzkdjvi2db9ev74rstc.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,", + "FriendlyName": "Laser Tag Visor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-28T21:55:28.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 580, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "81x4cfhnckh1a4ni8mxn9trzj.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T19:04:48.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 740, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9z5r4i4elegluux9nocggv8lh.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,oT1vLrHW6UC2hWLIT3f_SQ,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:02:48.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 741, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1mu93bk5rhp7yqekc2k5aukn9.png", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,-e6bmXnwZU2EVwZ8RupKbg,4e378419-d043-4e86-ad0e-66288ac21cf4", + "FriendlyName": "Laser Tag Visor (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "123de433-a005-4994-8a11-d2d84ad66b11", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:02:52.063Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 574, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0mhwz2ogugpedmv34mtp8p649.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,", + "FriendlyName": "Laser Tag Sensors (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:34.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 575, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1gwx6x19onp32q7iolw1hdeck.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,08b7c12d-3317-4d27-9b40-34737a709454,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:35.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 742, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4b2c3lu6x2lix8wwu23es1z9t.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,jkuw9EhCpEKn-2lKJXWd3Q,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:05:11.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 743, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2mtxs666msasqmqw57iol4efk.png", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,2Z_FxSPiyUODtlFZWF9DFQ,959ed05b-0215-4bbe-bb3d-64e4448d676b", + "FriendlyName": "Laser Tag Sensors (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24d94673-f394-42e6-a3c8-276f8b8aa722", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:05:17.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 579, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "15nrb536jxz09ck6emxavel7l.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,", + "FriendlyName": "Laser Tag Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T18:30:43.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 581, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dzm37a1dmgdsj0aetwi19afes.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-12-01T19:04:55.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 744, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0qqunbtr2dxrzl797zjamcpg9.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,EC0ttvk6eU2HEMacLAx2Iw,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:06:17.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 745, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "13cf7bho89ewryskk93gh33it.png", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,mhsglVEtREmElF5wuLw3NQ,3b825377-d2eb-41e4-a736-b107f88f62d6", + "FriendlyName": "Laser Tag Gloves (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3044adce-90c0-4f66-81f5-39c4fca86fdf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-04T20:06:20.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2248, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eYDxcWNR4kOlLWhyBLjM2A.png", + "AvatarItemDesc": "665d4fe9-88dd-4fae-97ad-201884a6ca02,", + "FriendlyName": "Lasso Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "665d4fe9-88dd-4fae-97ad-201884a6ca02", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:04.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 958, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dl9x5bl0fz2s03jn0xnqwzao0.png", + "AvatarItemDesc": "mfFLpKQRE0anhrmeUvW0bA,", + "FriendlyName": "Laurel Crown (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44bf199-11a4-4613-a786-b99e52f5b46c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:24.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2608, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KgPqo5yaDkibaNuclBpcJA.png", + "AvatarItemDesc": "mfFLpKQRE0anhrmeUvW0bA,p5TStfnDXk-EY7k6UdHxnA", + "FriendlyName": "Midnight Gala Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44bf199-11a4-4613-a786-b99e52f5b46c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:04.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2855, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5Zki1DsZ0UW_pMecyP3JoQ.png", + "AvatarItemDesc": "37b68987-73d9-4aae-8e3a-edd24ae890a9,", + "FriendlyName": "Layered Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37b68987-73d9-4aae-8e3a-edd24ae890a9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-03-10T22:40:41.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2856, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_uhKCE9kyU6LOgc4Sixxqw.png", + "AvatarItemDesc": "37b68987-73d9-4aae-8e3a-edd24ae890a9,UCxz68a5E0WaZkocDfEf5A", + "FriendlyName": "Silver Slugger Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "37b68987-73d9-4aae-8e3a-edd24ae890a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:43.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2199, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZN_075PMnUq0_A0OEIo51g.png", + "AvatarItemDesc": "32b7a724-1112-476d-9ddc-2d04655c9df0,", + "FriendlyName": "Leaf Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32b7a724-1112-476d-9ddc-2d04655c9df0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:54.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 759, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b89jss76zlj72ms617ah00h3b.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,", + "FriendlyName": "Letter Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:24.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 874, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6m42sjjil9iqwyjnymvy516a7.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,apSQiQ1eO0mEzRsbugeu9w,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:32.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 873, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6mcbxskzp5pfwcnetac762guc.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,VpqHNpPiY0CfRJEjRRM-mw,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:31.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 872, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eq8wyyutlzcjeneqryph1cfzk.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,Z1F9rjKqCE-Hd2-Ip5g3gA,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:30.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 871, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4xku2dldqk5zfbjc6m9uw2v07.png", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,iufpnteVCEiiZu_m-yPovQ,fz_0gZ6RXEC78NpYsx_eqA", + "FriendlyName": "Letter Jacket (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ceae8b0b-e2d4-4761-b6d0-f002300464b0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:28.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 762, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0luffpfh5ls3knh5u8r6w0x5u.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,", + "FriendlyName": "Letter Jacket Cuffs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:27.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 937, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "amfc2bzt0jj15t76n2lnhv25e.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,UUyjVU4mvECPhI9xTMYR1Q,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:13.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 938, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4hbdjfasjaponwtrx2tmjnwa3.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,HWc25-d8dUmNN2F3n8mV5A,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:15.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 939, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "39guqwt3fqru9qq6psoua1yff.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,Y0e7o0_BKUm7XJSLediN5w,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:16.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 940, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8uiva9ffm7qrizcvqx99frs29.png", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,MS7EPNLJEEyMLC6_yHQYtA,4EHIyZkOOEqnM-PuD_a5xw", + "FriendlyName": "Letter Jacket Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb4cd1d9-dc8c-4cee-bf15-de5fcdf8089f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:17.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 756, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7hwrwj6lty2rur9opjp05fgej.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,", + "FriendlyName": "Plastic Sunglasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:21.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1641, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8oaudlg81glmnhd3cqy8t650s.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,z-yaXP9ImkysLOGuCQiblw,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:46:59.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1642, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ec4tf5t9oqy8loqqvqfrw3rl5.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,SmRcrycMZ0qYNsHuQQOORA,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:01.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1643, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "egy0vqy333fqme7jsrxachsgt.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,0svDrGn2NkmhUjC1VJUgRg,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:03.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1644, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0hvhuyqd3i33hptb6ljvlazeo.png", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,Aq4fjEW5gEyzetXMoG2tdQ,S21TW8NwMEWLV9zqHxWfFw", + "FriendlyName": "Plastic Sunglasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f533147b-f79a-4c1c-8e47-d9c30eb8a695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:47:05.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1832, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ew0vvmubkulgd7xjvlqhfuc2.png", + "AvatarItemDesc": "affdc9ad-8312-4421-8f56-e35dfdaeb63a,", + "FriendlyName": "Lifeguard Whistle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "affdc9ad-8312-4421-8f56-e35dfdaeb63a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:16.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "e97f05da-57d3-4927-aa22-c124a94d8432,", + "FriendlyName": "Life Jacket Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e97f05da-57d3-4927-aa22-c124a94d8432", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1703069062 + }, + { + "AvatarItemId": 1900, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ct8ojf0jqnnc7an3o2t5zs1a5.png", + "AvatarItemDesc": "6b9e022c-0b68-48fd-8eca-da8573c18900,WWVPdeMMi0S4_Dmn85yEqQ,FRUp0aYZTUmAXsybauOs7g", + "FriendlyName": "Director Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b9e022c-0b68-48fd-8eca-da8573c18900", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:06:09.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1417, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "75icni7eg78l4eyuy6hwwrzzk.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,", + "FriendlyName": "Luau Dress (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:21.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1418, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "63oehgwwbgnddpx5bauhi2ztm.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,ZXeI9jooq0-dQaX-FlkNuQ,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:23.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1419, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "08kjv45btrzy2apeo276vys08.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,IzdUC8x0F02i3BhPfEUOYw,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:24.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1423, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "du4ay6j6cuhxjyg0f72e9dwtt.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,nPUQvBsxu0mzhQg9YnWEgw,CAqT_iLfQ0qQ-GbIdAsZow", + "FriendlyName": "Luau Dress (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:51.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1422, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "67fl0ih7xgt9itoq9vvq1op9o.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,wyKAZSZwT0u9cuSaQ_-IHg,vUgpsaXrO0ak7huhkHqlUA", + "FriendlyName": "Luau Dress (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:48.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1421, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5uifh9814ayb3sywh90qxniz4.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,_OM7UNZENEejgO5h7H1ztg,vUgpsaXrO0ak7huhkHqlUA", + "FriendlyName": "Luau Dress (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:46:46.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2236, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "jWeFkQl2_Uu7fhibuWdtXA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,D9U3j_XgSUesfHtSNoAdLw", + "FriendlyName": "Luau Dress (Cow Print)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:35.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1149680221, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "s-VTHTHagUa_Xh0Oa3PAJA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,I8SCbauJX0OLAKWOjy78yQ", + "FriendlyName": "Strawberry Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1876125901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PO4F_PVCVkSGejhtWCvxVg.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,SiraUm3ZaUOCeXCekunJuA", + "FriendlyName": "Pineapple Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1965281381, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FVH53dlIV0yvAH9GU2tgxA.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,9Z95fSKS00GscDxK8-ARlA", + "FriendlyName": "Blueberry Gamer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1480692753, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0_EW1WuF1U-UBFVVcVjAGw.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,wLpOk1g2rUyDeCKb5uOeSg", + "FriendlyName": "Gamer Dress (Raspberry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1421360180, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aKVQYVQFDkCP9Sy-U3FWXQ.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,RKftA1Fw30-hriuZNMGsoQ", + "FriendlyName": "Gamer Dress (Grape)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1341801044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "A8eUc7t5xEK6b8yAZPGUdQ.png", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,P6w6X7-Dak2G6O8HDRwlVg", + "FriendlyName": "Gamer Dress (Lime)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7aa994d2-6499-4918-ab2a-2531c17e27a7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.512778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1444, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9hmp8axdgbs7r6mungzjw2kzt.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,", + "FriendlyName": "Luau Headband (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:25.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1445, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0gg1ctgab27af7zjuvqitr0zi.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,fV4H_1ZXx0-OVtlasI-xGQ,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:27.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1446, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7nuouk86sz6hwwhfoh6ca9e3a.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,B9DJPqTeYUuPnPHRbKzEJA,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:29.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1447, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1u22xuxo0l7hbu4dipegf91f6.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,yXZO-donI0SHB9-TT0jUdw,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:31.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1448, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4sf5uwmo0j53djcg0konx31j5.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,J5VsmblfWU2MeUjNo0fz1Q,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:32.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1449, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "afqbc9aj7hwgkwft5kyonc1xk.png", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,Asfe0ZRi50eryD5wlJzgzA,5ekjxLQZwUm7-iXJ0bYSiQ", + "FriendlyName": "Luau Headband (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3eb14fcf-5d20-47d3-b991-97808798a225", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T23:28:34.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 189, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "beio49fzwv1l86rp7tcvhz8t3.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,", + "FriendlyName": "Luchador Singlet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 499, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eazhrt40qa5kdt73v7vcqhsdm.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,916384ca-e209-42a0-bd64-c64f5a40b56f,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "FriendlyName": "Luchador Singlet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:23.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 500, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6m59lwkz3pdm7ylfg5q1hwbzf.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368", + "FriendlyName": "Luchador Singlet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:24.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1733, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28qzx00rr1mcsdfj73tvv70qd.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,TLrw3oncCE2MeuVifXSWTg,-_ViYCHHRUip7oN_AnH0Og", + "FriendlyName": "Luchador Singlet (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:45.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1513, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0dhyx3nk36z445fgi9v0vyvce.png", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,7Jx8mrxoi0aQtZEb03CdRA,-IgxYllxkEyr7V1gosWXUA", + "FriendlyName": "Luchador Singlet (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c34c2c1-07d7-4180-94a2-be8c9caaae84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:37.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 122, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8dvvtd9gw9krmqu0dvvlk1rv0.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,", + "FriendlyName": "Luchador Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1734, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "72gv1b98ofujgx42ctd0lwdf3.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,_VfdBTi7PU276qzsYKayow,qSsEcadcm0-twR1TvVcgYQ", + "FriendlyName": "Luchador Cape (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:53.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1512, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2xop370rhid69r8mypwbzzbe1.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Dtis2HpVzkelZb6M9mW5TQ,iqGv6iRJ-UGbofv8-aHKVA", + "FriendlyName": "Luchador Cape (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:30.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1609, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6gheyta77ei8tn3jvtdoimp48.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,xNjX_6-aREm0cYZObVLiAg,OE-PtL0-0kWcJPwn1wvuVg", + "FriendlyName": "Luchador Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:08.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1610, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "32o16d0w2ku6u0tavwqbw9lq2.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Kcyp8sv0202Puym1Wf86xg,OE-PtL0-0kWcJPwn1wvuVg", + "FriendlyName": "Luchador Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:11.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1679, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e1cquzpfu1rcke1xofv7ks1oj.png", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,r_QZLd-Aw0SsdQb_uYroog,eYGWiwqSGUG7Hx1R3I-11A", + "FriendlyName": "Luchador Cape (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:24:48.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 63, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bgxz7thjzppr81bia4pwvg2qh.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,", + "FriendlyName": "Luchador Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 487, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4jk1xibdjjddzpvrwcw05usiu.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,916384ca-e209-42a0-bd64-c64f5a40b56f,96b36c98-cba1-4337-91dd-c74c00316f97", + "FriendlyName": "Luchador Mask (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:08.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 488, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "59dj5f6fpgmoilwpcpugnwomn.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,96b36c98-cba1-4337-91dd-c74c00316f97", + "FriendlyName": "Luchador Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:09.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1735, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cwfcqra4nyurw86ksh4qv0cjq.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,RA7cizxxYE6J8yV_0XwPxA,VSLcLM0TJU2kJU5gS4KZGw", + "FriendlyName": "Luchador Mask (Pouncing Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:20:56.65Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1511, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a6g3l9h3i3ads39fgxf0lnwaq.png", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,5_UnQ7vZ9E2T-EhJ7M-mQw,jL-LAsd03EeN1J-Zp8opew", + "FriendlyName": "Luchador Mask (Green Lightning)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1596c113-aa3a-4f5c-b398-e5c3575548e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-15T19:18:23.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 64, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d7reyk0b2dqeh51xmv3wcihld.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,", + "FriendlyName": "Lumberjack Cap (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 829, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cjqz06mkg3l90vu87fnh371yt.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,7amHTgEv-UOJSGl0ofo4mw,lb5tuJOqnUKp9prjjlgHmA", + "FriendlyName": "Lumberjack Cap (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-23T21:21:48.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 830, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "drjic5tpew7fpjaelvkqowvgg.png", + "AvatarItemDesc": "b537c30c-faef-4649-9a1f-be3985936f93,cKHAetNcgUOxFfDkAPPF9w,lb5tuJOqnUKp9prjjlgHmA", + "FriendlyName": "Lumberjack Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b537c30c-faef-4649-9a1f-be3985936f93", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-23T21:21:49.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 245, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0t92rww5x13bmx7jh4n78jqtx.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,", + "FriendlyName": "Lumberjack Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 775, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8u0ou663lluquhfy88m3nuvu9.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,7amHTgEv-UOJSGl0ofo4mw,CUCKsNVnIkavHjcspWAv_A", + "FriendlyName": "Lumberjack Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:58.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 776, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ba5xrtahc2k6jzt6i6q17wg7u.png", + "AvatarItemDesc": "350c5c1e-318b-4233-a7e5-1f49909d8e65,cKHAetNcgUOxFfDkAPPF9w,CUCKsNVnIkavHjcspWAv_A", + "FriendlyName": "Lumberjack Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "350c5c1e-318b-4233-a7e5-1f49909d8e65", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:00.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 107, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6fn7llrs7vzf0mr1pws7ys253.png", + "AvatarItemDesc": "61ba3c90-c81e-4deb-bc79-50c0f1fe3e83,", + "FriendlyName": "Lumberjack Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "61ba3c90-c81e-4deb-bc79-50c0f1fe3e83", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 768, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "301onvpom3avfdh6fxup3q7fv.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,", + "FriendlyName": "Anime Tiara (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:49.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 922, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2oaezhhf77m3tlzy73ehvtapu.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,lhfOKGDAgEKWucerCmH7Fg,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:56.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 921, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dzietotul0k7x0rgoxmeqn53w.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,kLv_wsbbGUmlfIUO1QLYLw,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:55.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2oen9cny4wzp5qnb6rdx5nchw.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,f0pjsV0XMUWoE_z2kqOlSA,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:54.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 919, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "79soc27032oiz7ezyzb7bj6w3.png", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,EDLxuGjEVEGdOPXDqyN-xA,mG60EPIQIkaJNgOkonVmZg", + "FriendlyName": "Anime Tiara (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "48e73e85-d33e-4eb3-85ff-548d94e30bc2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:53.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 771, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d9jbl9u6drfoj1reb16xtye8f.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,", + "FriendlyName": "Anime Bow (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:54.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 903, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "13c35i4i2yh37p6u3kwbblvup.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,Ot9EG-FfKEauT0u6pSZuWg,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:34.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 902, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "54xi44ymvq9amsim1078bvqlt.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,i3rD_hnQG0a-5f6Ve4AfWg,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:33.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 901, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "35wve61we8efcghfumwpogrnp.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,S9uymuvraUCOM8gmB7bnEA,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Dark Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:32.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 900, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "31o9bg37u28wwxm2fpf5h095x.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,_uh3kj6hKUyBKOSR3R8dog,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:30.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1822, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dc2oyr445lq5s6rcxguz7tjdd.png", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,kaRBW1u3dkODGjPMePW6Uw,M5z2wk-GDkOAjvZzqsfcNQ", + "FriendlyName": "Anime Bow (Rose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96a1e45a-67b2-421d-aeb1-c9fc7ba46dda", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:49.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 777, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ouv3swjffh6u9o5pks8yim6q.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,", + "FriendlyName": "Anime Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:01.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 870, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8pxavigoe6em3oy4686ym8ue2.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,VQjkyb5aoEKHKo_eZsq9ug,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:27.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 869, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1b7zb62caqbw5yuafz8hg7nxn.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,kTP3xKhzBkuQF3SqvmYFKA,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:25.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 868, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b79pggvoulv5z0dkn70z5syru.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,hYu9itGe1Ee5Neanhj9VtQ,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:24.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 867, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3unj20ah5pwz86qh67lqz9qvh.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,FE7_gQNBt0CXzMN6di5HWA,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:22.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1820, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c52ga28yxp7v5z0t8ow8e4rtw.png", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,dYPaOkwy3EGAfvl8RyN4pw,2UNd6eViFUO359xjSiuu2A", + "FriendlyName": "Anime Skirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60039930-f931-48d7-b301-a6f5dd97d743", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:39.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 781, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "by0enw83tqi7qq14gmpytpavy.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,", + "FriendlyName": "Anime Glove (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:06.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 941, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1qj8ezhvwgdu6g4waborypyet.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,zaSTWOUy_k6cnoVVYzypWg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:18.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 942, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6tq3bm6g7idt1uv2itnhf5qb9.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,1FcyrVGyGEC9dmOq_51nFQ,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:20.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 943, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6qwl1qaaam1kkaku033yyeppd.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,xpkEQ6yjhU-WfFPSjvPdHg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:21.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 944, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2dr1z7istjp1d0bnqdoebk8ne.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,N9CBuYaMtUOrCks5UcOycg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:14:22.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1821, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9y4m58z1b4kasw620juo4nihy.png", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,LDjFJtM-WkeYhtwA7cyDOg,ZtCqMnFTQUWv2h0MfZ4m6A", + "FriendlyName": "Anime Glove (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afe82f79-2780-4632-bd33-f6c3ed8376f2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:43.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 190, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dl8ieqkdftxjtgfey8gb41yid.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,", + "FriendlyName": "Cheerleader Sweater (Pride)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1645, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aidgb349qpp5nx0tlmqvfmxvy.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,6secgNPvmUyFnI3NC5dIYw,5V_1JEDrRUa1UPKhtrSB2w", + "FriendlyName": "Cheerleader Sweater (Victory)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:50:04.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1646, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "120gze52rf81cvsx2extomyi8.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,X5SuBHIGBE6wy8N8KDop8Q,5V_1JEDrRUa1UPKhtrSB2w", + "FriendlyName": "Cheerleader Sweater (Rally)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:50:08.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2424, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zwCsgKFNm0KCFbci6h6_ag.png", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,bc-aVFn__E61pyA4SvUoGA", + "FriendlyName": "Space Suit Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:51.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1952325815, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,dDSR-6ck8UGOeCSXR8Xnug,OFIqUvE4r0yMLHSMnlWMhw", + "FriendlyName": "Male Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.513779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1964834389, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "5cd08cfb-c729-4c30-96d9-6a99bb934d91,EaJMu_M0OUyUZ_3x-X-d7Q,OFIqUvE4r0yMLHSMnlWMhw", + "FriendlyName": "Male Cheer Sash Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5cd08cfb-c729-4c30-96d9-6a99bb934d91", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.513779", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ckirjfll5885ahrmczp8ry2l.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,", + "FriendlyName": "Class of 2020 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:08.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1890, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dhfwe25d3r0oxtup5xd695cyf.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,oMz5rMzCxkiw9690ZkvlGw,gW2zFv0sFEmiodfw8p9P-Q", + "FriendlyName": "Early Access Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:00.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1903, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "reccon", + "ThumbnailImage": "7c0xvai9f3e3orr592s3eq4da.png", + "AvatarItemDesc": "b3949221-9416-4afe-8777-440f01ec4c1c,zlBPAWMbOE2RuKoIy1Ps8Q,UwQiLub0-kqf4dNvQc-R0A", + "FriendlyName": "Rec Con Shirt (TFO)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3949221-9416-4afe-8777-440f01ec4c1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:31:04.757Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 192, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "au05f75bl482johdfb0fvjxjw.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,5864d23d-1a86-40a6-a52c-46ee670e4508,2ee3944d-e9fb-4959-973e-570a7fc6cc57,167ba0d4-4fac-4143-b5ae-1dbd8e02dc76", + "FriendlyName": "Class of 2016 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 336, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "01iubrr0qyuaj94by5oj4xg3m.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,14afd295-bcd0-4595-80dc-d477618b9ab9,2ee3944d-e9fb-4959-973e-570a7fc6cc57,a74e6c0d-1300-4928-9141-977a711b71c4", + "FriendlyName": "SFVRCC Promo Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-27T19:16:00.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 337, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "eyc48682dl6k4obf67w3822z8.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,f8635fe2-90bc-42c8-9bbe-3387b5157e18,2ee3944d-e9fb-4959-973e-570a7fc6cc57,49934059-cc45-46ce-8277-f96959e1f6ba", + "FriendlyName": "awesome sauce", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2017-04-27T19:16:03.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 362, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e03fl2rpw0xd46aawjspj2i92.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,922f229c-124f-4f07-b4e3-19138420ca19,2ee3944d-e9fb-4959-973e-570a7fc6cc57,ceb07e4f-50a9-4ed0-b50b-c59d92ba66b5", + "FriendlyName": "Birthday Shirt (1st)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:02.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 565, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "b1k80npgw88paq57cw285zhgv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,0c496f32-0011-4c4d-9778-59446f70c032", + "FriendlyName": "Rec Room T-Shirt (Crew Neck, Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:23.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 561, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2stqqgiaqmpxhz7kyzokxny1x.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,c0ff7719-c56a-4934-831b-506ad0f51cca,3259238f-4a9e-4e06-99de-6e7324fa0894,e38294cf-6279-4863-9aea-1a19aeaf8476", + "FriendlyName": "Laser Tag Alpha Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-08T18:59:57.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 562, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "69q96bwnfq08dvg7z8i3423dd.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,381a9f13-5b44-48e6-8a49-221422a963f5,0440f08f-ef1d-49d8-942b-523056e8bb45,9e782751-71c6-4208-87da-3a3cffa6a4f0", + "FriendlyName": "KO Skull Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-08T18:59:59.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 567, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5fseh1kw7c5ej962dbiugr5ns.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,befa2f11-b9fd-457c-b1d5-9d82e90b70ce,0440f08f-ef1d-49d8-942b-523056e8bb45,770e7674-6f9b-41f8-b023-ae79738db0d3", + "FriendlyName": "Laser Unicorn Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:26.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 566, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bp1j53987blav97zjuw9die57.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,bde08851-a9e4-4faa-8f04-a0a661672472,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "FriendlyName": "RECS Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:25.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 578, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "afeyng27plnoeojutrac3dvvj.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,5e7a48ed-8f08-4990-a3ae-05722c9f59a8,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "FriendlyName": "RECS Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-29T01:25:03.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 568, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "92ns2hqejpkog0560clmw3qjv.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,4d52bbd6-ef74-45ae-a6c2-bde10ab8eb16,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "FriendlyName": "Robo Logo Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:27.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 569, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3tf587nd10cq6wv4dr1lk0r9p.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,bda7a57e-5227-49f4-a119-e3f166604738,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "FriendlyName": "Robo Logo Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:28.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 570, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "85nms7ezyti5i4wp8l9azl90l.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,6c395d04-2aa6-4a3e-8c2c-695932bc9736", + "FriendlyName": "Squad Goals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:29.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 571, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "19u0u4m2c6wsyaocczd4hv6pd.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,703ff56b-560d-4ff4-8c63-d195e879a328", + "FriendlyName": "Alpaca Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:31.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 572, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dckjq7bzoptwnhc7299yac8sm.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,fe304452-b57b-45bc-9c1c-b85b70bcec12,0440f08f-ef1d-49d8-942b-523056e8bb45,6a007c73-d930-408a-93df-041a715e0dd4", + "FriendlyName": "KO Skull Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:32.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 573, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "eu5x1j13qp2b88ukdkh7eax1v.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,8f19d7f9-4f28-48ab-a1c5-d8f4f788514c", + "FriendlyName": "Hang In There Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-16T22:47:33.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 594, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dtf1nt1wah54rmo77jxzohkg6.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,75162f12-44e7-499f-bc6c-9b0612e9164b", + "FriendlyName": "Class of 2017 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-11T18:54:38.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 623, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "26xtdwd2iy25ifneaxeal9nji.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,ba6b6e1a-a09a-4ba0-9523-552869f03336,0440f08f-ef1d-49d8-942b-523056e8bb45,d461ca71-45c9-415e-8e09-ba93e8d73450", + "FriendlyName": "Happy Tree Shirt (Blue, Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-04T17:02:17.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 731, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2w7aitx6z9si6pezgai622y01.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,rnM1qDlYBU6_Dl1bjg0J1Q,0440f08f-ef1d-49d8-942b-523056e8bb45,OZb8bb5FNUuFYOkwANvxAA", + "FriendlyName": "Rebel Rose Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:48.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 732, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4yagacwve48lrxa8yoo14k83r.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,0qh4mcxJH0Wsrb3nz8UeGw", + "FriendlyName": "Emerald Syndicate Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:51.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 733, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "70j1efvr06uolk29w6i31vbbq.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,Ib_iMwvUm0CJ-t_Thwt77Q,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "FriendlyName": "Cyber Trash Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:53.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 734, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8o1cue9ixf7cfijq9eqg5xk8j.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,yaQ7AlUc3kGq8_RW3eTSmQ,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "FriendlyName": "Cyber Trash Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:54.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 735, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2ad06ofz4ewso7onvvyfzerog.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,dDLQbD9jtkOmw3PR5QJdfg,0440f08f-ef1d-49d8-942b-523056e8bb45,2tFUNn-4kkK6ICo_Zp065g", + "FriendlyName": "No Laser Ham Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:56.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 736, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "aqkgdsqeztkohtgzqy9eu3ee6.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,g4diCmk0wEmIKUVozk9tDA,0440f08f-ef1d-49d8-942b-523056e8bb45,nHg4Uifww0GEX15L27WonA", + "FriendlyName": "Laser Graffiti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:57.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 737, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8q53ymq8bhq82a6s9fqt8a814.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,MPfe2wahQECCDGfhZE-drA", + "FriendlyName": "Hack Graffiti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:15:58.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 738, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5k6xiv9l2oaeteyfht5ak4epq.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,nLRxEIBhUkyQsfrk3nzsDA,0440f08f-ef1d-49d8-942b-523056e8bb45,n4hqSCv9OUewI30KnbPhFw", + "FriendlyName": "Cyber Junk City Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:16:03.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 739, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "agqloaqzftkyi0wa9v9j439d0.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,M1JVd5ur7U2ZtpvhEhPNhw,0440f08f-ef1d-49d8-942b-523056e8bb45,tzMFIJyxZE6cZNuLdaKs7w", + "FriendlyName": "Hyper Noodle Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-30T19:16:09.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 965, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "58ls1z246ivu3x64bsenly4v9.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,6kPcLZtEqU2Hs4yBFvc_sA,0440f08f-ef1d-49d8-942b-523056e8bb45,NxMm0sSBdUatynU16o0_Zw", + "FriendlyName": "Class of 2018 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-22T23:43:30.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "978ivwbrlhtfjt9lpibr7eqx3.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,TtncjvYcK0qHqWN-IMMoqg,0440f08f-ef1d-49d8-942b-523056e8bb45,hHhjZeLcaUGN1OyFoYHznw", + "FriendlyName": "Happy Tree Shirt (Purple, 2019 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-04-05T19:47:41.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1130, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a9mzwqn1pzmy5ptemp2sjk6r3.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,fwIgUAM_C0WhKds0O6DyPg,0440f08f-ef1d-49d8-942b-523056e8bb45,fJVxsLWuTESd-2xTCJPZ2w", + "FriendlyName": "Recollage Shirt (Pink, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:28.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1295, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ansni4asvqpm50bcuomwb4i4l.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,ZNPzHmrU6kGVQW7Q5ei03w,0440f08f-ef1d-49d8-942b-523056e8bb45,fJVxsLWuTESd-2xTCJPZ2w", + "FriendlyName": "Recollage Shirt (Yellow, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:42.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1246, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "971lp3mdb0j453fkjy0cckqud.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,d3-sxPTJOk2rKo-Jgy3wvw,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest T-Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:36:17.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1563, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ded2g4m0rj9eastu0comvx8bw.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,v12eJ8louE-cW7-pCGzVLQ,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest T-Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:32:35.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1kt3y6b06rh6qdox2huzn0c6f.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,6kPcLZtEqU2Hs4yBFvc_sA,0440f08f-ef1d-49d8-942b-523056e8bb45,br9hzhDoZUmIevgl7SJkmg", + "FriendlyName": "Class of 2019 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-06T22:36:11.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1369, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3d9h318l8ubxs4x703bpvjqcy.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,TuAjeDmPuUmFr_ESx4q6WA,0440f08f-ef1d-49d8-942b-523056e8bb45,AXtRUZcYWU6g1ZtM-L4vfQ", + "FriendlyName": "Video Contest T-Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:07.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1499, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "anq7dknee5z82yqq5bymquby0.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,LrQ1z-mTHE2vIePBwm8l_Q,0440f08f-ef1d-49d8-942b-523056e8bb45,omS7gF10C0ec0ozEoJGJXw", + "FriendlyName": "Recollage Shirt (Blue, 2D Art Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:19.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1839, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0jdw82n61uuapunyx297i85ng.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,owoIOIEM70e6WxnYdlKYWQ,0440f08f-ef1d-49d8-942b-523056e8bb45,t2do8WSejE-Y0aiEirKw9g", + "FriendlyName": "Video Contest T-Shirt (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:19.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1840, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "84fxnyn9eqldulf6rpki5uuz7.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,eb1vYTP_KEGQH-f33_whQg,0440f08f-ef1d-49d8-942b-523056e8bb45,t2do8WSejE-Y0aiEirKw9g", + "FriendlyName": "Video Contest Shirt Reward (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:21.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1841, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zhwxqyn2zie0fpfnaf18z5ve.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,evgH6VWUgkKJNews32hUxQ,0440f08f-ef1d-49d8-942b-523056e8bb45,v0GsPFC7cU-_JNG4-ERfog", + "FriendlyName": "Video Contest Shirt Reward (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:24.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1889, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4dsx1k2farwthtgsfgem735gx.png", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,UUlB4S3GVUGyil6ZK-EbWQ,0440f08f-ef1d-49d8-942b-523056e8bb45,omS7gF10C0ec0ozEoJGJXw", + "FriendlyName": "Recollage Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d0a9262f-5504-46a7-bb10-7507503db58e", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2021-08-18T21:22:47.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7zgp3njcd8tpgi1f8j1679bwa.png", + "AvatarItemDesc": "c8a97aeb-e8dc-40ba-b6bc-a7c7be907381,", + "FriendlyName": "Feathered Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8a97aeb-e8dc-40ba-b6bc-a7c7be907381", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-30T20:20:27.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1324, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ucp3apcgf7mvka7m2albckl8.png", + "AvatarItemDesc": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,", + "FriendlyName": "Mardi Gras Mask (Orleans)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-31T00:38:06.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1651, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3lmgppocqd4j182ijbbxl0eo0.png", + "AvatarItemDesc": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1,mkkk2N_2-EKX3pVfk7U5aA,UQe6v62i6ESImdpc7gB_8w", + "FriendlyName": "Mardi Gras Mask (Parade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb830b12-c1b5-4796-b9cf-ccf0f0e1d4f1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:35.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1325, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjo835jgyqtwaijuhly8otljo.png", + "AvatarItemDesc": "43354e59-4938-44bc-81e0-4fba47d0ac12,", + "FriendlyName": "Mardi Gras Mask (Carnival)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43354e59-4938-44bc-81e0-4fba47d0ac12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-31T00:38:11.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1652, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9x7rlsmit2ca70h5xx2vgtwez.png", + "AvatarItemDesc": "43354e59-4938-44bc-81e0-4fba47d0ac12,4xp-xZyNWEi_jJCIhAPI3g,Ulie-RkcJEmblLyoucx-JA", + "FriendlyName": "Mardi Gras Mask (Revelry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43354e59-4938-44bc-81e0-4fba47d0ac12", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:38.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 725, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bt8tu5acrblwkf3nc68uhi3jl.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,", + "FriendlyName": "RecFly Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-27T22:48:20.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1084, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "66rxidv3887cwdjojesos240j.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,kkvNbVRjcEKtChpy3W1bqQ,nbhzQy_Hdkeohh4S1IezwA", + "FriendlyName": "RecFly Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-12T23:52:26.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1671, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4gn26corqk34q37e2hx5ouc9q.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,GVoh3mdWf0WNA9lfqL1GEw,w9qG5gHlAUqh8f9ZgnezsQ", + "FriendlyName": "RecFly Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:05.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1672, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cyarqq8aeox95hehu1py5nbbc.png", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,x4PPvuhg6USE6GSFgZZnCg,w9qG5gHlAUqh8f9ZgnezsQ", + "FriendlyName": "RecFly Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74c65554-0d15-409b-b9a8-0b2620a7d7e2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:08.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1681, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eewujb3it57bm2qlk725l2aji.png", + "AvatarItemDesc": "d4f86b2b-0fa3-4264-9eac-cf15b62463bd,", + "FriendlyName": "Resistance Shades", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d4f86b2b-0fa3-4264-9eac-cf15b62463bd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:33:42.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1680, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ekvm0nia4j7hfolms572ortek.png", + "AvatarItemDesc": "c4622dca-80dc-423b-ae22-2065174e817b,", + "FriendlyName": "Resistance Trench Coat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c4622dca-80dc-423b-ae22-2065174e817b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:33:36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1931, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3iw1qqexx0h5n5ksh78jfpf4h.png", + "AvatarItemDesc": "c4622dca-80dc-423b-ae22-2065174e817b,pCEBoSP2pESLl9ih-J-55g,8SzXFoWD8k20cekFe6cLKg", + "FriendlyName": "Resistance Trench Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c4622dca-80dc-423b-ae22-2065174e817b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-20T21:24:53.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2279, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lQCcPfTrxkuFrwpWyZRiiw.png", + "AvatarItemDesc": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,", + "FriendlyName": "Mecha Helm (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:15.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2703, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b-rwdReQyEOex38lYG4k7g.png", + "AvatarItemDesc": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5,GhulL0k9q0euEooMTUKTHQ", + "FriendlyName": "Science Helmet (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "03ce3b52-0d0c-4b72-a027-ade53b7a5cc5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:39.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2487, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "PaCn_gU9XUWR1kli2ZdIYQ.png", + "AvatarItemDesc": "40258a3d-0d5b-4b67-bb99-4bafc6410311,", + "FriendlyName": "Science Helmet (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40258a3d-0d5b-4b67-bb99-4bafc6410311", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:04.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3clewgmvjgao6vqaxfjuzx41z.png", + "AvatarItemDesc": "b74ef8c3-405a-4d68-a2bf-01579a9cc397,", + "FriendlyName": "Mecha Helm (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b74ef8c3-405a-4d68-a2bf-01579a9cc397", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:03.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "deey8zsy6ga64seu2ecdgbscy.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,", + "FriendlyName": "Mecha Armor (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:11.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2289, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8u2ve7984kGPN_F3hbDzFg.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,eG1Lf1vG60qNQiCBRsAhqA", + "FriendlyName": "Mecha Armor (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:51.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2489, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "zYe-zV8o90qtz6ryq0Q3aA.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,a9oUAXZpHUaW1_ncqhkYZg", + "FriendlyName": "Science Armor (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:09.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2706, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wWzp1pLLbEq0Zrt_O6yaMA.png", + "AvatarItemDesc": "08dfa3f9-d545-4707-be94-6342db90c17c,CeebPv2TZUixq8cmVD5lUA", + "FriendlyName": "Science Armor (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08dfa3f9-d545-4707-be94-6342db90c17c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:48.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ar7fmx8roe1n7n2hcnjomo65l.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,", + "FriendlyName": "Mecha Wings (First Release)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:05.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2283, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QGWG7sL090695CW4JWWvvA.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,cS-oBNlLE061SIB40QjiDg", + "FriendlyName": "Mecha Wings (Forbidden One)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:30.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2488, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "JEZj0fwAP0a4qOcclGw70g.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,rFcMAp5UvkuGalprkr_ARQ", + "FriendlyName": "Science Wings (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:06.757Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2704, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sQ5AG_RCAk-j6MKfyvnriA.png", + "AvatarItemDesc": "a936c982-28db-4e7e-bc79-ec5eca975beb,oSoW8v6emEW4g1IduUNzOA", + "FriendlyName": "Science Wings (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a936c982-28db-4e7e-bc79-ec5eca975beb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:41.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1466, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vpmmt96jkto0ifnjtv9nwy2a.png", + "AvatarItemDesc": "16359b51-96c4-4be0-b598-319c7549e18a,", + "FriendlyName": "Mermaid Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16359b51-96c4-4be0-b598-319c7549e18a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:44.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "16359b51-96c4-4be0-b598-319c7549e18a,3ya4VDfxLU-krfBh_H0VAA", + "FriendlyName": "Mermaid Crown Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16359b51-96c4-4be0-b598-319c7549e18a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1142113680 + }, + { + "AvatarItemId": 1464, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5rcywgkgsd6v3ov5jeez13xtz.png", + "AvatarItemDesc": "fd7774bb-b29c-40b5-84b1-5c2bbe574255,", + "FriendlyName": "Mermaid Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fd7774bb-b29c-40b5-84b1-5c2bbe574255", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:35.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "fd7774bb-b29c-40b5-84b1-5c2bbe574255,499irXZltkSvv9pWsdBSyw", + "FriendlyName": "Dress Mermaid Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fd7774bb-b29c-40b5-84b1-5c2bbe574255", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1339048088 + }, + { + "AvatarItemId": 1465, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f44q2mf0bi5g9hcmaw85jm3j6.png", + "AvatarItemDesc": "c8c00394-4cd9-4bad-9774-a825ded78f80,", + "FriendlyName": "Shell Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8c00394-4cd9-4bad-9774-a825ded78f80", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-17T20:43:40.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "c8c00394-4cd9-4bad-9774-a825ded78f80,Qevoxmtsa0eNzGss_aG2gw", + "FriendlyName": "Mermaid Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8c00394-4cd9-4bad-9774-a825ded78f80", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1547269846 + }, + { + "AvatarItemId": 2067, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "k7-wf2BSPUW8hyIsgm1DCg.png", + "AvatarItemDesc": "51b85276-3a40-4c40-93a1-9df307c3e78e,", + "FriendlyName": "Messenger Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51b85276-3a40-4c40-93a1-9df307c3e78e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:24.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1958, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wtce4myle20jpvnkao5yy6q4.png", + "AvatarItemDesc": "745fd5ef-705b-40f0-bfd7-dbf4af5a82e8,", + "FriendlyName": "Mighty Princess Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745fd5ef-705b-40f0-bfd7-dbf4af5a82e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:09.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1959, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8icc0glgl3g068z6zkzlc81e7.png", + "AvatarItemDesc": "7652db86-bc67-4665-8937-f5199d27b5c7,", + "FriendlyName": "Mighty Princess Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7652db86-bc67-4665-8937-f5199d27b5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:12.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1997, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1rltz0xd7rtu9knzw8dxe4tnf.png", + "AvatarItemDesc": "7a5cb589-5eab-4d95-9040-e9bfdc9590fc,", + "FriendlyName": "Miraculous Cat Noir Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a5cb589-5eab-4d95-9040-e9bfdc9590fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:14.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1996, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "bkqrk31ohvuvc5euhf8ftmopj.png", + "AvatarItemDesc": "dece6d88-b6b1-4e01-b341-8ca4171353cc,", + "FriendlyName": "Miraculous Cat Noir Ears Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dece6d88-b6b1-4e01-b341-8ca4171353cc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:12.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1995, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2dpq3dcn5tt6gohq1e92zn647.png", + "AvatarItemDesc": "f0abcb90-d8e7-414a-b81b-a2aafacd4dac,", + "FriendlyName": "Miraculous Cat Noir Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f0abcb90-d8e7-414a-b81b-a2aafacd4dac", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:10.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1994, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "56uyxkfexq3rv8uwgrvrv1uc2.png", + "AvatarItemDesc": "e441a297-22ba-4a43-92ef-3549c6cc45c1,", + "FriendlyName": "Miraculous Cat Noir Hair Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e441a297-22ba-4a43-92ef-3549c6cc45c1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:07.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1993, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2qk8k1ry8kme3fai13fs7y5na.png", + "AvatarItemDesc": "fe85784d-070d-4b33-82a2-e0875da053d6,", + "FriendlyName": "Miraculous Cat Noir Mask Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe85784d-070d-4b33-82a2-e0875da053d6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:03.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1998, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0swzhsru2itov2t18lj5ptr8y.png", + "AvatarItemDesc": "19d83cda-692f-4ec1-8da0-15211f3a0c34,", + "FriendlyName": "Miraculous Hawk Moth Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "19d83cda-692f-4ec1-8da0-15211f3a0c34", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:17.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1999, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "01v0cofrfhvsecj4d83kd6qn6.png", + "AvatarItemDesc": "a28d3819-e5e0-457f-8363-09aaed6a1382,", + "FriendlyName": "Miraculous Hawk Moth Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a28d3819-e5e0-457f-8363-09aaed6a1382", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:19.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2000, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "53f1e75rivkkk1v3yiqcezzwd.png", + "AvatarItemDesc": "696f46ae-eaf3-43b5-9635-70a78972826e,", + "FriendlyName": "Miraculous Hawk Moth Mask Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "696f46ae-eaf3-43b5-9635-70a78972826e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:44:22.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1992, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ddhkud4mfi72rzypwa7k6q892.png", + "AvatarItemDesc": "26b8c05a-e960-4039-9e50-4e27befaa44c,", + "FriendlyName": "Miraculous Lady Bug Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "26b8c05a-e960-4039-9e50-4e27befaa44c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:56.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1991, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "e055b8bmrxrpkco54wa2vg5e9.png", + "AvatarItemDesc": "9815999a-c4d3-4741-a34f-2545b0cb8e11,", + "FriendlyName": "Miraculous Lady Bug Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9815999a-c4d3-4741-a34f-2545b0cb8e11", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:54.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1990, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "4f78dqx34vgtcv34rpbpetpea.png", + "AvatarItemDesc": "34023e02-f59f-4e90-b245-e298cbc1a6d8,", + "FriendlyName": "Miraculous Lady Bug Hair Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34023e02-f59f-4e90-b245-e298cbc1a6d8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:51.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1989, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6yostvo78m555vx70ouaibzah.png", + "AvatarItemDesc": "ad21788e-1995-4c2b-9c7f-610bbc1019c4,", + "FriendlyName": "Miraculous Lady Bug Mask Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad21788e-1995-4c2b-9c7f-610bbc1019c4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-06T00:43:46.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 348, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bgvzostookysj7axrphthrkbq.png", + "AvatarItemDesc": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,", + "FriendlyName": "Mirrored Glasses (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-05-16T23:01:05.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 392, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0p43fda4clqqc4kkkt9xq6vsz.png", + "AvatarItemDesc": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,f841cdfa-45ba-4f09-a117-503eef34cb4b,9c55b609-b7dc-4070-a3ad-6351a0054a06", + "FriendlyName": "Mirrored Glasses (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:30.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1160, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0y8i0p2err8v094d39pt4hrsl.png", + "AvatarItemDesc": "a4c26527-8f75-44da-b150-7aaf8ef739c1,", + "FriendlyName": "Stunt Shades (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a4c26527-8f75-44da-b150-7aaf8ef739c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:02:54.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1136, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "12z70obzqdrm08ikd55tdgqj6.png", + "AvatarItemDesc": "d57cf97e-b5d2-4e47-8231-a226a161f6e3,", + "FriendlyName": "Stunt Shades (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d57cf97e-b5d2-4e47-8231-a226a161f6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:15.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1139, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9t35j4q9ykj4qblmofdh0c5vg.png", + "AvatarItemDesc": "87792b6e-1468-45e1-b28c-83f27ada7299,", + "FriendlyName": "Stunt Shades (Fuchsia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "87792b6e-1468-45e1-b28c-83f27ada7299", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:26.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1135, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5e5ro3jz91njip3g49yxza47y.png", + "AvatarItemDesc": "973dc863-b9a5-40d8-8f15-4096e75bd2cb,", + "FriendlyName": "Stunt Shades (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973dc863-b9a5-40d8-8f15-4096e75bd2cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:10.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1138, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0bc165hvvokoljesm0z26hcxx.png", + "AvatarItemDesc": "e2620c2c-c28e-45ce-a83c-0588329b9c81,", + "FriendlyName": "Stunt Shades (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e2620c2c-c28e-45ce-a83c-0588329b9c81", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:22.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1140, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6k0yaoht3qdepv964z904hrm7.png", + "AvatarItemDesc": "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5,", + "FriendlyName": "Stunt Shades (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-08-15T18:01:31.913Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 557, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "02t4ai6twkgrds4d3p7uhoirv.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,", + "FriendlyName": "Mobster Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:50.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 626, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5durkqoejr41mlqicbjoralgg.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,40283127-2ffc-4989-82f6-29c3381bf9e2,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:45.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 627, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bcipyj02mi2pt13ubgaeou2w9.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,1421caa9-9a0d-42c2-b847-1500ba2e483a,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:47.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 628, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cyemgjkr87sifsyr57umkly9v.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,390a476a-5545-48eb-955c-1bf1a16012bf,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:06:49.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wy0v5yb2753kb3b6g2r6wmdp.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,46970903-9799-4d25-b90a-7162c1f5dbef,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:09.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1129, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "b4yf4gpicr0pa3pz4cdefjtu3.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,PZx7Iae0pkiDaxT9t3-vgg,d759c88d-a159-4c26-86dd-b054bb05b3ed", + "FriendlyName": "Mobster Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:13.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1462, + "AvatarItemType": 0, + "Tooltip": "A hat given to the ethical hackers of Rec Room.", + "Rarity": 50, + "ThumbnailImage": "cjcge17sgy7ehaobbb1d90lb2.png", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,8XajS2adOEOqzqnLy6-zcQ,P12CUkIg8EuFihGQsvPlxg", + "FriendlyName": "White Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:11:31.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,_Fl6LKUUykSBrdEgaN-GFw", + "FriendlyName": "Mobster Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76369aef-aeda-46d2-996a-cd00594d0543", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1396740312 + }, + { + "AvatarItemId": 558, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28199zgfnoo8pyinqru3bjurg.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,", + "FriendlyName": "Mobster Suit (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:54.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 559, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "28rrxlerexeuvbdhwah6qw82t.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,531b4297-b38b-4362-9854-4f2139ea43be,33ee376a-586e-4aff-a7e4-4213445a3e92", + "FriendlyName": "Mobster Suit (Herringbone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:34:58.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 716, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cqin0ca6oahx1odk9wqkjoqzv.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,46970903-9799-4d25-b90a-7162c1f5dbef,c2052d8c-fbcd-4462-8f08-bcf5b3dfde2f", + "FriendlyName": "Mobster Suit (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:17.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 966, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a6mox6qxfhus2uphu75sycnf6.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,4VkPVbBVLE-eJHilwGMAbQ,HTjlzAlWZ0ezY6w3ngSQSg", + "FriendlyName": "Mobster Suit (Hearts)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-01T22:12:59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1131, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2ohuwbw1u9rnkyon9psv94x54.png", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,newGzX_zA0eGUX3Iayq5Ag,6Jv4AJGBYUGP_n4sDcz2Kw", + "FriendlyName": "Mobster Suit (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:30.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,352obeXyMkyLIv62h2g-Ng", + "FriendlyName": "Mobster Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4308da04-af84-40bb-845a-7a9e1a2726ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1791921697 + }, + { + "AvatarItemId": 560, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "76jy5kgg3ywcce4irqjp40e7o.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,", + "FriendlyName": "Mobster Cuffs (Pinstripe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-06T22:35:08.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 717, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5qsejkrvzfabn61cbd7tzrav2.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,531b4297-b38b-4362-9854-4f2139ea43be,c2a9c007-2748-4fa2-ab9b-0081eef792f1", + "FriendlyName": "Mobster Cuffs (Herringbone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:18.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 718, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bo4sinu9h61ugjoyoi0qu6aqc.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,46970903-9799-4d25-b90a-7162c1f5dbef,05cff8b8-d437-485a-b4b8-41d35becd022", + "FriendlyName": "Mobster Cuffs (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-11T22:02:19.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1132, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7oklc7x4ju4r3afyx70po80cx.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,newGzX_zA0eGUX3Iayq5Ag,QBzMp7MdAUOLIofvddui1A", + "FriendlyName": "Mobster Cuffs (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T21:13:32.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2825, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "1Z97coFGmUSgGU9P-pU5KA.png", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,6ItbfT4igECHl2eLuhGLIA", + "FriendlyName": "Mobster Cuffs (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:41:02.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,326QgcQbnUGJBs2xzFKVOA", + "FriendlyName": "Mobster Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08d56627-4500-4724-8692-a23c96ddb3ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1214487053 + }, + { + "AvatarItemId": 87, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2l73l9kt7me20f6m5vhkstse2.png", + "AvatarItemDesc": "e49d5d78-157e-4bc8-b878-ff7c4963fdc4,", + "FriendlyName": "Mohawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e49d5d78-157e-4bc8-b878-ff7c4963fdc4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2212, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kykH4YwaaUSlrQTBZsT8tA.png", + "AvatarItemDesc": "e21b9dd6-c3d1-42cb-a90d-1b60ffb826fd,", + "FriendlyName": "Monkey Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e21b9dd6-c3d1-42cb-a90d-1b60ffb826fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:30.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1806, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25fe9p8vszllgyike2qzq66fc.png", + "AvatarItemDesc": "5f6a7dee-0b44-4138-b3a7-a079dbc63e83,", + "FriendlyName": "Monkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f6a7dee-0b44-4138-b3a7-a079dbc63e83", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:23.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2291, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "emdth023j0ib2692bu9zs42fg.png", + "AvatarItemDesc": "3bceb51c-ef29-4942-8b8b-a15ed4a41451,", + "FriendlyName": "Moon & Stars Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bceb51c-ef29-4942-8b8b-a15ed4a41451", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:37.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2292, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6l7ba33mqkha8zbtzg8cty02.png", + "AvatarItemDesc": "3bceb51c-ef29-4942-8b8b-a15ed4a41451,4lwF00Rvb0Kr3FJz07HlOQ", + "FriendlyName": "Moon & Stars Earrings (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3bceb51c-ef29-4942-8b8b-a15ed4a41451", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:39.193Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1509, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7gjdy5ae0sascpzzv1vx2hn97.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,", + "FriendlyName": "Moons & Stars Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-08T21:59:42.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1930, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1453pgb9cjdtfwanmhqks0fn.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,eutz2irupECSYGMtru3NMQ,Gruqp7EGM0WJbAM_F8hWbQ", + "FriendlyName": "Snowflake Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-10-13T19:00:38.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1700877762, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qhUHWc_oWEanxQNBSskKJQ.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,4PI6XssggEKRxkGlWeo8KQ", + "FriendlyName": "Jigsaw Moon & Stars Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.517784", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1209731358, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "APU0Kt0HrE6E_zMnX0MMJw.png", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,Mp_TzaiSTUaULweziW9_AA", + "FriendlyName": "Dress Moonand Stars Dress) Moonand Stars Dress Shirt Jigsaw Aurora Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "936d7126-a3b7-48a2-bceb-6cad9221b267", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.517784", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "eb91dbb0-0dc1-497e-b6d0-b7cd4482bacf,", + "FriendlyName": "Moon Necklace Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eb91dbb0-0dc1-497e-b6d0-b7cd4482bacf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1407377184 + }, + { + "AvatarItemId": 2857, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CXVseWtLVkWtwM2EQ4f2Rg.png", + "AvatarItemDesc": "ab446412-84cd-490d-84e0-2e666633b72f,", + "FriendlyName": "Dirt Bike Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab446412-84cd-490d-84e0-2e666633b72f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:44.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2858, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "p0HdGaW4sE2gqP6I_UnOTA.png", + "AvatarItemDesc": "3028ba83-0c85-4e27-a47a-56609fa58b33,", + "FriendlyName": "Dirt Bike Chest Protector", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3028ba83-0c85-4e27-a47a-56609fa58b33", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:46.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2859, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Mz1ko5b7O0GghTIcaFyRTw.png", + "AvatarItemDesc": "40c1f20f-182c-4751-afd1-b572b0fd061e,", + "FriendlyName": "Dirt Bike Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40c1f20f-182c-4751-afd1-b572b0fd061e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:48.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 359, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2o15q3ks9rbgmv8h3mf4bdpj3.png", + "AvatarItemDesc": "2b7f4dae-51da-4af0-b009-12d6005c85c6,", + "FriendlyName": "Mullet Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b7f4dae-51da-4af0-b009-12d6005c85c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:26:58.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 815, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "056efjnqghgyjgzcgyuz5vbq6.png", + "AvatarItemDesc": "Lli8-aqt1EmCmyXwBRQaYA,", + "FriendlyName": "Mummy Head Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f9bc582e-adaa-49d4-829b-25f005141a60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:39.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 816, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "549e5xz7cvnjwkcrv8qnxssm6.png", + "AvatarItemDesc": "oRUh266ohkWQil14uxcEdQ,", + "FriendlyName": "Mummy Shroud", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "db2115a1-a8ae-4586-908a-5d78bb170475", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:42.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 820, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "32o1jvjp0zv2e13meoz6ly7pc.png", + "AvatarItemDesc": "zpjkkb1HtUORoPaODFuuxg,", + "FriendlyName": "Mummy Hand Wrap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "91e498ce-47bd-43b5-91a0-f68e0c5baec6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:52.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2760, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70axtgsjoojk0df4289594v6q.png", + "AvatarItemDesc": "25e76b00-4c56-4259-968f-046c14fb36fd,", + "FriendlyName": "Earrings (Musical Note)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25e76b00-4c56-4259-968f-046c14fb36fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:06.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2761, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bah-hC34O0mkCKiY2pH72A.png", + "AvatarItemDesc": "60cc933c-5bc8-49c1-aa6b-9c2adfef088e,", + "FriendlyName": "Necklace (Musical Note)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "60cc933c-5bc8-49c1-aa6b-9c2adfef088e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:08.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2160, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CCrSa0uGc0u9LuDrRzUoeQ.png", + "AvatarItemDesc": "f4f372b8-33a1-462e-a9f7-8a0e85b2c0d7,", + "FriendlyName": "Mystic Mantle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4f372b8-33a1-462e-a9f7-8a0e85b2c0d7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:37.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2119, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hVhS_lAUy0O3ga-alhshaQ.png", + "AvatarItemDesc": "b79a6f2c-d960-4d8a-b05f-24e818ec5f60,", + "FriendlyName": "NBA Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b79a6f2c-d960-4d8a-b05f-24e818ec5f60", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:38.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gq5kauTaAU2sDicXqi98Qg.png", + "AvatarItemDesc": "fe2721b5-16c8-4d9d-bca5-0233ab008fbb,", + "FriendlyName": "NBA Wristband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe2721b5-16c8-4d9d-bca5-0233ab008fbb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:40.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1270, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "brfgb9cf9tp8tgva2k3qzm6iv.png", + "AvatarItemDesc": "5e9bbda6-d681-42b1-9cc4-5758acf34bc0,", + "FriendlyName": "Formal Necklace (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5e9bbda6-d681-42b1-9cc4-5758acf34bc0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:55.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5fwhywu2l5mxei877fhrhwhq1.png", + "AvatarItemDesc": "b95678aa-351a-4929-8a0e-0274a183eb18,", + "FriendlyName": "Formal Necklace (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b95678aa-351a-4929-8a0e-0274a183eb18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:51.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1274, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0rq3ue0rxap0g0375himga11m.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,", + "FriendlyName": "Party Hat (Shindig)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:12.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1564, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aal6en3akiu96g363ok04sx99.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,AfqXBWLBQEiiag4cEtoEiA,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Hoopla)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:37.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1565, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aiqpa9xefa8z86zduzd2gyjw8.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,RROvzwtshUGyRudVIBn8bw,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Celebration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1566, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "e9evvmcwo9jnfy5clp9ztr4vz.png", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,69vv21jfTUu1E_LjK5bytg,MrrsmY0J0EKduH-f5wUj6g", + "FriendlyName": "Party Hat (Jubilee)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:33:46.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,IUDG__cu9EG9wK57ajRTHQ", + "FriendlyName": "New Years Hat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8df990d9-141a-4757-b4f2-138d5e68ab74", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1348963899 + }, + { + "AvatarItemId": 1273, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9alk9hbmjw74ccjkcwn2i7s3a.png", + "AvatarItemDesc": "86dd9ff9-643f-4f11-8243-93ad948db4cc,", + "FriendlyName": "Happy New Year Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86dd9ff9-643f-4f11-8243-93ad948db4cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:08.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2611, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cbi2t8gd1f5tzytciebrrxndh.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,", + "FriendlyName": "NFL Arizona Cardinals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:35.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2612, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxeoecs08mv3iy5dgdogqbb94.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,G9cFrBRvw0S-nDp-Fn494g", + "FriendlyName": "NFL Atlanta Falcons Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:35.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2613, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7n3s5s977ulc5z7jhiencdnrw.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,KcsFLjkhCUmg8HbamtChbg", + "FriendlyName": "NFL Baltimore Ravens Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:36.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5n2ngxovy8uj9lfz3essaxn29.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ye7GYcd6PkObqAsla6BJaA", + "FriendlyName": "NFL Buffalo Bills Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:36.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2615, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eslxy84me9m2oxtnk8o18n5nn.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,xVPRKfkYG0-u4MFhJYGe9g", + "FriendlyName": "NFL Carolina Panthers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:37.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ah2mm6n1rgh6pvzk4fx9rnj6.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,84OVMNAiv0mQwX2bpkw2yg", + "FriendlyName": "NFL Chicago Bears Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:38.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2617, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "831h9zt2yylg0ao93kyxmy4t3.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ishXZTP3bkeVNk68PBZAiA", + "FriendlyName": "NFL Cincinnati Bengals Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:38.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2618, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cz3bgo3si4042vf9xb2dtypxf.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,cLswsDJEmkCqYDGI8DJmPQ", + "FriendlyName": "NFL Cleveland Browns Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:39.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2619, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "220fwlypah2hrdvg8fi0gswf0.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,fdmR0dt0hEakaNp8mpjGmg", + "FriendlyName": "NFL Dallas Cowboys Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:39.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2620, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ca68ce9flmrpl7ig3zwhbqrv4.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,5mFA4uHLmku8oCc1rDJmXw", + "FriendlyName": "NFL Denver Broncos Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:40.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2621, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7g7ym06cw3m98l2f9ziwdlpia.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ry0fWzcysk2fsr-zIvzjUg", + "FriendlyName": "NFL Detroit Lions Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:40.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2622, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1p6ky76pvprr7cwbobzngw01f.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,S9YQnGF2WkaFSG07d-5TUw", + "FriendlyName": "NFL Green Bay Packers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:41.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2623, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bmav2pqie439kaf4223zzgxlg.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,pcEwV_ZPxkSKP39UgPh6fw", + "FriendlyName": "NFL Houston Texans Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:41.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2624, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f2apsx992xa88goodejq341lr.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,nHa0WjWfl0-g-sknorb10g", + "FriendlyName": "NFL Indianapolis Colts Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:42.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2625, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "68ou7wz2ezhyh6y39hxqeyihw.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,w_8Bff5J_Uej5iCAz2WTBw", + "FriendlyName": "NFL Jacksonville Jaguars Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:43.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2626, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dz7a5njd87o8hv2l5etgyisnj.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,kdExSysXREOAYqobK_Wzpw", + "FriendlyName": "NFL Kansas City Chiefs Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:43.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2627, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "351j7myhryxcppvy8gvdz4oix.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,XlkkIjP4EEKq0ObjXYGtBA", + "FriendlyName": "NFL Las Vegas Raiders Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:44.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2628, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ddaagd1il03gi4nrec2vd1xei.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Rlxn533MdUqMefvUSoZrrQ", + "FriendlyName": "NFL Los Angeles Chargers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:44.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2629, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8ndck6ysf75lg3kvjiz1bhh3w.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,_BV42pO3n0e0V-eOf9CyGg", + "FriendlyName": "NFL Los Angeles Rams Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:45.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2630, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5uj5qvvm6izou0whgm634hnme.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,g6PcjAOH0UinJtrGmxVuyw", + "FriendlyName": "NFL Miami Dolphins Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:46.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2631, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d32jj9kz3n5jceagnlpmq5vpa.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,i1e3GavcdEu7EJFzwpBXvg", + "FriendlyName": "NFL Minnesota Vikings Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:46.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2632, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "44ohtkl0bud9zkb1hd75qdgph.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,ebjoqQ_pnU2qdFVR1yDIzA", + "FriendlyName": "NFL New England Patriots Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:47.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2633, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8t9dbig6zpecho4qe8scj3bpt.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Fr--3nBIUUuD6EYmj1iklA", + "FriendlyName": "NFL New Orleans Saints Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:47.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2634, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b0joufd9x01y01brs3e7krk43.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,hrTGnJLb4U21P9PCi7u3xw", + "FriendlyName": "NFL New York Giants Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:48.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2635, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwlxb29ug9oqs1apya0icyjsc.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,coRfJXTxhUKsFs1tzSBgUQ", + "FriendlyName": "NFL New York Jets Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:48.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2636, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxnxsmeklw8dkemz9vrpajd7t.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Y75a0D5Wj0qmpfxG18LvWQ", + "FriendlyName": "NFL Philadelphia Eagles Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:49.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2637, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5t2128zn5sfwk2uc2qclkhlb9.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,VExSaZXZ9kan-0qjP6YPzQ", + "FriendlyName": "NFL Pittsburgh Steelers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:49.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2638, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "du30i4p3o8j2i1t6evwaw6x3b.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,Yec2FLXhpEmrocbVUOkhhQ", + "FriendlyName": "NFL San Francisco 49ers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:50.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2639, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "40mqyx1i4dzjcumjlzblnr5jl.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,EziQI39wZUWqIy8zs5I7LA", + "FriendlyName": "NFL Seattle Seahawks Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:51.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2640, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5gvpye8hcsb60wbuk15bi7r5t.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,PWMkuXHWFEKGKfSiT8rWNg", + "FriendlyName": "NFL Tampa Bay Buccaneers Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:51.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2641, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9ftzba4l162233dbbcqiaszc4.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,xdO8lwFuG0e1v2FRmza4dQ", + "FriendlyName": "NFL Tennessee Titans Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:52.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2642, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "30ubxldr0r27wafqn2ejkmtgv.png", + "AvatarItemDesc": "745b4725-5bf2-4b78-9847-076f02eab97e,vtQ3bx6n7kSFdES121M5IQ", + "FriendlyName": "NFL Washington Commanders Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745b4725-5bf2-4b78-9847-076f02eab97e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:22:52.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1413, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "09zvd3ohdxjul97glak50v2zt.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,", + "FriendlyName": "Nightcap (Zonked)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:09.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1414, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5oixy16bep4bh46cp4ma645d0.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,2vawoQvJPUegnClmr2RNAQ,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Zzz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:10.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1415, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7wpy1gh40rv5ibcltdnowpd05.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,kIcd-yEnsEqxoeogquvmRw,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Dozy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:12.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1416, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0415gb7q368nr2qadddcg1i0k.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,qHyKfCsrbUqSWomjLXf7Hw,RR69IGV4j0Sd2xtmyN5wFg", + "FriendlyName": "Nightcap (Sandman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:45:14.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2064, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "g4VIHBisj0-q7bWIQ6IMfA.png", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,ZvUwd0aVfk6SjA7oyouXUQ", + "FriendlyName": "Starry Night Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "250bbb53-1fae-440d-b25a-46a16c271367", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:16.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1569, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6gfimjrsbl126ir365lqnmv8n.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,", + "FriendlyName": "Nineties Outfit (Da Bomb)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:09.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1568, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1oix7x6qvyo7ps4kk7lftxq9j.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,ulOBBvFEJ0ioFA1qmL390A,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Aiight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:07.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1567, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1czdhhr33h8ztob05qmon2562.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,V4CVhCB8eU-nrfYgticVOA,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Booyah)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:34:05.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1920, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eakv6vkwmx6lwc95u5k9tvb07.png", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,6ZtRAbD-xUufWNLCrlrrCg,u9yHtj_tfkuowGp0h_YpFg", + "FriendlyName": "Nineties Outfit (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ca218759-7eed-440e-b5dc-dba15f81e598", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-29T23:25:09.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2566, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e1-lO7c1i0m5eg3UkCPczg.png", + "AvatarItemDesc": "a600fa80-ed4e-4acb-8d6d-72fa87d2bef3,", + "FriendlyName": "Nutcracker Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a600fa80-ed4e-4acb-8d6d-72fa87d2bef3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:02.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2567, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6-Sz9f-O00uY5pWih9NC_w.png", + "AvatarItemDesc": "beee4e09-9d93-4023-bc78-c5dc551a0ab8,", + "FriendlyName": "Nutcracker Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "beee4e09-9d93-4023-bc78-c5dc551a0ab8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:05.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2381, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "pEnzLpycJU603AzcfO2Mnw.png", + "AvatarItemDesc": "9b636782-47f0-4218-9a1d-be65e550dc44,", + "FriendlyName": "Bongo Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b636782-47f0-4218-9a1d-be65e550dc44", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:50.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2382, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "YzXiKP_YPkW_dray4uvVpQ.png", + "AvatarItemDesc": "96ca8d50-0721-47a3-a988-018a1c157677,", + "FriendlyName": "Drumset Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "96ca8d50-0721-47a3-a988-018a1c157677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:53.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2383, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "music", + "ThumbnailImage": "I_TDOXrCVUe1V3HKxNvzAw.png", + "AvatarItemDesc": "c8d2503e-5d43-4828-8d1d-40c64a577753,", + "FriendlyName": "Harmonica Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c8d2503e-5d43-4828-8d1d-40c64a577753", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:55.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1838, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5aq3d9icptakxqxo8zitb57dn.png", + "AvatarItemDesc": "caf1e229-9660-4abf-8074-f706709f4097,", + "FriendlyName": "Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "caf1e229-9660-4abf-8074-f706709f4097", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-23T17:41:53.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2860, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "A6Gyszd5SkyByb7oryYm_w.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,", + "FriendlyName": "Overalls Skirt Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-03-10T22:40:50.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2863, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bGS9YZZvhkSZTQaLBEnbYA.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,sCj73U9p5E6DQreOsbutqw", + "FriendlyName": "Red Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:58.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2864, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "jAr4ImFmQ0u4xa689IeaXA.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,HDpCyFn40kOxJ0P_xJr4-A", + "FriendlyName": "Yellow Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:41:01.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2862, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T3O9LKgezU2qgWjCAoDPfQ.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,OZ1v-s_XFUyfMA5mfd47FA", + "FriendlyName": "Green Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:55.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2861, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LP6loSF2uUmBukTEKkAN5Q.png", + "AvatarItemDesc": "ad3961ef-4b91-4ed8-971d-4d23c32548a1,e8aNxGFlBUWy59Bfrvvv7w", + "FriendlyName": "Brown Overalls Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad3961ef-4b91-4ed8-971d-4d23c32548a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-10T22:40:53.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1561517472 + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,08593d9f-aaed-4662-973d-0f30e77f1955", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1085700839 + }, + { + "AvatarItemDesc": "a9404818-d7d0-4061-8eb8-90f23e82115c,e002caa2-3447-4102-8d9f-570d1942101b", + "FriendlyName": "Overflowing Book Bag Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a9404818-d7d0-4061-8eb8-90f23e82115c", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1362900866 + }, + { + "AvatarItemId": 1351, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "729s6a9tw6yv1xyun76b5zwkz.png", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,", + "FriendlyName": "Paintball Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eda29bce-5a07-4439-a2a7-cafa11e2ed62", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:51:06.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 105, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "42cr1t40qzbgiloer9d71sb28.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,", + "FriendlyName": "Paintball Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1946, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "52yxsadxjnh09e02kzlza70z7.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,kS7jek5tPkSkIgjIF369Vg,_Jx5VqGbVUCPwl_SaUCTRA", + "FriendlyName": "Paintball Goggles (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:35.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1947, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4bndj29hiu03hkt63qwabedlj.png", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,3HAMDzfFO06jtYNXm2Bsjg,_Jx5VqGbVUCPwl_SaUCTRA", + "FriendlyName": "Paintball Goggles (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34144f4c-f6ef-49e5-a286-1781e82e6782", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:38.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 281, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "agzh2svq1rj75i5f37dk13i34.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,", + "FriendlyName": "Paintball Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1948, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "841fudm9eezzeqyfx2wqdrpj2.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,-Moek0heVkWtKpTw9oZ0EA,FsirX90YM0y5lk7mnqDnEQ", + "FriendlyName": "Paintball Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:40.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1949, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cvkzqrgyihp9bv1n8b6ie95fl.png", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,D-XJyw1BPE-m0gXpvcXrQw,FsirX90YM0y5lk7mnqDnEQ", + "FriendlyName": "Paintball Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64ad8789-1645-45f7-819a-a413ff7c9622", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:41.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1332, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1xj0gybaafqrogg7hnsgqbscb.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,", + "FriendlyName": "Mechanic Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:27.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1333, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0q7ysb2tsjy0mbxx1nes33jdx.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,C9cPjOoiCUCHrCBi809CMQ,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:32.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1334, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20bhcftlu43jwrw06imhg37ox.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,24zVmiGiaESrM5qcRfF6gA,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:36.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1335, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0v5t8d3m8790n1lzjjp3qd9z5.png", + "AvatarItemDesc": "0c537a84-9095-406f-8c87-1464f3a774ba,LOeHr4xoik6VOv1fOeImjA,3pdsIrhM9EW-y-LNnjbFxg", + "FriendlyName": "Mechanic Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0c537a84-9095-406f-8c87-1464f3a774ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:41.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 197, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "94farwa7bctza7e4db865is8x.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,", + "FriendlyName": "Paintball Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 654, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "274io1pn2peum3e1dm08i0tlj.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "FriendlyName": "Paintball Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:41.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 655, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0hdenghs8yfyhmtnz229g0hd1.png", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2", + "FriendlyName": "Paintball Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:42.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2941, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b4RkTVS6UeA0lrNcM_BAg.png", + "AvatarItemDesc": "07116e9b-b182-40b4-90c2-99738e7aedd6,", + "FriendlyName": "Parrot Purse", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07116e9b-b182-40b4-90c2-99738e7aedd6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-24T15:25:33.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1255988719, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "74e6878f-94e0-41f2-93ea-de6412238385,", + "FriendlyName": "PartyBand (1710)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74e6878f-94e0-41f2-93ea-de6412238385", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.519776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2568, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fuCyF2LQ6EaCTanw2VqOwA.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,", + "FriendlyName": "King Rat Cape (Rouge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:08.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2571, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VoZTTqgfBEOCWqIxKoS-Pw.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,gYXppJSpDUG9MB0xej4-kQ", + "FriendlyName": "King Rat Cape (Jaune)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:22.687Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2570, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_Hj47OdDmUeTB3wTf2r2Kg.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,VF7QrfNdC0ex5p62bkdt7w", + "FriendlyName": "King Rat Cape (Vert)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:18.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2569, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WEL2fL-6q0qFtXp51G5Wng.png", + "AvatarItemDesc": "56da4739-f628-4894-95cf-e21a600f2124,eEsEJ78Me02dXnp6gTuZRw", + "FriendlyName": "King Rat Cape (Blanc)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "56da4739-f628-4894-95cf-e21a600f2124", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:12.417Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 760, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3r88aoow0xky1yenciez1enyj.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,", + "FriendlyName": "Peacoat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T19:04:25.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 791, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2i0jzvjgsic4vsv7uod1p0o5k.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,fvoaMN4M10WS6jiA6XlOJQ,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:46.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 792, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6fdpcq0zhuz9xr09wlcj3j7bx.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,PwsLxCnzOkSW-RGz3tNb1w,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:25:48.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 794, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "beldcz68emei41q0ndhzzbtr7.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,uiBklotTUEqRI2F67vyJyA,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:11.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 817, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dhqntf98nka1vp9oh5hi5ybtn.png", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,jXBi28cRV0CEQgKIrpD0_A,AZnPWCN41kmNwFwDqWGFWQ", + "FriendlyName": "Peacoat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:43.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5db418c3-1aae-42d3-af59-4ad03a9a3bf4,", + "FriendlyName": "Peacock Gown Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5db418c3-1aae-42d3-af59-4ad03a9a3bf4", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1042258582 + }, + { + "AvatarItemDesc": "e457f946-b985-417c-9dec-0d8d3a7da71a,", + "FriendlyName": "Peacock Gown Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e457f946-b985-417c-9dec-0d8d3a7da71a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1725552604 + }, + { + "AvatarItemId": 1271, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bjtkko8cs4inet2bswk24bky0.png", + "AvatarItemDesc": "7be2eafc-21ea-4266-ae64-8888c06798d6,", + "FriendlyName": "Pearl Necklace (Freshwater)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7be2eafc-21ea-4266-ae64-8888c06798d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:00.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1272, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4kmcbo8n4ht1kadrspll7f1oy.png", + "AvatarItemDesc": "7be2eafc-21ea-4266-ae64-8888c06798d6,sqPMH0LhLkeG-4XkxpU-sw,URWHffwjGUKgXeV01upePA", + "FriendlyName": "Pearl Necklace (Saltwater)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7be2eafc-21ea-4266-ae64-8888c06798d6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:16:04.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1479, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6a428adukettbzkckyu9updp2.png", + "AvatarItemDesc": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,", + "FriendlyName": "Pegasus Wings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:17.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,RRmRsm6R6kOyn5HfbXNJWw", + "FriendlyName": "Pegasus Wings Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1841210169 + }, + { + "AvatarItemId": 2603, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zzqrxKCL00W27uhhyfZuqA.png", + "AvatarItemDesc": "b66cb611-c079-42f0-ae04-6f51384dbe90,", + "FriendlyName": "Penguin Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b66cb611-c079-42f0-ae04-6f51384dbe90", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:46.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2264, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Av7hkeKA7Ei1Stn-Fk2txw.png", + "AvatarItemDesc": "3cd52c5d-0c46-41c0-8254-c4542805da31,", + "FriendlyName": "Pioneer Bonnet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3cd52c5d-0c46-41c0-8254-c4542805da31", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:14.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2265, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2o_-7a2C30eYta_KTWhvIw.png", + "AvatarItemDesc": "ad55bb62-671a-4a29-aec8-36a1e3104f05,", + "FriendlyName": "Pioneer Cuffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ad55bb62-671a-4a29-aec8-36a1e3104f05", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:16.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2266, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "-__TuY2TCUu0TPihPV1SqQ.png", + "AvatarItemDesc": "bb6ee7fa-7a31-4a16-a97e-be6eac42c595,", + "FriendlyName": "Pioneer Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bb6ee7fa-7a31-4a16-a97e-be6eac42c595", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:19.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 600, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bu0ii93b6dmvvc93szjfnal02.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,", + "FriendlyName": "Pirate Captain Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:24.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 611, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0rvymvsae95r30k0psb64iavr.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,0f71b9a2-dd88-435c-a571-4503d12dcf2d,9b2102f0-e16b-464f-9f1c-f068de52bb89", + "FriendlyName": "Pirate Captain Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:47.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 612, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0pyrhi595qqyqin50b6zfj2ws.png", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,f4ad4b9e-dd42-4711-9437-5beae51966f6,9b2102f0-e16b-464f-9f1c-f068de52bb89", + "FriendlyName": "Pirate Captain Belt (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38b38e24-d636-41c6-b80e-a285ae3106dc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:48.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 599, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "dl4lc9lujy638xdphs121fg3k.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,", + "FriendlyName": "Pirate Captain Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:22.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 603, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5o6eyu7tmzl4ffq27fn0ucofj.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,17040a88-87f1-4613-b439-3316e33dbd48,71fd06e3-cac8-4923-a09c-ef91183712d2", + "FriendlyName": "Pirate Captain Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:32.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 604, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b90z3pydklkb08tidovz5jd1g.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,f9aaa7c5-59bc-486c-9a6e-27a8ac618f84,4cc0ea9f-65af-486f-8319-02b85ab5197f", + "FriendlyName": "Pirate Captain Hat (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:37.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1128, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7zbbv8oqo767y92w7mp1p6brm.png", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,jQwNBXyoU0Gk4FyNw6jFxg,4cc0ea9f-65af-486f-8319-02b85ab5197f", + "FriendlyName": "Pirate Captain Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3df394de-9c3e-4141-aba4-12eac4742102", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:01:03.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 601, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5ynt63fbqq9vg0dnp6obm10jk.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,", + "FriendlyName": "Pirate Captain Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:26.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 607, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5ftzyq0wpssi4a8hsrmla5jml.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,556141f7-6fbc-4065-9faa-8d8240a93d7f,00ac4e37-6eac-4ed5-b46c-2c075038bc3f", + "FriendlyName": "Pirate Captain Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:42.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 608, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "69yr1pl69y8de2fpn7tvc0f34.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,d81d0b62-e052-46c2-9252-4bc5fb219f53,00ac4e37-6eac-4ed5-b46c-2c075038bc3f", + "FriendlyName": "Pirate Captain Jacket (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:43.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1112, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bm2ft9q0v5z0q0i1x8nutm0yw.png", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,xGmnUGF-Ck-jP43r8tVVjg,52053afd-9f12-4fec-a358-7eb3fe8c19bb", + "FriendlyName": "Pirate Captain Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:14.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 602, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cwpa2fe7uhjhy2fg8z2bjoqqf.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,", + "FriendlyName": "Pirate Captain Cuffs (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-13T00:04:28.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 615, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "1ouywwp83tgc11yi3x9wro2bd.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,556141f7-6fbc-4065-9faa-8d8240a93d7f,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:52.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5384s8w1fosxnurfe756wgd0n.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,d81d0b62-e052-46c2-9252-4bc5fb219f53,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:53.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1105, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cijsojywwytastkrfmnur8m2l.png", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,xGmnUGF-Ck-jP43r8tVVjg,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132", + "FriendlyName": "Pirate Captain Cuffs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "eceb7d49-be41-4228-86a9-2973d62e7135", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:48.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 619, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dfy3wgi2sfer51uhs3af8y5da.png", + "AvatarItemDesc": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,", + "FriendlyName": "Pirate Eyepatch (Skull)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-28T01:18:41.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1121, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dkbvx8r9yxtlxb2uhcv5qn1az.png", + "AvatarItemDesc": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7,clFrpYWFQEmDkE14f3o_Ig,DE1C-RJkuEWIvpR0q1i42g", + "FriendlyName": "Pirate Eyepatch (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e1a13235-e3b3-4b39-8e6f-e6449d4321b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:04.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2884, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "j7uEUd9qPEerGchhJ6rmaw.png", + "AvatarItemDesc": "11b3413f-2c18-41cb-8ef2-55fb825ed476,", + "FriendlyName": "Plush Bunny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11b3413f-2c18-41cb-8ef2-55fb825ed476", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:12:59.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2885, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B08PvxHhZUOcI_bnrefOFQ.png", + "AvatarItemDesc": "11b3413f-2c18-41cb-8ef2-55fb825ed476,DNGqrCGe8kKrNmJaGzjjew", + "FriendlyName": "Plush Bunny Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11b3413f-2c18-41cb-8ef2-55fb825ed476", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:02.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1823, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0xxjg720qofphga2otdghooga.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,Ris_kNVcgU-EK4SoMckC2Q,74J7skzs9EaIq09bZArjiQ", + "FriendlyName": "Reclympics Shirt 2021", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-04T16:12:53.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2017, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6JmHNL-jsE-M9URUaMcvuA.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,PkMkvHzXek-yKyqJESZ3tw,nbgwLqKBnUyvieWX0i47dg", + "FriendlyName": "Scrubs (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:10.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2018, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bCXA7saMUUuD_C4_Qy0t5w.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,P49CJ5imfEuxH1rryrfFRA,nbgwLqKBnUyvieWX0i47dg", + "FriendlyName": "Scrubs (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:12.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2685, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "efVnvXfV-0-RgOpfnNi_bQ.png", + "AvatarItemDesc": "b3b59410-d3df-464b-a75b-500a9e5ea154,logtSrZSTUqwEmBw9i0qjA", + "FriendlyName": "Rec-On T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b3b59410-d3df-464b-a75b-500a9e5ea154", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:00.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1884, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "eafwn78k3h4uaugzy9kay26kr.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,tMsCUhDslkmdNBfogeSz7w,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:18:04.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1883, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "crpsa73v3nvk6saa71tdx8k9u.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,cypMpMaVeUuRjm4FLH4czQ,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:18:02.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1882, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7h6z4ycgbt833y56ccc3rtl3p.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,whWKAUSGp0qprsJomq9Nwg,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:58.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1881, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "84jgglt9m8uawzj1481xfk4fw.png", + "AvatarItemDesc": "06306723-ca20-4aa6-b7b3-917113f41ac3,LTXy6g4xe0abuFzOLpVDUg,PIjEKhXiCUqtA8FBhLBcmA", + "FriendlyName": "Cat-Eye Glasses (Dark Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06306723-ca20-4aa6-b7b3-917113f41ac3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:56.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1848, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4hpfukjpch71fzmfokc3tx3s6.png", + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,", + "FriendlyName": "Poodle Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:44:57.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2558, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BHgotDTbX0umzVt4i36gBw.png", + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,tPAJqFHiX0yUQpDOE0MJ6A", + "FriendlyName": "Planetary Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:53.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,zhLs4Hgw1UCrcNekAWd4RQ", + "FriendlyName": "Poodle Skirt Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1708534829 + }, + { + "AvatarItemId": 1070, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5sb23yvxjkh7ozjfothb1ul6f.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,", + "FriendlyName": "Floatie (Creamsicle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:59.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1071, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agoyhlxxj14bkqor8pthssxge.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,LRv1X0ZrjkqO7azGpZ7uMg,sx10-Hsfl0OS62pIFvHH8A", + "FriendlyName": "Floatie (Donut)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:01.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1461, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c5zj93bwe1qme6eb14dtwbcjt.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,NMmOn0zQZ0mJLxCMnsbIVw,v1ZOE9G9gEWAqNJX1A9f2A", + "FriendlyName": "Floatie (Watermelon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:09:35.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1844, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2iyjpxumims1bzvq7x6kqrmqx.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,1w_u4d4AjESxw5dxRkKHrw,CylZksC7KUGXF2VeJhlLSw", + "FriendlyName": "Floatie (Cheeseburger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:37.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2158, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dVpzRz2OY0avaXnWbmbm_w.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,6wCu0bPODE6tayAY_aicRQ", + "FriendlyName": "Floatie (SummerCamp Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-05T20:23:33.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2244, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "n9H1eWtdiE25SebY5g4Ukw.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,7BMBl3iA6USKijNCKfCXqA", + "FriendlyName": "Floatie (Neapolitan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:52:56.2Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2299, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SlacPLiWtE2CpmO5WHd-RQ.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,akqD9mYQQ0qks9SJe80MbA", + "FriendlyName": "Science Floatie (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:38.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E_MFRZ-dI0GiU60k_m8LSw.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,RCit-BlLyE6cvzpbfCVShA", + "FriendlyName": "Science Floatie (Invasion 2)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:28.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2817, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QMhjhzpkIUOPw6-mpbsclQ.png", + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,1Z24Obg1MEm6dC_blOy4IA", + "FriendlyName": "Floatie (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:39.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "75-HXZBOAEiDBqclnOn8YQ,KrNRgSR3qEq9f6hfcsTFQQ", + "FriendlyName": "Pool Floatie Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d879fef-4e90-4800-8306-a7259ce9fc61", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1941160248 + }, + { + "AvatarItemId": 1072, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6o6o3vze30qmak32kdlkeus4l.png", + "AvatarItemDesc": "awOFE0_GSECr8GBNBo6a_w,", + "FriendlyName": "Floatie (Giraffe)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1385036b-c64f-4048-abf0-604d068e9aff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:03.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1073, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aasr0sd35kmv6vhgbs9znxafg.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,", + "FriendlyName": "Floatie (Flamingo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:04.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1074, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxj3pn647bzr2jpxm9myh6ret.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,k3dbqP5z30KU37MVPpsEPQ,GtoHkTpyA0iGIHsxphwivg", + "FriendlyName": "Floatie (Dino)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:07.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1451, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8t2f1nmayxxozgvqs90t59neh.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,hM0OmxjWLESbsJ-M0Z3QvQ,qZzkrGXpPkGq_nnGLZorwA", + "FriendlyName": "Floatie (Parrot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:05:42.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1824, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bs25xo0gnja1akbu4eg7q8dor.png", + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,9WpjdWJbhkCoEgDljqZkdw,Uyn3QOl1zkGR2_OUOhNWjQ", + "FriendlyName": "Floatie (Rubber Ducky)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:36.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "EJFGAW-TnUCaeD59parEHg,zxDWWqb0S0uDJgYeVQQ3oA", + "FriendlyName": "Pool Floatie Bird Head Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01469110-936f-409d-9a78-3e7da5aac41e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1424015386 + }, + { + "AvatarItemId": 2106, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LHx48xU5E0OkqfpqJBes6A.png", + "AvatarItemDesc": "2565d597-9d17-4c47-a66f-b683583f0cf8,", + "FriendlyName": "Floatie (Bunny)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2565d597-9d17-4c47-a66f-b683583f0cf8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:19.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1825, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bdeaw4skm15u253skgs4rtpe9.png", + "AvatarItemDesc": "efac28ae-ff44-4322-9bc0-61eaaef95399,", + "FriendlyName": "Floatie (Dragon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efac28ae-ff44-4322-9bc0-61eaaef95399", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:42.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1453, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9srp5d5aba29z5vzoj3qkqmpg.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,", + "FriendlyName": "Floatie (Zebra)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-13T21:06:11.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2280, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lLR7SX5N30af5zpgdD5M6A.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,p379ObEL9Eea0DX9Ld4qnw", + "FriendlyName": "Horse Floatie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:18.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1158295129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0vpwih9r_UeG9tov9Mm6VQ.png", + "AvatarItemDesc": "84574356-98ec-415f-8668-82cb52016bd3,ljbO0JAKVky8M97MsjKnPg", + "FriendlyName": "Floatie (Armored Horse)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84574356-98ec-415f-8668-82cb52016bd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.521776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2190, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dOpBUgepYk6c0zUHhvZxfw.png", + "AvatarItemDesc": "187569d3-23b4-4118-862a-ace7f50fec45,", + "FriendlyName": "Rammy Floatie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "187569d3-23b4-4118-862a-ace7f50fec45", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:21.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1471, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "chtv23889bkxqdhplm9mrfmxz.png", + "AvatarItemDesc": "7a98503f-0460-4d25-a0d5-72a32db8f4c2,", + "FriendlyName": "Floatie (Turtle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a98503f-0460-4d25-a0d5-72a32db8f4c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:25.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1954, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c0c3e51sc58rcze7q10j1aiq5.png", + "AvatarItemDesc": "7a98503f-0460-4d25-a0d5-72a32db8f4c2,A8ht7G1iqEeLvP0SW6SwyQ,QppxPdf550-jT7dK7XrTzw", + "FriendlyName": "Floatie (Penguin)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a98503f-0460-4d25-a0d5-72a32db8f4c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-19T00:15:14.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1075, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "64nsdc03lld08y8ouiugpbt3a.png", + "AvatarItemDesc": "UGkQa4atNUe1cE5TN79s4Q,", + "FriendlyName": "Floatie (Unicorn)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6b106950-ad86-4735-b570-4e5337bf6ce1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:26:08.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2231, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "47gm8nyvqd8aiokq2iddqfdko.png", + "AvatarItemDesc": "e633d56d-5121-4492-b606-40759108a00e,", + "FriendlyName": "Ice Pop Earrings (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e633d56d-5121-4492-b606-40759108a00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:03.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2310, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d54dk7h9h3ru312k9t4ncpscd.png", + "AvatarItemDesc": "e633d56d-5121-4492-b606-40759108a00e,6CFwXqWT50mH1v6WueFsjw", + "FriendlyName": "Rocket Pop Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e633d56d-5121-4492-b606-40759108a00e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:03.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2579, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rDwV9FzDSE-8tcpVeQsaFw.png", + "AvatarItemDesc": "7a97838f-24ac-41a0-8390-0c9308644eb1,", + "FriendlyName": "Power Sword", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7a97838f-24ac-41a0-8390-0c9308644eb1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:38.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1843, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1lnhgu9j4uf3ypg48ra7v0by1.png", + "AvatarItemDesc": "76a73866-5b60-424d-b751-a105e030f46e,", + "FriendlyName": "Prankster Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76a73866-5b60-424d-b751-a105e030f46e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:34.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1842, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aijv697pz04ho70d9l3lxcnvd.png", + "AvatarItemDesc": "5b3cae00-88bd-4179-b75a-9444c5a52fa5,", + "FriendlyName": "Prankster Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3cae00-88bd-4179-b75a-9444c5a52fa5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-02T21:11:28.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2060, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XSHhlJsxRU2OwZD3eTZWJQ.png", + "AvatarItemDesc": "5b3cae00-88bd-4179-b75a-9444c5a52fa5,ZM6W70pGt0aAPLYf4DykeA,icHpJch4uUeGdhoLkawyyg", + "FriendlyName": "Prankster Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3cae00-88bd-4179-b75a-9444c5a52fa5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:55.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2019, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wf_PLXUTzU6UXiDZCDulRw.png", + "AvatarItemDesc": "06475461-f3ab-4e92-ad92-243c00b19ff0,", + "FriendlyName": "Prodigy Princess Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06475461-f3ab-4e92-ad92-243c00b19ff0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:25.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2020, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "SdP7P425CkCbPhAIfZCtaA.png", + "AvatarItemDesc": "076e98b6-dc1b-48f8-bb02-aa65d26b33d1,", + "FriendlyName": "Prodigy Princess Bracelets", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "076e98b6-dc1b-48f8-bb02-aa65d26b33d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-26T17:41:29.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1456, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e08mipx3bu50m127eoa5c3js9.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,", + "FriendlyName": "FNAF Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-05-19T23:01:58.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1587, + "AvatarItemType": 0, + "Tooltip": "2020 Rec Con", + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "27ormucbs1iox5kvr9tka0fpc.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,DOY6LpHq8kui8yDWGsNgZw,48yVsRt8sU6jf3P4wlPA8Q", + "FriendlyName": "Rec Con Hoodie (2020)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-13T20:11:27.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1783, + "AvatarItemType": 0, + "Tooltip": "2021 Rec Con", + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "0w037o55hb7xrlbbz5ndkt3qk.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,HUxqkzeE-Umq96TgSn0Hbg,48yVsRt8sU6jf3P4wlPA8Q", + "FriendlyName": "Rec Con Hoodie (2021)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:01:47.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2162, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IezDzyHOWUy4vzhhR8erIA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,QMScBauef0uOE48-YcDcCg", + "FriendlyName": "Dino Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:31.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2237, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "oYY2GYyfUUmrxi__mx94Gw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,25X6s0sTZkyzUOKiIt4cGQ", + "FriendlyName": "Creator Conference Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-08T16:24:39.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2284, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "sg_RSadtBUepyNJrDn81nA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,tkuP0sNwjEO3XH5Lf_KqkA", + "FriendlyName": "Rec Con Hoodie (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:33.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2733, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "84yMtUUfQUGCcvFe82Y2DQ.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,G_QPd8YBe0-n8ywAGLMKBg", + "FriendlyName": "Hoodie (Jessia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:25:00.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2684, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ja4C7EX3zkKj0VnySl1Muw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,Kuh-HrhzTUmuGaw7cpLUMg", + "FriendlyName": "Broken Heart Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:01:56.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2705, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "JpkOsmJyvk23HfK4fWMwJA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,SJ7DV0meUU6QjksIcAAeRw", + "FriendlyName": "Pullover Hoodie Shirt) Pullover Hoodie Shirt Color Block Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-21T19:41:44.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2799, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "grtmSgDkekKbMf5XDtoA3g.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,bS-St6XLHkSwKt41J7BmKw", + "FriendlyName": "Alo Yoga Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:29.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2823, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "cDunfbR1wE-D8-LUSD_mdA.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,jir883uk5k-cEgyAPDtQww", + "FriendlyName": "Pullover Hoodie (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-17T18:40:55.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1177382318, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pPyQA-HfpUirnUHijRJeYw.png", + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,a2mUuKOr-UOM5nPpR8dSMw", + "FriendlyName": "Hoodie (Em Beihold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.521776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,501dae9f-2fc4-45fe-a276-f606b93f434f", + "FriendlyName": "Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1545540299 + }, + { + "AvatarItemDesc": "4a0e91a1-4562-4a82-92be-cb7491f1592b,b8cfa1ab-13bb-4e71-afe0-490820aef449", + "FriendlyName": "Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4a0e91a1-4562-4a82-92be-cb7491f1592b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1335772728 + }, + { + "AvatarItemId": 1522, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "44boqpgj0k38m24uqczos4upt.png", + "AvatarItemDesc": "9ad40310-7cb9-473d-920c-5c18d7972030,", + "FriendlyName": "Pumpkin Cap", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ad40310-7cb9-473d-920c-5c18d7972030", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:17.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2368, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "KFjbHzLz7UezIItKG7vasQ.png", + "AvatarItemDesc": "9ad40310-7cb9-473d-920c-5c18d7972030,mQTpoW0-AEufdFmApqtRXg", + "FriendlyName": "Pumpkin Cap (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9ad40310-7cb9-473d-920c-5c18d7972030", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:12.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1523, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "9vaxzwj4b0l1ty30cspscr3di.png", + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,", + "FriendlyName": "Pumpkin Costume", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:26.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2369, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "BtXG-VrTYU-1n1VDsKOgjQ.png", + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,Be2v1bpBSEu4lwKJePscPA", + "FriendlyName": "Pumpkin Costume (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:14.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "84600de6-93fc-4a89-961e-1f18f4b80643,p4X1ilkxU0OjEWXdmQrVwQ", + "FriendlyName": "Pumpkin Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84600de6-93fc-4a89-961e-1f18f4b80643", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1015871793 + }, + { + "AvatarItemId": 1524, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "asfkh306x4wyzvnccvqkhby0d.png", + "AvatarItemDesc": "7e6e24b6-6f9c-4315-b457-73eba52d824c,", + "FriendlyName": "Pumpkin Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e6e24b6-6f9c-4315-b457-73eba52d824c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:19:31.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2370, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "F7i3Au-z5UmKBdm2ZZxm_Q.png", + "AvatarItemDesc": "7e6e24b6-6f9c-4315-b457-73eba52d824c,Mnpd7r3eu0CNmZYSPTMASg", + "FriendlyName": "Pumpkin Gloves (Ghost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e6e24b6-6f9c-4315-b457-73eba52d824c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-22T21:47:22.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2445, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "94mq42cmonwiia61g4kx9jsmu.png", + "AvatarItemDesc": "07bebdb7-bbe1-49ea-a746-c1d00e11f174,", + "FriendlyName": "Pumpkin Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07bebdb7-bbe1-49ea-a746-c1d00e11f174", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:30.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2446, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "0qumzncb3mqqivji6bma4w0vd.png", + "AvatarItemDesc": "07bebdb7-bbe1-49ea-a746-c1d00e11f174,ciwVtCaRHEO72BFPe4Ujmg", + "FriendlyName": "Jack-O'-Lantern Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "07bebdb7-bbe1-49ea-a746-c1d00e11f174", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:32.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2800, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ae49cxd8vs4g8bpfmd9l2uiv.png", + "AvatarItemDesc": "cfce35b4-d182-4505-a631-5a8e5011f550,", + "FriendlyName": "Pink Pop Rock Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cfce35b4-d182-4505-a631-5a8e5011f550", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:33.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2801, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZOOhVVu5AUmtTX571xSvXg.png", + "AvatarItemDesc": "98e4c868-df73-4755-84b6-c4d12723b811,", + "FriendlyName": "Pink Pop Rock Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "98e4c868-df73-4755-84b6-c4d12723b811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2802, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lA0R461bQ0ST6HUFBbaWCA.png", + "AvatarItemDesc": "cb061cf3-05fb-4494-be5e-091ddb080271,", + "FriendlyName": "Pink Pop Rock Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb061cf3-05fb-4494-be5e-091ddb080271", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:39.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 368, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8zf6s3vsds90klh8gnb66rq7t.png", + "AvatarItemDesc": "ce250e13-8137-4f62-9e52-2b1cebe7d0ad,", + "FriendlyName": "Punk Hawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ce250e13-8137-4f62-9e52-2b1cebe7d0ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:07.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 372, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d4xgo6yv2c0ciprp97kwmfjbz.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,", + "FriendlyName": "Punk Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:12.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 373, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f4451n0e2f0tbm5fmb0h59n72.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,4169bfed-8403-4590-a29d-4941275ac0b6,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "FriendlyName": "Punk Jacket (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:13.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 374, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ovn4a6rs6eyhc8cqwowjycsp.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,0724b166-9ace-46ff-a191-b13f135b00a8,d738ad4c-c9b5-41e6-8979-d586c964fb6c", + "FriendlyName": "Punk Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:14.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2755, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "rlFO-9ZKCE2yfkHlavGzig.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,MFsWxydybE-dw3KO2hkuZA", + "FriendlyName": "Yellow Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:36.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2750, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qEYJHcXWwkKASHqpFug-TQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,9bb0x4qgokyQLbt1jA25Wg", + "FriendlyName": "Punk Outfit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:22.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2752, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d21ms4sSZUe8LXA5sCgB6Q.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,YceWQmfB7U-R7OeBIr2Ogw", + "FriendlyName": "Punk Outfit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:27.987Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2754, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "30PBljeph0mO3nz4ycwRAg.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,G2ncsS9U8E6fyjvliFqdOw", + "FriendlyName": "White Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:33.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2753, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "XTqgvYBK3UeAHs9ot0OlpQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,AdJ3ExsUq0ixzaKDHK5RUQ", + "FriendlyName": "Purple Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:30.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2751, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f4hUyaoXvUGqD6pHI4u8WQ.png", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,TC79rbThdkCZHGWaxk-qYA", + "FriendlyName": "Olive Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4ba395e-f291-4e7b-8193-45f4422f41ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:25.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 376, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4d1njsjeb0jzje2jyt4qwnpjf.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,", + "FriendlyName": "Punk Wristbands (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:15.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 377, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1px8h9mdfc4sqsqsgszmxrx0h.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,a6796264-9c7e-4605-830c-06db8d0dc780,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:16.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 378, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8b0gvz7zqfujt7dgbozqj2clf.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,f3b2330a-b981-4c7e-902f-1f57382f2388,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:17.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 394, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0kiv90a8q8p0qlzhtvo821u6h.png", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,baffdefe-de21-4551-9330-b50873965bdd,dd7472ed-bdcb-4811-81ba-5ed682fb7675", + "FriendlyName": "Punk Wristbands (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:30:01.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 202, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2amyw5vvgk1c9r6w9pdloakyg.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,", + "FriendlyName": "Long Coat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 472, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6nqdiagvhy4jttofvcepefq2y.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,b42d903b-b393-4f49-af5e-38d0fb6c42b0,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:40.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 473, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cdi7d06wp2qv0t93m630vtp9s.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,8cab8cc6-9a2d-434c-8b72-e66edbdd223c,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:42.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 474, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "asgwfydw5ghc9650zs32gm2b3.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,657f28d5-672a-4f27-86c6-3cb00f25bb84,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:43.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1628, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3q0iocg562i3lzbn5n65t13lw.png", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,w_aK2K9bHEeU6oSQ7BK0qg,3ba69a39-a4e7-4505-bae7-8db31360ec1c", + "FriendlyName": "Long Coat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "745c5110-104e-4b37-aabf-7c690fd54811", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:42:28.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 969, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agu0otilxkbnfpbo3ad7xiozv.png", + "AvatarItemDesc": "e2ObiQFqCUOdKniKL9a1EA,", + "FriendlyName": "Cupid Headpiece", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899b637b-6a01-4309-9d2a-788a2fd6b510", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:01.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 971, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d3mpn71rzkxnchgl4zc2c29lm.png", + "AvatarItemDesc": "BqpRbr_T1kO_YJqB1787kA,", + "FriendlyName": "Cupid Quiver", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6e51aa06-d3bf-43d6-bf60-9a81d7bf3b90", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:24.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 970, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78t1ffywj0qqumsmn2pc9yr9b.png", + "AvatarItemDesc": "PryEX4MEvkGKwST6dqJHjA,", + "FriendlyName": "Cupid Toga", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f84bc3e-0483-41be-8ac1-24fa76a2478c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-04T21:17:10.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2899, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nSSGXZLjwEGclxA93atFUw.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,", + "FriendlyName": "Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:25.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2903, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MD5DVsHC4U2RMM-e36mYDQ.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,pqtwCB_c20yRmOEs6jlH3g", + "FriendlyName": "Purple Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:38.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UhJ8FHTUiEWTy2_6oM5T3g.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,SSstW2ZPk0SB47qYlgbZzg", + "FriendlyName": "Black Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:32.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2902, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dkNHvcM-PUeaHpc9SxCaTA.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,p5zYbKLGU0m0ZutOicfjsw", + "FriendlyName": "Blue Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:35.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2900, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eJWJ01dRW02aA6rSCX8mJg.png", + "AvatarItemDesc": "691b2198-2ee7-425b-b59f-bbadda1253e1,wgIwYsXqiUekieE6pSt1Ng", + "FriendlyName": "Beige Quarter Zip Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691b2198-2ee7-425b-b59f-bbadda1253e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:28.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1321, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "357bwxxf0kvoz6yd778gmm1k6.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,", + "FriendlyName": "Regal Crown (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:46.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1322, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8kgbf2gxx8ajp1uw1zrx306an.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zljmtj5WqUaRvB3XQTgJXg,XFuSiGupZ02Fut1Dxum_Iw", + "FriendlyName": "Regal Crown (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:52.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1323, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5zb2ksxrxwp3n2qlecml4u0l5.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,rmq1AOQvtUGOHvbPUximwg,XFuSiGupZ02Fut1Dxum_Iw", + "FriendlyName": "Regal Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:59.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1895, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "18orrg4k9zm7dsjfj9k2dz23q.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,rxMCIkLb2UO0Z3tsqtYuSw,x8nPf0xEwUiRdBQU00Bu1g", + "FriendlyName": "Diamond Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:55.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2423, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "jlklbsk_UEW29bsjWRwmUQ.png", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zu3hTm6JyEa0sE72KtVGXg", + "FriendlyName": "Space Crown (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d395a84-4342-4b1c-940c-213ec2eb56f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-19T17:06:49.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1296, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "52v2ad9kyewcmaefknqmm6l5i.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,", + "FriendlyName": "Quilted Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:21:56.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1297, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8je25aruza2779n4njv1gmsgd.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,QBglgj80zkuvUkk55Y5Feg,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:00.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1298, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bldx6nf85lql3l8l0v9q3wmag.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,-HYrY2A5Tk2UWMi4_hmYbg,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:04.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1299, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7i0g7hs6aziwfqxrm0xnv67gg.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,IjRMopsYlUqSE6327I9TIQ,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:10.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1936, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8ntv07mm6o9xzz2hcltgnxkur.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,zTsJHduQ5kirH9-6mewV4g,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quilted Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:36.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202199258, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "GriWpcHg50eSLAxY5yc47g.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,ons8s4ILoUmkdtliouAscw,ACrBLiAIgEur9V6l_B0CvA", + "FriendlyName": "Quiltedcoat) Quilted Coat Shirt Teal Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.523776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2559, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "2GOgnQwKxk2NkTSIxV9t_Q.png", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,j3I0vVz0yUOYLX9wCOMyyA", + "FriendlyName": "Science Coat (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-31T21:10:56.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1675, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d4c3lyj93p5m9r7r9lvo7z2br.png", + "AvatarItemDesc": "be75b46c-ce22-4758-8810-c409c5aa1c50,", + "FriendlyName": "Puffer Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be75b46c-ce22-4758-8810-c409c5aa1c50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:38.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2008, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "87cdvvpwz27kl5fqj4a53il38.png", + "AvatarItemDesc": "be75b46c-ce22-4758-8810-c409c5aa1c50,ZLaDizWS7kOmnHVGlLLJ-g,TUZ21v0C9UK9GPsfbRwpsw", + "FriendlyName": "Flannel Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be75b46c-ce22-4758-8810-c409c5aa1c50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:04.647Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1853, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chi1z36iwxalp98kkl4coqu8h.png", + "AvatarItemDesc": "45695fba-5abc-4c5b-b9eb-949d5174d1f5,", + "FriendlyName": "Racing Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45695fba-5abc-4c5b-b9eb-949d5174d1f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:12.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1899, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7trthor2vpnt3r9e6aywi0ohc.png", + "AvatarItemDesc": "45695fba-5abc-4c5b-b9eb-949d5174d1f5,oFjt2tSsa0CJ7j0s3P6b4Q,XD9dH8LF_ECmL0TwXJW1zg", + "FriendlyName": "Racing Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45695fba-5abc-4c5b-b9eb-949d5174d1f5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:41.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1854, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5ll88rg9iiispe9c611fj575e.png", + "AvatarItemDesc": "9fb62d0c-67ff-4675-85d3-a2869c2330aa,", + "FriendlyName": "Racing Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fb62d0c-67ff-4675-85d3-a2869c2330aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:22.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1896, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b5jt7jrhmi5ch4z1axfan8u95.png", + "AvatarItemDesc": "9fb62d0c-67ff-4675-85d3-a2869c2330aa,ijCbiOeGD0C4uznegq9CXw,VzyoPsQyCUaW6RSusgVUGQ", + "FriendlyName": "Racing Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fb62d0c-67ff-4675-85d3-a2869c2330aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T20:34:43.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1852, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7lyaxr5m3766nt3tdurlv2gik.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,", + "FriendlyName": "Racing Suit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:08.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1898, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1ohpxd83hqnj4iz7zqf0eqy3w.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,lhqUg1VMNkC8nrsGZF4edQ,uFERkSfSl0i9BPFBVOICeg", + "FriendlyName": "Racing Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:38.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2115, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qvzY9bthF0SXys1g8FCeNg.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,yO5pgI2quE2i3tNBlvJPGA", + "FriendlyName": "Racing Suit (Sponsored)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:25.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2484, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8uEeIbBPq0eGrt1TFDEHTg.png", + "AvatarItemDesc": "7adae61e-cd62-46c3-ae42-83ef0bced5c7,jGNpSO25HkO7KbR3v05nkA", + "FriendlyName": "Racing Suit (Astronaut)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7adae61e-cd62-46c3-ae42-83ef0bced5c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:36.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2002, + "AvatarItemType": 0, + "Tooltip": "This item may come in handy at concerts, afterparties, or in dark, forbidden lairs...", + "Rarity": 50, + "ThumbnailImage": "es10uj7wkevzcr8uv905avgbr.png", + "AvatarItemDesc": "e4888429-6f5b-4f57-9b9b-2559d3a54856,", + "FriendlyName": "Rainbow Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e4888429-6f5b-4f57-9b9b-2559d3a54856", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:38.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2928, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZZVGN49VWEOBaIP-aV-9gQ.png", + "AvatarItemDesc": "24781418-38ad-411f-925e-2321fe616b51,", + "FriendlyName": "Rain Cloud Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24781418-38ad-411f-925e-2321fe616b51", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-07T18:16:03.14Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1357, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1c69e4rsxwolxiw7iq522lr5k.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,", + "FriendlyName": "Raincoat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:41.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1358, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2y3vygtoygwgkb4vvi4ueelz8.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,GH9q8_0O9EqjGdz7lGWD2g,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:46.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1697, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7uwfpp71odpm2zjf7dopjufm1.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,M-N9vQt0CEefqgmeLp3l4g,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:43.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1696, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "647nsozzr00smthvpc1qrymze.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,q_HcScqHgEG-x70g1wtfLA,T7zwcIIc8Ee3ud8a2JCuwQ", + "FriendlyName": "Raincoat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-20T01:35:42.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1482, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9oppuuz5leb1br8st56q8pzgc.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,gDP3y0J2EUayzVvBT9-92w,dMDMDExSmEKOF_kQIHaowg", + "FriendlyName": "Raincoat (Getaway Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:47.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2809, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6MqtAecx3EmgN2hug-kDWA.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,kBmmFoqo-0eLSSAhen-VLw", + "FriendlyName": "Black Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:53.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2811, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "zxxN4bCyXESVjKRUDYv7iw.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,tVbu4hUFHkmEKEz38GG2Nw", + "FriendlyName": "Pink Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:59.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2810, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3yrQDfBRi0i3HHeiqNPY3w.png", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,m8x6fiLEw0m4pJywntEpzg", + "FriendlyName": "White Raincoat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9e6397dd-b7b0-4d32-8c86-1910106a8478", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:56.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1359, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5slbcv7tlqy8kpl75ol21jidq.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,", + "FriendlyName": "Rain Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:50.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1360, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0ds3vsjpe01l47ks32nxbst5q.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,v3af60p1Q02NpdCqIRO9lw,1hVOI7IuY0mKvtr2PZXLww", + "FriendlyName": "Rain Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:29:57.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1481, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "agse5pe0hx0ij3ujgprl3xcsz.png", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,kX7ocqn8X0-4M1nw-KrwFQ,qC66Hu6yKEu2Res2o2z1Kw", + "FriendlyName": "Rain Hat (Getaway Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2bd53190-46f8-4956-9388-87c5a04efc4f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-08T19:52:44.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 688, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eabi5cm8xoboqpioo9h705aih.png", + "AvatarItemDesc": "f277af6a-0807-4716-8948-1bee236ffb74,", + "FriendlyName": "Ranger Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f277af6a-0807-4716-8948-1bee236ffb74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:08.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 697, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "58ktghs1gt48t0xa8cgyrvrhr.png", + "AvatarItemDesc": "f277af6a-0807-4716-8948-1bee236ffb74,d0bb468c-b78f-4e80-916d-65fec3ec0010,0272eb19-fc0e-4ad6-8db9-cc0cda2528b0", + "FriendlyName": "Ranger Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f277af6a-0807-4716-8948-1bee236ffb74", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:47.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 693, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e2oxz7ylbhkwldqs5mqnxnqzb.png", + "AvatarItemDesc": "fb907eb2-91c8-45ff-84a5-462bb86ea7de,", + "FriendlyName": "Ranger Uniform (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb907eb2-91c8-45ff-84a5-462bb86ea7de", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:18.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 700, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6r3jp2iifvxn0dpu7pmhfzw0a.png", + "AvatarItemDesc": "fb907eb2-91c8-45ff-84a5-462bb86ea7de,b415fe60-3e9d-4f4d-a53a-ca15eac6af7d,d21e1b6f-9c9e-42b4-bad5-201ec43161e9", + "FriendlyName": "Ranger Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb907eb2-91c8-45ff-84a5-462bb86ea7de", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:51.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2572, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "i5YnDBV6w0ef6GvGX7uWBw.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,", + "FriendlyName": "King Rat Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:26.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2575, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ma2zBsTYbEu0Crl3zZaDJw.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,RTrcYpLtukm83lwAfWi5WA", + "FriendlyName": "King Rat Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:35.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2574, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "m90YcY3tGkWS_3aDgwpp9g.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,3oar1K99PU2OTGaT0i7Hag", + "FriendlyName": "King Rat Hat (Vert)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:31.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2573, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "T9OIhzV2uUiPCPipuBhCkQ.png", + "AvatarItemDesc": "5d4b097b-2850-4a13-b26b-280be7ad3f57,T5z4W4H8sU-2NxpepVH0Qg", + "FriendlyName": "King Rat Hat (Noir)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d4b097b-2850-4a13-b26b-280be7ad3f57", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:06:29.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1769, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "reccon", + "ThumbnailImage": "dt1vtd0wu908uat9x5cnwqgmz.png", + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,", + "FriendlyName": "Rec Con Lanyard (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:00:29.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2152, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "QyumHCBqFkSaxEc2T7bMaw.png", + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,cxwdcbGTfUS_LYQoXpcQng", + "FriendlyName": "Video Partner Lanyard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:19.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ab1d252a-c15c-4a97-8e48-da0813d33b00,b6b5aad9-1211-48ce-ac2e-8053a879ccbd", + "FriendlyName": "Rec Con Lanyard Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ab1d252a-c15c-4a97-8e48-da0813d33b00", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1103472285 + }, + { + "AvatarItemId": 1829, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1efy3zkdik26gdvivvvd0ap3k.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,", + "FriendlyName": "Reclympics Medal (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:00.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1830, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7vqxnzgn42vejyf08m7zft4f3.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,zNLzEvpTpkOIi0J3fIvh1A,OqC4ny2rk0yJJOHemspV7A", + "FriendlyName": "Reclympics Medal (Bronze)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:03.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1831, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "alczfhgjdnbcvf9orafqcp6ag.png", + "AvatarItemDesc": "9fd1be12-1577-4800-b79f-6da29f299826,mVj9t_XVCEqdx89v2zdjHQ,kJ_pNxSWKE6HZX4Sc7Dv0g", + "FriendlyName": "Reclympics Medal (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9fd1be12-1577-4800-b79f-6da29f299826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:06.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1667, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aya5t0pnayzq9jz4mfs6jidhc.png", + "AvatarItemDesc": "9656a307-0c79-4b8f-910f-e67c454faefb,", + "FriendlyName": "Rec Pods (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9656a307-0c79-4b8f-910f-e67c454faefb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:46.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1668, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0f7hl0uorhvk24n6ag35m0pxu.png", + "AvatarItemDesc": "9656a307-0c79-4b8f-910f-e67c454faefb,mG0HW8fJkUOE5u7Whg4kag,3CLmVNB_p0Gd5N8dN74TBw", + "FriendlyName": "Rec Pods (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9656a307-0c79-4b8f-910f-e67c454faefb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-03T18:59:49.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2037, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "X2KqX8rMj0WzdutVcCYmhQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,", + "FriendlyName": "Rec Trooper Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:54.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2038, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4iczDP9r4EqWpK0IGEcJ8g.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,WkI_v-Sua0WlhmMf8R4aGA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:56.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2039, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nmOyhPcHDkG33U0RQ55jZg.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,3bqY_T4_HEy406HcXrfLuA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:59.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2040, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rVOFYXLD1UOM6Gx-rJT_-g.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,I4lNZy7lR0uyUN5Vk3fabA,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:01.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2041, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1825duyZL0qQprGQokp2Lg.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,SMeMLRUF-UWrOcXYJzgScw,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:03.413Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2042, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "I1pEVWdT3E-DLY2hkN88qQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,aaaG7Fg8fky2dkrwVsA30g,m-fk_L_e4EiOMUZ_UUjMjg", + "FriendlyName": "Rec Trooper Helmet (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:05.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2081, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5lE9WIsg6U67neLntaKrvQ.png", + "AvatarItemDesc": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d,GSO7t0cNs0SIbU-jQjnX5g", + "FriendlyName": "Rec Trooper Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f833d1a7-d667-4baf-b1ff-ac82ea87eb7d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:22.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h3kstogj0skzqb4uky1jpnsn.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,", + "FriendlyName": "Rec Trooper Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:07.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2044, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b94th4lvtw4iu7e3pxussoiwk.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,WkI_v-Sua0WlhmMf8R4aGA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:10.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2045, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "07vec7ppqlcv6a60pn9voj1va.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,3bqY_T4_HEy406HcXrfLuA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:12.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2046, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3v252zqwrqkn4wxjcllebsnwx.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,I4lNZy7lR0uyUN5Vk3fabA,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:14.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2047, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "afspou17ntsll0egoozdkp3md.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,SMeMLRUF-UWrOcXYJzgScw,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:16.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2048, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "25yoikgkjnlbo9c84x1r7rvuf.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,aaaG7Fg8fky2dkrwVsA30g,MjaMSFJlsUisPzveOPrH4w", + "FriendlyName": "Rec Trooper Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:18.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2082, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "14cf5es3wgsam3e771iwjkzbm.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,d0D2htQZvEKajd0oAesNow", + "FriendlyName": "Rec Trooper Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:24.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2401, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3b5ur52zpzgwpmifcd9wxjdhq.png", + "AvatarItemDesc": "ef937765-d827-45c1-babe-5bcf6d68c08f,W7E9KfRTLEK-1EZTNKM3Yw", + "FriendlyName": "Rec Trooper Vest (Space)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ef937765-d827-45c1-babe-5bcf6d68c08f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:39.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2031, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "glEms9DPgUePBYRHEkwVCA.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,", + "FriendlyName": "Rec Trooper Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:39.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2032, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cHR8FF5MZkaGrOTZcAvR-A.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,WkI_v-Sua0WlhmMf8R4aGA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:42.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2033, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3kbfjdmvbkav8SJ0HVhIfw.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,3bqY_T4_HEy406HcXrfLuA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:45.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2034, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "GLCx4lFj6E-Z-FE9wkiDoQ.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,I4lNZy7lR0uyUN5Vk3fabA,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2035, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NVXRDfl_ZkOY9c5Nq_W1Wg.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,SMeMLRUF-UWrOcXYJzgScw,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:49.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2036, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5bl8JvLg7Eqp39q9_7m5dw.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,aaaG7Fg8fky2dkrwVsA30g,8ubY2vSZQEOmhqIg70Gqyw", + "FriendlyName": "Rec Trooper Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:09:51.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2080, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ziuv9nfKPkOE__m_o9Hvxg.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,9ir4Es5d0Ue-8bn2EUcCZQ", + "FriendlyName": "Rec Trooper Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:21.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2400, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NXhmwZeIG0SGdfTzbBGsaA.png", + "AvatarItemDesc": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73,XaNgcjkwP0uxS5_S1JaQrA", + "FriendlyName": "Rec Trooper Gloves (Space)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af3ce4a2-e4a4-492a-a5b4-8dcb91e60e73", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:35.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 203, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3q137204ani35lep2nt9a2rx9.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,", + "FriendlyName": "Belted Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 204, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cm21tbv0v6i0hf0k70bpbha7b.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,5b58d879-2bc4-47ed-a996-1af2ace16425,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 205, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "74y7j6vz7569lkpcqb2fje2p0.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,d320ad9b-ba5c-45ea-b2b8-0d3e409a9db1,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 206, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2ikovokydhafpprj55cpdyk5m.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,d61711fd-589a-4669-b991-9408a58fffd9,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Cream)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1607, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5l9wcvzyx9vvzrqe84ohl0s72.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,oJFfs62tR0WVRry2Smt8yA,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Light Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:39.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1608, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8ur3yl22591shtrv2px510hqi.png", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,MIfCkcrFOEasJqfDmxThdw,3d971439-41d1-43a2-8434-704f9ec8d906", + "FriendlyName": "Belted Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f51a7ed5-52ea-485f-a430-303697f6fec9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:18:43.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 207, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "djd1hn8umzgdrbijc8oam3pqa.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,", + "FriendlyName": "Flowered Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 475, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e9vo4xrmjapg3t56dbabbraqj.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,1acb1f56-4509-449f-9c6a-065bdeaa9ee3,2d7361d2-31a2-404b-90da-10e89114e1f2", + "FriendlyName": "Flowered Dress (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:45.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 476, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "930kpdjun0nt4qnb2awrj922c.png", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,e98c3f98-7844-4d6e-a21c-05027033c18c,2d7361d2-31a2-404b-90da-10e89114e1f2", + "FriendlyName": "Flowered Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "515dfc36-7f7b-47bc-abee-380d68f41be6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T23:07:47.1Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1790, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4i7ompfsh19q8naq86uvyacc4.png", + "AvatarItemDesc": "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4,", + "FriendlyName": "Red Panda Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-05T17:42:31.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1964, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5m6k9qeuysh54wzun74y56isp.png", + "AvatarItemDesc": "51e5467f-d310-44f8-80c3-17d7e27450bf,", + "FriendlyName": "Retro Gamer Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51e5467f-d310-44f8-80c3-17d7e27450bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:31.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1016431692, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "06VrP-0hN0-WkSvHS02YfA.png", + "AvatarItemDesc": "51e5467f-d310-44f8-80c3-17d7e27450bf,pue_FwobQEiaVubgc5eMUg", + "FriendlyName": "Dark Retro Gamer Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "51e5467f-d310-44f8-80c3-17d7e27450bf", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1963, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ehoq4v330bykz8stgk9bf58j.png", + "AvatarItemDesc": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,", + "FriendlyName": "Retro Gamer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:29.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1134686645, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IFDGFDNDlUCCJx3FM5dBHQ.png", + "AvatarItemDesc": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318,ZYjUekG8JkC9uPkm4xH8ww", + "FriendlyName": "Dark Retro Gamer Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "55b1ada2-aa1a-4b8a-bbeb-f10ba77c8318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1962, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5y0de11wldvhcpeo87sdq0vyq.png", + "AvatarItemDesc": "352b49fc-99a2-4f05-a688-866ade1cd394,", + "FriendlyName": "Retro Gamer Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "352b49fc-99a2-4f05-a688-866ade1cd394", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:27.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1132364555, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "q-bZ8zTCIkOyfHecJwDDng.png", + "AvatarItemDesc": "352b49fc-99a2-4f05-a688-866ade1cd394,PabSuDB_GEejMahhLvLkJg", + "FriendlyName": "Dark Retro Gamer Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "352b49fc-99a2-4f05-a688-866ade1cd394", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1961, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ehevha4vehw8jlosk83rag8tj.png", + "AvatarItemDesc": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,", + "FriendlyName": "Retro Gamer Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:25.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1933117342, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "uxAsLya8MkWuyusZMQmV0Q.png", + "AvatarItemDesc": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71,EM1x0p66L0Gf6BNBP8-1kA", + "FriendlyName": "Dark Retro Gamer Shoulder Bag", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d3d4ba2-5d98-4034-8dc1-cd00263e2b71", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1960, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "em7ibip6ffvp7q0jpp5qcx4j5.png", + "AvatarItemDesc": "4885683e-a87e-4d27-990a-c16f1df7a28f,", + "FriendlyName": "Retro Gamer Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4885683e-a87e-4d27-990a-c16f1df7a28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-03T22:58:22.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1138935250, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_uzRLVwLNku8gCel1chHsg.png", + "AvatarItemDesc": "4885683e-a87e-4d27-990a-c16f1df7a28f,hKBSHovOeUy84yCbvutkOw", + "FriendlyName": "Dark Retro Gamer Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4885683e-a87e-4d27-990a-c16f1df7a28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.525776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "e036f042-92c1-4c35-ab71-e9c7d5fbf301,", + "FriendlyName": "Ringmaster Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e036f042-92c1-4c35-ab71-e9c7d5fbf301", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1683507107 + }, + { + "AvatarItemDesc": "73994140-58f1-40b8-a7a4-baca020ab2fc,", + "FriendlyName": "Ringmaster Mouth", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "73994140-58f1-40b8-a7a4-baca020ab2fc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1011431567 + }, + { + "AvatarItemDesc": "53a73a1a-70d1-447b-a594-3c96890f52ab,", + "FriendlyName": "Ringmaster Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "53a73a1a-70d1-447b-a594-3c96890f52ab", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1084249225 + }, + { + "AvatarItemId": 1807, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "duw3kiu7wtbc2uhma5er2l480.png", + "AvatarItemDesc": "1fed823a-22bb-4225-bdc4-491da6eba875,", + "FriendlyName": "Apocalypse Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fed823a-22bb-4225-bdc4-491da6eba875", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:25.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1802, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5dszhnzwez2lmqbracuxzqjsf.png", + "AvatarItemDesc": "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a,", + "FriendlyName": "Apocalypse Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:49.917Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1804, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3k1od2jqyltyokb1zgvbyxoll.png", + "AvatarItemDesc": "3e307eca-0850-4d85-8cc1-f5c176bfa7e9,", + "FriendlyName": "Apocalypse Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e307eca-0850-4d85-8cc1-f5c176bfa7e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:03.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1808, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dqdlvwys8ok8v51xsyvdtnxs.png", + "AvatarItemDesc": "23944856-b567-4075-815f-261cb4655cff,", + "FriendlyName": "Apocalypse Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23944856-b567-4075-815f-261cb4655cff", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:29.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1901, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a6631bxfoa0zyvs7fxyzubtji.png", + "AvatarItemDesc": "23767c66-31de-4e6f-81f5-b17769a0fd5b,", + "FriendlyName": "Neighborly Cardigan", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23767c66-31de-4e6f-81f5-b17769a0fd5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-08T21:13:49.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "7f4317a3-0f47-4fcd-bfeb-ff62e33d37c9,", + "FriendlyName": "Robot Arms Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7f4317a3-0f47-4fcd-bfeb-ff62e33d37c9", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1505598127 + }, + { + "AvatarItemId": 1719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "446uvbhzordij4cso3587acsi.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,", + "FriendlyName": "Robot Gauntlets", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:10:59.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2111, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "shxxE1X6sE6_4lH3hEXGSg.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,gIDTSqjSvECgxxd4YtOZIA", + "FriendlyName": "Alloy Power Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:27.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2110, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SIkjnmq_P0mIn1aA3HyRTg.png", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,BoaDgMfzEEycbxVmWneHNA", + "FriendlyName": "Alloy Power Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d33c2f3a-6831-4deb-82b2-062d1c4224e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:26.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1874, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4syp3adr5p8obc2cv6ef22vvr.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,", + "FriendlyName": "Rogue Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:34.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2442, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "OzofyASWI0-3nEAN_uzMww.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,C64nh9MHv0mlUEP9-s7sJw", + "FriendlyName": "Constellation Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:17.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2444, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "K8ehibem7USe2u0ui6wmDw.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,ay9ktLnHfUKdj0-q52Gvyg", + "FriendlyName": "Constellation Cape (Sunset)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:24.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2443, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "C3GGuNb9x0qmewSkRHP33A.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,WJ27X_UnfUmFKpY6mRk_Rw", + "FriendlyName": "Constellation Cape (Aurora)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:20.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2560, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "zW972rrvQ0-DezwkPcNYvw.png", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,YVkcCfJZekq7GvdMUy2IWg", + "FriendlyName": "Science Cape (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "63b5196e-881a-4b96-82c8-9df54de4a506", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-07T21:47:00.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1870, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5f0fab7drkewxktsldpgux1wv.png", + "AvatarItemDesc": "4b72736e-6fca-4961-98f2-1d4e199eb1e0,", + "FriendlyName": "Rogue Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4b72736e-6fca-4961-98f2-1d4e199eb1e0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:20.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1872, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "edcczm6mc1nc1pcanunxzgprq.png", + "AvatarItemDesc": "155c13b9-3c72-4314-85ac-6fbac19cdc76,", + "FriendlyName": "Rogue Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "155c13b9-3c72-4314-85ac-6fbac19cdc76", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:26.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2803, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dWn_mPxj7kqBYwHjUkgI8w.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,", + "FriendlyName": "Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:41.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2807, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7TM3VMXrDUSYrIPIMneOSA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,P8w3-vcH5kaHaMEsFsKDjw", + "FriendlyName": "White Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:49.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2804, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-PKF_-QCLkiKRwC3epBnZA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,KUSVg33DSUK8aYXxJzOTdA", + "FriendlyName": "Rose Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:43.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2806, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "myhvbSN3lEG_bApVoStEcA.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,g8h43tuiN0CCifW9XbnFhQ", + "FriendlyName": "Red Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:47.12Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2808, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kABVMxEknUWZArqSsxRLFg.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,T0PMM2iQqkCCMZoznrdkug", + "FriendlyName": "Yellow Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:51.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2805, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FWfZEwnuIUSoM_qCk6O_ag.png", + "AvatarItemDesc": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7,sA0KRBVMikOo6BBfk0dC1g", + "FriendlyName": "Blue Rose Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc48b6a5-5331-4070-bf0e-3d2d2f5410e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:45.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2285, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4h1x24hz3beovhoamej45mz90.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,", + "FriendlyName": "Rose Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:37.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2286, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bf3m7280dfcfs560g1sg22rd1.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,4ve7AWD5pkGjvmtC6JRdiw", + "FriendlyName": "Rose Earrings (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:41.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2288, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "63yh6x5s2jp8owgzlji3dlq1b.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,10nOM4HBRU-e7Bb05SrtLw", + "FriendlyName": "Purple Rose Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:46.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2287, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "044d8yuos4umj6mmduxqxzgkx.png", + "AvatarItemDesc": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f,F3m52YaKyEqwbgUyPVfFxQ", + "FriendlyName": "Rose Earrings (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c6812d-17f3-4d30-83a9-0dcce44d4e6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-03T16:30:43.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1221, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "emi0fu2fmbr4yo7ar29206bpi.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,", + "FriendlyName": "Round Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T23:28:26.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1915, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "conhirr4han3hgcb2cd702mbx.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,2pnfLqkYZ0-GbpeE8g0vVA,hGtyDIIWckGoRgCaSG6GBw", + "FriendlyName": "Round Glasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:29.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1914, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "99d174z75khtsr89u9oy0e8ac.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,vw2iLomK9keTfEWsqJrmhA,hGtyDIIWckGoRgCaSG6GBw", + "FriendlyName": "Round Glasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:27.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1913, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8i5jwartl3iqnxvt1wo4k0n5h.png", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,t_y2xmFYLU2v6Jb6PX6v_g,4J9GPTKjjUOjyLVPmJyxWg", + "FriendlyName": "Round Glasses (Tortoiseshell)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc7299ac-be2b-404f-b8e6-402e2c0660f0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-22T18:04:26.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2180, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gsJJq-rFYEqEYVYeX8ig2g.png", + "AvatarItemDesc": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,", + "FriendlyName": "Basketball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:37.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1806077292, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Jo3RL0WeG0e_NtvrpG87MA.png", + "AvatarItemDesc": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4,G_fqC7KPaUWFM4Dk3stJYw", + "FriendlyName": "8 Ball Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8fec17b6-f5fd-40b5-ac9e-a52faf5621c4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.527778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 720, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bf58ryhvrbk2aecunq5n3d31i.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,", + "FriendlyName": "Royal Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:08.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 772, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bxnimeiegto1m9zphsy7qlcz3.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,5_-QLURc_0W8qeeN3Q3iGQ,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:55.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 899, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bi9ik7y9m6zg5fxaf10v08uj.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,mqdncbYcuUSGFvFqEU0z4A,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:29.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1309, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "auudmnyvmvid9ohssear4bwrb.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,MbusQ9bte0i9zirlxtNkSQ,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:51.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1310, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7h38lx0795mk0b8xq52d9qxpl.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,_9QlnTPXuEO5OQ3H0CwFkA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:55.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1503, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ttt8tqohiquycu310oyhuoql.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,cKLBS6pbf0SFuavWyeZ7GA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:04:18.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1758, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bwspz3moa7kwzc2t95xaqy4m.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,DZyW-gOG9U-QLGOMFBhOoA,HTU3Wp_ri0GNYGFSezFHSg", + "FriendlyName": "Royal Cape (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T01:40:31.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1805, + "AvatarItemType": 0, + "Tooltip": "For earning $50,000", + "Rarity": 50, + "ThumbnailImage": "7na5kk0hvnqgktgkcfeeae46d.png", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,pEiU6yZm2U-X9ip_CM1SAw,PIV6RtD7Pk-UTcUp2wF1ww", + "FriendlyName": "Royal Cape (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a44766dc-19ad-4476-849c-5113a5faabd0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:12:07.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 719, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "76b2m8j7wi2u4yhfscwdacit8.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,", + "FriendlyName": "Royal Crown (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:00.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 962, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "auqhtl8pre7pp954pnbtgfdm6.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,E1GfXDLUaUKaG9hTLAfFPA,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (Silver, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:27.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1303, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6e9fx1i4uk389yevktgtc9s2e.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,wCe2yC2U1kyYvPd8Z1rX2w,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:15.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1304, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aq5apzhwgqow1ci3zzpz4gmlg.png", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,QkfA71-1z0qOiZAVrbzhtA,hCeaYHy1TE2s6rTPxAQP9A", + "FriendlyName": "Royal Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "40734c4d-e1fe-426c-8921-930f7463d8e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:20.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 721, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2awsas0vcecc7a11lr9fprbmy.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,", + "FriendlyName": "Royal Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:10.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 866, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "a73qlfdd1y677wh2wfqn4hheb.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,0ps19D7b_U2iejdKQFORyA,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:21.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 865, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4chofv403g5ompt8exr51v7ai.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,t1MeoroFOUioYmPfT9mCeA,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:20.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1313, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "856n9ywg3kp1796sqc8zm9yvz.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,pjkTGVHkIE-BzCVi1KAesQ,PoMG28QAskagun1LSd4C_A", + "FriendlyName": "Royal Dress (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:12.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 963, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4sv3incjefp9fawup30vx3ih8.png", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,QP38EMygY0S-KIvdGatARA,m5Vw_MMCWUiSOOCL94QO0w", + "FriendlyName": "Royal Dress (White, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0ab6c95c-e157-48bd-a172-2642896b4ffc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:35.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 722, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c7mqflmgoajhkoupate9o25ll.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,", + "FriendlyName": "Royal Tabard (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-20T23:58:11.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 864, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "247il28eoprdj9km80orhaqg4.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,5TJPDmvEBEOkP82ZWLFqtg,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:18.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 863, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9q4xzeoi13tu32l6zxmci5daj.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,rCeRk8Pdl0W5M47JAQPhpw,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:12:17.043Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 964, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6i0vxy286tiuc0bmfbvghl17k.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,8zXWUxwZHU6tThCV4p5iGg,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (White, 2019 Fantasy Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-01-18T21:17:37.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1314, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eq5nxte0f2lf5p4vau1f14d8j.png", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,s2kCppLsN0uY7Hy6qjQc-g,50NUFrDxbUKmXbyAHBenvQ", + "FriendlyName": "Royal Tabard (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e13f6558-d49c-4314-820c-962ef48eb0c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:16.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 380, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9s3qbhep0iagvjuzfxnb10ab1.png", + "AvatarItemDesc": "418ed21e-766f-4c69-9d57-6f866dcc7feb,", + "FriendlyName": "Safari Hat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418ed21e-766f-4c69-9d57-6f866dcc7feb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:19.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 389, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ah0cj8axzd6ex6de2x2474x3m.png", + "AvatarItemDesc": "418ed21e-766f-4c69-9d57-6f866dcc7feb,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,0dabda0d-842c-4d39-a6b9-a8ca3ec20825", + "FriendlyName": "Safari Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418ed21e-766f-4c69-9d57-6f866dcc7feb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:28:26.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 375, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1v72ao5s3ko2tz94379807tki.png", + "AvatarItemDesc": "1041d745-c5e9-4218-a3e1-198f5c9c418c,", + "FriendlyName": "Safari Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1041d745-c5e9-4218-a3e1-198f5c9c418c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:15.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 393, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "702vaqfirb8tw9cqhy7977bcq.png", + "AvatarItemDesc": "1041d745-c5e9-4218-a3e1-198f5c9c418c,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,84c016ad-133f-45e3-a412-cac35612540b", + "FriendlyName": "Safari Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1041d745-c5e9-4218-a3e1-198f5c9c418c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:29:49.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 370, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "28rrsgo2cs68zome98ef6on9s.png", + "AvatarItemDesc": "f47f81e7-fcdd-4687-9b99-013147bef30f,", + "FriendlyName": "Safari Belt Sash", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f47f81e7-fcdd-4687-9b99-013147bef30f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-29T22:50:10.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 385, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9d9fdme5qc72avvqaj8m7u2b1.png", + "AvatarItemDesc": "74365234-cb72-409d-8cf5-9a7209f707e1,", + "FriendlyName": "Safari Gloves (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74365234-cb72-409d-8cf5-9a7209f707e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:24.933Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 395, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cpfts5y9qfb5p04u9nnjxia23.png", + "AvatarItemDesc": "74365234-cb72-409d-8cf5-9a7209f707e1,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,fa9a3d8e-16c9-4a7e-9166-3226e0d13c39", + "FriendlyName": "Safari Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74365234-cb72-409d-8cf5-9a7209f707e1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-05T21:30:09.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 382, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e4rsuhl9ws520phjximdfsour.png", + "AvatarItemDesc": "02a5b48c-abba-4385-967b-ba900424829e,", + "FriendlyName": "Safari Monocle", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "02a5b48c-abba-4385-967b-ba900424829e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-30T23:38:22.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 246, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bbp0mhypju37l680pb4b7mrdr.png", + "AvatarItemDesc": "2c679f89-c76e-4cfb-94e9-448c8fd44d55,", + "FriendlyName": "Saija Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2c679f89-c76e-4cfb-94e9-448c8fd44d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 282, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6mjsnrcz0704f1ablk7vhwper.png", + "AvatarItemDesc": "50c9c6f8-2963-4ef3-95d5-e999a898269f,", + "FriendlyName": "Saija Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "50c9c6f8-2963-4ef3-95d5-e999a898269f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 65, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "98wzkbq0ttvls6my69cezxa1o.png", + "AvatarItemDesc": "274cb9b2-2f59-47ea-9a8d-a5b656d148c6,", + "FriendlyName": "Saija Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "274cb9b2-2f59-47ea-9a8d-a5b656d148c6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1904, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "670kc3b5s4g784sztj0kyl4xe.png", + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,", + "FriendlyName": "Samurai Belt Sword (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:42:56.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2054, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mFZE17grJkezYN_vmo5ZnA.png", + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,XIluUn9vQEabkEpqyYBG0g,9sSCSLRkYUCs4CCHK8FfAA", + "FriendlyName": "Samurai Belt Sword (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:41.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "76f8f9b8-0793-4d98-962c-9e6a71979540,d6525663-1028-463a-be52-7b8ca5de99ba", + "FriendlyName": "Samurai Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "76f8f9b8-0793-4d98-962c-9e6a71979540", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1465178662 + }, + { + "AvatarItemId": 1905, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f278x818nbgl8s8jjebtz9s9z.png", + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,", + "FriendlyName": "Samurai Helmet (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:42:59.273Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2057, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "huMYQIkj9kGAZLeIjvaUTw.png", + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,tdECtZp1UUuRRKHV60coyA,PIg3ywG2J0CiGUPpm1A6Kw", + "FriendlyName": "Samurai Helmet (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:48.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3,689e15e9-7e7e-4fd5-927d-c015d69c1178", + "FriendlyName": "Samurai Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5fb2ea11-cd9c-45cb-be1e-e7ff3e0fd3c3", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1144250768 + }, + { + "AvatarItemId": 1906, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6u8vpdm9um3l371w3nl9q0j0g.png", + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,", + "FriendlyName": "Samurai Armor (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:43:02.603Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2061, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tcgeftujX0my-JOBuLu7kA.png", + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,c7wiHNZ-X0KazEW4cY9PeA,5fVeR6S6R0yo_NcT-TB1lQ", + "FriendlyName": "Samurai Armor (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:03:57.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec,5d21a059-fdc1-4edc-898a-5fef98eed137", + "FriendlyName": "Samurai Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dbecb1a5-f342-4017-b5c8-9cdfcd1254ec", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1797755030 + }, + { + "AvatarItemId": 1907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0n5obvj9pnbfm6a7x8t010av0.png", + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,", + "FriendlyName": "Samurai Wrist Guard (Jade)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-09-15T20:43:05.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2062, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AShCqsZgOUmxqJasgumjXQ.png", + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,h-uMOz2Hz06ymEjtz4wuzQ,4Ooi0h3SrkObClvS2cGk9Q", + "FriendlyName": "Samurai Wrist Guard (Red, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-16T19:04:00.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "dfcc5a47-5f73-496c-84b4-d44702231f7e,ec8b5518-1fda-450c-ae57-d0cf06700532", + "FriendlyName": "Samurai Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dfcc5a47-5f73-496c-84b4-d44702231f7e", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1208756254 + }, + { + "AvatarItemDesc": "f0fd87b4-4891-40a0-b463-7ce1da2c8929,", + "FriendlyName": "Sandcastle Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f0fd87b4-4891-40a0-b463-7ce1da2c8929", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1197877174 + }, + { + "AvatarItemId": 1934, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "8fzadq5f6ni6nboo4jmt4z0wb.png", + "AvatarItemDesc": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c,", + "FriendlyName": "Jolly Hat (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:06.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1935, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "8qodk6xu0694kgb6xwrvd2pb9.png", + "AvatarItemDesc": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c,veF50UiWeUS8ugHSm6g_3g,7KDavHUPDUKTJI0fFzJHpQ", + "FriendlyName": "Jolly Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e005f9ab-47bc-4c5b-8c45-b0cc6c55ab1c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-03T21:35:13.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 596, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4b0c6i1j57butbjhen02dvnxd.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,", + "FriendlyName": "Scallywag Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:13.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 613, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3rvrjylez3infpqjran7vpvjs.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,53dd9e44-7096-438e-a7c3-11d7c307426f,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:49.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 614, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a8hpjaq8s1osstq81y66f1yk0.png", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,c3c9f89c-580c-43c4-b38b-07394044e0e1,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Belt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3af4c506-d49c-4126-9437-1d3bb1658863", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:51.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 595, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "d1wurf50yoyooz47syoelb2ri.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,", + "FriendlyName": "Scallywag Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:10.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 605, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "7qkfqsjmmjcd9v4hxmipdbu36.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,53dd9e44-7096-438e-a7c3-11d7c307426f,a4903da3-249d-4f0a-a746-b547fda371ff", + "FriendlyName": "Scallywag Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:39.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 606, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3llu49o19577q8gvi3uv7qu29.png", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,c3c9f89c-580c-43c4-b38b-07394044e0e1,a4903da3-249d-4f0a-a746-b547fda371ff", + "FriendlyName": "Scallywag Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:40.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 597, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a6uf8rc48kcjj4lj2fsir9vsr.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,", + "FriendlyName": "Scallywag Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:14.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 609, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "3m6lx9rzs88omzsy182j1yuxb.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,85eb7f47-bb31-4c66-a8b3-562d35028e6c,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf", + "FriendlyName": "Scallywag Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:44.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 610, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00u595ofjuu0ky28diplmypee.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,39f04eee-3a58-4e53-9f5e-7e995d46ed71,005699fe-2c69-4bec-831f-031fe8ad7f0a", + "FriendlyName": "Scallywag Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:46.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1111, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "80jjzxrcdq2pf4nuv4ev13nx0.png", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,6y_fzLhG7E65Px3-CYiyXQ,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf", + "FriendlyName": "Scallywag Vest (Crimson)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "664485e9-a159-4fcb-b32c-2b114b4a1218", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:11.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 598, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6ko9exiyd39fsdh9zptnhw8ba.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,", + "FriendlyName": "Scallywag Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-07T02:21:16.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 617, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "f0s9ro8uu6ibrjgn1bicm5vf8.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,cdca593a-8840-4140-9145-fe962732040c,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:54.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 618, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "cxo3ckfyyp09zazkttzy0mjz4.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,62bd5dda-08da-4528-83c1-f0ca3812dc6e,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-02-24T02:34:55.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1104, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "00nybvpdwpb8lqxe6gkzo9y7x.png", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,X3MkhL55Rk-0UK6h4dw0Eg,ce3782aa-56cc-40d7-9019-8ce714cb7748", + "FriendlyName": "Scallywag Bracers (Crimson)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:44.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1178, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cbojb5iebhb2p1tra7gh61j7y.png", + "AvatarItemDesc": "d960b47d-f8bd-4c2e-813c-a6a09876ea18,", + "FriendlyName": "Scarecrow Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d960b47d-f8bd-4c2e-813c-a6a09876ea18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:03.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1179, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5saxy9nrha61uuej237wtnnsh.png", + "AvatarItemDesc": "d960b47d-f8bd-4c2e-813c-a6a09876ea18,TZ3oQBp_3kWY4Vzscgv5Wg,xCpdHx1iM06Gg0tYoGv0DA", + "FriendlyName": "Scarecrow Hat (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d960b47d-f8bd-4c2e-813c-a6a09876ea18", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:08.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1174, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "acu72xlzacl1k4mnepqsfd2td.png", + "AvatarItemDesc": "1fcaf4fd-9012-407e-b691-fbc3ccf22421,", + "FriendlyName": "Scarecrow Overalls", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fcaf4fd-9012-407e-b691-fbc3ccf22421", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:25.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1175, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9mad5t9bu3npfwut2s2mhtvkp.png", + "AvatarItemDesc": "1fcaf4fd-9012-407e-b691-fbc3ccf22421,S0AKRB_m2k-I7snHnUigtw,uTkbswCedUGyuZcaX0W6Ww", + "FriendlyName": "Scarecrow Overalls (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1fcaf4fd-9012-407e-b691-fbc3ccf22421", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:31.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1176, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6vc9gb07nhld11lkb3agtr3xq.png", + "AvatarItemDesc": "3c9acb9e-3353-4359-aa5a-ae9512b086b4,", + "FriendlyName": "Scarecrow Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c9acb9e-3353-4359-aa5a-ae9512b086b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:52:54.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1177, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8otr4d3cafso2wpwxnuskm5o5.png", + "AvatarItemDesc": "3c9acb9e-3353-4359-aa5a-ae9512b086b4,dro7VLV8BkGjqpPAbJheYA,bNA44H2wzUy-FkWgyOq1Kw", + "FriendlyName": "Scarecrow Wrists (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c9acb9e-3353-4359-aa5a-ae9512b086b4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:52:58.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 94, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2t2ujgsm8dnv8j9ax7bmlsnsd.png", + "AvatarItemDesc": "f17a2f9e-1eab-44e8-b10a-1f53c5d0778c,", + "FriendlyName": "Scientist Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f17a2f9e-1eab-44e8-b10a-1f53c5d0778c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 247, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "17egpclh4qfjwpauvfot65edy.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,", + "FriendlyName": "Scientist Coat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 795, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xvbjj5mp2xmnu3w4g7m7c9px.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,KTidYfH0ckWxJGs5qzUBTQ,w07bkcUoQEiNJTVDeSywzQ", + "FriendlyName": "Scientist Coat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:23.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1100598754, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "oDUFSXJI5E6fmcsxJqMskg.png", + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,X0nzvhAtgEKqzjQPb9d3qQ", + "FriendlyName": "Science PPE Coat (Restoration)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.529776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "fe34adaf-47d3-435b-b2fd-b2fee05fceae,8OdkUQPXzkGqXFa3LNXupw", + "FriendlyName": "Scientist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe34adaf-47d3-435b-b2fd-b2fee05fceae", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1664244968 + }, + { + "AvatarItemId": 99, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9g1cba0qnig6whctstb0qaxyc.png", + "AvatarItemDesc": "11e508a7-fe7d-470e-b270-2dba33e2f7a1,", + "FriendlyName": "Scientist Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11e508a7-fe7d-470e-b270-2dba33e2f7a1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "11e508a7-fe7d-470e-b270-2dba33e2f7a1,chxai_T-BEOc0-uZv6OAQg", + "FriendlyName": "Glasses Scientist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "11e508a7-fe7d-470e-b270-2dba33e2f7a1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1577926849 + }, + { + "AvatarItemId": 283, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3sd1nkxa7ilnnjgnwm2xtqw6b.png", + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,", + "FriendlyName": "Scientist Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 802, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6r1rr4giperexziw91ocwu6f9.png", + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,KTidYfH0ckWxJGs5qzUBTQ,VSZX99mSt0e7gPRM2_XzMw", + "FriendlyName": "Scientist Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:45.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6,mRNobpJiWUCpih1zETyaZA", + "FriendlyName": "Scientist Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9d68e9f4-6ea0-4717-bb78-f2783a14a6b6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1306056020 + }, + { + "AvatarItemId": 1510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aun7sd8qe3iqtu86eeyec9uq1.png", + "AvatarItemDesc": "380a5e35-b4d0-4119-9721-8ce4c0840fbd,", + "FriendlyName": "Starship Helm", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "380a5e35-b4d0-4119-9721-8ce4c0840fbd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-08T21:59:46.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 102, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0lm206mtws500pk9e4v0zaxuv.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,", + "FriendlyName": "Space Visor (Nova)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 909, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "av1v53eoyk53w8zi6bwd633xb.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,SXG9rdEkxkOR31Nk0aTOww,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Quasar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:42.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 908, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c9qgvf1jfrdwzsjud14bqa9pp.png", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,FFIpxInE-EKbk2GY-pIkyg,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Nebula)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b89be768-a169-4b24-8022-751e7b817865", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:41.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1800, + "AvatarItemType": 0, + "Tooltip": "Unlocked by visiting ^WelcomeAndroid", + "Rarity": 50, + "ThumbnailImage": "1x302shgihlyh5d4ewddkiln8.png", + "AvatarItemDesc": "8691fa78-3069-41e6-8431-01c294e8d56b,", + "FriendlyName": "Space Visor (Orion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8691fa78-3069-41e6-8431-01c294e8d56b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-21T18:25:26.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 783, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9cfmiay0o7q5lwqa8wx2pm6ug.png", + "AvatarItemDesc": "KNZ1DvCLC0WHyH4opYAXtw,", + "FriendlyName": "Space Visor (Creators Contest 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0e75d628-8bf0-450b-87c8-7e28a58017b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:25.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 914, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "80gor8nb4uaucnbfr9tmpumkr.png", + "AvatarItemDesc": "KNZ1DvCLC0WHyH4opYAXtw,FwiizGjYnUaM1VJ53R1cCQ,uB4gFxfrh0W5OtkmZdFN1Q", + "FriendlyName": "Space Visor (Eclipse)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0e75d628-8bf0-450b-87c8-7e28a58017b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:48.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2483, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "BUTSu7FlzEOaiirSftT3Aw.png", + "AvatarItemDesc": "54ab4eb0-e28a-438f-9977-1ae8d642d136,", + "FriendlyName": "Space Visor (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "54ab4eb0-e28a-438f-9977-1ae8d642d136", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:34.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1235, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4eqc3t4t4z5gnf92sqf844nea.png", + "AvatarItemDesc": "1736a60b-6fb6-49bd-948a-fd3f3fc1be50,", + "FriendlyName": "Space Visor (Neutron)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1736a60b-6fb6-49bd-948a-fd3f3fc1be50", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:22.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "66dcdd7c-7dee-490f-b2d6-1113daa782a7,", + "FriendlyName": "S Cloak Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "66dcdd7c-7dee-490f-b2d6-1113daa782a7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1696752929 + }, + { + "AvatarItemDesc": "50721e23-67e6-4f16-8e70-b0eccc2d9c3d,", + "FriendlyName": "S Cloak Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "50721e23-67e6-4f16-8e70-b0eccc2d9c3d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1144628819 + }, + { + "AvatarItemId": 690, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1x9c7tsi208t2ykfb3m4oc9je.png", + "AvatarItemDesc": "f60e093f-33be-4527-b827-060adc77b1f4,", + "FriendlyName": "Scout Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f60e093f-33be-4527-b827-060adc77b1f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:13.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 698, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "29hnpj9lbliny7rio545fdave.png", + "AvatarItemDesc": "f60e093f-33be-4527-b827-060adc77b1f4,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,464fcf20-361f-4dc9-b074-150c3bc27523", + "FriendlyName": "Scout Cap (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f60e093f-33be-4527-b827-060adc77b1f4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:49.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 694, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1p79pekt3n19qb30ijvjrm1pn.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,", + "FriendlyName": "Scout Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:28.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 701, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "46gvey2edecat2kkb5igdbv3b.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,0d2830e7-8eb0-4fa3-99bc-7c80f01dd991,5814a331-6155-4df4-b921-3ffc6059e44c", + "FriendlyName": "Scout Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:52.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2390, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "O9Zd5keRSkKyg2NA2V-QEQ.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,OX1ituawfUqYC-caC1WcCg", + "FriendlyName": "Zombie Boyscout Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:38.737Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2403, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "hsrSnSftIEikNWVCh1v1GQ.png", + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,5owofMF_Y02WX2vwVL2aew", + "FriendlyName": "Zombie Boyscout Shirt (Mutant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:48.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99,b3LMvviE_0-ObF1x5s1dtw", + "FriendlyName": "Scout Neckerchief Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a96e168c-0c4d-4c70-9f8a-c8f07164ce99", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1494988446 + }, + { + "AvatarItemId": 689, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5p66sv7hukzmxmyh14z8b5781.png", + "AvatarItemDesc": "7c1552a6-bc36-46e6-884c-1de6b8111a92,", + "FriendlyName": "Raccoon Cap (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7c1552a6-bc36-46e6-884c-1de6b8111a92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:12.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 704, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3x3ft6vz0rl2rj8hpyhovqfpw.png", + "AvatarItemDesc": "7c1552a6-bc36-46e6-884c-1de6b8111a92,926ae943-76cb-46dd-9c66-a6ee44ea9194,f1cd10fb-3d0b-436f-a173-65bc0d88d8bf", + "FriendlyName": "Raccoon Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7c1552a6-bc36-46e6-884c-1de6b8111a92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:08:41.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 692, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "10h045rqcvgang1sy8hxigaa2.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,", + "FriendlyName": "Scout Sash (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:16.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 705, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9bcfdhumbtjizrtjd0clcoxgy.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,67490e3b-0f9d-4bf8-84fb-54957b981c8c,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:49.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 706, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1reomg1o3fiimjissaj3cz8gd.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,b520f3bc-599a-498c-8a4f-97cc25aa741f,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:52.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 707, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e9pz1n0sr3t8a3p5llhnnh5l8.png", + "AvatarItemDesc": "bf9dc196-5727-4752-8126-7ad5fcd96d55,639f5e2c-6fbd-4ab4-aa10-7772e0932798,66cc536d-8cb5-4262-b7f0-49a33b97ea10", + "FriendlyName": "Scout Sash (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf9dc196-5727-4752-8126-7ad5fcd96d55", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T21:12:55.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 695, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8usltv63g871msvqw5fh4v055.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,", + "FriendlyName": "Scout Skirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-05-31T00:52:30.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 702, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "636348tvnldqryf6iouybgnoy.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,2bb65868-ee12-4871-b50d-64bbc9066f0b,e65530f7-543c-43e9-bf12-b6cda2bc755c", + "FriendlyName": "Scout Skirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-05T19:45:53.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2404, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "KhdXc-odtUiy82uWA9Hyfg.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,XxVqGAlweUKta__sKdMP3Q", + "FriendlyName": "Zombie Girlscout Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:52.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2405, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "3krD9e7_gUezvWyLO30zZQ.png", + "AvatarItemDesc": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291,VL_OnsbYNkGgGAV16x5tQA", + "FriendlyName": "Zombie Girlscout Shirt (Mutant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:02:56.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1242, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "768szgyob0dtu1479hlls39oa.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,", + "FriendlyName": "Scuba Goggles (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:39.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1243, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3uvxr9vhe7g8piy0o8yyctuu7.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,DRS8xu3AqUOy2L0RKF3f7g,A7Y52vGIN0ijEKsWR_GQLg", + "FriendlyName": "Scuba Goggles (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:40.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1244, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1qslsywz60fw9vjpnsmkdf7gi.png", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,ZSOuWuKVCkqaIKc5d23G8Q,A7Y52vGIN0ijEKsWR_GQLg", + "FriendlyName": "Scuba Goggles (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9c28e450-8e12-455c-a4c8-c0a2160d4190", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:43.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1236, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4bal97uh9so459loudao1d5pk.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,", + "FriendlyName": "Scuba Wetsuit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:24.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1237, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6n0p0vohzjctpgowbi5u1zyvq.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,VqtOYBiKRkaSW4xAlBxQgg,xTqtLPQVyEKjAQZYl6HGow", + "FriendlyName": "Scuba Wetsuit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:26.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1238, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0xnsnma7zo835x2f5qjbn78m1.png", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,J-K34UX_B0yObjQI9tn0vQ,m0R4Jtf4HES8krxMnwIWFg", + "FriendlyName": "Scuba Wetsuit (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:27.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,ib2g1jTBgEeuj_s_uD1LKA", + "FriendlyName": "Scuba Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "57552652-d902-43b7-a68e-5c7da87582bc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1519911932 + }, + { + "AvatarItemId": 1239, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7f4gij6i4zbxcp7r843b74p12.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,", + "FriendlyName": "Scuba Tank (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:29.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1240, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "29lilphy1aywcuix2e39k2ll3.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,V_duN2oWaUmmao1xHhzCJA,Uvarm_CwTUWWQYZvzI8OaQ", + "FriendlyName": "Scuba Tank (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:35.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1241, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "64h9st85tv92639yx7n2hvkgm.png", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,GWi1rEebqkCV_TS2WzQhcA,4zPm9miF5UOouEWFpOxYZg", + "FriendlyName": "Scuba Tank (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:37.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 66, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ajleo52n1fi9sfdklgfwqj0fo.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,", + "FriendlyName": "Captain Hat (White, Blue, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 67, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "abjufv7nbd5ja0vwk6hsiu952.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,f5403c89-9769-479b-8a05-c71607f9b189,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (White, Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 68, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ifixksiv9byqkl1zzr14g6m8.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,36e63797-5a42-4779-a5cc-ef5c2e7d17d4,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (White, Black, Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 69, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8v6gk9097bsjdumvvi0xe4uk5.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,70112887-dc8d-49d5-96ed-0845204e0b58,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (Black, Black, Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 554, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6hqhj2g8w7mvm85ibz82mgre5.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,2738ab5f-8c94-46b3-bac6-4a8111c41c21,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (Pink, Black, Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-09-26T21:31:46.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1222, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "af4dh1nguqj29w27x7rk4ev5y.png", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,hhs9GY3XZkWb-8TXODvo6A,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9", + "FriendlyName": "Captain Hat (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5c9743f-e77c-42f2-ba63-9843342cbf8e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:17.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 208, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20un14zo8e1w83oiatx99lwpt.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,", + "FriendlyName": "Captain Jacket (Blue, White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 209, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "41op70bhqlf5s2l320femq7fa.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b9f51861-ad10-4878-a9b1-d849bccf532a,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 210, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5iuznrq2zpc14fctl8tnp4odk.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b5d62931-e756-4a8a-8f3b-760beba49319,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 211, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9c3baiau5sgxnhlikff1pph09.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,6145292a-9002-41c6-8cd5-43679b6f542f,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 212, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "45br9sqobjiwgoh4alz5l9qf8.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,7c1aaa7e-503b-46ef-b1c5-0c61a75b916e,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 213, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "00jh263iuxouyk40imb4wjv0o.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,27af51bc-8226-4911-8d47-22d28ea8e378,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Blue, White, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 214, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7ergraacf4272gslnvieytiw3.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,857fdcd8-704f-43d0-92b9-bca26619b153,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Blue, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 215, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3jnhwwxklxb0ojoszaexejbfx.png", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,3df5d13b-ba77-4b51-90b2-7a946e4d0be7,8ec5de40-73fc-4f09-9bf3-ddf4701107bc", + "FriendlyName": "Captain Jacket (Black, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1245, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3jysjn20nxxud7yahb9m78toy.png", + "AvatarItemDesc": "d86e2e65-5d87-466d-b885-8c52ac2eeeb0,", + "FriendlyName": "Captain Jacket (High Seas Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d86e2e65-5d87-466d-b885-8c52ac2eeeb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:45.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 111, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5whnrhprjsn09q0089w2ztby2.png", + "AvatarItemDesc": "c161dc73-6ce3-49c4-8d15-45e6911f84e4,", + "FriendlyName": "Sea Captain Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c161dc73-6ce3-49c4-8d15-45e6911f84e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1871, + "AvatarItemType": 0, + "Tooltip": "Drops occasionally after Rec Rally games.", + "Rarity": 20, + "ThumbnailImage": "aqft3txgw2utq98o392d8z647.png", + "AvatarItemDesc": "9b62cbca-694f-4a32-b910-14da66bee9bb,", + "FriendlyName": "Settler Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b62cbca-694f-4a32-b910-14da66bee9bb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:24.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1869, + "AvatarItemType": 0, + "Tooltip": "Drops occasionally after Rec Rally games.", + "Rarity": 20, + "ThumbnailImage": "dcmbdw7xs41bspzcqqgmm66z0.png", + "AvatarItemDesc": "8cc01536-2f43-4de4-ac63-7304d8beb73c,", + "FriendlyName": "Settler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8cc01536-2f43-4de4-ac63-7304d8beb73c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:13.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2834, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1jTfJYB_7EGuMfrAABvtrQ.png", + "AvatarItemDesc": "e27d33ca-8a81-4c16-a3c1-ffdbc9104d7f,", + "FriendlyName": "Pincushion Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e27d33ca-8a81-4c16-a3c1-ffdbc9104d7f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:44.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1133, + "AvatarItemType": 0, + "Tooltip": "Doo doo doo doo doo doo", + "Rarity": 50, + "ThumbnailImage": "8y0zbo7xaq7ymt78orz2j8b42.png", + "AvatarItemDesc": "13e70c80-69a4-4ad5-90e5-4dd5e03da677,", + "FriendlyName": "Shark Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "13e70c80-69a4-4ad5-90e5-4dd5e03da677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-07-16T18:20:43.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2197, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "znzGBCjJOESSUfgrMXG-dw.png", + "AvatarItemDesc": "13e70c80-69a4-4ad5-90e5-4dd5e03da677,ln5aTG458UOVh4sR820FJQ", + "FriendlyName": "Shark Hat (Tiger)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "13e70c80-69a4-4ad5-90e5-4dd5e03da677", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:48.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1877719043 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,ppkByMiIi0inxbXKX4fu4A", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1679852489 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,KaTmQ95fV0KAUM-nvOdHwg", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1933742111 + }, + { + "AvatarItemDesc": "157d609c-3827-45e3-ace1-be7cc250d132,1GKZY3pku0Wuq4_vdiO21Q", + "FriendlyName": "Shark Bitten Boogie Board Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "157d609c-3827-45e3-ace1-be7cc250d132", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1603771490 + }, + { + "AvatarItemDesc": "45e9bd7b-b1df-4751-b3ac-994652a5745a,", + "FriendlyName": "Sharky Fin Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "45e9bd7b-b1df-4751-b3ac-994652a5745a", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1869424493 + }, + { + "AvatarItemDesc": "c0e3d978-c4c8-4606-a1af-b37de8995dbc,", + "FriendlyName": "Sharky Glove Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c0e3d978-c4c8-4606-a1af-b37de8995dbc", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1467914069 + }, + { + "AvatarItemId": 723, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "597y9xwas90z92z2w9b6wwif0.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,", + "FriendlyName": "Detective Cap (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-21T01:05:53.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 747, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "73q1jtkz7ele0ps0zjryw0veq.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,fEPwvuJCWkGBe4IekXpmLw,axPBGoqorEe77Oqqjfzb5w", + "FriendlyName": "Detective Cap (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:11.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 748, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5x3p1zbvxjej9uzfpfkzw5g75.png", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,MQOxmUk-mUG-Ee9m9nX31Q,axPBGoqorEe77Oqqjfzb5w", + "FriendlyName": "Detective Cap (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5bde836f-9343-4934-b01a-606f707020ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:16.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 724, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "abhyjwyie97h1rlznlk29up36.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,", + "FriendlyName": "Detective Coat (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-06-21T01:05:55.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 753, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0dwm2kiido15zo6nhtcwi0vma.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,Z1Ib9r_fkUqgoZk0uid_Xw,9ZP617DQk0mZ10J3d5q9oA", + "FriendlyName": "Detective Coat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:25.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 754, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ec3q3cbtla7cn866qybjjfdaw.png", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,msOFHoFN9UORkeXXtdqBsw,9ZP617DQk0mZ10J3d5q9oA", + "FriendlyName": "Detective Coat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "033e328f-d9d2-4e53-af20-44f18e3ea208", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:27.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 959, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d4dej37re0qtqjy0r8z93pdoo.png", + "AvatarItemDesc": "pNOfTDOgJUGQvGAlTjBoCg,", + "FriendlyName": "Wide Neck Tie (Promo Room Drop)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4c9fd3a4-a033-4125-90bc-60254e30680a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-12T02:22:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2215, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "62A9TZ7RIEOpOwFfOh9vXA.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,", + "FriendlyName": "Skateboard on Back (Skull Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:38.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2233, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IVFZ9-hSgUC7-LCqkg6e_Q.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,RDkLPQ8OukGIuLgTwnyqfw", + "FriendlyName": "Skateboard on Back (Vaporwave)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-01T15:35:25.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1511682151, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QhCcLdIsJE6qCUuMXJNKzA.png", + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,7WzGZTf51EWClLTLNhhGVg", + "FriendlyName": "Skateboard on Back (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.531778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "159aa0f1-38b7-4a31-b1c3-111bcde687c1,655a48e6-ab7c-4c77-9c52-262f5bb8cbc2", + "FriendlyName": "Skateboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "159aa0f1-38b7-4a31-b1c3-111bcde687c1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1710365958 + }, + { + "AvatarItemId": 2910, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0GJO9X6LHUCt29vIeqSe6A.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,", + "FriendlyName": "Yellow Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:52.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2912, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CuNGqjAKSU6GCIYRVZDJjQ.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,y6NrbfS070WvASkKmPK-9Q", + "FriendlyName": "Burgundy Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:57.55Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SbLa3ck7dE2KhA1vlS01Hg.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,MVXeErsEE06KenrYaEm7Kg", + "FriendlyName": "Blue Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:31:54.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2914, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1kMIu6ND0CfZWLprY8CHw.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,7IYi1twr6kO3xJiQo5CB4w", + "FriendlyName": "Skater Fit (Checkerboard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:02.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2913, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "hvNdEmSkiUWJ0ctgd3uXlw.png", + "AvatarItemDesc": "be1fdff3-ef1a-4be5-9d65-9107488f45d5,qwQ8OV3E60WG_VEl9ECHoA", + "FriendlyName": "Green Skater Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be1fdff3-ef1a-4be5-9d65-9107488f45d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:00.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1182, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "21qmin6ucgfg45pvh611izmvq.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,", + "FriendlyName": "Skeleton Mask (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:30.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1183, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "08urzyypklh9ip5h98tczkuaj.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,s6DQxJ3gY0CG8L7JmcmRTQ,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:33.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1534, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "02lomjpecz0msvhsrrykghrca.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,DJnKyNXtuEW1tbKBFscIng,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:38.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1535, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "965z4xrxm4jg3xkv7a7vkdam0.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,ZY0rVNemjEuEMGMjuPNZyA,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:40.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1536, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "23626wp454o5pqka9wlihhylb.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,jhPUIkf5bkinMSsqPTK3fg,DfyODsHfIESXxVjcfZmBOA", + "FriendlyName": "Skeleton Mask (Yellow Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:43.833Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2307, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "Z6ugLoGUCEiPuvn_pWpROA.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,FO32QjZ17E66PiSEDyf0xA", + "FriendlyName": "Cyborg Skeleton Mask (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:56.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2303, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "KlT8BALxYUisE34P3vbNZg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,rv8cpfd8kUCK5NbwjNmExg", + "FriendlyName": "Cyborg Skeleton Mask (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2304, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "i_i6gp2yhEyV8tCehNVavg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,kHPSqeM7JUqzMDtVxBGyBg", + "FriendlyName": "Cyborg Skeleton Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:49.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2305, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "4Sc2MIKVHkWz1i42WTaUvA.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,7BrllPDzNEyD9UZgdgM1Dw", + "FriendlyName": "Cyborg Skeleton Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:52.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2306, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "LKhhC901_Ei4OyKUfa82Hg.png", + "AvatarItemDesc": "28e5dd74-7963-42cf-aa68-f8005aaf312e,Ftonil9tzEWQnmRQuTxwxQ", + "FriendlyName": "Cyborg Skeleton Mask (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "28e5dd74-7963-42cf-aa68-f8005aaf312e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:51:54.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1184, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "4yv9lieuu39ybfs061keni4bw.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,", + "FriendlyName": "Skeleton Glove (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:38.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1185, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "5yfuk6ffcuv1z2vqg6ffd7b69.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,s6DQxJ3gY0CG8L7JmcmRTQ,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:41.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1537, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "ejkvm0j7lbzn3f6m3h8vzq464.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,DJnKyNXtuEW1tbKBFscIng,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:47.253Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1538, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "bac6n0vzhk6sgr3r9ltyf5xm0.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,ZY0rVNemjEuEMGMjuPNZyA,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Skeleton Glove (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:49.71Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1539, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "dl5czy3ptauoh49mbo0ae06zs.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,jhPUIkf5bkinMSsqPTK3fg,GXS9LQgPU0mJIEuXo_Oocw", + "FriendlyName": "Yellow Glow Skeleton Glove", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:52.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2322, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "5hcwuha7qbinh2y6q3kskmmp3.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,GYeYHzKcekODxx_zbtk8Qg", + "FriendlyName": "Cyborg Skeleton Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:44.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2323, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6g7hoxuvnw5065l9r0fptljij.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,lwyGrpMk-kGZwIkioHr8Sg", + "FriendlyName": "Cyborg Skeleton Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:46.4Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2319, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "3clh44ctdz1digv1oiwv505ae.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,KWYMlbpZiEWRUb0F7CYDjA", + "FriendlyName": "Cyborg Skeleton Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:36.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2320, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "369wpmjr71m46vryx3x1ynzm7.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,CsPK8sqkHUSV1u9d4LijHw", + "FriendlyName": "Cyborg Skeleton Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:39.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2321, + "AvatarItemType": 0, + "Rarity": 20, + "TagList": "halloween", + "ThumbnailImage": "7x0z1jskh28m0i22yy1hel8xd.png", + "AvatarItemDesc": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b,dPM-UHMtSEKC3rN8l4JsUQ", + "FriendlyName": "Cyborg Skeleton Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6a98bbb6-bbe0-4dc3-97a9-e253dc506a2b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1180, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "1b7g9kq7p2543j7d970f1bkw2.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,", + "FriendlyName": "Skeleton Hoodie (White Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:21.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1181, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7hrs6xlvsngnvndq0px6erei9.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,s6DQxJ3gY0CG8L7JmcmRTQ,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:53:24.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1531, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "0s8dsvnn6kc6q646bv4ipcqzy.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,DJnKyNXtuEW1tbKBFscIng,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie (Pink Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:29.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1532, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "5z8kh7zr2w90iw14n6828rdeu.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,ZY0rVNemjEuEMGMjuPNZyA,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Skeleton Hoodie (Blue Glow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:32.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1533, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "8duxsgrggyqxtmflb6g47toll.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,jhPUIkf5bkinMSsqPTK3fg,eZGiYEx6sk2PsN39RzQwlQ", + "FriendlyName": "Yellow Glow Skeleton Hoodie", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T23:55:35.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2314, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "fU2o342vgECqLjsyiP8vCw.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,x_UvGO3yQEmprFEXhPExDg", + "FriendlyName": "Cyborg Skeleton Hoodie (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:18.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2315, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "uHnG7V2W1Uu8H2VhCYGJgA.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,WRi0rK1O60qEpDPgOH5P6g", + "FriendlyName": "Cyborg Skeleton Hoodie (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:22.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2311, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "cFJR0iw4gUGSumlZz_qxXA.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,NvnHqZ-nb068DOSrxZPYIQ", + "FriendlyName": "Cyborg Skeleton Hoodie (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:04.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2312, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "6Oec9FiiLE-7m4bafgo2Jg.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,2mwfYNx7iUGbSf6u8rRXwg", + "FriendlyName": "Cyborg Skeleton Hoodie (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:09.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2313, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "nRtJQf3QR0iu2fICET7LtQ.png", + "AvatarItemDesc": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7,xtjgHdnYREm_gx_tLsTpvg", + "FriendlyName": "Cyborg Skeleton Hoodie (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "df0e06c4-8ba6-4d7a-bfb2-3759d8fe88c7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:13.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2580, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "k9Vy4pHmyUWpUmPbGE0xuQ.png", + "AvatarItemDesc": "6936d8e0-9d4a-4e22-99b6-04e5182c1109,", + "FriendlyName": "Skeletor Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6936d8e0-9d4a-4e22-99b6-04e5182c1109", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:40.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2581, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SAPYQD9BqUWYP73yXsGZkg.png", + "AvatarItemDesc": "ba96458f-84c3-42b7-acc0-e4d96f31ec84,", + "FriendlyName": "Skeletor Hood", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ba96458f-84c3-42b7-acc0-e4d96f31ec84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:43.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2582, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2cM1bj5ynEux57OhmNIcfw.png", + "AvatarItemDesc": "7ee02f33-1135-4adf-a626-44a1b6d1b0cb,", + "FriendlyName": "Skeletor Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7ee02f33-1135-4adf-a626-44a1b6d1b0cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:45.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 946, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "81f46rtgrs77zl3to2piot49a.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,", + "FriendlyName": "Ski Buddy Goggles (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:16.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1276, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "948ckejd6ibxj5plgmycvqr20.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,fXNkMWFMV0aXalK_y_8SHw,SSY8ZYMgaE2Bl4oFmMjKQA", + "FriendlyName": "Ski Buddy Goggles (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:39:56.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2597, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4doxnMsj30aGV6CnhP58sQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,n741vKj02kyt8B5fT6s2og", + "FriendlyName": "Ski Buddy Goggles (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:30.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2598, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eqLzMORoeEGX-z-yYZtUsQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,iDnij3VaEkSAnpt5QRevig", + "FriendlyName": "Yellow Ski Buddy Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:33.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2595, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "VFE9I4VMiU6FDWdRZaZlRQ.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,oCB1ZeT0hU6uQGhHNsEvkg", + "FriendlyName": "Ski Buddy Goggles (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:24.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2596, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-KcCp_tuMkalrEo6T6ql_w.png", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,IQMO6E5ofkyC3S-IgrKyIw", + "FriendlyName": "Ski Buddy Goggles (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1523fce5-020a-4bfd-bd0a-1bc5eae34667", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:27.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 947, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b3xepw6yux7925w5til94vmmw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,", + "FriendlyName": "Ski Buddy Ear Muffs (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:19.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1277, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "502z6klf3k4u48eow06006kxl.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,KwCEye0nsESFs-yj4h9sjQ,c_t9XDbn-Eqml1O-wC5zJQ", + "FriendlyName": "Ski Buddy Ear Muffs (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:02.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2601, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aiYHJsbXHku2qBmKihSApw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,m2znGrdH-EOhrbLbFsmyOg", + "FriendlyName": "Ski Buddy Ear Muffs (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:40.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2602, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "JHSLD9otmEODYVGGix00gQ.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,ojrXnWkUkEOGM3_oT6V29w", + "FriendlyName": "Yellow Ski Buddy Ear Muffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:43.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2599, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Cl-lyYb_7EyL8pudY8bgOg.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,sipxCGA1uUu72KV07vAWFA", + "FriendlyName": "Ski Buddy Ear Muffs (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:36.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2600, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cEOrlpC5uEi63XRZAeiOxw.png", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,1_vRe9nJMEWcNX8kAwgh9g", + "FriendlyName": "Green Ski Buddy Ear Muffs", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6fd04e9-7a1c-4e0c-adf5-c280b3ad62df", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:38.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 945, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7e765tmsry0a7xzqwbwtih0ga.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,", + "FriendlyName": "Ski Buddy Jacket (Summit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-30T02:53:14.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1278, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2rr9ojhz8lb65xzu05oejy4i4.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,fXNkMWFMV0aXalK_y_8SHw,HQQWccdyakaEmntPhvC6CQ", + "FriendlyName": "Ski Buddy Jacket (Chill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:15.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2606, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zPU9jyLBykuvxdbYwUE6Sg.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,1BQdC-mTdUqfm4vUf0lx-g", + "FriendlyName": "Ski Buddy Jacket (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:57.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2607, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "Tk070G0_FU-z-K1AbYodRw.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,S7C2A8nga0SlIeOEkeYaUw", + "FriendlyName": "Yellow Ski Buddy Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:45:02.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2604, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "h2VEGWuIQke-wT_k7W0sTA.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,vpxBkYv6LUSVV4NhYAS1Fw", + "FriendlyName": "Ski Buddy Jacket (Slate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:48.787Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2605, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "klVYrqwIukW57n4kEMxC6Q.png", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,0mwtvRpkmECaT5_4aqCttQ", + "FriendlyName": "Green Ski Buddy Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "587018e9-574c-467a-98a4-ad0d18749719", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:44:52.983Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2721, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "iCZlDOC3T0yMCT2lIaEPrA.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,", + "FriendlyName": "Skis Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-01-10T00:14:03.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2723, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nT7I-G6ixUSQ6Om25gsUGA.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,sNtmG8EODkOoiAdOi5Hetg", + "FriendlyName": "Ski's on Back (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:10.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2725, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5I7iBS_W3EG0-nf--ECAJg.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,HQ1M8kINaEu-0HJtz7f8Gg", + "FriendlyName": "Red Ski's on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:16.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2724, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LdbyV0Td6Uuj5MUaG1720Q.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,hPVbRnQq-EW3g0q17N6G5w", + "FriendlyName": "Ski's on Back (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:13.727Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2722, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BP7vd50iWE-mLN5Go0g5hQ.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,OScwXIzJHUKzb-NC6gUnxw", + "FriendlyName": "Ski's on Back (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:07.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2726, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lCMwxmYnFE2_25yjGunDzg.png", + "AvatarItemDesc": "2f8ba691-4508-4254-adb1-c2b554db4925,QVsGyxlefEeomtJO-dFAOw", + "FriendlyName": "Ski's on Back (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f8ba691-4508-4254-adb1-c2b554db4925", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:19.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1201, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bb4i0uki9qbf8p96v4jfo349e.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,", + "FriendlyName": "Trucker Jacket (Sleeveless, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:49.823Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5hykjpkvkk03yf3qjii3sm1ii.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Sleeveless, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:53.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1203, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0x06arb0upzvorpzdr0ci4qjo.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Sleeveless, Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:58.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1204, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3t7w6vu3h429ps4eurav4diui.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Sleeveless, Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:01.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1205, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dn6y0cqf5ukxig24sgivzxa2i.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,4Eck_dNL1UKlIZWsA1YppQ,WeyKw_WS70Cx3NZdOaxGeA", + "FriendlyName": "Trucker Jacket (Sleeveless, Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:04.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2763, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AmL-fduRx06s08e_Pzs1tg.png", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,wMo3Bq1aXkCMFSRJft81BQ", + "FriendlyName": "Trucker Punk Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3e3f5ed5-d810-4054-b648-d93167b90033", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:13.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2272, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TulnHZMqkE2HIMXRNacnsQ.png", + "AvatarItemDesc": "a79e855a-c4fc-4ffa-be2f-9f4fef0ab1d5,", + "FriendlyName": "Outlaw Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a79e855a-c4fc-4ffa-be2f-9f4fef0ab1d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:19.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2273, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3zjJ5Sl4WkCfXJUE--1kTg.png", + "AvatarItemDesc": "cc30f7f7-6c12-4a96-acfd-11aec984c2e4,", + "FriendlyName": "Outlaw Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc30f7f7-6c12-4a96-acfd-11aec984c2e4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:21.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2274, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4nxCvy9s-EWMBpdXowp34w.png", + "AvatarItemDesc": "86e0a4cf-b112-40d2-94b9-7ef35abe5a61,", + "FriendlyName": "Outlaw Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "86e0a4cf-b112-40d2-94b9-7ef35abe5a61", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:24.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 70, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ero4520aj78qgzdiz2knkg422.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,", + "FriendlyName": "Small Brim Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 489, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "1z40s482g9xq9sirwfx26sfce.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,67fc9269-8dd8-4d6f-b594-c77e716f2482,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:10.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 490, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "azus8th8ecez3uybgfgvqq0o4.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:11.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 491, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "77ytai8z0lk4qz5x8il84ybvx.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,d6823e01-69f0-4f85-b94a-74894356a2cf,65ed43ff-dfc4-4cab-b0b9-77dc94165e24", + "FriendlyName": "Small Brim Hat (Maroon)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:14.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2481, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "R5_LD0bgPkWiDdYxa1A_mA.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,08r7prRREUi-Hvp1Xze-wQ", + "FriendlyName": "Mini Droid Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:30Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "C2N_ATMr50e2W4aeVk04iQ.png", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,7qVilU94X0Kldpvku2mcng", + "FriendlyName": "Alo Yoga Bucket Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9cdb4965-6f00-4304-afed-e301e73a2b3a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:05:27.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 310, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "16fi077y627ijhrcmqr0t298e.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,", + "FriendlyName": "Smuggler Vest (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 311, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zkofpgk5o77fd65ks8x95cxv.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,e821aa74-2d47-4388-bcfb-1893b8a83a52,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 312, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3seplqtowg3fi113bsetxytpu.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 313, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5iogm3iyqg7sp05x3hgihphdy.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,90869e86-cbc5-4024-9d08-fcdc41c64eb5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 314, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c02xchxpap45jxgdksgsouq6h.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 334, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ed58vssma43zkdcb28nce7hhe.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,002b07bb-4256-44ce-8ca4-25510e70b6b5,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Vest (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1110, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2ce642u8nppm5zamly1hq59fs.png", + "AvatarItemDesc": "25d4c652-1730-4925-925e-31a290a2272a,bzYdPb8GWUS-CgT4wrziIA,970a8c7e-d091-4719-bb9c-e3be8d46ee68", + "FriendlyName": "Smuggler Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25d4c652-1730-4925-925e-31a290a2272a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:09.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 321, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2bncfuckcjmwaa7wano2sn1od.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,", + "FriendlyName": "Smuggler Gloves (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 322, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "648nagff1d7nh5vgo6tx3tpqr.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,e821aa74-2d47-4388-bcfb-1893b8a83a52,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 323, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7mr1xqf0fhgcv6v8p96yba2sx.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 324, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "97ynsh86vqwidiuno90esdvqs.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,90869e86-cbc5-4024-9d08-fcdc41c64eb5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 325, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7botyxuklwv4dbcddyrwdpcbc.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 333, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9vn2nq6cpu4mjyomplb7mld09.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,002b07bb-4256-44ce-8ca4-25510e70b6b5,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "de8u98lwjl6796f9eeq8g7zfr.png", + "AvatarItemDesc": "3dec4865-cf23-4c19-a460-dd2db80ee851,bzYdPb8GWUS-CgT4wrziIA,8ffce0d6-d03e-4097-bc46-09b8894bc1d3", + "FriendlyName": "Smuggler Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3dec4865-cf23-4c19-a460-dd2db80ee851", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:41.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 305, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aipia6o2kym88ur3brk8ufjud.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,", + "FriendlyName": "Smuggler Goggles (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 306, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3940162c53c4psfxm652thn6s.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,e821aa74-2d47-4388-bcfb-1893b8a83a52,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 307, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "20x5x09hzrg6c4zcpys1iykz4.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,fed760b2-15e2-46b2-b1e3-cc3c8d780d5d,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Cherry)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 308, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "72lfd8j7a44mhv0viedavv2ht.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,90869e86-cbc5-4024-9d08-fcdc41c64eb5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 309, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7a494od9prmwajsibg29z7jaq.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,076c96ae-9f9b-45bf-9dc7-ce73d5ddf422,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 332, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ayx5bk4f6mojyvfiarqfavc3n.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,002b07bb-4256-44ce-8ca4-25510e70b6b5,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-20T18:24:56.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2bkz14lxmqlga9phjxk98i0j7.png", + "AvatarItemDesc": "529015c7-3b4a-4a73-b2c3-33103339b6e3,bzYdPb8GWUS-CgT4wrziIA,fe01b6ed-3c36-47b6-bcfa-47864ef9d610", + "FriendlyName": "Smuggler Goggles (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "529015c7-3b4a-4a73-b2c3-33103339b6e3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:00.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1068, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d8r0drrwrxs7ot1bgzz145sm6.png", + "AvatarItemDesc": "rJ9zPPGdlk2i24sxLfkqng,", + "FriendlyName": "Lifeguard Uniform", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3c739fac-9df1-4d96-a2db-8b312df92a9e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:51.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1063, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5bzqn0b3ouxa0kemvmre0a06k.png", + "AvatarItemDesc": "22pyI3Kz-UullhcebC6DLQ,", + "FriendlyName": "Snorkel", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "23726adb-b372-4bf9-a596-171e6c2e832d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:25.297Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2707, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lhJKy4WMsEKemSq-lsUulg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,", + "FriendlyName": "Snowboard on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:51.183Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2710, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Gb6e46akCEKEXpH2qSvTlg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,PI3_sfTcNEW5TcAmHbsUOw", + "FriendlyName": "Snowboard on Back (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:57.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DsRdBkQ4c0WsfkMYTXQsXg.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,K1-5YGg2rEiJFRWUKEfEMA", + "FriendlyName": "Snowboard on Back (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:59.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RlqSyYcF8UeGktdGy3dvmw.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,DSZKCNhXAEiAxMM1Gqfhsw", + "FriendlyName": "Snowboard on Back (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:53.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "sf_MJ_CbKUCFSn6Iky3HxA.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,E6md4xTNYkS9geB8zkNRmA", + "FriendlyName": "Black Snowboard on Back", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:41:55.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2712, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rfwXR61Q1k2Kyd4vTEsAyA.png", + "AvatarItemDesc": "75e51889-defa-4689-b3fb-32882e08e5d1,2dGY5MHfxEunzr7jtSFRVw", + "FriendlyName": "Snowboard on Back (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "75e51889-defa-4689-b3fb-32882e08e5d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:42:01.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1660, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3bu1frdg68km45i2m32p4tuf5.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,", + "FriendlyName": "Snowy Owl Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:56.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1708, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1s8l5fozxazwvdbf7an9mewb0.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,W83EASFUS06FBVMfL80ruw,Ydd6tdb9dUGooM6WAczNjQ", + "FriendlyName": "Snowy Owl Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:38.83Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1709, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0mjop3uc4xo79ox3fcqpa6wjj.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,8M72ydR_uUSsli43PH2Eyw,wQxO4xmXCUGeC9ZHfDNGTw", + "FriendlyName": "Snowy Owl Cape (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:43.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2257, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U8zL4biQO0utrAL1LCl_Zg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,F0o3eeWbCUaUAGRUdI0z7A", + "FriendlyName": "Floral Cape (Midsummer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-06T16:31:51.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 5880, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6crHGcPzUUefWWZmQFnzOw.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,NnqAnqqLmUypWnNR2s-_sg", + "FriendlyName": "Cottagecore Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-10-10T17:15:03.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1543394911, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5Rk5icw_oEqqERltIynyTA.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,qvK8xgbyuE-dW3--Ku-mvw", + "FriendlyName": "Floral Cape (Midsummer Obsidian)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1633983614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2jE0M2FxUUKRdwB-Z981Dg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,hRl4UTGoBUi5dmu0f4s76A", + "FriendlyName": "Floral Cape (Midsummer Verdant)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1144281523, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4YKttOYfZUCJWTM47OGSsg.png", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,07qFFGmwm0qmIikemncBkQ", + "FriendlyName": "Floral Cape (Midsummer Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9441142e-981c-4f35-8f9d-b4b3604da9a4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.535776", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3li2gob8aeawnh0bgtt1lfs2q.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,", + "FriendlyName": "Snowy Owl Mask", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:58:36.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1710, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "48ikxhz5n438dh6imyqdnqqdx.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,I_3bHRSG_EmC6novYyy2dA,Cq8BOWcHb0GTDLvXl1iIcg", + "FriendlyName": "Snowy Owl Mask (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:46.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1711, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cabsqekth6kemqe8zn7s45v7k.png", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,HWAWBHI3zkGsh06he9CoPg,UAeSz-XeZUaZsk9FNlPX5w", + "FriendlyName": "Snowy Owl Mask (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "771863b6-70d1-4f48-9c64-c621ce0fea46", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-04T22:32:49.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 509, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2vqxpnzd5r6nuyacqrsrw5loh.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,", + "FriendlyName": "Soccer Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:35.953Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 778, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9bshjyb8x1qk9xkany8c3ngke.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,OnQeVxdstkS2zGxLBAuM3Q,i8bvbGAKREmAEbjKO-geOQ", + "FriendlyName": "Soccer Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:02.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2316, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "rRsmZUSOC0q9ITRtTz45Zw.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,-v3ddzUMF0aKxJR82SDevw", + "FriendlyName": "Soccer) Soccer Shirt Recpro Orange Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:26.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2317, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "mJIUWEd9WkWLnKurCXXFEA.png", + "AvatarItemDesc": "b6610b7b-3978-46fd-9af6-41b4568c9b4d,mtR99SoHQ0aJLJfXPsKW1w", + "FriendlyName": "Soccer) Soccer Shirt Recpro Purple Combination Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b6610b7b-3978-46fd-9af6-41b4568c9b4d", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:30.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 513, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0we6tjqz2a38v2ojhop58m34f.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,", + "FriendlyName": "Soccer Goalie Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-14T23:02:39.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 782, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "azz8fjlgu2rjy5q07n278fb0b.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,whEHo6Rhr0CxsDBsBD32WA,av4PwLz6YkmpBRImQZgn4Q", + "FriendlyName": "Soccer Goalie Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:07.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2324, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "ZftdQQ7kwkKCDZuTV-DWFw.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,3by5X5XZ4EW7eaERsaBJKg", + "FriendlyName": "Soccer) Soccer Wrist Recpro Orange Combination Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-08-17T15:52:48.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2325, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vupu6d_gGEaywmPePUvf1A.png", + "AvatarItemDesc": "94eb7ef3-528f-4f59-92a0-48b0ec287527,l2g5qO9dokiNepH92oPmug", + "FriendlyName": "Soccer Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "94eb7ef3-528f-4f59-92a0-48b0ec287527", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:50.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1059, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bij80e36orzdtzehef7fmjeru.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,", + "FriendlyName": "Soda Clerk Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:04.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1326, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "crm789xheizu2cbu14b3wavpq.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,XFCpIdeDXUqeYCIgh4fGtg,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:47.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1327, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8etl00yx6u11l07jed4sl7xnc.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,oKxmH-1s2EyIDkZoOQSQQw,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:52.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1328, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5tdjwtkcesfgqls3gzy24y3hf.png", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,MxnAq_XOw0uTzQ8IIxqb_Q,sV_ByCug30G1Gw-77KUe1Q", + "FriendlyName": "Soda Clerk Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dcb8e65a-fb40-4ad5-8bef-5013b724b489", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:48:56.813Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1069, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7x52aisqe0e84v27jn0madibj.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,", + "FriendlyName": "Soda Clerk Uniform (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:54.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1329, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2cvsw2qbbncs9zl5vsv7mxqkk.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,XFCpIdeDXUqeYCIgh4fGtg,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:07.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1330, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "447rhi816e4hfny2r4ktjynv5.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,oKxmH-1s2EyIDkZoOQSQQw,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:12.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1331, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "di4jb7lfnmlgt0x975ppcx8h0.png", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,MxnAq_XOw0uTzQ8IIxqb_Q,MBuxBAIkpkWI26d-arAKQg", + "FriendlyName": "Soda Clerk Uniform (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3fbaa889-43b7-4a4d-bc4b-886a42663560", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:49:16.767Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2384, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0MfRmLg2yUGhr0zEhMC8Pw.png", + "AvatarItemDesc": "341dcb4a-818c-4d7c-9ae3-1dabbb79519a,", + "FriendlyName": "Solar System Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "341dcb4a-818c-4d7c-9ae3-1dabbb79519a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-30T17:42:57.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 315, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "d0pc71o2msynlo2gg6zblsdb3.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,", + "FriendlyName": "Space Marine Armor (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 316, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bn2zkvbfzcikfewpjgtd2bgl2.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,c8cc9589-4831-4e73-a865-ef8c139a2ba7,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 317, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9matrdtcoznln9t9wjrus86kx.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,a089bcc6-798d-4777-98e2-7afd290dc6b6,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 318, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2z7ww93ccq8rsr8l3bu6m7ntd.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,a36d252b-16ac-4a82-bece-de145147e7d9,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 319, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1a78ihg1rn6i5jh196fh166vv.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 320, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "90qc2umq7k4h8g9c9wgxcr18v.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,68273c9e-96d6-4ddb-aaab-d99229e864d5,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1101, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5rzpff3hf3m92tijpthgokni5.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,4ElTihrMA0ipcgeb9UG01Q,7ac81d60-05b8-4a85-982c-f69213a93818", + "FriendlyName": "Space Marine Armor (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:32.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2490, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "tmcuRuyNAkaX-uyikLs9ww.png", + "AvatarItemDesc": "4ce7a70f-15a0-4cad-98ee-5cbb40827338,1pJN1AfxgUmUN_bTaH3rng", + "FriendlyName": "Science Iso Suit (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:12.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 326, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7izb4xifehd19st4qgaf70jqg.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,", + "FriendlyName": "Space Marine Gloves (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 327, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "anqd573o831mzengh9221nawt.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,c8cc9589-4831-4e73-a865-ef8c139a2ba7,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 328, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5zeohu1rh1ox5hoix80fbfl50.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,a36d252b-16ac-4a82-bece-de145147e7d9,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 329, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3qre9n3k1ubvucbztpra48in4.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,a089bcc6-798d-4777-98e2-7afd290dc6b6,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 330, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0k5d0e2dbwkixi838nrtd06ll.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 331, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0u3e41638g5e5afvuyqxror2x.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,68273c9e-96d6-4ddb-aaab-d99229e864d5,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1106, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0crijq94h4xfrkph110y254b5.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,4ElTihrMA0ipcgeb9UG01Q,7634b7ef-9607-497a-b41f-c0191dc75998", + "FriendlyName": "Space Marine Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:53.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2326, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lmmMFWBKYUqm66trPr35Yw.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,LVnNy7-nFkyvi19nNcdFgQ", + "FriendlyName": "Science Gloves (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:52.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2713, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9nzqt8ExHU6STyCJinXlUA.png", + "AvatarItemDesc": "4763436c-2dcc-482a-bf37-a6b16d7aae6e,uWe5nxA4qkaicmXm3tMw9g", + "FriendlyName": "Science Gloves (Invasion 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4763436c-2dcc-482a-bf37-a6b16d7aae6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-21T19:42:03.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 299, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2blhcp3hyn511tz9gxh7s982l.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,", + "FriendlyName": "Space Marine Helmet (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 300, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6wlpi8vp4n0f8yrjuywdx8dzr.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,c8cc9589-4831-4e73-a865-ef8c139a2ba7,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 301, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2xmq1ji9y74k37fj1ft9nmnvf.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,a36d252b-16ac-4a82-bece-de145147e7d9,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 302, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3v8rhp57dz9s41uhg5l4rytbv.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,a089bcc6-798d-4777-98e2-7afd290dc6b6,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 303, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8syjfjzce4brzrmudgrhv1mvo.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,d78643e4-c0d2-4836-bf22-b5a0e2d65b28,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 304, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7dx5tt4aplt5y8oxs4j4ojbqc.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,68273c9e-96d6-4ddb-aaab-d99229e864d5,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-17T21:15:42.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1086, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ebj5kkpndee5u9d3b5fcifo7s.png", + "AvatarItemDesc": "59d9560d-e5af-4205-b860-3afea1cc31a9,4ElTihrMA0ipcgeb9UG01Q,b94411f6-5b71-48d0-ace1-ac6f63167065", + "FriendlyName": "Space Marine Helmet (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59d9560d-e5af-4205-b860-3afea1cc31a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:20:13.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2679, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KnhWwAmT6kOTA9oXrhmGMQ.png", + "AvatarItemDesc": "bc0a1c68-2eb5-4dde-a242-c4e19940699c,", + "FriendlyName": "Space Monkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bc0a1c68-2eb5-4dde-a242-c4e19940699c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:29.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1507, + "AvatarItemType": 0, + "Tooltip": "Unlocked during Xbox launch", + "Rarity": 50, + "ThumbnailImage": "6t5em8ljarz9lnn8erh5ndc1b.png", + "AvatarItemDesc": "b37582a1-7904-4288-a47a-7a63a3cb28ae,", + "FriendlyName": "Space Soldier Helmet (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b37582a1-7904-4288-a47a-7a63a3cb28ae", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:06:05.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1508, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ey0vb0vehvdu8qhub0g2unvrw.png", + "AvatarItemDesc": "88ae6d12-6d39-4770-ac2a-f8c4602a10a5,", + "FriendlyName": "Space Soldier Helmet (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "88ae6d12-6d39-4770-ac2a-f8c4602a10a5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:27:18.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1760, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ba7xldqaol3atwgv9n84yszra.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,", + "FriendlyName": "Gunslinger Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:19:11.58Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xe9d7zlk5k1ybhox7zl3n47i.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,jXRXO3CVE0-H_iaxbMhFhA,VZyHWGKNJEGSxDT2GO3ebA", + "FriendlyName": "Gunslinger Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:46.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1812, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8e5uyhuugy9bj5wqx2g8ucwlc.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,YSnoHFmJDUa_XCK6JMMh2g,VZyHWGKNJEGSxDT2GO3ebA", + "FriendlyName": "Gunslinger Hat (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:43.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2269, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-ejrJeBy7kaQ8QEETufdXg.png", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,JilOXG1I_Ei65mw6TzFWgg", + "FriendlyName": "Gunslinger Hat (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a471995f-1fe7-4b77-a05f-8eea4e0970ab", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:11.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1761, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "11cf76c54n13whwtqa3rjs7s1.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,", + "FriendlyName": "Gunslinger Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:19:21.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1817, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2dhxtutyxumnzuwg5rz5nbm1l.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,1-_ssucgeEalgr6EDL3S9g,Ru4HN3stVUutQ9rUh7ro1g", + "FriendlyName": "Gunslinger Vest (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:59.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1816, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2mqqtndrscn1090v5nnmhizyz.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,cbn6vE0gDUOuklVhJv1vNA,Ru4HN3stVUutQ9rUh7ro1g", + "FriendlyName": "Gunslinger Vest (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:57.003Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2271, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "x5M_A1sHQE-nk0Uj7ghTzA.png", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,2eTBbHTkf0uxy9JqA507Hw", + "FriendlyName": "Gunslinger Vest (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:16.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1759, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "el2614e47hfl8u1fca9qvtyp2.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,", + "FriendlyName": "Gunslinger Poncho", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-04-02T16:18:35.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1815, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5yb187ueehdy6eu34my5nep7x.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,Gep43Ix44EWybJ80XAyDbg,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Gunslinger Poncho (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:54.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1814, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6m083hwotwj7o3sa08b291vyg.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,EmTrJKm8cUy8_BHAvP8Meg,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Gunslinger Poncho (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T19:13:52.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1403059708, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9586th835v7422nhmsim3nzod.jpg", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,VnRSTH4HqEqADLtryqSQNQ,kxkqq5UA_kCM6lFsFe3dvw", + "FriendlyName": "Spaghetti Cowboy Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.537778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 2270, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BEUiX6fKYE-xt4747ftpcA.png", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,T670e7nOakqKl3L6LTlTtA", + "FriendlyName": "Gunslinger Poncho (Steampunk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "62a28dea-adcc-40b3-9045-917ae4f38c63", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:14.51Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1572, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4shtbpjixq5n883npkahv8c5v.png", + "AvatarItemDesc": "e29d044e-79fb-4280-bca9-aa1e1257e968,", + "FriendlyName": "Martial Arts Gi (Speed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e29d044e-79fb-4280-bca9-aa1e1257e968", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:38.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2203, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "-yUmlgdAekKg0XMSaP8K7g.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,", + "FriendlyName": "Sports Scarf (Buckateers)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:05.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2204, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bnXH-F2F6EG-wTVhwSmaaA.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,ggpNxFtZMEaHlvWXsDl9Tw", + "FriendlyName": "Sports Scarf (Goblins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:09.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2812, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KiAiCi-3AkKa90OHtKGapg.png", + "AvatarItemDesc": "2b325613-84c1-46d8-86b4-a167d79661da,Jd_ahlwRskehLggn1T-GdA", + "FriendlyName": "Sports Scarf (Shamrock)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b325613-84c1-46d8-86b4-a167d79661da", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:02.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2109, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XMdVSPwYlEm9q62jRsWEOA.png", + "AvatarItemDesc": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,", + "FriendlyName": "Spring Bloom Antlers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:24.93Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2504, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nRaGYENtIUSgiJpKpJtfdw.png", + "AvatarItemDesc": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9,MtbewwIGSEu-F9Ks_Ibb4g", + "FriendlyName": "Spring Bloom Antlers (Solstice Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d2a0b8a-10f0-4f09-b145-84a4b4dc58f9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:53.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2915, + "AvatarItemType": 0, + "Tooltip": "First stage of growth", + "Rarity": 50, + "ThumbnailImage": "QrS8jVr1HU2v-T0h7SMZgQ.png", + "AvatarItemDesc": "c2df65e0-2e85-4666-bea1-e8efa654b155,", + "FriendlyName": "Gaia Clip (Sprout)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2df65e0-2e85-4666-bea1-e8efa654b155", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:05.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2916, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "PXVbvtMXvUuHbJItHMqazg.png", + "AvatarItemDesc": "d1de5e76-0367-42f2-be88-6f28a140e5f6,", + "FriendlyName": "Gaia Clip (Bud)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d1de5e76-0367-42f2-be88-6f28a140e5f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:10.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2917, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "QbAsltVVv0qUrxJJRoYbRw.png", + "AvatarItemDesc": "4fb6e083-ae10-4c71-898a-6a55138a68a3,", + "FriendlyName": "Gaia Clip (Flower)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fb6e083-ae10-4c71-898a-6a55138a68a3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-04-03T21:32:12.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4t9vou7x5a8b92tyr0k4wzzza.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,", + "FriendlyName": "Square Glasses (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 646, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2p1yfnex9n9efclfrdg5v75cm.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Square Glasses (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:30.333Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 647, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9rp7mz69wmhupgeve23u4kbt6.png", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,c5884778-a87f-4612-9717-86fbb65d440f,e0186718-4a18-434b-b96e-0c9c912f0d1f", + "FriendlyName": "Square Glasses (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f527ffaf-da03-4519-82ed-46a9cb981dc3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:31.443Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 750, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ccjied0pmdne9fu4abyc5giwn.png", + "AvatarItemDesc": "kghEf_iMKUSxPO-h5iRhSQ,", + "FriendlyName": "Classic Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7f440892-8cf8-4429-b13c-efa1e6246149", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-14T18:47:21.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9q6v1yay56q7ckhj5we9h1ksn.png", + "AvatarItemDesc": "899f1d17-eee1-4b7d-a112-fbbba597e738,", + "FriendlyName": "Squid Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899f1d17-eee1-4b7d-a112-fbbba597e738", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-07-30T22:14:05.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2198, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "x-RovcwIAUOk4uruqtQEUg.png", + "AvatarItemDesc": "899f1d17-eee1-4b7d-a112-fbbba597e738,t8kKXvBU-kSM_MHaqZYpkQ", + "FriendlyName": "Squid Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "899f1d17-eee1-4b7d-a112-fbbba597e738", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:52:51.023Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1655, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c5xy43pxfjwaaum08f6rtf6um.png", + "AvatarItemDesc": "34f45c5b-fea7-40b4-add7-bc7a37000ab5,", + "FriendlyName": "Star Captain Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "34f45c5b-fea7-40b4-add7-bc7a37000ab5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:02.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2168, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PbHPcqrg4EqJp9-zB9HpRQ.png", + "AvatarItemDesc": "3ce86805-987e-4958-bcc9-57d89c4a5b1a,", + "FriendlyName": "Star Glasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ce86805-987e-4958-bcc9-57d89c4a5b1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:49.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2779, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Qs_cSEoU-Uy3_fGkJeIHKg.png", + "AvatarItemDesc": "3ce86805-987e-4958-bcc9-57d89c4a5b1a,PGZihr096kC4XZvHfmVMzA", + "FriendlyName": "Star Glasses (Popstar)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ce86805-987e-4958-bcc9-57d89c4a5b1a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:42.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2780, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bsf3ipTBlUC2I-jUSFPw9Q.png", + "AvatarItemDesc": "b4c93141-f387-4e42-9b04-12b7a97c9416,", + "FriendlyName": "Star Glasses (Popstar Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4c93141-f387-4e42-9b04-12b7a97c9416", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:44.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 784, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bts86kl55yir0z5wcudycjvob.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,", + "FriendlyName": "Steampunk Topper", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:22:45.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1079, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eb13jqd3eks4b9bwsooqjxlp4.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,AAbGHOOitUu9enweOXbrkw,fXnd_COSzUeznwe7P4ZYRg", + "FriendlyName": "Steampunk Topper (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:53.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1827, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3gjby0eo4e1tmv2wuecnz6rnf.png", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,Jy2TN1KBTEeoVehy_x32Ng,fXnd_COSzUeznwe7P4ZYRg", + "FriendlyName": "Steampunk Topper (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c2e20283-816a-4e22-a094-5c6f693cf178", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:17:50.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 805, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dmp4y6ycyxupuom25j3hzjnwn.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,", + "FriendlyName": "Steampunk Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:54.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1081, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5ozwlaf5193fr6v3ysxikeo2e.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,fD5vtKxBWECH54Jj4FbnUg,C8gCmBZ8RUG83vvDw_UYew", + "FriendlyName": "Steampunk Vest (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:57.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1835, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8b66w4zmh5zq171rxofxcml3h.png", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,gy-xeYKKREaBlx9fUmuW2w,C8gCmBZ8RUG83vvDw_UYew", + "FriendlyName": "Steampunk Vest (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8853d513-4585-463c-be3a-e411b68d1efd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:35.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 803, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b1x1g3wbr7kr9cld4mtw2wn1t.png", + "AvatarItemDesc": "sOREgAGs-06CeGGsG-WIkw,", + "FriendlyName": "Steampunk Pauldron", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8044e4b0-ac01-4efb-8278-61ac1be58893", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:48.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1837, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9a9ta5xg1e227zgwpmygwy1cz.png", + "AvatarItemDesc": "sOREgAGs-06CeGGsG-WIkw,rSzG7s1ocUmx9_VrJLOcHQ,ISycihytUUyoypVb-kXN_Q", + "FriendlyName": "Steampunk Pauldron (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8044e4b0-ac01-4efb-8278-61ac1be58893", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:43.033Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 806, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7p4x9oq3c0f415llgnjzustxv.png", + "AvatarItemDesc": "qxovV6Vym0ufL2cTCyKjOQ,", + "FriendlyName": "Steampunk Cuff", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "572f1aab-72a5-4b9b-9f2f-67130b22a339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:55.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1833, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "en36simwv139x0vu9nj5hit04.png", + "AvatarItemDesc": "qxovV6Vym0ufL2cTCyKjOQ,64Hvmzrb3Uu9X_dCA3M3Jw,rPpYm5fIzky4vqVB26guAw", + "FriendlyName": "Steampunk Cuff (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "572f1aab-72a5-4b9b-9f2f-67130b22a339", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:20.923Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ajxapi81nzit5fp49tbhidav5.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,", + "FriendlyName": "Steampunk Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:39.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1080, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4e35e3ndy0mrxjrfnrs3kay0.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,vuSwk04zykCe3ANKN838OA,Uhw5NBQy-EOIiCWiZFiL7g", + "FriendlyName": "Steampunk Hat (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:55.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1828, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3k0gmjgtjhawlaqbc76p3n7c4.png", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,ZUMIXcyOpEGiHgslbHQbwA,Uhw5NBQy-EOIiCWiZFiL7g", + "FriendlyName": "Steampunk Hat (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6a76625-87f3-44c8-a0f4-5fc79664ce7c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:17:53.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 766, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9a1u3xocxzaogyzpudo3nopsn.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,", + "FriendlyName": "Steampunk Corset", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:48.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1082, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bcv3r4hoktif6vyvq5xi14rez.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,A6VnGtpY9Ey0atf3zaLo3g,GJRG8sCM7kSGfUd9gLwpgQ", + "FriendlyName": "Steampunk Corset (Time Travel Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-07T00:12:58.553Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1834, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d34cbgen23yzb0kmri6koo0j4.png", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,rsUfUnTwbU6zrjrHWps1yA,GJRG8sCM7kSGfUd9gLwpgQ", + "FriendlyName": "Steampunk Corset (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b723af2c-c2dc-41b9-b02d-171c80e3af8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:23.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 765, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0dl5ao69a8mzsn6jlyoei1qmd.png", + "AvatarItemDesc": "CdeM6RJmPUumXtdmdM3RXA,", + "FriendlyName": "Steampunk Goggles", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e98cd709-6612-4b3d-a65e-d76674cdd15c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-04T20:13:41.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1826, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bu4jy4ulmhtqc9baxhaqtabrf.png", + "AvatarItemDesc": "CdeM6RJmPUumXtdmdM3RXA,mZ2ASMrs80axStLp9g1enA,dGzmSibucEe7i1HD7lPcNQ", + "FriendlyName": "Steampunk Goggles (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e98cd709-6612-4b3d-a65e-d76674cdd15c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:13:49.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 804, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bq9tt6z6r54gl79e7oun3l7dw.png", + "AvatarItemDesc": "iBQ-v6xaqkipUlfzkSHEeQ,", + "FriendlyName": "Steampunk Satchel", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf3e1488-5aac-48aa-a952-57f39121c479", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-07T22:26:52.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1836, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f1t23z0rwzoefrj56j19gnhgr.png", + "AvatarItemDesc": "iBQ-v6xaqkipUlfzkSHEeQ,bH3dgwnwt0KEw8oCDVIcEQ,erPtGSgq_kmqfeSlZWw4VA", + "FriendlyName": "Steampunk Satchel (Silver)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bf3e1488-5aac-48aa-a952-57f39121c479", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-06-11T16:18:40.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2727, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "tP86FtoqYkyI7ZU9u21Ryw.png", + "AvatarItemDesc": "42f1a780-1fe1-402e-bf11-176b768e33b9,", + "FriendlyName": "Strawberry Side Purse", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42f1a780-1fe1-402e-bf11-176b768e33b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:22.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2728, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "mAe4p7EG9kGOltV99JqDCw.png", + "AvatarItemDesc": "42f1a780-1fe1-402e-bf11-176b768e33b9,jzGTu3tSB0WM430AiMGAAw", + "FriendlyName": "Strawberry Side Purse (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "42f1a780-1fe1-402e-bf11-176b768e33b9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:25.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1571, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "89it9c90iy4bi8otctbxs4kl0.png", + "AvatarItemDesc": "25769d1f-4ecb-4d92-b692-322b38c013b5,", + "FriendlyName": "Martial Arts Gi (Strength)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "25769d1f-4ecb-4d92-b692-322b38c013b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:33.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1279, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "holidays", + "ThumbnailImage": "3faagp8a1v4tthau00hha26mx.png", + "AvatarItemDesc": "b795c631-f075-4d32-ac87-c732b36cb932,", + "FriendlyName": "Light-Up Necklace", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b795c631-f075-4d32-ac87-c732b36cb932", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T21:40:21.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 23, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8vw62vvj318mjoz88zhu69qod.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,", + "FriendlyName": "Sun Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2210, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "SwnYTGNYiUG4Ua78QnfFEA.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,YS-fXtVLTkG-wl40HPBLOg", + "FriendlyName": "Sun Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:27.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2211, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FoMbe_63g0WNL9Sl7y_rJw.png", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,dzH_40F-MUCy_7gATqL4PA", + "FriendlyName": "Sun Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703f1987-2b31-4b6f-bab7-1939b1105ac0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:29.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1483, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ijpwku25zvpdgkp4y12e3yyv.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,", + "FriendlyName": "Sunflower Sundress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:41.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1484, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9iwljaztbqbk7v3c1w2zryiah.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,OJYpuFw810Sg4bTw0XQf-g,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Lodge)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:47.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1485, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d1rcizuvnlnebb16r91lfnd5i.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,t4gMAKp4L0WBaTGf_rtqFw,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Yacht)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:53.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1486, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4j8oqc693u6bw3exf1fz40gqg.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rGMFdPKmK02_OvIIFYlAxA,NqU5Mw75BE6kPaD8t11Qow", + "FriendlyName": "Sundress (Veranda)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:05:57.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1487, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "17odpv44phqhpwrrvimszr6y6.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,wxvSDMommkenIl5hTY2fdQ,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Seafoam)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:04.11Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1488, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8vqcr3rvn7ctiddmrvtkcq1oc.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,-sqfCQpzpUyjEShEXH9JYQ,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Sunrise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:09.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1489, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7lu5zwmfs5awwdn6oo40jy7z0.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,as9fjqDWiUi1w7yYavKZXA,thwvF0DFRk-mV99ruqYFoQ", + "FriendlyName": "Sundress (Sunset)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:13.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1490, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "96vw242yfg6iw9nr4lt97iqqg.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,sz-J7RjesE-ke3bKwumzZQ,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Meadow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:17.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1498, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5ov29z2kh47zws4vv1r89tx28.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,QlZt51mdn0WuiqKoJaIRyA,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Vineyard)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:10.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1491, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4iobr7kq8wwwa2wclut3agqo3.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,6PyJEQxyd0K9TPRnvt_86Q,mkNzDvyGfEijbdzAxKYrPQ", + "FriendlyName": "Sundress (Estate)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-15T19:06:30.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2262, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "EPFpkkBDokyWw7QwCJe9Hw.png", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rsh2uKli90OoEnJZRFc2Ww", + "FriendlyName": "Boho Sundress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "adf2aa89-61c4-4b7a-9307-e39e72db23d3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:09.317Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2826, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3e4iynr3546eem9i3x1apmhk4.png", + "AvatarItemDesc": "e0c4c93d-ea3e-420f-9e8f-7890b5bd4b6f,", + "FriendlyName": "Sunflower Earrings", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e0c4c93d-ea3e-420f-9e8f-7890b5bd4b6f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:54.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 104, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "f0xdfrv8p6p8tly3s9tdqx0kh.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,", + "FriendlyName": "Sunglasses (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1877, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ebmp0xvyd2y9i4tqvuzl5ns1v.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,dZM1bs6_MU2F6Vz1mH0QKg,KfM7veOVlE-dNZmsbDNZBA", + "FriendlyName": "Sunglasses (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:47.347Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1876, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "59lin822pwng10iwcthz1bb9d.png", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,0Ytm7G4_lkmZWPA_YfaIeQ,KfM7veOVlE-dNZmsbDNZBA", + "FriendlyName": "Sunglasses (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6427bd57-fcb2-420f-8b3e-d1da43470b84", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:45.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1468, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ainuo5affylw7gah48hs640la.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,", + "FriendlyName": "Hero Gal Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:11.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2012, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "diaifnoquzrh4x2mmkz1mf8zh.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,zw-evwOmg0q14P040EYYiA,a3I7EyRNVUilLfE6hriYtw", + "FriendlyName": "Hero Gal Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:24.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2100, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wBjDMVQgx0uxNDVdCLu4qQ.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,bhYZTPD1nEaEercLIZRucw", + "FriendlyName": "Hero Gal Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:08.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2101, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "P8mS2NTJS0e0H0VqDKvZUA.png", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,_C4RxcDBQE-mHh2oyL9vGw", + "FriendlyName": "Hero Gal Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1988692c-aae3-450b-acd6-e9015b831f5c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:10.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1470, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cls959ibdrc0xabl1rbe41fux.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,", + "FriendlyName": "Hero Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:18.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2011, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aiplwveih9o7f5q4ok7hr4a2t.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,-K940hWJXkee1HDrlurepg,ItJCR1utoU6i-syjJpxQqw", + "FriendlyName": "Hero Crown (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:20.963Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2103, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7ejV0XPkWEuQwpYQ8UBwQg.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,SdSjmCkq-ECoboR2PwxJfQ", + "FriendlyName": "Hero Crown (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:15.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2104, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vXnwcxkJbE-7MComJenMwQ.png", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,iGhiA5wU8kG3NH6P9NZvGg", + "FriendlyName": "Hero Crown (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:16.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1467, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6lr4h9o5e2b37vr6d9nn7tkw9.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,", + "FriendlyName": "Hero Guy Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:06.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2009, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "56tgu6gfzoy6bro091y5liea7.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,JY4zXsTbX0W1sU6g1o4kuw,Y-XQzHOjIkC_-it1j9m9jA", + "FriendlyName": "Hero Guy Armor (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:11.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "iVc5S7VHGE6_DP6V_NdRXg.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,W_AVltwf7kSzmftSsGdijw", + "FriendlyName": "Hero Guy Armor (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:20.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2108, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bGfb7o8B7U2W6pcvZdElGw.png", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,trFPTiehdUm2Q3bdvBQr7Q", + "FriendlyName": "Hero Guy Armor (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffad1160-8383-4b4a-a5b9-223476b49b92", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:22.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1469, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d6wng88epebl3fbw0v5p6llyl.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,", + "FriendlyName": "Hero Bracer", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-24T21:34:15.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2010, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "duphs7n0pfrpd3ttpnqv1dzot.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,oCeStQfVkE2ZwCcQjtV1bg,r94RX2106E2TRK2wQ-u8-w", + "FriendlyName": "Hero Bracer (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-20T01:07:17.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2112, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dfP4cWSPL0qZYIPW3aBHmA.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,nNJ8Tkg57k6pTS_0HSLkUQ", + "FriendlyName": "Hero Guy Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:28.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2113, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "xukCmMKpjUaYrqcBdb18OA.png", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,VmHQJIt7HkO4INJEXCBbNA", + "FriendlyName": "Hero Bracer (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa0e701c-5e80-46f4-8817-6e2dbf904734", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-15T22:20:30.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ffddbd79-0158-4f93-b8ec-dd5573c035a7,", + "FriendlyName": "Surfer Puka Shell Neck", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffddbd79-0158-4f93-b8ec-dd5573c035a7", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1672191014 + }, + { + "AvatarItemDesc": "5b2d0a6c-238d-43a7-a20b-bddcf14315f1,", + "FriendlyName": "Surfer Skimboard Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5b2d0a6c-238d-43a7-a20b-bddcf14315f1", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1582373149 + }, + { + "AvatarItemId": 730, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "cg6s53psdqilkxbzgkx72gri3.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,", + "FriendlyName": "Poindexter Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-07-11T22:30:21.907Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1647, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8c3gd19ccfwts3cqzv6pojvyj.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,lq59KZwElEStrviDEQLLcA,olyS074RHU-ojE0GzBBYJw", + "FriendlyName": "Poindexter Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:24.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2214, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "gYtC-UGMRkC9gKrg3689Fw.png", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,PSpysyn5gEKXCEi7f6gDoQ", + "FriendlyName": "Suspender Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "be8d0aa5-43b8-4b27-ae8a-9287036a9438", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-11T19:02:35.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2888, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "IMgfGlThV0q8ilEF8MG5ew.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,", + "FriendlyName": "Blue Sweater Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:10.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2890, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d6Ry5ZkLGke8F2jv7aYoxw.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,LVuUiGBtn0mp4Wfs-2F5EQ", + "FriendlyName": "Sweater Scarf (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:20.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2892, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XrXf6JrQaU6tG5MV94TFSQ.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,R8VYB2EGPkmQX4dbmGLuRA", + "FriendlyName": "Sweater Scarf (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:27.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2889, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HGZ5flmXY0Ow5pQH-q3L-A.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,wCCq14_wc06hkmjg6TTFxw", + "FriendlyName": "Sweater Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:17.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2891, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MsgTxG94f0-timzwekDXSg.png", + "AvatarItemDesc": "efc10aa4-a203-4465-a2da-eb4590ada8ef,0JVWqxXYoUqgI-5ORIr0bA", + "FriendlyName": "Purple Sweater Scarf", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "efc10aa4-a203-4465-a2da-eb4590ada8ef", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-20T19:13:24.203Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2756, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SWIYABA3u0KSKjMg4tbN1w.png", + "AvatarItemDesc": "97c8e2f3-e786-42e5-8852-4abec5dc645d,", + "FriendlyName": "Sweet Tooth Hat (Milk Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97c8e2f3-e786-42e5-8852-4abec5dc645d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:39.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2757, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WNWMQVjKaEC9CXqo_esiGA.png", + "AvatarItemDesc": "97c8e2f3-e786-42e5-8852-4abec5dc645d,_8OmgC7c80CsvQN41U6XyA", + "FriendlyName": "Sweet Tooth Hat (White Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97c8e2f3-e786-42e5-8852-4abec5dc645d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:41.62Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2758, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tGRQjD2mEKhf-jbbGpp7w.png", + "AvatarItemDesc": "39854685-4793-439b-9c64-c98825456a03,", + "FriendlyName": "Sweet Tooth Shirt (Milk Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39854685-4793-439b-9c64-c98825456a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:44.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2759, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NRd9D3di1kKciSpywTnJwA.png", + "AvatarItemDesc": "39854685-4793-439b-9c64-c98825456a03,qVeFYveZQEmobBTEMg1RTw", + "FriendlyName": "Sweet Tooth Shirt (White Choco)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "39854685-4793-439b-9c64-c98825456a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:45:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2583, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "wy-_Y3jSdUC9bKLa63iIpA.png", + "AvatarItemDesc": "a928c6dc-442f-4cbb-b705-cc5a3969b34a,", + "FriendlyName": "Teela Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a928c6dc-442f-4cbb-b705-cc5a3969b34a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:47.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2584, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yCbSGCAoBUyP8osA23LjDw.png", + "AvatarItemDesc": "f159e878-7563-46db-b59d-c838612a0a78,", + "FriendlyName": "Teela Tiara", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f159e878-7563-46db-b59d-c838612a0a78", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:49.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2585, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HwOW8GYsaESO0xHdf6rxQQ.png", + "AvatarItemDesc": "38e7e182-5166-42d9-9ed3-b525b98579c1,", + "FriendlyName": "Teela Armor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "38e7e182-5166-42d9-9ed3-b525b98579c1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-15T19:50:51.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 366, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c6y5y8wwcc2dcp81jcdqvac5r.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,", + "FriendlyName": "Tennis Shirt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-06-21T23:27:06.523Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 441, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "22s0jdhj08deg2hm1agcg0ap2.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,304e5cf1-5c94-481f-8e49-e6a943e6b300,90e0da8b-1941-4ca8-b7e5-b4e31921873c", + "FriendlyName": "Tennis Shirt (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T20:40:11.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1370, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1eu2yq3p1x77hmixyzmubdux6.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,NpMkoIK62k63QoLVEXId6g,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:15.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1371, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "duxjnjbu2n1zbarvejwuhhzgn.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,PLRhiOhhb0KhatFH7f3pwA,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:21.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1372, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8zfsf3e3asjbwc8dz1kq9acae.png", + "AvatarItemDesc": "74b91f2b-d4d3-4e41-8946-b1296ab7a773,RwgnKTfMEkWrAp_OMGy__Q,iT7UGMahoEu7BZZqeKXKww", + "FriendlyName": "Spring Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:10:26.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4g834pzncxe2djcfe8ri5ow6t.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,", + "FriendlyName": "Thrilling Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:46.337Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1529, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6iumwjj0hu0bnlky89lc0xdjd.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,7-lAnMa7M0u0n0nmeIINgw,6GfSjqIb4ECCvIPC8vO8_A", + "FriendlyName": "Thrilling Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:25.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2318, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "inGiSDO8JESERdMjtdk9-g.png", + "AvatarItemDesc": "hTHM3Kjqy0ykqBD8UCRQJw,8HR0DwDl2ECtHf5aEkqSHQ", + "FriendlyName": "Thriller Suit (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "dccc3185-eaa8-4ccb-a4a8-10fc50245027", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:33.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 821, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5zwxtidy5x8bl647kq837917l.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,", + "FriendlyName": "Thrilling Cuff (Red) - Available again until 10/15", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-09T20:28:54.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1530, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5tmz1m7roox1cl2u7cf3bgrnc.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,Q1knTl-n-EeAUwotFspNew,FHB5XHwNWka7aUt5km3LNg", + "FriendlyName": "Thrilling Cuff (Blue) - Available again until 10/15", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-06T01:20:29.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2327, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "FqT9BAyTM0qDBFE5vkveUA.png", + "AvatarItemDesc": "BR0mZKsKaE-t4R8CzEwuyg,B1IotYmwbkiUAfdZiqzq8w", + "FriendlyName": "Thriller Wrists (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "64261d05-0aab-4f68-ade1-1f02cc4c2eca", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-17T15:52:54.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2293, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "O2g6r5QKqk2REyXvLqLyeQ.png", + "AvatarItemDesc": "7b568bd0-ce59-456f-af69-537364a52f69,", + "FriendlyName": "Lightning Hero Suit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b568bd0-ce59-456f-af69-537364a52f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:40.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2391, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5MjNgKFX1EuMIDbmbkqxZg.png", + "AvatarItemDesc": "7b568bd0-ce59-456f-af69-537364a52f69,_fabGqrxrkW2mfx0bejHsA", + "FriendlyName": "Lightning Hero Suit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b568bd0-ce59-456f-af69-537364a52f69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:42.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2294, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ww6XZOkMQEGPfHNxwJ9pbg.png", + "AvatarItemDesc": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,", + "FriendlyName": "Lightning Hero Cape (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:43.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2392, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kk6TuKH2gEeN-PCQlSGzGA.png", + "AvatarItemDesc": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94,46WaeyRZnUCuLkEpxECRMg", + "FriendlyName": "Lightning Hero Cape (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8896a6f6-0f54-4f0b-aebe-7b121f3efd94", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:45.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2295, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2RiLsCbQ0Uubmw-2hVgnPg.png", + "AvatarItemDesc": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,", + "FriendlyName": "Lightning Hero Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:47.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2393, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "JLbQE-oojUqVUNpFNs_Cxw.png", + "AvatarItemDesc": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5,8NY4MevLp0C2lP178fIZ9w", + "FriendlyName": "Lightning Hero Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "703e486a-1329-4e7b-8f3b-d9db9b9db0b5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:48.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 133, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "66fwuamhzcs860kylwr1qfv8m.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,", + "FriendlyName": "Tied Scarf (Striped, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 462, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bcknoanm16wf9xuuxd8n236ez.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:27.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 463, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6qg6j1yialrvz8slxa9jeyu87.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:28.257Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 464, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ehbtcde8j9yfepf412zdnzsqs.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23", + "FriendlyName": "Tied Scarf (Plaid, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:29.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 465, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "15m86aopz28g8iezgelpzwej3.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:30.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 466, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9dfutygcqtdm7xo7lv2j8y5fy.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:31.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 467, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "a6a5lj41oooaly6njbbnnox5o.png", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e", + "FriendlyName": "Tied Scarf (Dots, White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5d1c42a0-7e36-431a-8e78-a8b74046d153", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-06T22:02:32.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1267, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f561z4j5s60ih6h3mp3me6ath.png", + "AvatarItemDesc": "05ec4e87-088f-4281-a118-2e63c2585200,", + "FriendlyName": "Business Tie (Gold Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ec4e87-088f-4281-a118-2e63c2585200", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:43.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1614, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cm7rzgd535abr6dvbm4wfb061.png", + "AvatarItemDesc": "05ec4e87-088f-4281-a118-2e63c2585200,Wm30oV5BtEuFQZFk3dPK_w,tfiSfpRez0y_R4QjAhO0bQ", + "FriendlyName": "Business Tie (Red Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05ec4e87-088f-4281-a118-2e63c2585200", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:46.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1265, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "76fk7csv3rzv0frnesf1d575f.png", + "AvatarItemDesc": "0d4e7ebe-efd9-4e68-9900-5645916e7596,", + "FriendlyName": "Business Tie (Silver Sequins)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0d4e7ebe-efd9-4e68-9900-5645916e7596", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:35.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2781, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yn-2YY0OvU-VMl9fBtdbuA.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,", + "FriendlyName": "Tiny Backpack (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:45.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2786, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TQGXsPW7Hki2VKZiU0b-HA.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,AtF5be-huUeADedbRurRcQ", + "FriendlyName": "Tiny Backpack (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:58.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VzzHEVAKM0ChjhmxaHnhow.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,T9XdU5xAFEqX3QV__SjXYA", + "FriendlyName": "Tiny Backpack (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:55.693Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2783, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e-QC6uq5zECcdfkIRxtpiw.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,cxWuO4qBtUOKYmCXI0D5Jg", + "FriendlyName": "Green Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:50.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2782, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9xjIqLGxDkOZsXIexIon3w.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,FIZdMEUgykOHb7fVlOfa5g", + "FriendlyName": "Black Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:48.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2787, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lfkspyvSk0CIkZx0gXhMog.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,5GEhQr7Iy0-kB0yCA3uJ6g", + "FriendlyName": "Tiny Backpack (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:14:00.897Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2784, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "y91-mA5ypkiHUOTxxjG6iQ.png", + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,wrobkXV-10CD1JJtgyy6Fw", + "FriendlyName": "White Tiny Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:13:53.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,8e5280a8-7771-4278-b985-c325f2490f9d", + "FriendlyName": "Tiny Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1327578016 + }, + { + "AvatarItemDesc": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77,545ed5e0-beab-46d8-9d14-7de0420ff90d", + "FriendlyName": "Tiny Backpack Shoulder", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af4bdd0b-3c30-41d6-9bf2-167a5a47bb77", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1500824834 + }, + { + "AvatarItemId": 1504, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "49614bvc1m6rzznnvix44dnbb.png", + "AvatarItemDesc": "00f0b914-aa22-4d14-9af1-996e1af61f4d,", + "FriendlyName": "Tiny Crown", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "00f0b914-aa22-4d14-9af1-996e1af61f4d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-17T20:05:46.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2068, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "higzQQvRGEa6dDK9f0TTvg.png", + "AvatarItemDesc": "591af332-a01c-454d-89dd-5ed6ae3ed09c,", + "FriendlyName": "Lucky Clover Top Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "591af332-a01c-454d-89dd-5ed6ae3ed09c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:25.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1897, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "56r92qv64w04tvm8920bhyjlv.png", + "AvatarItemDesc": "2f77696c-618c-4fb8-9220-33839cd3bfcd,", + "FriendlyName": "Tiny Top Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2f77696c-618c-4fb8-9220-33839cd3bfcd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-30T21:02:30.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2730, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "K_CvxmWWCkugsiOwB8nZAQ.png", + "AvatarItemDesc": "0bd750d9-82ff-4d77-9f2e-7e2651316f51,", + "FriendlyName": "bwahhh", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0bd750d9-82ff-4d77-9f2e-7e2651316f51", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:30.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2729, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "UNu6rZ-7CU-S5MeZzybZKA.png", + "AvatarItemDesc": "74902dee-08a5-4a8b-a113-80f450a0de0a,", + "FriendlyName": "aaaaaa", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74902dee-08a5-4a8b-a113-80f450a0de0a", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:27.613Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2731, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EwmmD_7_NUeiFXLFKImuQw.png", + "AvatarItemDesc": "b0a2e988-9942-4c52-a714-f1bc12fa6290,", + "FriendlyName": "Rec Rocks Shades (Tokyo Machine)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b0a2e988-9942-4c52-a714-f1bc12fa6290", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-10T00:14:33.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 72, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7beqslha3807spvktpk1rat13.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,", + "FriendlyName": "Top Hat (Black, Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 645, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "d8wvm262ivsmterpwyjcf0d3i.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,bd4a84e2-b67a-4269-a26a-17fb23ddb09e,ccf1ccc1-e229-4157-bb74-f2cdef01e547", + "FriendlyName": "Top Hat (Black, SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-04-30T23:07:28.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 915, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e8kcw6vyqqzb5temhwr493mtf.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,DzgN67HoikexE2rTlZLdYQ,vm1ESK9ls0SaxSwHplNALQ", + "FriendlyName": "Top Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-26T19:13:49.663Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1732, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3nihpsb8d8hrh8ovb5pzd5f1a.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,xeK_acj2hE-sYGhsMRwjhw,HwhHShudGkGjXagJxCN2uA", + "FriendlyName": "Top Hat (Patchwork)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:16:15.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1801, + "AvatarItemType": 0, + "Tooltip": "For earning $10,000", + "Rarity": 50, + "ThumbnailImage": "5948oo28re9hcl2wswbr309wb.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,Em__GeYWaU2pAEqWYCBKpA,DG83ruIdI0Wu-GSRg4icYQ", + "FriendlyName": "Top Hat (Gold)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-28T16:11:46.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2482, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zcove5uhGUSvwxXtkvlWow.png", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,n8t0gIYFbUaPMhyQeWkPSQ", + "FriendlyName": "Top Hat (Galaxy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:32.06Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1374, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8zss1xn4q3bwumq65zgy4xq5g.png", + "AvatarItemDesc": "7dc6c916-b887-4929-93b5-dcea3022e6a9,", + "FriendlyName": "Top Hat (Wonderland Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7dc6c916-b887-4929-93b5-dcea3022e6a9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-01T19:11:18.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1356, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1h9wa7v6ma9wzhwdgvumqlyp3.png", + "AvatarItemDesc": "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03,", + "FriendlyName": "Top Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-03T01:43:48.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 972, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3ki0gazw736edmje4kpkv8bvb.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,", + "FriendlyName": "Traveler Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:40.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1430, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "31sglupqv6cqh6rzb8embhfwz.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,Mt2a9ygWPkS-DyXj1q-Kvw,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:41.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1431, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5eujgf1g27oi4kl20v8si25ry.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,LEUQnDz-k0m-hY-AX3Z9Sg,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:42.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1432, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1dcjmvlujpg9zj2zccxxccpcw.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,cSFt9MWLj06CDzIzATqrTg,znNiHfNdTE-szOIAnIWFQg", + "FriendlyName": "Luau Hat (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:44.383Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1433, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9ozq264dxzs2juj87d64edd1i.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,B6qNcZQ770eJel13RskuIg,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:46.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1434, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6qw0otr02xea0hnbor3sp5kas.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,p3g8YAOhTUKAiE-Wh-X-aA,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:47.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1435, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5seeahrp9r3hyie3y148dclsb.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,SqHAYcqBXU2PtF2urWGruQ,sIP83lqv3kG31TH5Rw4gqg", + "FriendlyName": "Luau Hat (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:48:49.483Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1731, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "can59da4g2r699415y5w5vr03.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,Pu_1I6Zq-0G1HBMHa5MrKg,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Tourist Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:57.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1730, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6v72ng5qy6zmim5nx1i6k7hy2.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,7Rw_gt9IUEOYsvVcOTbVBw,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Tourist Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:55.007Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1729, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e25fr88d2412i6acn3s2e2khj.png", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,aiBfwCNwpUyJ0rYKCMa7qw,Cjy-bE0PikWLCRrL4U2NGQ", + "FriendlyName": "Traveler Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "4fa503d1-39d7-4bc5-a29e-97d96a811ef1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:14:51.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 974, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eati57vuo2tgumb24q5t0je5y.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,", + "FriendlyName": "Traveler Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T01:55:53.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1424, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0x7hflju5oo8crqrrj3r0b5so.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,otQaNOsODUSTfHlCX0IVOA,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Lily)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:46.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1425, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bp1h0205pg13g8p8aj2j9vhdi.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,NKf5-r2S_0iP0SaO9bjvaw,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Paradise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:48.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1426, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "chd2q6f8set0nr8jh2vr2m7qd.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,adJVA26PyUSVvmZgxkCAmQ,0WOBGm34H0ySBC10VToa9g", + "FriendlyName": "Luau Shirt (Poppy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:50.153Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1427, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "56zylqig2v1wahn347cxlipyg.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,4ijeE3LqUUuSplKrMto6Kg,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Begonia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:52.38Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1428, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9896kxku8xyotg3kpo30sep01.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,d27e5m1BLUWI42Pxnj6F4A,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Dahlia)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:54.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1429, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5a104x4ogdqr0eqzaqirydl55.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,RZFWcTzxPkOfUuTIQ1mzkg,ytfIq0TiYU6EUdd0ruaGAQ", + "FriendlyName": "Luau Shirt (Primrose)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-04-28T22:47:56.323Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1725, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3gp6gte16x8khurqqu7nzhfhd.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,XT_txjZfIUOT400UPHLaRA,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Tourist Shirt (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:05.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1724, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a744qz6lz4fslmbrp602yan1l.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,iEvS6I9LjkSaGAmjcxz8uQ,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Tourist Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:13:02.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1723, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "87fumlw57etytmbtflbzgbrxm.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,gwdtq3NiEEmMKYtn1VPoiw,M-mi8QUHwEeIewXE8uAlrQ", + "FriendlyName": "Traveler Shirt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:57.68Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1202549920, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_a0Yxn41bUmWYogStG-J6w.png", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,6taO0KRM_kuKoaTnOIsHRg", + "FriendlyName": "Raindrop Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.543783", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,7WLoulmRgEKX3Eshtn_cxw", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1718528065 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,BzzPuw8i0km4VQLd77mXEg", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1462034470 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,er8BswhSkUqh5TVV5fDAng", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1696359360 + }, + { + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,zUZHddmmhE2rXfCVuz_GGg", + "FriendlyName": "Tourist Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "32a534bc-b276-4acc-965f-04fe96470a48", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1381816991 + }, + { + "AvatarItemId": 976, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "35n8yj17afa5u5gc5w40oz7de.png", + "AvatarItemDesc": "cbN5e-t_jEKa4hzir5JAxQ,", + "FriendlyName": "Traveler Shades", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7b79b371-7feb-428c-9ae2-1ce2af9240c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-06T19:10:59.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2154, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "BO1szcANSECxRSF0upetrA.png", + "AvatarItemDesc": "59bbb9d4-4fdb-4400-90d5-244643dca4ce,", + "FriendlyName": "Toy Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59bbb9d4-4fdb-4400-90d5-244643dca4ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-30T00:16:23.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2485, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "invasion", + "ThumbnailImage": "9aC9wW3rVkKmZlMNJSwZkg.png", + "AvatarItemDesc": "d834be2b-b107-49aa-b0ec-b54bdb4c61aa,", + "FriendlyName": "Science Iso Helmet (Invasion)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d834be2b-b107-49aa-b0ec-b54bdb4c61aa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-04T17:41:40.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2004, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70dggr2u22pi1bpl844m6ozue.png", + "AvatarItemDesc": "2b7a32da-eda1-45e1-b28a-f9f882cda7fa,", + "FriendlyName": "Toy Jetpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2b7a32da-eda1-45e1-b28a-f9f882cda7fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-12T22:10:14.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 251, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "5gm9mbr01cwr3bmzl4nsv5ko7.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,", + "FriendlyName": "Tracksuit (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 501, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "essmk8w84jurmnzu791bfueh2.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,026ca50e-3a81-43cf-87f5-950687b7bbdd,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:25.457Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 502, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "bvsa18aen05ajtkzw2h9y672b.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,6584dada-499e-4d7c-958d-466955b20640,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-07-11T22:35:26.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1648, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8v9x6a68s45783jrnnqykohcu.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,uUOO-k5KS0mU7B5BLaOpnA,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:41.88Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1649, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "222uif2yybv04a5a1ialb8rze.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,Ijf-j7EgEkGYOKo9SQNLRw,adae82eb-a4b9-4541-acf2-76cea839a593", + "FriendlyName": "Tracksuit (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:55:44.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2182, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "SPNmVkKb1UWv0spykc088w.png", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,J7l1YwWjqkutybwykwih7w", + "FriendlyName": "Tracksuit (Squid Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:41.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 763, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "1265j9ozr0rlgadvfmpq5yyzv.png", + "AvatarItemDesc": "-SQLtL4rdEWTJdw2IroQCQ,", + "FriendlyName": "Treasure Hunter Hat (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b40b24f9-2bbe-4574-9325-dc3622ba1009", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-08-23T00:08:02.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1704, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4nzqsmjss5f6hoabn1ihimj3.png", + "AvatarItemDesc": "-SQLtL4rdEWTJdw2IroQCQ,CoSVCyrI-0qg-EkqB2R0hg,JO9lJOQSREWbPyDBZH_cyg", + "FriendlyName": "Treasure Hunter Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b40b24f9-2bbe-4574-9325-dc3622ba1009", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:20.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 779, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5jvdbqiv7unk1h9fed6w0xzm1.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,", + "FriendlyName": "Treasure Hunter Shirt (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:04.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1698, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "4mi4ucsloz64q4qidq36cfyf8.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,H0sqS2_VPEieVrHSyMNo7g,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:47.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1699, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "05ockzuu7hohv73owaltasco3.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,TjeZwaUp10uhM72q8j4R0A,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:49.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1700, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "6gg5wzc84qgzuedb40n52oe0i.png", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,ZlO_DqkSKUStV4-gGPvQOg,9HVoTIf90Ea8_czICVg9AA", + "FriendlyName": "Treasure Hunter Shirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "06a5ae40-ff55-40c1-a249-b27d46423074", + "FriendlyNameSource": "avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:51.073Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 773, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5h0gufhuvlgih1xqqy2oje03x.png", + "AvatarItemDesc": "t_66r_j5oUSh9awIPmdUKg,", + "FriendlyName": "Treasure Hunter Bandolier (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afbafeb7-f9f8-44a1-a1f5-ac083e67542a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:56.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1705, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0s4l4l3cvdmfvz1eza6ts5dia.png", + "AvatarItemDesc": "t_66r_j5oUSh9awIPmdUKg,YTlnBuEjhUG1hSfAs8qwuw,IcR9Z_MxKkS9uh4uFYuFzQ", + "FriendlyName": "Treasure Hunter Bandolier (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "afbafeb7-f9f8-44a1-a1f5-ac083e67542a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:26.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 774, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5vu32ph9qbrum8aq8hkvaa7d1.png", + "AvatarItemDesc": "ImwG0snB9ECc_gqRRBoz6A,", + "FriendlyName": "Treasure Hunter Belt (Brown)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2066c22-c1c9-40f4-9cfe-0a91441a33e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:07:57.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1706, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7p5hywqgv256aipqmufecvfc4.png", + "AvatarItemDesc": "ImwG0snB9ECc_gqRRBoz6A,lAjUeUtTLU2-3bG2OXBukg,LFB_YqTym0-tQmUIj-YcYA", + "FriendlyName": "Treasure Hunter Belt (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d2066c22-c1c9-40f4-9cfe-0a91441a33e8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:52:30.783Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 780, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "d6no5z0xu0alfa3sfd7tchss3.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,", + "FriendlyName": "Treasure Hunter Tank Top (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-09-05T23:08:05.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1701, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2squicogn0nbezwccj6ahi6we.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,3qu7mxSNpkKRrYMhsWHVGg,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:52.473Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1702, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5fzlk5daghmydxowkt8y6x3ba.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,Ij8MXrl1Okip4M7y-gF23Q,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:54.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1703, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ets8ha47bzly0bvux6s0lw1t7.png", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,qEojxjSXm06CX52hRDJAew,K5NXVecrqk6Nb6SkBu08ww", + "FriendlyName": "Treasure Hunter Tank Top (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8bc53a4e-c5a4-4085-b9b5-05d691988af4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-02T23:50:56.617Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "ffdda4d3-a768-4335-a883-aa3f9dc897bf,", + "FriendlyName": "Trihawk Hair", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ffdda4d3-a768-4335-a883-aa3f9dc897bf", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1335410714 + }, + { + "AvatarItemId": 982, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bfcff0ql8rwt35vnexuh55sc1.png", + "AvatarItemDesc": "ReqOfohe6UamzaiHG1x3Ow,", + "FriendlyName": "Trucker Hat (Pickup)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e8eea45-5e88-46e9-a6cd-a8871b5c773b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-02-26T18:15:18.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1365, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4dquic4wqv5qwn7664v0i7fpi.png", + "AvatarItemDesc": "ReqOfohe6UamzaiHG1x3Ow,lDXambcP60CeY2C8qeGDFw,uiAwVt3HK0mRVtWp9B-gTg", + "FriendlyName": "Trucker Hat (Flatbed)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e8eea45-5e88-46e9-a6cd-a8871b5c773b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-03-17T19:31:41.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1206, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8b8w29yppr8ire3rmnz7dihi2.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,", + "FriendlyName": "Trucker Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:09.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1207, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0xzrzv7jtrqilh8yio1uf4b27.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:13.493Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1208, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "c5h60lx2j7phvk8hmvih50s2o.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w", + "FriendlyName": "Trucker Jacket (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:16.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1209, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "71nsut92sx3eet8hzyon7q6mt.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,o2WfRyjJy0CKoUg8YIxNjQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:20.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1210, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3ko6st266abg6sg11o1nsxdxj.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg", + "FriendlyName": "Trucker Jacket (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:23.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1473, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "a0lqwqfuolj7hkvhf1glhqc15.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,sscD2O3LEUOAKkqjGpGe8g,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:33.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1474, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6v3w4z6b6gofs8vtz0up7olo8.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,hRxyLsKfsESFAsXAyZQjoA,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:37.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1475, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "1c2vx4eqcpf3ulizhf8xymy5k.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,kgCKjtSjFEmFq9ez2llxKQ,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:41.23Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1476, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "22a6bzdqbg2mnctnuw24oqszc.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,KaZqNx5VKkarRrOJcTuD9A,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-07-01T22:04:45.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1722, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "3oy21va74rnfmipzz83x24n3q.png", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,eOJkmpSL4EypK-AQz7j8gQ,uZjxYOMa10GzJ9wQkRge2w", + "FriendlyName": "Flannel (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8b065e2a-b106-4f11-b415-4c47ce87993e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-03-15T18:12:48.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1211, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "evjppeeirfwnfcqr1gdexa1og.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,", + "FriendlyName": "Trucker Cuff (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:28.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1212, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9z0jwf8oh2kcafax37xcxp68x.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,yA0KhVg19kWqG1UkyxQogQ,yBNsYcZXzEiuR0u3tWnnWQ", + "FriendlyName": "Trucker Cuff (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:32.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1213, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3d4u0uq00scnglmtqdwyvp55u.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,P9bBSL0YLEyjN8PxMfgbKQ,yBNsYcZXzEiuR0u3tWnnWQ", + "FriendlyName": "Trucker Cuff (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:37.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1214, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "emod4iboh8jp8hcuu07ylugks.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,o2WfRyjJy0CKoUg8YIxNjQ,sHyNs-dpc0-_Ah-HJVczcw", + "FriendlyName": "Trucker Cuff (Tan)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:40.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1215, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4hs50cqx2o2w24tay05gzsxem.png", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,bgRKH1wfqkWVHcuSnFYnlQ,sHyNs-dpc0-_Ah-HJVczcw", + "FriendlyName": "Trucker Cuff (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "973d84ee-5b88-475c-b579-a76c34a7c533", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:06:44.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1347, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bqm1574wf7rpdwjlok49b0p77.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,", + "FriendlyName": "Mechanic Overalls (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:45.03Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1348, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "5ka8fpkswhsrqk4yzqu9zq6hx.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,iokdlBfjuEuAwqIMOQwxdA,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:51.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1349, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "dvw88qyim2kmbtb852qiitri8.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,nKauYAyWhkm2AMHFyC-fLg,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:57.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1350, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8l97dx6qhjdo9t7s2dpksymii.png", + "AvatarItemDesc": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc,EiGgN7uVukqoRE7UJcMLTw,H3OQDBef1UaacSD52p8Hdw", + "FriendlyName": "Mechanic Overalls (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:51:02.18Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2021, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "deVNr6Y5KU6p3WI9O9RCpw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,", + "FriendlyName": "Tucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-26T17:42:24.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2022, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "yD9WlEoR7Uuxoz4pegMXnw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,iIjl6PamKECL_JWvrZNHfg,2IFVtBYV8EqQXs8XHqeqdg", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee Shirt Latinity Mask Tucked Tee Shirt Latinity Swatch", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-01-26T17:42:27.27Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2049, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ubK_rEh-IEm7I_Dm8X_6hg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,vtXUyu-xmkaYtOCL9r3PsA,VtepaI2fvUWhcvNmpQ8r6Q", + "FriendlyName": "Class of 2021 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:37.92Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2069, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "fH7sFkQfakOQNGz1LNIeDQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,r4O0JuB0dke1IoOEqW4nkg", + "FriendlyName": "Art Festival 2022 Shirt (Gray)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:27.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2070, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HafOMtY0B0eYptZba4mZ2A.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,UJRSCFfPq0K0yV0ETS3uSQ", + "FriendlyName": "Art Festival 2022 Shirt (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:29.64Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2157, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3JZYjcH1rEilhQIrqVialw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,3bWtQstEmUeHJXngec632A", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee 2 D Contest Gold Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-30T20:55:50.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2156, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "tgdvdzDpgkOt5c2sU-g6og.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,NEU8mYk_TkqS78l0zY9GAQ", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee 2 D Contest Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-30T20:55:48.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2096, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "5xSIXFJJUEm8gEi413DBeQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,K4_AIRWX-U-f-nXsBOc82g", + "FriendlyName": "Tucked Tee Shirt) Tucked Tee Shirt 8 Bit Logo Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-03-09T20:24:32.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2250, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "eAx_UfHqTEmu_aRnqNXzGA.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,9lPZEIVbbUK1hdR1aVPLwA", + "FriendlyName": "Cowboy Boots Shirts", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-22T16:53:09.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2813, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ffb9sMGT0EqSNh85meeoJQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,W_RwnySBMk2GlrknM7NDrg", + "FriendlyName": "Class of 2022 Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:05.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1489631510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "B0Y_RojdNUWlUY10xCmzCg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,LXR5H_-7JUO7F3JkJh3ouQ", + "FriendlyName": "Strawberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.545778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1413305249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_TRYe6zmekyTjU6_Sa9d5g.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,mKWQMgBuTEq9EwXBTusuJw", + "FriendlyName": "Tucked Gamer Tee (Pineapple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1176199818, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qHJWmNTPqUag0af__vphuQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,N2ofIXeSBECHTBAoP2yQ6w", + "FriendlyName": "Grape Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1198761907, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LmTBToLyKUyYe-lIJ4jKiQ.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,rg6sMjE1XkijY4Pq73pjcg", + "FriendlyName": "Lime Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1755749293, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DTvdUNjKGkmhKbNFYXzdkg.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,t9-7AjmLgEWawcyWCD4xVw", + "FriendlyName": "Raspberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1511681798, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qEEw3iKEAkmL_Kmj7syfCw.png", + "AvatarItemDesc": "618b979e-c6e6-434e-98e5-9b51e117fb69,5Q9E76b580CnMWIg6zbPwA", + "FriendlyName": "Blueberry Gamer Tucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "618b979e-c6e6-434e-98e5-9b51e117fb69", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.546778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1234, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "3qareqet5g5gwgynxe4tw9gub.png", + "AvatarItemDesc": "9b5406c4-8589-47e4-9f69-4fa4733615e7,", + "FriendlyName": "Turkey Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5406c4-8589-47e4-9f69-4fa4733615e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:19.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2486, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "mt3RP76mEkGxwfRV3p8aoQ.png", + "AvatarItemDesc": "9b5406c4-8589-47e4-9f69-4fa4733615e7,yXDNXmcJvUC4eCx9oGItcA", + "FriendlyName": "Turkey Hat (Snowy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9b5406c4-8589-47e4-9f69-4fa4733615e7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:01.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2531, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "PcxjLeS1vEKxB0-e2aPilg.png", + "AvatarItemDesc": "5da97d5c-0d1b-4b0d-a2be-d255871d833d,", + "FriendlyName": "Turkey Backpack", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5da97d5c-0d1b-4b0d-a2be-d255871d833d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:40:04.9Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1570, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "4g2r02n1g5w09re7hl1ba2yyi.png", + "AvatarItemDesc": "002a0f2f-1a24-4439-b578-470818ef8325,", + "FriendlyName": "Turkey Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "002a0f2f-1a24-4439-b578-470818ef8325", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-28T00:37:27.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2491, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "thanksgiving", + "ThumbnailImage": "NjxKDOlshkq09A4VSq5gAQ.png", + "AvatarItemDesc": "002a0f2f-1a24-4439-b578-470818ef8325,d-scjeOeCESC0rQMKsozsA", + "FriendlyName": "White Turkey Sweater", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "002a0f2f-1a24-4439-b578-470818ef8325", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-11T18:02:14.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "b5a83be1-fd78-4797-b3e4-e5fa9730fab8,", + "FriendlyName": "Turntable Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b5a83be1-fd78-4797-b3e4-e5fa9730fab8", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1787211725 + }, + { + "AvatarItemId": 1193, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cccfr6msg1shob4s8fngcwqa6.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,", + "FriendlyName": "Turtleneck (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:53.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1194, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "18z0oxaf9r5hu5604hnfsb5b3.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,9b9t8_aKH0-gQXMhZycaOA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:04:58.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1195, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "aafnbk70rbkwsh5xsq4e8fk2j.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Zz4XatKfUEapReRAQHxKKA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:16.427Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1196, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "biyvmvzdwez09j9045jip174p.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Jt0QqNrAp0qzUR_RQoivVg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:20.24Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1197, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8o2jy3y3nfrfunsiqadtg7716.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,lBePHNL9LkyaAjEFyFE8Sw,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:23.957Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1198, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4aqh17g64pqv044ldezph3z37.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G4HIPUjekUigiU0-Kr9mdA,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-10-23T21:05:27.35Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1540, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "9d1oi3isvlv9xgg27pjqx7xbv.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,5mcp7-2ZEE6n3-zN3_CoWw,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:26.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1541, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8hdi7uyevqrhqw4zfu5knwqb5.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,R0UCacpdhUS906Xf7l142A,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:30.37Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1542, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "ackem1gqqmknsbpbqr1r0opsk.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G-VKgepBkkeATcwCaS6Rwg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Grey)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:33.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1543, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7nm5wdsxclddyf0y5up03zpg0.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,1LonkCbt4kykeAScYKpwyg,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Navy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:36.463Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1650, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "7sv7ea1zgexf9hov8dp1ky7im.png", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,IXWd396grEOWHt32L7fCpQ,rzoH8P44a0esN-HnYd3aQg", + "FriendlyName": "Turtleneck (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:56:12.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1087, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7swwjtbh6ko8afp6d0nmwspsn.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,", + "FriendlyName": "Tutorial Cap (Bachelor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T17:20:17.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1127, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b0ck22y2gp6w3ppasjughn453.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,gDH1WmW5rk2RdY4wJYYZow,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Tutorial Cap (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:58.513Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1126, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5y6bqiyhj3hhqy92tqzvjxo6l.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,wHJnbDOo8ESmUbQDr0k31w,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Tutorial Cap (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:46.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1247, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b9enslfg9k5mul9t9zukxjibd.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,GK3yolCNlEm3B_5DtHkE8w,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:15.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1248, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "amll8glk5rg14851jcmr390x4.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,U2ZbHn8WJEySUXVpzAY6ig,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 1)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:17.82Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1249, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1c3y650gb6irf7rhmbs9cf457.png", + "AvatarItemDesc": "M9weaekvIUimiJSVJqZl-w,L333w3dfIEWDbtt1Yn1gBg,GV8z2dlqGEqIAYaGrF1ZYQ", + "FriendlyName": "Teacher's Cap (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "691edc33-2fe9-4821-a688-949526a665fb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:20.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1099, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "37ulwbdd8vskdogo0hv1kascn.png", + "AvatarItemDesc": "ASHnWS0h9kOJhbOUwxWKFQ,", + "FriendlyName": "Tutorial Gown (Bachelor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59e72101-212d-43f6-8985-b394c3158a15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:25.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1256, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wia57rfapa2pl00ngn2vtv2t.png", + "AvatarItemDesc": "ASHnWS0h9kOJhbOUwxWKFQ,U2ZbHn8WJEySUXVpzAY6ig,6nmHu9zj8k-tEz0tBHblnQ", + "FriendlyName": "Teacher's Gown (Level 1)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "59e72101-212d-43f6-8985-b394c3158a15", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:54.017Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1098, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7x241yml5e8ojjul82gsjcu8c.png", + "AvatarItemDesc": "MJQiQ3LATEOymxCC8wfSjw,", + "FriendlyName": "Tutorial Gown (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43229430-c072-434c-b29b-1082f307d28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:23.857Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1257, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ac5q83c1bed0a7fr6fqc9zz7e.png", + "AvatarItemDesc": "MJQiQ3LATEOymxCC8wfSjw,L333w3dfIEWDbtt1Yn1gBg,lSDrKR69rkSFULy9GA__1w", + "FriendlyName": "Teacher's Gown (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "43229430-c072-434c-b29b-1082f307d28f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:58.537Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1097, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bogc32ezqq4e260fqnwn6hunt.png", + "AvatarItemDesc": "0yFzgVESokC3gFd3hrS8bg,", + "FriendlyName": "Tutorial Gown (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "817321d3-1251-40a2-b780-577786b4bc6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:21.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1258, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bsih24pcifs39vkgb3nj2y01r.png", + "AvatarItemDesc": "0yFzgVESokC3gFd3hrS8bg,GK3yolCNlEm3B_5DtHkE8w,yKb2A8M_m0GQDhFWCRUHTQ", + "FriendlyName": "Teacher's Gown (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "817321d3-1251-40a2-b780-577786b4bc6e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:15:04.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1118, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5frgjfs2olhxlc6p6k1xzi1gc.png", + "AvatarItemDesc": "vP9EKTMXP0yni8U7uCz2-g,", + "FriendlyName": "Tutorial Sash (Master)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2944ffbc-1733-4c3f-a78b-c53bb82cf6fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:43.497Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1252, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3sux0cg60z82am4w6irvfph44.png", + "AvatarItemDesc": "vP9EKTMXP0yni8U7uCz2-g,FsATqzRVekO9hQ3u_q7keg,Q7hqvEo7DUyD9AqIOunWWA", + "FriendlyName": "Teacher's Sash (Level 2)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2944ffbc-1733-4c3f-a78b-c53bb82cf6fa", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:31.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1117, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f20t6rhmbvzsu0byj0ag8itk7.png", + "AvatarItemDesc": "O8VqC11xNkSKpDh6Omp_0w,", + "FriendlyName": "Tutorial Sash (Doctor)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b6ac53b-715d-4436-8aa4-387a3a6a7fd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:59:40.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1253, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9y51i5ld8baokfpueqd67oc07.png", + "AvatarItemDesc": "O8VqC11xNkSKpDh6Omp_0w,mvj5RDTuh0qoql0OyBFI9g,YQQamy-tQkCkpZPXKepUrQ", + "FriendlyName": "Teacher's Sash (Level 3)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "0b6ac53b-715d-4436-8aa4-387a3a6a7fd3", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-12-03T01:14:37.863Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2084, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "rZx9UCnz6kWaUo6z050exQ.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,", + "FriendlyName": "Tutu (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-02T19:44:29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1738760043, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ts-p4lQTo0qU3LNY7YIW3g.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,UjSxxUK7z0GGdjvI_NsJ3Q", + "FriendlyName": "Butterfly Tutu (Monarch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1968011120, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0ItXnzo230qoec69LEGbBg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,lBkOSQNDZUOvoknntnEMew", + "FriendlyName": "Butterfly Tutu (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1920210778, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NpnrKR1oZke3h0i8odmHGg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,sq5Qv4nVAUqzhMdB7U2MVQ", + "FriendlyName": "Purple Butterfly Tutu", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1044129999, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kafLNepzrUqLnxh9kG5Nxg.png", + "AvatarItemDesc": "f28aa59c-d6d7-432e-9863-f894dc0e8e32,6fp7nDdMvU6Ig-EmvjMpIA", + "FriendlyName": "Butterfly Tutu (Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f28aa59c-d6d7-432e-9863-f894dc0e8e32", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.547778", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 858, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b1blz138qua11xtiprlgui282.png", + "AvatarItemDesc": "jUpFzO5MPEGTbS-E0a99Nw,", + "FriendlyName": "Dracula Jacket", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cc454a8d-4cee-413c-936d-2f84d1af7d37", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-14T01:26:26.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2610, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Wir5kzMxjkaVf8C17Tg5wQ.png", + "AvatarItemDesc": "a994fc19-974e-444e-ae9e-c2f6c32432bb,", + "FriendlyName": "Midnight Gala Tuxedo Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a994fc19-974e-444e-ae9e-c2f6c32432bb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:21.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "246e6caa-c94c-420d-9805-9f0282c6d052,", + "FriendlyName": "UFO Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "246e6caa-c94c-420d-9805-9f0282c6d052", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1580435119 + }, + { + "AvatarItemId": 2918, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "m4UIuZjNzEWsCP1gpZBgjg.png", + "AvatarItemDesc": "95a519de-f2cb-429c-b014-508477f20d42,", + "FriendlyName": "UGC Pullover Hoodie Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "95a519de-f2cb-429c-b014-508477f20d42", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2023-04-07T17:07:07.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2184, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "KXfytDhXzES2yco-rwqDSA.png", + "AvatarItemDesc": "c5d70cb4-71dd-4fe4-b719-34fe2073c611,", + "FriendlyName": "UGC Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c5d70cb4-71dd-4fe4-b719-34fe2073c611", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-04-19T23:40:30.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2195, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5K9fUPIZ1kaQahcIYrlZCA.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,", + "FriendlyName": "Umbrella Hat (Rainbow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-27T16:02:36.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2267, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qcRLmi1ST0OCkd8H8YzgYw.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,Ii2WNQxqtUeoIfGItUkVbQ", + "FriendlyName": "Olden Umbrella Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-07-12T17:03:23.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2828, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_ER1egNWrEmQDiXRt-SAmQ.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,U-KT3nJlW0-JT8GSQuQXhg", + "FriendlyName": "Umbrella Hat (Mushroom)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:59.593Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2827, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "D69pyDco3kKsZJf_pN2FHg.png", + "AvatarItemDesc": "c6ae0a86-e590-4097-b77e-ba6fce83a826,3L-AGgM50kCCz2V3UJtErA", + "FriendlyName": "Umbrella Hat (Ladybug)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c6ae0a86-e590-4097-b77e-ba6fce83a826", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-23T22:04:57.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1060, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6xa1zsivxbibzmgrkhclxi34k.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,", + "FriendlyName": "Unicorn Horn (Spirit)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:16.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1061, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "43eqpst8khtmcw02hx4g56yts.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,FNe9LkWJMUeC3frYJMyEaQ,nW_fBsuZA0OfIjTMXm6ZCA", + "FriendlyName": "Unicorn Horn (Harmony)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:19.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1062, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8txs4ivnem6d79jibl2zf6qtx.png", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,mudt-Z5-5USdnIq-xaDgOw,nW_fBsuZA0OfIjTMXm6ZCA", + "FriendlyName": "Unicorn Horn (Joy)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "bd87c6d9-3554-40bc-ab3e-ff9871dc5dc8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-06T19:25:21.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2116, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "rgbXleaBw0Wb9fZ3vTUtbA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,", + "FriendlyName": "Rec Room Untucked Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:28.947Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2117, + "AvatarItemType": 0, + "Tooltip": "Mr Beast's shirt", + "Rarity": 50, + "ThumbnailImage": "CF81YKO-nUGbzKI3slBEtg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,vjO5ctMe0UC8GN6p61IC4A", + "FriendlyName": "MrBeast T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:30.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2170, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f4svRkfHhE-6a9L_W0YSwg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,hbF6ChMyEk-L8O4f6o_l6A", + "FriendlyName": "Sports Fan Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:53.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2169, + "AvatarItemType": 0, + "Tooltip": "Rocked out at Rec Room's Prom!", + "Rarity": 50, + "ThumbnailImage": "LA32UPI01kqXUcm7xSrlmw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,rQaQALql2E-zdM8072gfCg", + "FriendlyName": "Prom T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:50.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2216, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "NkEwfQcQFUiQ_C-0VKPJhQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,y78S5o6VXUGva1Wpd6sp3A", + "FriendlyName": "Untucked Tee Shirt) Untucked Tee Creator Reward Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-05-11T19:02:40.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2241, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "E9mf1x4jp0yyg72cw7s-OA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,S_dLd3dzvk2hM6ZA2DKTWg", + "FriendlyName": "Tennis Ball Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:38.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2240, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "LvZbawPbAEWmg5aVGi-M_Q.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,GU267AQ5T0m1jco3mNw_uw", + "FriendlyName": "Maker Pen Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-06-14T16:33:30.353Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2298, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "VasCRGTf-E-zXE53kLLl1g.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,SV2UmeQDjEaMK_A2Q4tt3w", + "FriendlyName": "Rec Royale Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:56.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2296, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "pVwmecVkBEm37eRY3Jc_Qg.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,V-vCW0ICl0Ke3QPAGo9GSg", + "FriendlyName": "Bee Yourself Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:49.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2297, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dH9FOddSYkSjUWt2Ux_kSA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,g_f9bH8tykGq3yRwWuQPOA", + "FriendlyName": "KREQ Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-10T15:40:52.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2688, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ZOvGH1iQ_kqzVB8EkYBpeQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,Eh1jgOjxpE-AEModcPAFMg", + "FriendlyName": "Jazz Fluence T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:11.423Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2687, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "VT-6wtBaC0Gs78iXxiZS1A.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,_aQEMQzp20eMT_Izfc64wA", + "FriendlyName": "Retro Cassette Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:07.49Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2689, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "2_H41S2JX0a9J5AqYS7boQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,ppH8miE39kGaSuACQv34WA", + "FriendlyName": "Untucked Tee Shirt) Untucked Tee Shirt RECDLC Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-12-12T21:02:17.7Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2686, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0UwHPWuWWEacVUf3ynSV8g.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,hZvsroTOnU6ew9HBWaqxHg", + "FriendlyName": "Rec Rocks Shirt (2023)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-12T21:02:04.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2764, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "U0QKGodCxUOP2Vvs0UCeKQ.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,QQVh8FdTakO26iOFdzGoBQ", + "FriendlyName": "Rec Rocks Tee (Akintoye)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:18.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2744, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UDBwvAX1qUWFKJmxqFGxzw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,mzu-a8fcsUCW591ENABAgg", + "FriendlyName": "Rec Rocks Tee (Haven)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-23T22:38:58.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2843, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XapimDoowkSNnhZVot3imA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,JpaGA1tG6U-VLsHH_vklzA", + "FriendlyName": "Fourth Friday Tee", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:31:05.52Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2765, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "XEh7w_iBSEWOPDJHeE18nA.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,42YaouiGVEe3zSjTsvyfoA", + "FriendlyName": "Rec Rocks Tee (Clinton Kane)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-01-30T17:21:22.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2814, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WtNLFBwRdEqYa6RZtR82Lw.png", + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,fzQzxcpDNEW8Rx122zmjAw", + "FriendlyName": "Rec Rocks Tee (Charlie Curtis-Beard)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-13T19:06:09.027Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,b11GS6Jd4E-UhO8wxhfQXw", + "FriendlyName": "Untucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1640435160 + }, + { + "AvatarItemDesc": "1940c740-695d-4481-a802-bb11b6561635,cd9d37fb-51a9-45dd-8e87-2756eb62f48b", + "FriendlyName": "Untucked Tee Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1940c740-695d-4481-a802-bb11b6561635", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1312778606 + }, + { + "AvatarItemId": 831, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6dn83vim1zahdfgjcl98s0u6d.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,", + "FriendlyName": "Vampire Hunter Necklace (Turquoise)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:46.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 836, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7pryvhofcoe894jd6lj9k3fpt.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,lvDIOeSI10SNxnCO_M_9zA,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Diamond)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:40.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 837, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "0e9k0ehhqbwmok3pfnwg4a9k3.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,0S7Nqm2kv0qMDTh-5Wy0MQ,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Ruby)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:41.777Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 838, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8dp37nmbznt4ddn3fpjb7i882.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,poqpmOloPUKotHH9p0YAyw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Amethyst)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:43.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 839, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dhvs55ab5etr8whu54dyu63mq.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,F7luQ18ZXEaITFiCqqwkpg,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Topaz)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:44.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 840, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bmf0h0l2wd8hhxzq331rdtd2n.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,okpg4ftsak-wc8Zu83uytw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Opal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:45.467Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 852, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "2qjoypuya1mw4gd0o6jhz9c0k.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,OfenZaa1-UGdQ8YVOrXjzw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Aristocrat Necklace (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T00:46:56.303Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 855, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "czzdjcyf31hpujiah45yuse4n.png", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,WOxMXWKGrUWhnNInttZ-Zw,StxMBEprVkqMa_IoDG-Q9A", + "FriendlyName": "Vampire Hunter Necklace (Amber)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "af73208e-cf30-4b92-8be8-3a8814aa5c0b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T21:38:59.543Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 833, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "84ln91je4llsa10o7904ovuwq.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,", + "FriendlyName": "Vampire Hunter Jacket (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:48.78Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 843, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "b6rtey8gih1qt69aci1qc2op4.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,Z8Bfk0MTb0evZehvvgzuBA,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:48.903Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 844, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "csm6i7gnanlmwjohok0nf865k.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,9QG6qp5FBU-u0-JK-yMMqA,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:50.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 856, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b80np2tvmtp3siyl63qj51enc.png", + "AvatarItemDesc": "JWfs9IiQs0Weu8MmyGpqLQ,CavRDqIlmUeTZBtCLn1hZQ,x10FeRBsQU2DIIpG8XIlwg", + "FriendlyName": "Vampire Hunter Jacket (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f4ec6725-9088-45b3-9ebb-c326c86a6a2d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-13T21:39:01.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 835, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "cimomkml6k4toyowd1voh7hqm.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,", + "FriendlyName": "Vampire Hunter Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T17:51:50.733Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 847, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bzkxvaged4oeb5kres30cpf1l.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,D0tP58wC30mJ00vNxw8hqA,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:53.807Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 848, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5colkn1hn7j9z19pn2vmuyjq5.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,Vxq6v55a_kGPODzwB5MC1g,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Midnight)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:55.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 849, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "79l7swn4ouu72xp8nmx7n1meu.png", + "AvatarItemDesc": "_OWVy3z6iU-M3-zbQgSLig,iJW8XGJfU0G12iXarjxhdQ,N5wN1l0tYEau5np-9PoIiA", + "FriendlyName": "Vampire Hunter Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cb95e5fc-fa7c-4f89-8cdf-ecdb42048b8a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-11-01T21:35:56.653Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2275, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "TtA9vfjAmUSSZ0HliWFsDg.png", + "AvatarItemDesc": "3ec19cb7-d0a3-4c35-a7e0-dc1f8609d373,", + "FriendlyName": "Pretzel Vendor Tray", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3ec19cb7-d0a3-4c35-a7e0-dc1f8609d373", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-08-01T19:00:26.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1231, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4ktwnvhbdb6vrphngnp2a7pne.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,", + "FriendlyName": "Formal Vest (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:10.867Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1232, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3h61nzryygyv3hlalnxl445sd.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,ffHKr5EcIU24PbAtWSnalw,bVrkIyADCUiGNYWdmP_HPw", + "FriendlyName": "Formal Vest (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:14.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1233, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8a8z4irf5f2f0qemw44a9ksy3.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,m4W5D0Oa9kaWa-8I96YYzA,WF_fg80f30qvHit0vGBwCA", + "FriendlyName": "Formal Vest (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:17.36Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2050, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "l-xQVuiMukee1yhQpZD6Dw.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,9RPbhHJWBkqwpDbMg-QumA,Qzklnrvw0EOiZ1mrj_nAYw", + "FriendlyName": "Formal Vest (Premium)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:41.717Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2561, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NMm13SJapEefUXYk6zAzcQ.png", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,bJXcVtY7qUK101QUAgr05A", + "FriendlyName": "Midnight Gala Gilded Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3d9d52cb-6a3d-436b-90e7-e76f238329ee", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-07T21:47:03.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2001, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6ik8yccgzd9kcci799wrss8n.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,", + "FriendlyName": "Vigilante Cape", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-01-06T00:44:33.217Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2205, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "EsJbgLpY9E2nOTvBpQupHQ.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,mO5wjf7_cEWNCHFR6ymGxA", + "FriendlyName": "Vigilante Cape (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-05-05T16:53:12.703Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2407, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "FTP3W6-lVEm0dHlsJU-OAA.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,EjNmQJBRnUet_ovEQ3HnDA", + "FriendlyName": "Vigilante Cape (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:04.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2532, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "8__P61NSO0yRenF0XuCZVQ.png", + "AvatarItemDesc": "b92f9873-5fbd-4529-803e-80151f5685c2,oy67Rd9BskuB8yloqSv0Jw", + "FriendlyName": "Vigilante Cape Shoulder) Vigilante Cape Shoulder Green Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b92f9873-5fbd-4529-803e-80151f5685c2", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-21T16:42:06.387Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2408, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "3e_aTt0gJUOLEV5MPnaBFA.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,", + "FriendlyName": "Vigilante Gloves", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:07.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2410, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "hNQCYaLS6ES3S7MsMnbjYQ.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,eBm7gRXomkivUz4Qg_ab8w", + "FriendlyName": "Vigilante Glove Wrist) Vigilante Glove White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:11.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2409, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "0seg4oV6PE-6014-iF3t0Q.png", + "AvatarItemDesc": "b96426ec-093d-4bfe-95e1-b88489479198,agkPLQG_Mkmqf3WkDaFaWA", + "FriendlyName": "Vigilante Glove Wrist) Vigilante Glove Blue Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b96426ec-093d-4bfe-95e1-b88489479198", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:09.313Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2543, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yCllwTBTl0yS2I5e-uCk5A.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,", + "FriendlyName": "Secret Mask (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:08.29Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2548, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5cJBLmj_M020xDeOBjTGfg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,l1cBXE49rkKTDEl1NvmWog", + "FriendlyName": "Secret Mask (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:21.747Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2550, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "dbd2B8AH_ESr4B382zo8zg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,MYCVPJDuz0Od_aZJekuWGA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:25.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2549, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "G-NyjtfwGk6tNxEwpb3r1w.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,kfZalp0wCkypX-QDhF0YoA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Red Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:23.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2544, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "udVHYjEfKUWkjL-2WA5Etg.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,2U3ZEvBzoESCb7kk4zSokQ", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Black Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:10.367Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2545, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1oQmhaLu1USd36z3vIXSkw.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,S5IPVluRPkWzapjf7Bdtxg", + "FriendlyName": "Secret Mask (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:11.993Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2547, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "zAnC5sdLVE27dsVXv-pKug.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,oDkSnfL7Vk64ZdxkuenEmA", + "FriendlyName": "Vigilante Mask Glasses) Vigilante Mask Pink Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-10-26T15:37:20.16Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2551, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wAPE0uRkEkivSTVbtq2xDQ.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,8kAqQHAgnUypHdxnwVSQwQ", + "FriendlyName": "Secret Mask (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:27.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2546, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "WGlj0EddtEuIpKKzqJ0XVA.png", + "AvatarItemDesc": "fa68bdae-5b0a-4544-a4eb-84b051d4b913,pUHlnnttXUmqP-9Rab8YaA", + "FriendlyName": "Secret Mask (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fa68bdae-5b0a-4544-a4eb-84b051d4b913", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-26T15:37:13.753Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2414, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "qsnBFyRE20CfNh68iWd0lA.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,", + "FriendlyName": "Vigilante Suit", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:22.76Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2416, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "Xxv_F0hPeE-3CmWWVz9lgg.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,rixBVVHS1E2Q4Y43ix4ySA", + "FriendlyName": "Vigilante Suit Shirt) Vigilante Suit White Combination", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2022-09-13T17:03:27.81Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2415, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "jgtZjEvX30q8Y9obf4VFqg.png", + "AvatarItemDesc": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b,zvs7t0P2Q02wIELJzMa2TQ", + "FriendlyName": "Vigilante Vest (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "84805fb5-91c4-4619-92d0-d7c19ffa3e5b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:25.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2417, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "scABHZiEIEOr3uyq1HhN5A.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,", + "FriendlyName": "Vigilante Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:30.403Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2419, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "wWHYtq4i-EeTTmy0RHGJqQ.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,0Ua_WtxcNEWc_KoRuuHhtg", + "FriendlyName": "Vigilante Utility Belt (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:34.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2418, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_pw8woutPUabGOMaUuOihw.png", + "AvatarItemDesc": "3b792b59-50c0-4c28-b367-fc694b1d65d1,PXssHhcJsUmUKGA8T4u85A", + "FriendlyName": "Vigilante Belt (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3b792b59-50c0-4c28-b367-fc694b1d65d1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:32.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 71, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "drqcby4q8dncgmtl1ndocn4kj.png", + "AvatarItemDesc": "c87239ca-a255-4b13-bbc5-1b38236fb4cc,", + "FriendlyName": "Viking Helmet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "c87239ca-a255-4b13-bbc5-1b38236fb4cc", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 24, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "bpe9ub501z2dr6rgv1b5zhi1u.png", + "AvatarItemDesc": "8c51fc1a-b1ad-4002-a13c-4313abd92c7f,", + "FriendlyName": "Visor", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "8c51fc1a-b1ad-4002-a13c-4313abd92c7f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1497, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6f370azhmwpl1vkwy7fwf4kzo.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,", + "FriendlyName": "Silly Shorts (Donut Dreams)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:04.91Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1496, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "65sqa2rlvhzcxamj55t9w8684.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,nu_UGtkJOUK-5MWaW83MUg,pgBFpOtl00K2zsTJcfrefQ", + "FriendlyName": "Silly Shorts (Watermelon Rain)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:42:01.563Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1495, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8u3din8ubpb13mug1b7ls4lnw.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,sJioq5ARDUyMHFji_6OxFw,bjXO36Nu6EWD2dU1UC4Uwg", + "FriendlyName": "Silly Shorts (Pina Colada)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:58.31Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1494, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2lgy30wam8vhg848fzjhiys66.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,56qchovVVEaVwL9S52kneA,BAjMoTHbD0mSCjoG9jmzjA", + "FriendlyName": "Silly Shorts (Sour Popsicle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:49.797Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1493, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e0hiapycpgqmkydxsy7vminkj.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,zJMD-_HR80WCAUqYGe7_NA,rAzhaLh1nEWMd9v1D-HWcQ", + "FriendlyName": "Silly Shorts (Lonely Island)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:41.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1492, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "49x7h4tmx4xozv5opw2vrtmnl.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,9P8ARRUYdk6hCoGMKhix4w,o__JORNtYEiJEyyoBIimPA", + "FriendlyName": "Silly Shorts (Pink Flamingo)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-08-11T00:41:35.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2167, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "nIUJ-iQCTECRCeCD0vn96g.png", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,Pg2cVQonrkmbXZdsNcn-KA", + "FriendlyName": "Wacky Shorts (SummerCamp Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ae180fb1-3e74-4232-9a9d-388fd488eba1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-13T01:18:45.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1463, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "08qct3wo7gai3uz597vre1rid.png", + "AvatarItemDesc": "767684e7-0eec-4f6e-ad58-3d5ae715206e,", + "FriendlyName": "Retro Sunglasses", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "767684e7-0eec-4f6e-ad58-3d5ae715206e", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-06-10T18:11:46.42Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1339, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "drpze5r42trhdfe0kkmbirisr.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,", + "FriendlyName": "Server Apron (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:05.17Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1340, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9f3l01qhr5bcg8ed2l0i4d4wy.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,HdwZhjrqxE6wuf68MnDhDw,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:09.803Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1341, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6k82rsbhcoow0xzix9ty8q06z.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,b7oobDcDtE-69fhM-5g5AQ,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:14.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1342, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "3on8a1l5752ih0y8i5uu1zha8.png", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,Blev_VFjRUysY_nymGWAVg,f5z_jhdcqEGjo-1WUmldaw", + "FriendlyName": "Server Apron (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:18.63Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1343, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6840rnvh9m72ksb1xw4y69cp3.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,", + "FriendlyName": "Server Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:23.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1344, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "9cewy2sbl6u8ip4456pu20bad.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,GQSpXmD0B02_gwzpq6StFA,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:28.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1345, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2vipnhbzbw003kcmye8831wbk.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,YvPeCbQS2UOcLjPfoIML_g,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:32.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1346, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "8657c4mdn19tdzvke8uu9mztu.png", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,zGOD45zNAk2i6P_zPsNPfg,ORmS9HkfoUeFuO4RhqcW7g", + "FriendlyName": "Server Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-02-20T21:50:38.177Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1794, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5c3hj3unazrmeie5zbuj4d1yk.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,", + "FriendlyName": "Wedding Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-17T16:05:09.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2511, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KXWrEO68d0Grp3m1mwkRjg.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,nuR8wvvCXU-EMXjBZuTXJA", + "FriendlyName": "Sugarplum Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:05.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2513, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "of8r-gp3HkCGzkv5aES9aA.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,TqhSo1hbPE6wdB_6lvLhFw", + "FriendlyName": "Sugar Plum Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:10.397Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2512, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PUAM3Iq0rUqQ6Pi5AYQeYg.png", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,SExtD3FBk0GwseMZDdGJlw", + "FriendlyName": "Sugarplum Dress (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-21T16:39:07.86Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,96556f7f-a6e8-48f4-a4e4-3fad9be5df4b", + "FriendlyName": "Dress Wedding Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "AvatarItemIdSource": "Generated local server ID", + "AvatarItemId": 1724539158 + }, + { + "AvatarItemId": 1521, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "5gcobmtf2n9884p336ea9z10x.png", + "AvatarItemDesc": "05b7af56-71f2-45ba-a377-c105bf6a6f7a,", + "FriendlyName": "Werewolf Wrist", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "05b7af56-71f2-45ba-a377-c105bf6a6f7a", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:21:05.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1520, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "dqy4lj2b3ng70db6wvws3916n.png", + "AvatarItemDesc": "833e8ab3-9b54-4e15-8f4d-ee1bac5823c9,", + "FriendlyName": "Werewolf Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "833e8ab3-9b54-4e15-8f4d-ee1bac5823c9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:21:01.48Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1519, + "AvatarItemType": 0, + "Rarity": 30, + "TagList": "halloween", + "ThumbnailImage": "abqsaidncml8v5e6w3cus33fc.png", + "AvatarItemDesc": "16f4dc71-d35a-4269-b925-1a0cd85a43dd,", + "FriendlyName": "Werewolf Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "16f4dc71-d35a-4269-b925-1a0cd85a43dd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:58.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2394, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "xlMaAOhYFUiyeB9g8Ynt_A.png", + "AvatarItemDesc": "67d8d599-4611-428f-98cd-98d8a527ace5,", + "FriendlyName": "Wicked Sorceress Dress", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67d8d599-4611-428f-98cd-98d8a527ace5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:50.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2395, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "zgDeoqR2GkqwnMftPbASfA.png", + "AvatarItemDesc": "67d8d599-4611-428f-98cd-98d8a527ace5,4qSWIO-cy0q5JWHp7tagHA", + "FriendlyName": "Wicked Sorceress Dress (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "67d8d599-4611-428f-98cd-98d8a527ace5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:52.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2396, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2591QLU31kirjhVvKJKqcA.png", + "AvatarItemDesc": "6d3be838-85cf-46db-b33d-0985aec9609f,", + "FriendlyName": "Wicked Sorceress Cowl", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d3be838-85cf-46db-b33d-0985aec9609f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:54.557Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2397, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "89fpSMNTb02Y1qYpw1xf6Q.png", + "AvatarItemDesc": "6d3be838-85cf-46db-b33d-0985aec9609f,Vn2gH3yjiE6bwBRgh5ntZQ", + "FriendlyName": "Wicked Sorceress Cowl (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d3be838-85cf-46db-b33d-0985aec9609f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:56.89Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2398, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6wrK2BA4GkydRnaEpYN9DQ.png", + "AvatarItemDesc": "730453b8-a352-4e6b-9573-54294690c2ad,", + "FriendlyName": "Wicked Sorceress Staff", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "730453b8-a352-4e6b-9573-54294690c2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:11:59.113Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2399, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "_Hv84dtkbkuHQuNy36QnrA.png", + "AvatarItemDesc": "730453b8-a352-4e6b-9573-54294690c2ad,4yMvQ3-hnU2zZQf_XT9r-A", + "FriendlyName": "Wicked Sorceress Staff (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "730453b8-a352-4e6b-9573-54294690c2ad", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-06T17:12:00.233Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2505, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "pSMu4GZwPkaYjHeVvrdP0Q.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,", + "FriendlyName": "Winged Headband (Frosty)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:56.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2509, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9quIlEDlukSdbauAxP4n8g.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,g14i4cTzkUu6RCOELBZEZA", + "FriendlyName": "Raven Winged Headband", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:20.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2510, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "CHZHjVfzp0mycor85z3G_Q.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,JUOrYcAd7U-ChkFZRiBeKA", + "FriendlyName": "Winged Headband (Fire)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:22.853Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2508, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7cUt9iO5Y06r9k3KfGPwGA.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,yxBqgXjlVEeRUOC_6iv77g", + "FriendlyName": "Winged Headband (Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:18.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2506, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yMQVOtm_EEeDJjnd-mgvQQ.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,6JihVI8pm0eJ-wDWuXm0sg", + "FriendlyName": "Winged Headband (Hawk)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:27:58.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2507, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "F5yTEJXfq0GxW0BX6wKrmA.png", + "AvatarItemDesc": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1,wwT8NdC-bk-aAWLoJvef5A", + "FriendlyName": "Winged Headband (Ice)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f67e47de-64de-4291-8d2e-5e32d6cfa5b1", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-10-17T20:29:13.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1767, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6a9x79mupkr34kq5jdirl6jkn.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,", + "FriendlyName": "Winged Headphones", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T20:59:45.127Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1789, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "92zq8rl5cyj5107xzihrijuss.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,9Zm7zOFBXkGRHrtp0JnaeA,oEsjaKTnKUq-Zgg4vm-EfA", + "FriendlyName": "awesome sauce", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "merged-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:07:04.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1784, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "e0ivt24nvmykvpkg7ivnet274.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,0jnL66i2_kGjfzDBc8ZNSQ,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:54.79Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1785, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2wipr83z3k4w166t8juiq5q2x.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,pY3RdcA2lU6H5rVXp8csfg,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:56.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1786, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a4nb7rytqdlq8f433qn0cieme.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,1Cbw9819NE-Fmv1NQIYKow,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:57.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1787, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "295vi4pw9jv6gz93ql5te9zw3.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,5JQPQ2JBUkSFyXeYWvr4tA,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:06:59.33Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1788, + "AvatarItemType": 0, + "Tooltip": "RecCon 2021 exclusive", + "Rarity": 50, + "ThumbnailImage": "3agkplk68iooewqp7l2qckznf.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,omnSzDLD5ESCU6ndropzPQ,Dac47T16Wk6LmmDd2cs1LA", + "FriendlyName": "Winged Headphones (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-05-03T21:07:00.793Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 6613, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "EkFVi2G8T02B9WxMBsz-hA.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,chqfCVxsEUuuUfp6qIadrg", + "FriendlyName": "Winged Headphones) Winged Headphones Hat Emissive Blue Combination Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "Readable fallback; official display name unavailable", + "SourceCatalogCreatedAt": "2024-03-27T22:53:46.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2440, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "s", + "ThumbnailImage": "Mx131-oxr0WEOEUxE3QvYw.png", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,VtsT6sKDYE24bAU-PKkIDQ", + "FriendlyName": "Winged Headphones (S.)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-28T00:20:13.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1544, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "16g4r2mkpxrh4o9sywb6mqjwm.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,tXhgU1o_wkaNQ-7P3KXidQ,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Kingfisher)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:47.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1545, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "bwisaai6w6aje9vyxb5oi2qbp.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,nn86ATWxekWdIGlPePPgGQ,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Vintage)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:51.293Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1546, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "22evbpym92jss1q2zzd8hgkb7.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,Rf7CDh2bg0-HdG5n7phGaw,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Thornbill)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-14T18:27:55.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1559, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8czrpcicgmpn30oiwunhjkdov.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,oxi26T5a4U6L73pF-Pgrhg,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Grackle)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:55:59.53Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1558, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dym59p1zihb18nmd4gfuzr226.png", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,6547pqf6vUm2L89AG6D91A,njEWOjNEQEWCEqmytodpow", + "FriendlyName": "Winged Hat (Raven)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-10-24T01:55:55.84Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 590, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "byuimkku1xbrkury4h0dloo3g.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,", + "FriendlyName": "Winter Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:32:56.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 951, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7c8t5ha57jkfu3nt2csays2c9.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,6vjA_vHq70Gclypll_GS_A,nalaCGqtL0SFA4Itwqmp8g", + "FriendlyName": "Winter Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:17.21Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1669, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aqnqkk7wewefq5ptgg8ky4ll8.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,3UxfTkPsEkezOXO__wz_mg,nalaCGqtL0SFA4Itwqmp8g", + "FriendlyName": "Winter Hat (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:04:21.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1223, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5h97nfxudddfga6xddftkdv7n.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,xVwzV9-m6E-8PVpkg4D-AQ,NH0L99bd00OVKMyltuSnHQ", + "FriendlyName": "Winter Hat (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:30.643Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1224, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ujv4qof9f4vkfnmm5zhc3yqp.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,mraJrTA4V0eARb_Kq7yYVw,NH0L99bd00OVKMyltuSnHQ", + "FriendlyName": "Winter Hat (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:35.883Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1596, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "d2wg3243jobc3bipqut3250ii.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,09Cg6dOq2kC2-tI5yyGSYg,YFZimVHKuk--lXS8RCrZTA", + "FriendlyName": "Winter Hat (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:29.873Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1945, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dq0ld9pvcoy11h7778869v6w4.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,u1JNHHTxSU-N53Bs9pRCoQ,0zHD3DaMAUOogqOqyd59SQ", + "FriendlyName": "Winter Hat (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:24.56Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2677, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ogsxq40HrUGm_XGRT-bDrA.png", + "AvatarItemDesc": "24c4404a-839f-46bd-98b6-6848d747a98b,w7ybaJQVo0WCZFDPCJAtWw", + "FriendlyName": "Winter Hat (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "24c4404a-839f-46bd-98b6-6848d747a98b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:21.66Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 591, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "byw8djj38cmq7xhypade0jdsi.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,", + "FriendlyName": "Winter Scarf (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:02.683Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 950, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "8oldwy2zk4m8splglpnb00px6.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,2lD-mjl3pk2wWv24RY2tbA,dUu7b-JFOky9c2lIVCNmcg", + "FriendlyName": "Winter Scarf (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:14.47Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1225, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7vhlvcgeu2orqfj1v5ym2tw6b.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,OrqqGMhK1UKGowFxpJi2mw,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:39.15Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1226, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c374omg9e5qg2vfc2ferpr6rs.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,-tDd0gdxQkuvL2id10AweQ,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:34:41.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1670, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "c9kkdeflt9if8ses9jc4zceuw.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,TDcZZ2ynEU-Mgmju_qIVIg,6WVXb20giEWyZbW2gWGz_w", + "FriendlyName": "Winter Scarf (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:04:43.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1597, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "6vj33whkr0o942bwju915h7tv.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,09Cg6dOq2kC2-tI5yyGSYg,eUktNKKK3kqUR8-gA3rNxA", + "FriendlyName": "Winter Scarf (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:35.267Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1944, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dg0fgutjfxtnt68jv0ixfe86l.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,cj9OYinX5kyzlhBUQwZKog,AKWH3_SDqkG8SRm5sauROQ", + "FriendlyName": "Winter Scarf (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:21.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2678, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "K2uufB0PC0S-SCX_3wmmvQ.png", + "AvatarItemDesc": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c,NJcJw51-xkq0phT-nlJw2A", + "FriendlyName": "Winter Scarf (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "e15b13a7-9e9a-4b32-ba2c-0cb31ed55a8c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:25.223Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 592, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dll3aenct2b8668hjuionbuff.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,", + "FriendlyName": "Winter Sweater (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:04.94Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 948, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "779n1128jegvy95s8avlm32fs.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,LDqcPkvNXE2gFXxbpNkfhA,Too5jpb1w0iQenkjRRS-QA", + "FriendlyName": "Winter Sweater (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:03.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1227, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "5e6rkamgfx1r7o0j31z8lkrc8.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,XOBIPrKCr02xa4tKj8wN9g,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:00.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1228, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "f0y7tht9axiudht3tlhqeysgc.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,i33RmJJCwUem-ADkOwMNQQ,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:02.25Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1673, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "44uerp68eefsysf8d98xb2sj1.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,fq0xC9BjLECbsQIZfAvK9A,Yn1uv3Y8JE6VSxsf0LOBvw", + "FriendlyName": "Winter Sweater (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:17.633Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1595, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ajy0ulpcn5j6v22cwvub3mj4g.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,09Cg6dOq2kC2-tI5yyGSYg,3fQj_7meDEeQqm_7RfEfxQ", + "FriendlyName": "Winter Sweater (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:26.96Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1943, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aa8mp2wzw823pnrmg36w1gge6.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,fTWOc1eEl0aVKXwTYYjA9Q,31nK13dsTkaIf3tGFrZlJQ", + "FriendlyName": "Winter Sweater (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-11-16T22:16:19.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2609, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1ZjDS_OAMk6uv03QVqK4vw.png", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,PG6H6px5HkO4X_xPRd1idA", + "FriendlyName": "Winter Sweater (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3757149b-f478-4975-81e5-d808a1bed8d5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-11-29T17:48:13.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 593, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0hlts8hq83dvp3ifo504f57oz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,", + "FriendlyName": "Winter Mittens (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-01-10T20:33:07.01Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 949, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "aiitap49m6e7fam5fzxlgszq0.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,SWvJWvMDpE-it5PFoPgERg,jQjfiz2i6kaPfrkjnBGxBA", + "FriendlyName": "Winter Mittens (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-12-10T23:52:10.39Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1674, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0iiuzgowl2qneslonnzfktvaz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,83jS7d-cUUC_GNDcGpiDxw,jQjfiz2i6kaPfrkjnBGxBA", + "FriendlyName": "Winter Mittens (Vermont)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-02-04T00:05:24.137Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1229, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "00oq8k2l0ag3pn1lp8zpc36rz.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,afWY5VGiF0isGn6qpo4KeQ,_yvss6L59US4-8QYZgcCdA", + "FriendlyName": "Winter Mittens (Frost)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:04.827Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1230, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9uo8iwvaprweq4i5xoa11qb42.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,tyzdtSkR9k-YCnjNvPAT6w,_yvss6L59US4-8QYZgcCdA", + "FriendlyName": "Winter Mittens (Pine)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-11-19T19:35:08.053Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1594, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "9k6nv0za82bel8lvz8220tor9.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,09Cg6dOq2kC2-tI5yyGSYg,xq7bizx6UEGaMTHB9zI3pw", + "FriendlyName": "Winter Mittens (Snowman)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-11-23T22:21:24.43Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1975, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "e6jg4ugpje9gdl4iyf223r1e0.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,MmGNESXgOkiZ0xCG41E9ig,Nc6jwNeK_kyajH5xKzCNVw", + "FriendlyName": "Winter Mittens (Ugly Ham)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-12-08T23:21:21.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2680, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "zwdzcRCLDEeGDboAcpAzOQ.png", + "AvatarItemDesc": "3a790be3-2937-44d4-be01-b5d65353bd3d,e6hTzFc-fEiJe0L4jDVxRQ", + "FriendlyName": "Winter Mittens (Reindeer)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "3a790be3-2937-44d4-be01-b5d65353bd3d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-05T20:28:33.517Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1300, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "a1hz9fyly8abux6bynwxt67g6.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,", + "FriendlyName": "Winter Cloak (Nightwatch)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-22T20:22:14.713Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1616, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "90opi030nb86oj7r0um5a2iip.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,pXrZhtzjV02_qvIQoWEFWA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Hearthstone)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:19:57.95Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1617, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dtxdon4brg2jmucu6b9sga2wh.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,VK4YBLXO10G2_aEl3ZQUGA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Whiteout)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:00.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1618, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8yf569v4byj5czra0vsottv9w.png", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,A7l6Q2clsk2XyKf62D3dHA,Hj8WnsdidE6rJeNyOmWUfg", + "FriendlyName": "Winter Cloak (Ironbound)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-12-16T01:20:02.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 826, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "36zngdpuj89crp4vmobfqwgk9.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,", + "FriendlyName": "Witch Hat (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:24.13Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1171, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "byiqri4xizdtch72sub8lx40z.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,PcUZrdKXhk2XHa5Gy0YnnA,dpBx3DxB7kKJphxCvfmKKw", + "FriendlyName": "Witch Hat (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:50:36.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1516, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "dam6qhnya0lx1zj3twhobzuo1.png", + "AvatarItemDesc": "s6mUIqiPCk6e57UYCx3SZA,0cP9EcZRFEuMOvmZ_Kx9dA,smmfK-N2R0uoDiuIHrhImg", + "FriendlyName": "Witch Hat (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2294a9b3-8fa8-4e0a-9ee7-b5180b1dd264", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:19.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 827, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "avd2s93fmxs9qkst7n3y7zcxq.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,", + "FriendlyName": "Witch Dress (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:26.207Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1173, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "csihpdjmklkvoybyrc0do3hiq.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,uTHwo8qd6Ei3Oy4gz_j9Ow,AK4CoAB2IUGkwqECNaO75g", + "FriendlyName": "Witch Dress (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:04.343Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1517, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "7olbww29lo0vqi3gal2gidvgz.png", + "AvatarItemDesc": "7LSm-xCYakmgcAr--Fn-JA,rZ45-lecBESqHaLX0c7NAw,iDxHI0T4Gkifs3_GY5gdjQ", + "FriendlyName": "Witch Dress (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fba6b4ec-9810-496a-a070-0afef859fe24", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:39.087Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 828, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4zz4132pzz51bxxjxf570c1d3.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,", + "FriendlyName": "Witch Cuffs (Grim)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2018-10-15T22:53:29.123Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1172, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dab6kmwjuwqeqhmjfdhdx043o.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,Hk6svQ5Sr0GDKLxOyktk9w,IQvHmZ8ei02hVuXZiXb6TA", + "FriendlyName": "Witch Cuffs (Creators Contest)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:51:00.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1518, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0p6vqm6iz2rilzafurlc4bbfd.png", + "AvatarItemDesc": "wy2cfgb8mUqjMrIOwHH2ew,0ZKR_qxrWkiWkjpot8s6Cg,5aX524Lfok-0dC3UFv-OkQ", + "FriendlyName": "Witch Cuff (Emerald)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7e9c2dc3-fc06-4a99-a332-b20ec071f67b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-09-30T18:20:47.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 515, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "17c4sjyyimn5d2gw0f556bubi.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,", + "FriendlyName": "Witch Hunter Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:51.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 516, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4quvkumfq6a1nznmdsezcpwxq.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,64545dcc-a8eb-4c69-a539-78ee73a2d327,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:53.167Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 517, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "4110szxveghcz05ilxuwglfb9.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,e02838f8-4029-455a-b35c-dd40f972b05b,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:54.213Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 518, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6z583ji4uuq0r1nymbm4h8ou5.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:55.09Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 519, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "2boublopz20zq6qu6ncbx97n7.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,909ed249-ce09-4304-8b02-56794fa7567d,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:56.263Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1125, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "04egovo7xe0ud9w0z1a6z2glf.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,Tbrc2MlSlE6mKua8qSoluQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:44.407Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1305, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3wjvw25jvmg34uniq3dxxrv37.png", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,aBQ3R9boek-mYeSYSNvDbQ,32d91d32-b102-4093-927a-6dc3ac2e9e86", + "FriendlyName": "Witch Hunter Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "6d36091e-7083-4e3d-8729-94612602c8cb", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:33.657Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 520, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c6y6kr8pkktfh9j9jrwbdbiop.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,", + "FriendlyName": "Witch Hunter Torso (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:57.623Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 521, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dbbbxrdwflnaiaj1nx1ovh07r.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,64545dcc-a8eb-4c69-a539-78ee73a2d327,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:58.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 522, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "40hz8pm5dpxvycg72a9eqhqu4.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,e02838f8-4029-455a-b35c-dd40f972b05b,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:01:59.45Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 523, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6crngu0sszkwju23mkpdiihjs.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:00.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 524, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "cfce1ejdgdy1jvubqb4vyv8k0.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,909ed249-ce09-4304-8b02-56794fa7567d,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:01.117Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1096, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9hmvdv7ncpyo5f6agy7dh9rxv.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,Tbrc2MlSlE6mKua8qSoluQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:18.373Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1315, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dci33hnvmb7vrbgaa8u6zvrlg.png", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,aBQ3R9boek-mYeSYSNvDbQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3", + "FriendlyName": "Witch Hunter Torso (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "418db0e0-c7af-40c2-9cac-d461ce41e210", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:26.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 525, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aifzs5jdida9x4pu9dc5p3lvb.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,", + "FriendlyName": "Witch Hunter Gloves (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:02.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 526, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "8kbnanl6xae6f5zssdhow3qi6.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,64545dcc-a8eb-4c69-a539-78ee73a2d327,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:04.34Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 527, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "71i7ci2n620of8lk3jxy8p0xl.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,e02838f8-4029-455a-b35c-dd40f972b05b,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:05.433Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 528, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "99lamaljd63j9ijdm4si9n6nz.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,71e5a0fe-fc8d-420b-af3b-e74160e0d1f3,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:06.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 529, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "eiseqfjnni496s1qlbkfv63gw.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,909ed249-ce09-4304-8b02-56794fa7567d,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T00:02:07.973Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4baeboc0ifrxpbd0ko0ohkilh.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,Tbrc2MlSlE6mKua8qSoluQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Leather)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:56.237Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1319, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "ao77s83vraqs2p4xiwkoxdk8o.png", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,aBQ3R9boek-mYeSYSNvDbQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad", + "FriendlyName": "Witch Hunter Gloves (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "1974dd48-aa44-434f-879a-eaff0c7c06e6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:57.837Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 530, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "dvw439bvcm7yg6u5gwrrduckq.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,", + "FriendlyName": "Wizard Hat (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:09.147Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 534, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "6tvxvhjodc7ixkaqu2ncy473u.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,357eb160-360b-4a63-840d-65d11cc8ffc2,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:35:59.32Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 535, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "7866xyln6cpoizvv5eps253jn.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,a38254c0-00e3-429b-9d5d-b9e6a9812e69,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:01.677Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 536, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2mgmt7so1oho67truxxyy5key.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,d3a9b576-5433-4305-bd96-b08820c09212,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:02.99Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 537, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "6ts6u23lp3vvh9g7zmcwgm6yd.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:04.093Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1124, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cgrpr9ydok6zdsitoa7a14oie.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,RKmRHk2N_U2gd91nybo6vg,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T20:00:32.447Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1306, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "19rp9i0wdr1ua9vm9hgjiy1f2.png", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,d3KmqVuMQkCYK6sVTMOTOA,abdf525b-b871-41be-a1c8-810f49740ad4", + "FriendlyName": "Wizard Hat (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "01689baf-405e-4b38-9f09-b624e03865b8", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:05:38.187Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 532, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4u8g9x7yeqkmpqikqud6p0889.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,", + "FriendlyName": "Wizard Robes (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:12.44Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 538, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "a814kjf5tknfmtt221aq0rmcg.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,357eb160-360b-4a63-840d-65d11cc8ffc2,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:05.227Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 539, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "6xg4k21qpb73l0qe9wq3xs507.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,a38254c0-00e3-429b-9d5d-b9e6a9812e69,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:06.19Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 540, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "351pq2gydedb34tytcs9n87b7.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,d3a9b576-5433-4305-bd96-b08820c09212,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:07.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 541, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "c1nbb1yfgeptsgsxxakgr3ejf.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:08.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1100, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1dlznhq7lrq71suzc0045grre.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,RKmRHk2N_U2gd91nybo6vg,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:29.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1316, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "47ngqkmtgcfvbds2nhtksozc3.png", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,d3KmqVuMQkCYK6sVTMOTOA,8768aa33-b014-4062-b323-b146dd7d78d2", + "FriendlyName": "Wizard Robes (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "97eae086-cfea-45ff-9b77-1d184256d493", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:06:33.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 533, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "42c0usoynxjhmimj6coo11rd3.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,", + "FriendlyName": "Wizard Bracers (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:13.533Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 542, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "4kheccxh9b10611a6leuimejf.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,357eb160-360b-4a63-840d-65d11cc8ffc2,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:09.057Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 543, + "AvatarItemType": 0, + "Rarity": 20, + "ThumbnailImage": "ejoni0f6t3kbisv094qlswolb.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,a38254c0-00e3-429b-9d5d-b9e6a9812e69,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:09.97Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 544, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "e4s49nshq56cwk2ptse4evh4p.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3a9b576-5433-4305-bd96-b08820c09212,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (White)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:10.877Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 545, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cz0n5qaa95cagk71hmmypg87q.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,7afe1bb0-5ca9-4263-8f85-49d3a18353c7,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Black)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-14T19:36:11.85Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1108, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1n7pr3zdkb6nskzxhpvouxscx.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,RKmRHk2N_U2gd91nybo6vg,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-06-26T19:58:58.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1320, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "0vwlyjwom9p9ef29kfe7nowjz.png", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3KmqVuMQkCYK6sVTMOTOA,47514b9b-0d17-4727-97ed-dab67e7fe031", + "FriendlyName": "Wizard Bracers (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "22ef1089-d073-4fe8-b038-6f34adf0b85f", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2020-01-24T00:07:16.283Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 531, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "633t2xbzngvd2f2x91gj77qth.png", + "AvatarItemDesc": "27cdecd2-88f1-40a8-80a9-87abd827c3c5,", + "FriendlyName": "Wizard Beard", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "27cdecd2-88f1-40a8-80a9-87abd827c3c5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-08-10T20:48:11.04Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1873, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dachlbubxeuputz2ewly8je7l.png", + "AvatarItemDesc": "fb2a0e86-3363-4579-9f18-f41138dc85ba,", + "FriendlyName": "Wolf Amulet", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "fb2a0e86-3363-4579-9f18-f41138dc85ba", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-03T19:17:31.083Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2121, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "aTYlG6TI1ESQrh8Tyzi-bw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,", + "FriendlyName": "Atlanta Hawks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:41.587Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2126, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "vndWqkhOjEiG1tfAoDZoig.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,dpxEW-2Un0eeVIsg5_o6Xw", + "FriendlyName": "Boston Celtics T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:53.597Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2137, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cTeuIowAx0KzIVy0-WH2cg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,4UTuNYISo0OP70FuGzmOsg", + "FriendlyName": "Brooklyn Nets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:22.74Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2130, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1KoWvmLOa0m99qKETLTIOA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,yVNoqFjSeU6mPJxEJnzRLg", + "FriendlyName": "Charlotte Hornets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:04.047Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2124, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "r1RzRtaGuUKZ-8XAY3Dh2A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hE5scNMqJ0igY_jhufvNmg", + "FriendlyName": "Chicago Bulls T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:48.817Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2125, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "YC4ouQXwGEOyXXjSiDhPkg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ibmxOUIFs06gOpRYLe58xg", + "FriendlyName": "Cleveland Cavaliers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:51.143Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2136, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "up6nS7ge3kWq1ROeCQ9GTg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,LpUOG6fHz0uQPtmOLtUlKw", + "FriendlyName": "Dallas Mavericks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:20.173Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2138, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NMpKzkwrjkmapWagtUmY4A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,sVeZH49mP0yBRKrbVr-0LA", + "FriendlyName": "Denver Nuggets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:25.077Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2141, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-6_8xc5kqEyRKQ1vuvbNYg.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eAq9xK51mE-Nu4RQY0ds4Q", + "FriendlyName": "Detroit Pistons T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:32.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2149, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3ABGwcegD0CZluvQKynPlQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,eCEF5P5N7EC_cEjM9-5iUA", + "FriendlyName": "Golden State Warriors T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:52.6Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2143, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2t_BBgqutkWjsMndffuawA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,OpWc8pumTk6M6x1WSw3a6Q", + "FriendlyName": "Houston Rockets T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:37.287Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2139, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "QTk8hWoyH0KjtihHXsQ3ww.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ow_9g2xNj0G33-zWIhjW-Q", + "FriendlyName": "Indiana Pacers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:27.393Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2127, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "-Ct_cbd5DUeGp8zl21aLMA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,2MUexARJAU-Vc-Qebqqepg", + "FriendlyName": "LA Clippers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:56.22Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2134, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lrUa6D6QQUec3po_q7y2-g.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,9bQaIrqI00qtjdtkIkj_3g", + "FriendlyName": "Los Angeles Lakers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:15.247Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2128, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kf_qLY9VIE2q2gSCp5L1uQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,pvnZdyL9NEaYYX8T12AgVA", + "FriendlyName": "Memphis Grizzlies T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:58.697Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2129, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "AGZJkBZVWUy1O-nlqtYI3A.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,clStIXndXk2ayewq-1LKng", + "FriendlyName": "Miami Heat T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:01.5Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2123, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "ako-ThG7XEqcnjIt9OZnlw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,oflbr1qvHUyvwUxdFZMcmA", + "FriendlyName": "Milwaukee Bucks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:46.357Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2147, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "W9BC4K_6K0G3J8tY9fyJeA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,j3S5Gk92mEOKlD424seciA", + "FriendlyName": "Minnesota Timberwolves T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:47.57Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2140, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "NNC2C7t7sUaTbsPQB8wOmA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,JBFu1kZ8C0ya5JZLDh0ynw", + "FriendlyName": "New Orleans Pelicans T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:29.763Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2133, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "MKQctCVd20ONb5z9deG31g.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,a4r3MYNjT0Ok0uQX9BYH6w", + "FriendlyName": "New York Knicks T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:12.097Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2146, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "PLv73v1H3EaMsBsQJzBVqw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,ZDsylFpkEUi5PijTLSfYNg", + "FriendlyName": "Oklahoma City Thunder T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:45.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2135, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kOzNsMW2mUGpmPEeT0AnUw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,B3wjSG3Ui0ipGgN3h7BFiw", + "FriendlyName": "Orlando Magic T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:17.8Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2122, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "yjtuZ5Fb5kGrGUDMaeoyEQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,H_lV3FQG0EC4ebW5vfnRIA", + "FriendlyName": "Philadelphia 76ers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:36:43.937Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2145, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "HUAmHnGyBU-bYA17m4hILA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hU2rt41rvUqulLM1H5NX4g", + "FriendlyName": "Phoenix Suns T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:42.453Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2148, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1lch9FUPrk23Vdmwl61lHA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,49mpP0duKEylQ2v5ZU9SYg", + "FriendlyName": "Portland Trail Blazers T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:50.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2132, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Qficbcavm06029T8l_tmgw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,hoXpo41nnEOPGq20Tq-t9A", + "FriendlyName": "Sacramento Kings T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:09.607Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2144, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "KwQTRfiStkqFs1xlwiPgUw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,RddbstjtZ0ClxgY-s66tTw", + "FriendlyName": "San Antonio Spurs T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:39.773Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2142, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Ss8R7vuL6UqFDQeWpt-fxw.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,kk271nI1MUu-gZjec9djrQ", + "FriendlyName": "Toronto Raptors T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:34.69Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2131, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "Kgk79zh1rka-JRcVXk1xnQ.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,tkHJl8TQHEiSeD-seDV4jg", + "FriendlyName": "Utah Jazz T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:06.673Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2150, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "lqK4K0ia5kCA6osf4p36eA.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,M3vXSuDRw0GLNh9A4dF4vA", + "FriendlyName": "Washington Wizards T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-29T18:37:55.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2183, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "K5HHE0xLXUyHcne6GNjNig.png", + "AvatarItemDesc": "2ce4ce1e-92a8-4868-8402-c6a1a995292d,nEBw9Nh1-kWcOynsm7P8Pw", + "FriendlyName": "NBA 75th Anniversary T-Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "2ce4ce1e-92a8-4868-8402-c6a1a995292d", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-04-19T22:54:44.05Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1894, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "2tkpst7269jmrhwyhewrh4t18.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,", + "FriendlyName": "Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-08-26T19:21:43.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2837, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "78b4yr_R40a5Z_V4E85knw.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,B5WV1io6fkaAJdm4GbzM_Q", + "FriendlyName": "Monarch Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-03-03T19:30:52.02Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1262588196, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "RGy9sbGCrkuEWsufq0SbUg.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,bNWm_2_VE0-4fdcF6S0aIA", + "FriendlyName": "Purple Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1968469639, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "8VDdpE56dk-9OeT9sGG-dA.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,q9Nganq9nUaV3zuvx9s3nQ", + "FriendlyName": "Butterfly Wrap Skirt (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1600494107, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "DWzLX_RUc0yrZ_icbuAJgA.png", + "AvatarItemDesc": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5,y8yTimlV2EWhJMU-Ay9e8A", + "FriendlyName": "Mint Butterfly Wrap Skirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "cf1e5c42-b12d-4e1e-945c-5bb80cae3fd5", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2025-01-26T08:06:37.558777", + "AvatarItemIdSource": "Generated local server ID" + }, + { + "AvatarItemId": 1657, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5tzmcps62glt2hqx42rvrgotc.png", + "AvatarItemDesc": "9231a9c6-7638-4fe9-bd2f-eac6c943009b,", + "FriendlyName": "Championship Wrestling Belt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9231a9c6-7638-4fe9-bd2f-eac6c943009b", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-01-06T00:57:31.547Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1855, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4510ib9vx62wtdf62c22d4ll2.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,x3kGXCD0UEWkjfXqu_kuxg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:46:58.163Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1857, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "6utyv84yov0n8z090tcquifk4.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,fWxn_AarpESryNA9UN8wvA,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Teal)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:02.943Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1856, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "a2n00ipkx8cd6po7pkjti1rsw.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,sEKIlMfbJ0eED6VtcXcQrw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Army Green)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:00.87Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1858, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "2b2f6frv89cqaeuswhoqzwvfg.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Dg3DxHr8GU2e190TGveXqw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Baby Pink)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:05.307Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1859, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "c4c0hhlbwpkuvdbt67tg68guo.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,5rLtRfNWO0qT7X-0sJkUXg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Lime)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:07.527Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1860, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "bl1czxrbjmc23vhg7hy87v385.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,DiJGFIO-5Ue8rkEhvF5-Mg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Sienna)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:09.723Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1861, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "5s7gwmw9faljog1rm2hdo358p.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Sg5yKkqJEEulQeUvRhFgJg,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Prussian Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:12.26Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1862, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8w15137vxkyf85ljd9clb74kk.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,M0nOQl44DkG_GjTXzAEQCQ,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:14.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1863, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "4zk2f1zx0df30rx3r2rafuhsi.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Ai85jH88H06ze74VdWolfw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Pure Red)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:16.72Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1864, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "0saykzdj1mlrn24gfq57ps3yu.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,NRjJuinZ4ki8qaEPvP84tw,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Mint)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:19.59Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1865, + "AvatarItemType": 0, + "Rarity": 0, + "ThumbnailImage": "8k3ikhmifw4vcf0qeayqmlzby.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,Z2L6AqDdcUqxiN1mVw-X9g,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd", + "FriendlyName": "Wristbands (Light Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:22.28Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 564, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "btwnqsv2torue6ekxyp3zudoe.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05", + "FriendlyName": "Wristbands (SciFi)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-11-13T20:56:59.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1866, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "43wffwjx8jpzi7e26q188u0mg.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,j367nSYyg06rOMZ1GmfmFA,FS7kgc4DpkKuar24J9dW8g", + "FriendlyName": "Wristbands (Plaid)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:25.41Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1867, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "0vdopp5i9bqzer09tpkfgi76a.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,2qYs9IkE90eppx1DggNlsA,6Y43EyTpiEejGp0ODWTSjA", + "FriendlyName": "Wristbands (Moons & Stars)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:28.487Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1868, + "AvatarItemType": 0, + "Rarity": 10, + "ThumbnailImage": "c2ypxn1ve868vb7jw2i5vn060.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,JV60jZGerUWuzQ2rRnwquQ,gSb8iLTEZ0mi3g5-jZXifQ", + "FriendlyName": "Wristbands (Polka Dot)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2021-07-28T15:47:31.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2788, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0AD75TjNekWbBVUQv0B09A.png", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,gOP1m4pVHkmlR2k8RrcdmQ", + "FriendlyName": "Rec Rocks Wristbands (Ethan Bortnick)*", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2023-02-03T19:14:03.567Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2072, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "kuIHwuTu10Cr8dCY4ZBlzQ.png", + "AvatarItemDesc": "d9c025e1-561b-4997-9f69-c96eb3c00786,", + "FriendlyName": "Wrist Warmers", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d9c025e1-561b-4997-9f69-c96eb3c00786", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-23T19:00:34.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 256, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "3szxfcxg8z8qj8wcaoubbty0z.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,", + "FriendlyName": "Poofy Dress (Yellow)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 257, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "00voe87lgaogwdjrgkt66bxew.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,55901f12-d5b5-4fa8-b4c8-e479689ee39d,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Blue)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 258, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9z43yb4xdivb6qyou9niixqtn.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,64850553-cdfe-455a-ac00-dafbe63d613e,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Orange)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 259, + "AvatarItemType": 0, + "Rarity": -1, + "ThumbnailImage": "9c3ziz00oalv0ji0f7tke3j7w.png", + "AvatarItemDesc": "a302741d-313b-4682-82e5-ac76764e662c,31ac9d4d-3d6b-48f2-85d5-dc4b8165a17a,d30e3afe-414c-4b60-8be9-6fa4156a4b65", + "FriendlyName": "Poofy Dress (Purple)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "a302741d-313b-4682-82e5-ac76764e662c", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2017-04-05T23:43:35.977Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2051, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "kq1OCF490kKmBUv0YF5PPw.png", + "AvatarItemDesc": "459ff0a0-ad68-4337-9399-57e3b03e4bf6,", + "FriendlyName": "Yeti Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "459ff0a0-ad68-4337-9399-57e3b03e4bf6", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:47.08Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2052, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "y_99GWjFbUa8UCqUY9X7FA.png", + "AvatarItemDesc": "7524b3f3-130b-41b1-83f6-9c241bd92bb4,", + "FriendlyName": "Yeti Shirt", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "7524b3f3-130b-41b1-83f6-9c241bd92bb4", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:49.98Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2053, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "UHgH78a4iEmYmvBe9cKI7w.png", + "AvatarItemDesc": "184368a6-17fa-4cfd-bb57-a2862f6e17bd,", + "FriendlyName": "Yeti Wrists", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "184368a6-17fa-4cfd-bb57-a2862f6e17bd", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-02-09T19:10:53.107Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2118, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "f5pXQnYPQUK8GU0CzpeWig.png", + "AvatarItemDesc": "f5b01307-e495-469c-afac-c10f375cffb0,", + "FriendlyName": "Yoga Mat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5b01307-e495-469c-afac-c10f375cffb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-03-23T23:49:32.75Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2420, + "AvatarItemType": 0, + "Rarity": 30, + "ThumbnailImage": "xFNT1OMmg0O3cQ3aagkA8Q.png", + "AvatarItemDesc": "f5b01307-e495-469c-afac-c10f375cffb0,lIURXMnmn0GcYFUVSXHzWA", + "FriendlyName": "Yoga Mat (Boogie)", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "f5b01307-e495-469c-afac-c10f375cffb0", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-09-13T17:03:36.54Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1189, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "c9bcud5i7blg02i1i21anii66.png", + "AvatarItemDesc": "9168b3f9-c7e5-40b6-bb2c-9e708596e675,", + "FriendlyName": "Zombie Head", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "9168b3f9-c7e5-40b6-bb2c-9e708596e675", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:07.997Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1190, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "7q88u3fe1zgi5smic4akp7ow9.png", + "AvatarItemDesc": "de19b239-3ed0-4f03-acb6-da69b5aa52ea,", + "FriendlyName": "Zombie Vest", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "de19b239-3ed0-4f03-acb6-da69b5aa52ea", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:11.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 1191, + "AvatarItemType": 0, + "Rarity": 50, + "TagList": "halloween", + "ThumbnailImage": "amgn9mnr8d0gpwg27xhvbuvwf.png", + "AvatarItemDesc": "5f1bc3c2-dfbb-4258-9b10-608535e34db2,", + "FriendlyName": "Zombie Hands", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "5f1bc3c2-dfbb-4258-9b10-608535e34db2", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2019-09-26T17:54:17.707Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2643, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "cdu27a9jf28i0va22wre1e08h.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,", + "FriendlyName": "NFL Arizona Cardinals Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:03.037Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2644, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0znrqqn0ggnpfhcs942kyvrtp.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,VxmHqppE6UWiXVBXL2QvTA", + "FriendlyName": "NFL Atlanta Falcons Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:03.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2645, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "eexlvsjg8kezkcwttljr1690x.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,kjDg5GM1b0iepa52BX5wbg", + "FriendlyName": "NFL Baltimore Ravens Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:04.363Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2646, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "707jfnnrkawaq9bdwniic0pye.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,WlfHWDcnO0SBT5wTjEPSmw", + "FriendlyName": "NFL Buffalo Bills Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:04.967Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2647, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5lqm8xisnbpdwi6my605vy6uj.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,_egTT9EA-UK0aLkn_QnWFA", + "FriendlyName": "NFL Carolina Panthers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:06.013Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2648, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "1lkfrltzaelfhihaleygfgtoz.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,10nSNj7eRE6XwS9y2OvC2g", + "FriendlyName": "NFL Chicago Bears Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:06.627Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2649, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "axl8sqe1o3pyczpmutxk61ltf.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XPBWTcpQ9kaS0mabpdvYhw", + "FriendlyName": "NFL Cincinnati Bengals Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:07.437Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2650, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "bya9qdvmk13buz121v7p3omb0.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FokpgM8R5EuXZwe89T78yA", + "FriendlyName": "NFL Cleveland Browns Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:08.103Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2651, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7xj6xkknhas1ka2boco8j8tkf.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,v0nZrzbIMEi3AaGuNjmwDw", + "FriendlyName": "NFL Dallas Cowboys Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:08.73Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2652, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "dwh5sx4vh6v0gdfi3kkalxutn.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,TtthSwdIq0CuSNOKDLsldg", + "FriendlyName": "NFL Denver Broncos Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:09.507Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2653, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "7q30iarhlropnxgad6s0kjjdc.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,5fjPcf4xQkSEYn3T-f8xLw", + "FriendlyName": "NFL Detroit Lions Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:10.07Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2654, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3vg7up51ivibzltebmdutv694.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,sjY_XUbXAk6h2TGnjBGDeQ", + "FriendlyName": "NFL Green Bay Packers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:10.667Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2655, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9kjwm7fopyhvlu9oq60p8kqh1.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,1nFX70Rqo0-qQo2M_Z1KCg", + "FriendlyName": "NFL Houston Texans Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:11.3Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2656, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "621yuib76ktgb5dus5p65ciue.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,cOyJcjGyp0miaMLAOpz0FA", + "FriendlyName": "NFL Indianapolis Colts Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:12.067Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2657, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2w56qylw8p57q7ik25gtk5usj.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,nIUjGqY8L0u0ZbX45b4PUQ", + "FriendlyName": "NFL Jacksonville Jaguars Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:12.637Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2658, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "85q8el387hlfi6qwrheeqg2g2.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6JY7l1gtpE631NTmT6xv3A", + "FriendlyName": "NFL Kansas City Chiefs Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:13.277Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2659, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2hbcygm3cqto3mirz4d7og1d7.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,27Fg--SzD0ertvGEv1Jgsw", + "FriendlyName": "NFL Las Vegas Raiders Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:13.847Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2660, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "3inbs0h93lddkdaggk3h26yae.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,6EjRVha2i0mCUVLaL_em_w", + "FriendlyName": "NFL Los Angeles Chargers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:19.583Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2661, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "di6np6u43ih96rrsdk18jvwox.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,EtgbhY9a_EqO4j86Al9Kpw", + "FriendlyName": "NFL Los Angeles Rams Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:20.327Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2662, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "5h0qrqjtunac31f0srzvmmttz.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,oyvOdKXeGU65V6sKhhb0qg", + "FriendlyName": "NFL Miami Dolphins Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:20.893Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2663, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "en0jav3kz8odonu5j54j9ckk3.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,-0DfEFcbNEaNI2uvfHF9qA", + "FriendlyName": "NFL Minnesota Vikings Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:21.67Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2664, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "32qnngk3wsvsjk35n4vcfap7z.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,DVnSVZcj8kyLr1DXRu7kzg", + "FriendlyName": "NFL New England Patriots Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:22.243Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2665, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "70ki9yd0las1hfb2tvielgtqm.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QgECbFDwyk24sI-e_fXy5w", + "FriendlyName": "NFL New Orleans Saints Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:22.843Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2674, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "0tah1l71br2opihsogi3xwk34.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,zMWcGYnGdk6lVqVNuFwJHw", + "FriendlyName": "NFL New York Giants Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:28.46Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2666, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "b7zfebrr9rbe6rsoq8y16fx7w.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,jYJehFukzUCHKzWQAaM3uw", + "FriendlyName": "NFL New York Jets Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:23.61Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2667, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9j2025b6j83s88cooyi6ui7g8.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,4viU0bMT1UuasZHD8euFKg", + "FriendlyName": "NFL Philadelphia Eagles Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:24.197Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2668, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "2aupjxiovt27lh2rkk8a4q4h8.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,YePtDvVnOk6cZkDBVIv-PA", + "FriendlyName": "NFL Pittsburgh Steelers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:24.77Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2669, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4pa7azn3fyrni6imh3ip7dpg.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,q-Gjg0zzI0mTHxjn8wi5HQ", + "FriendlyName": "NFL San Francisco 49ers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:25.377Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2670, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "d4lmbw9jnhj1x5cb2dj6y7wdx.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,QD-SRdKijU2RVu60TcU_jw", + "FriendlyName": "NFL Seattle Seahawks Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:25.927Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2671, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4c9qxd5hhuxj27pqdxfocik1g.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,3m5Q5crrHUGZJ9_GuxubQw", + "FriendlyName": "NFL Tampa Bay Buccaneers Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:26.577Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2672, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "9u34u3inzwswkkhcwc7snict0.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,XNTUcdaSVEuS3rbzwIwYNg", + "FriendlyName": "NFL Tennessee Titans Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:27.133Z", + "AvatarItemIdSource": "Existing catalog" + }, + { + "AvatarItemId": 2673, + "AvatarItemType": 0, + "Rarity": 50, + "ThumbnailImage": "4ynu85lp1m1sqc2r1upivy5ox.png", + "AvatarItemDesc": "d6507e9b-4413-4487-8f9a-adbba3a1bb66,FnezvbcGD0-oyFOTkpUo7g", + "FriendlyName": "NFL Washington Commanders Hat", + "UnlockedLevel": 0, + "PlatformMask": -1, + "IsBaseAvatarItem": false, + "ClientAvatarItemId": "d6507e9b-4413-4487-8f9a-adbba3a1bb66", + "FriendlyNameSource": "2022-avatar.json", + "SourceCatalogCreatedAt": "2022-12-01T18:24:27.877Z", + "AvatarItemIdSource": "Existing catalog" + } +] diff --git a/Server/RadicadeServer-Config/steamwhitelist.json b/Server/RadicadeServer-Config/steamwhitelist.json new file mode 100644 index 0000000..e63f126 --- /dev/null +++ b/Server/RadicadeServer-Config/steamwhitelist.json @@ -0,0 +1,4 @@ +{ + "Enabled": false, + "SteamIds": [] +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf1.json b/Server/RadicadeServer-Config/storefronts/sf1.json new file mode 100644 index 0000000..48aa0d2 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1.json @@ -0,0 +1,1648 @@ +{ + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 3852, + "FriendlyName": "Bucket Skin (SciFi)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Bucket]", + "EquipmentModificationGuid": "VW3WV6oQOkqYshf1xc9n3A", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 39, + "ItemSetFriendlyName": "Carpet Couture" + }, + "PurchasableItemId": 2100, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 100000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 100000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": "2021-07-02T21:00:00Z" + }, + { + "GiftDrop": { + "GiftDropId": 1901, + "FriendlyName": "RECS Shirt (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,bde08851-a9e4-4faa-8f04-a0a661672472,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 36, + "ItemSetFriendlyName": "Hangar Tees" + }, + "PurchasableItemId": 475, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 700, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 700, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1915, + "FriendlyName": "RECS Shirt (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,5e7a48ed-8f08-4990-a3ae-05722c9f59a8,0440f08f-ef1d-49d8-942b-523056e8bb45,237e0aa2-5911-4704-8d2a-d446ddd2d842", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 36, + "ItemSetFriendlyName": "Hangar Tees" + }, + "PurchasableItemId": 487, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 700, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 700, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1903, + "FriendlyName": "Robo Logo Shirt (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,4d52bbd6-ef74-45ae-a6c2-bde10ab8eb16,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 36, + "ItemSetFriendlyName": "Hangar Tees" + }, + "PurchasableItemId": 477, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 700, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 700, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1904, + "FriendlyName": "Robo Logo Shirt (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,bda7a57e-5227-49f4-a119-e3f166604738,0440f08f-ef1d-49d8-942b-523056e8bb45,4ca3347a-dc87-412f-9eb1-799372e6fdd1", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 36, + "ItemSetFriendlyName": "Hangar Tees" + }, + "PurchasableItemId": 478, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 700, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 700, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1905, + "FriendlyName": "Squad Goals Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,6c395d04-2aa6-4a3e-8c2c-695932bc9736", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 36, + "ItemSetFriendlyName": "Hangar Tees" + }, + "PurchasableItemId": 479, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1908, + "FriendlyName": "Hang In There Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,815138e6-b0dc-459c-9538-20f5bbd37d6b,0440f08f-ef1d-49d8-942b-523056e8bb45,8f19d7f9-4f28-48ab-a1c5-d8f4f788514c", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 36, + "ItemSetFriendlyName": "Hangar Tees" + }, + "PurchasableItemId": 482, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1895, + "FriendlyName": "KO Skull Shirt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,381a9f13-5b44-48e6-8a49-221422a963f5,0440f08f-ef1d-49d8-942b-523056e8bb45,9e782751-71c6-4208-87da-3a3cffa6a4f0", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 36, + "ItemSetFriendlyName": "Hangar Tees" + }, + "PurchasableItemId": 470, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1907, + "FriendlyName": "KO Skull Shirt (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,fe304452-b57b-45bc-9c1c-b85b70bcec12,0440f08f-ef1d-49d8-942b-523056e8bb45,6a007c73-d930-408a-93df-041a715e0dd4", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 36, + "ItemSetFriendlyName": "Hangar Tees" + }, + "PurchasableItemId": 481, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2298, + "FriendlyName": "Cyber Junk City Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,nLRxEIBhUkyQsfrk3nzsDA,0440f08f-ef1d-49d8-942b-523056e8bb45,n4hqSCv9OUewI30KnbPhFw", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 37, + "ItemSetFriendlyName": "Cyber Junk Tees" + }, + "PurchasableItemId": 848, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2293, + "FriendlyName": "Cyber Trash Shirt (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,Ib_iMwvUm0CJ-t_Thwt77Q,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 37, + "ItemSetFriendlyName": "Cyber Junk Tees" + }, + "PurchasableItemId": 847, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2294, + "FriendlyName": "Cyber Trash Shirt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,yaQ7AlUc3kGq8_RW3eTSmQ,0440f08f-ef1d-49d8-942b-523056e8bb45,d42UBeUO4UO3Q2F8IpeRbQ", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 37, + "ItemSetFriendlyName": "Cyber Junk Tees" + }, + "PurchasableItemId": 849, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2292, + "FriendlyName": "Emerald Syndicate Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,0qh4mcxJH0Wsrb3nz8UeGw", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 37, + "ItemSetFriendlyName": "Cyber Junk Tees" + }, + "PurchasableItemId": 843, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2296, + "FriendlyName": "Laser Graffiti Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,g4diCmk0wEmIKUVozk9tDA,0440f08f-ef1d-49d8-942b-523056e8bb45,nHg4Uifww0GEX15L27WonA", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 37, + "ItemSetFriendlyName": "Cyber Junk Tees" + }, + "PurchasableItemId": 845, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2297, + "FriendlyName": "Hack Graffiti Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,jna5wcWK90WokJyoeJH1iw,0440f08f-ef1d-49d8-942b-523056e8bb45,MPfe2wahQECCDGfhZE-drA", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 37, + "ItemSetFriendlyName": "Cyber Junk Tees" + }, + "PurchasableItemId": 844, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2291, + "FriendlyName": "Rebel Rose Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,rnM1qDlYBU6_Dl1bjg0J1Q,0440f08f-ef1d-49d8-942b-523056e8bb45,OZb8bb5FNUuFYOkwANvxAA", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 37, + "ItemSetFriendlyName": "Cyber Junk Tees" + }, + "PurchasableItemId": 842, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1906, + "FriendlyName": "Alpaca Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,941c046e-4e95-49f8-a7d7-19071fcc3c94,0440f08f-ef1d-49d8-942b-523056e8bb45,703ff56b-560d-4ff4-8c63-d195e879a328", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 38, + "ItemSetFriendlyName": "Premium Tees" + }, + "PurchasableItemId": 480, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1902, + "FriendlyName": "Laser Unicorn Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,befa2f11-b9fd-457c-b1d5-9d82e90b70ce,0440f08f-ef1d-49d8-942b-523056e8bb45,770e7674-6f9b-41f8-b023-ae79738db0d3", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 38, + "ItemSetFriendlyName": "Premium Tees" + }, + "PurchasableItemId": 476, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2295, + "FriendlyName": "No Laser Ham Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,dDLQbD9jtkOmw3PR5QJdfg,0440f08f-ef1d-49d8-942b-523056e8bb45,2tFUNn-4kkK6ICo_Zp065g", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 38, + "ItemSetFriendlyName": "Premium Tees" + }, + "PurchasableItemId": 846, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 15000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 15000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2299, + "FriendlyName": "Hyper Noodle Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,M1JVd5ur7U2ZtpvhEhPNhw,0440f08f-ef1d-49d8-942b-523056e8bb45,tzMFIJyxZE6cZNuLdaKs7w", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 38, + "ItemSetFriendlyName": "Premium Tees" + }, + "PurchasableItemId": 850, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 25000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 25000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2017, + "FriendlyName": "High Five Potion (Laser)", + "Tooltip": "Give futuristic high fives for a time.", + "ConsumableItemDesc": "xHOwjwpXd0GDkvBz2VqieA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 38, + "ItemSetFriendlyName": "Premium Tees" + }, + "PurchasableItemId": 577, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 150, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 150, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1897, + "FriendlyName": "Headband (SciFi)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "40528de7-38a3-4a7c-8f93-6d3bfa5573f2,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 39, + "ItemSetFriendlyName": "Carpet Couture" + }, + "PurchasableItemId": 472, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 200, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 200, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1898, + "FriendlyName": "Wristbands (SciFi)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 39, + "ItemSetFriendlyName": "Carpet Couture" + }, + "PurchasableItemId": 473, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 200, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 200, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2046, + "FriendlyName": "Bow (SciFi)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 39, + "ItemSetFriendlyName": "Carpet Couture" + }, + "PurchasableItemId": 603, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2060, + "FriendlyName": "Top Hat (Black, SciFi)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,bd4a84e2-b67a-4269-a26a-17fb23ddb09e,ccf1ccc1-e229-4157-bb74-f2cdef01e547,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 39, + "ItemSetFriendlyName": "Carpet Couture" + }, + "PurchasableItemId": 617, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 25000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 25000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2063, + "FriendlyName": "Bowtie (SciFi)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,0809a5cf-a0f3-4614-9f98-71518524518a,3290f148-1bd1-4ffd-a947-61fd0529af05,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 39, + "ItemSetFriendlyName": "Carpet Couture" + }, + "PurchasableItemId": 620, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2064, + "FriendlyName": "Business Tie (SciFi)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,0809a5cf-a0f3-4614-9f98-71518524518a,8212812b-e7e7-4f6d-aa27-d1112af45473,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 39, + "ItemSetFriendlyName": "Carpet Couture" + }, + "PurchasableItemId": 621, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2028, + "FriendlyName": "Film (Jumbotron)", + "Tooltip": "Take some photos with Jumbotron!", + "ConsumableItemDesc": "Il4VmrnjDkqjmjzddqoIEw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 38, + "ItemSetFriendlyName": "Premium Tees" + }, + "PurchasableItemId": 585, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 100, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 100, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1910, + "FriendlyName": "Laser Tag Sensors (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,08b7c12d-3317-4d27-9b40-34737a709454,959ed05b-0215-4bbe-bb3d-64e4448d676b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 40, + "ItemSetFriendlyName": "Laser Tag Outfit (Blue)" + }, + "PurchasableItemId": 484, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1918, + "FriendlyName": "Laser Tag Gloves (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,3b825377-d2eb-41e4-a736-b107f88f62d6,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 40, + "ItemSetFriendlyName": "Laser Tag Outfit (Blue)" + }, + "PurchasableItemId": 489, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 950, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 950, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2669, + "FriendlyName": "Laser Tag Visor (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,426d0592-f8e4-4bc6-9fe1-a62b91fd57d5,4e378419-d043-4e86-ad0e-66288ac21cf4,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 40, + "ItemSetFriendlyName": "Laser Tag Outfit (Blue)" + }, + "PurchasableItemId": 1190, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1909, + "FriendlyName": "Laser Tag Sensors (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 43, + "ItemSetFriendlyName": "Laser Tag Outfit (Pink)" + }, + "PurchasableItemId": 483, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1916, + "FriendlyName": "Laser Tag Gloves (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 43, + "ItemSetFriendlyName": "Laser Tag Outfit (Pink)" + }, + "PurchasableItemId": 488, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 950, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 950, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1911, + "FriendlyName": "Laser Tag Visor (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 43, + "ItemSetFriendlyName": "Laser Tag Outfit (Pink)" + }, + "PurchasableItemId": 485, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2303, + "FriendlyName": "Laser Tag Sensors (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,2Z_FxSPiyUODtlFZWF9DFQ,959ed05b-0215-4bbe-bb3d-64e4448d676b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 42, + "ItemSetFriendlyName": "Laser Tag Outfit (Teal)" + }, + "PurchasableItemId": 838, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2305, + "FriendlyName": "Laser Tag Gloves (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,mhsglVEtREmElF5wuLw3NQ,3b825377-d2eb-41e4-a736-b107f88f62d6,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 42, + "ItemSetFriendlyName": "Laser Tag Outfit (Teal)" + }, + "PurchasableItemId": 840, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 950, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 950, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2301, + "FriendlyName": "Laser Tag Visor (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,-e6bmXnwZU2EVwZ8RupKbg,4e378419-d043-4e86-ad0e-66288ac21cf4,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 42, + "ItemSetFriendlyName": "Laser Tag Outfit (Teal)" + }, + "PurchasableItemId": 836, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2302, + "FriendlyName": "Laser Tag Sensors (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "24d94673-f394-42e6-a3c8-276f8b8aa722,jkuw9EhCpEKn-2lKJXWd3Q,959ed05b-0215-4bbe-bb3d-64e4448d676b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 41, + "ItemSetFriendlyName": "Laser Tag Outfit (Orange)" + }, + "PurchasableItemId": 839, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2304, + "FriendlyName": "Laser Tag Gloves (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3044adce-90c0-4f66-81f5-39c4fca86fdf,EC0ttvk6eU2HEMacLAx2Iw,3b825377-d2eb-41e4-a736-b107f88f62d6,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 41, + "ItemSetFriendlyName": "Laser Tag Outfit (Orange)" + }, + "PurchasableItemId": 841, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 950, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 950, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2300, + "FriendlyName": "Laser Tag Visor (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "123de433-a005-4994-8a11-d2d84ad66b11,oT1vLrHW6UC2hWLIT3f_SQ,4e378419-d043-4e86-ad0e-66288ac21cf4,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": -1, + "Context": 100000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 41, + "ItemSetFriendlyName": "Laser Tag Outfit (Orange)" + }, + "PurchasableItemId": 837, + "Type": 0, + "Prices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 1, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + } + ], + "SubscriberDiscountPercent": 10, + "StorefrontType": 1, + "NextUpdate": "2222-02-22T22:22:00Z", + "NewUntil": null +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf100.json b/Server/RadicadeServer-Config/storefronts/sf100.json new file mode 100644 index 0000000..db70de1 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf100.json @@ -0,0 +1,910 @@ +{ + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 1962, + "FriendlyName": "Scallywag Bracers (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,cdca593a-8840-4140-9145-fe962732040c,ce3782aa-56cc-40d7-9019-8ce714cb7748,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4102, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 14, + "ItemSetFriendlyName": "Scallywag (Brown)" + }, + "PurchasableItemId": 525, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1958, + "FriendlyName": "Scallywag Belt (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,53dd9e44-7096-438e-a7c3-11d7c307426f,005699fe-2c69-4bec-831f-031fe8ad7f0a,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4102, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 14, + "ItemSetFriendlyName": "Scallywag (Brown)" + }, + "PurchasableItemId": 521, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1954, + "FriendlyName": "Scallywag Vest (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,85eb7f47-bb31-4c66-a8b3-562d35028e6c,9f1dd474-34a3-47eb-bb72-dbfc8d2d6ecf,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4101, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 14, + "ItemSetFriendlyName": "Scallywag (Brown)" + }, + "PurchasableItemId": 517, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 1600, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 1600, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1950, + "FriendlyName": "Scallywag Hat (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,53dd9e44-7096-438e-a7c3-11d7c307426f,a4903da3-249d-4f0a-a746-b547fda371ff,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4100, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 14, + "ItemSetFriendlyName": "Scallywag (Brown)" + }, + "PurchasableItemId": 513, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 3200, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 3200, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1963, + "FriendlyName": "Scallywag Bracers (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,62bd5dda-08da-4528-83c1-f0ca3812dc6e,ce3782aa-56cc-40d7-9019-8ce714cb7748,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4102, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 15, + "ItemSetFriendlyName": "Scallywag (Red)" + }, + "PurchasableItemId": 526, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1959, + "FriendlyName": "Scallywag Belt (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,c3c9f89c-580c-43c4-b38b-07394044e0e1,005699fe-2c69-4bec-831f-031fe8ad7f0a,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4102, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 15, + "ItemSetFriendlyName": "Scallywag (Red)" + }, + "PurchasableItemId": 522, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1955, + "FriendlyName": "Scallywag Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,39f04eee-3a58-4e53-9f5e-7e995d46ed71,005699fe-2c69-4bec-831f-031fe8ad7f0a,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4101, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 15, + "ItemSetFriendlyName": "Scallywag (Red)" + }, + "PurchasableItemId": 518, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 1600, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 1600, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1951, + "FriendlyName": "Scallywag Hat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,c3c9f89c-580c-43c4-b38b-07394044e0e1,a4903da3-249d-4f0a-a746-b547fda371ff,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4100, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 15, + "ItemSetFriendlyName": "Scallywag (Red)" + }, + "PurchasableItemId": 514, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 3200, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 3200, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1947, + "FriendlyName": "Pirate Captain Cuffs (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4102, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 16, + "ItemSetFriendlyName": "Pirate Captain (Blue)" + }, + "PurchasableItemId": 510, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1945, + "FriendlyName": "Pirate Captain Belt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4102, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 16, + "ItemSetFriendlyName": "Pirate Captain (Blue)" + }, + "PurchasableItemId": 508, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1946, + "FriendlyName": "Pirate Captain Jacket (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4101, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 16, + "ItemSetFriendlyName": "Pirate Captain (Blue)" + }, + "PurchasableItemId": 509, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 1600, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 1600, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1944, + "FriendlyName": "Pirate Captain Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4100, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 16, + "ItemSetFriendlyName": "Pirate Captain (Blue)" + }, + "PurchasableItemId": 507, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 3200, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 3200, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1960, + "FriendlyName": "Pirate Captain Cuffs (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "eceb7d49-be41-4228-86a9-2973d62e7135,556141f7-6fbc-4065-9faa-8d8240a93d7f,7a7bc314-4ffd-4ac8-9ed6-6ca73589c132,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4102, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 17, + "ItemSetFriendlyName": "Pirate Captain (Red)" + }, + "PurchasableItemId": 523, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1956, + "FriendlyName": "Pirate Captain Belt (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "38b38e24-d636-41c6-b80e-a285ae3106dc,0f71b9a2-dd88-435c-a571-4503d12dcf2d,9b2102f0-e16b-464f-9f1c-f068de52bb89,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4102, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 17, + "ItemSetFriendlyName": "Pirate Captain (Red)" + }, + "PurchasableItemId": 519, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1952, + "FriendlyName": "Pirate Captain Jacket (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3b00c71f-79c2-4a04-9fc0-a23b4c85562f,556141f7-6fbc-4065-9faa-8d8240a93d7f,00ac4e37-6eac-4ed5-b46c-2c075038bc3f,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4101, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 17, + "ItemSetFriendlyName": "Pirate Captain (Red)" + }, + "PurchasableItemId": 515, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 1600, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 1600, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1948, + "FriendlyName": "Pirate Captain Hat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3df394de-9c3e-4141-aba4-12eac4742102,17040a88-87f1-4613-b439-3316e33dbd48,71fd06e3-cac8-4923-a09c-ef91183712d2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4100, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 17, + "ItemSetFriendlyName": "Pirate Captain (Red)" + }, + "PurchasableItemId": 511, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 3200, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 3200, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2030, + "FriendlyName": "Film (Ghost Beard)", + "Tooltip": "Take some photos with Ghostbeard!", + "ConsumableItemDesc": "1c0Djlp090uBDczEobYNQw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4105, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 24, + "ItemSetFriendlyName": "Ghost Beard's Treasures" + }, + "PurchasableItemId": 587, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 250, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 250, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2016, + "FriendlyName": "High Five Potion (Explosive)", + "Tooltip": "Give explosive high fives for a time.", + "ConsumableItemDesc": "YEfbJTnsR0yT_p7e7tb_kQ", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4105, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 24, + "ItemSetFriendlyName": "Ghost Beard's Treasures" + }, + "PurchasableItemId": 576, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1936, + "FriendlyName": "Scallywag Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d17031a-c0a5-4145-92b7-1f0c2d8f0ce6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4104, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 18, + "ItemSetFriendlyName": "Scallywag (Black)" + }, + "PurchasableItemId": 501, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 20000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 20000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1937, + "FriendlyName": "Scallywag Belt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3af4c506-d49c-4126-9437-1d3bb1658863,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4104, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 18, + "ItemSetFriendlyName": "Scallywag (Black)" + }, + "PurchasableItemId": 502, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1938, + "FriendlyName": "Scallywag Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "664485e9-a159-4fcb-b32c-2b114b4a1218,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4104, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 18, + "ItemSetFriendlyName": "Scallywag (Black)" + }, + "PurchasableItemId": 503, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1939, + "FriendlyName": "Scallywag Bracers (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b9e0f03b-2609-4911-82e9-1e324dbbe8be,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4104, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 18, + "ItemSetFriendlyName": "Scallywag (Black)" + }, + "PurchasableItemId": 504, + "Type": 0, + "Prices": [ + { + "CurrencyType": 100, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 100, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + } + ], + "SubscriberDiscountPercent": 10, + "StorefrontType": 100, + "NextUpdate": "2222-02-22T22:22:00Z", + "NewUntil": null +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf1000.json b/Server/RadicadeServer-Config/storefronts/sf1000.json new file mode 100644 index 0000000..7dae178 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1000.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1000, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf101.json b/Server/RadicadeServer-Config/storefronts/sf101.json new file mode 100644 index 0000000..d4148de --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf101.json @@ -0,0 +1,623 @@ +{ + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 2520, + "FriendlyName": "Aristocrat Cuff (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,Wj7I2YiVoE6zp8J_HYs8tw,8kdJkmwNM0WQmMVrb8fK8A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 19, + "ItemSetFriendlyName": "Aristocrat (Green)" + }, + "PurchasableItemId": 979, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 20000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 20000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2518, + "FriendlyName": "Aristocrat Necklace (Emerald)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,OfenZaa1-UGdQ8YVOrXjzw,StxMBEprVkqMa_IoDG-Q9A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 19, + "ItemSetFriendlyName": "Aristocrat (Green)" + }, + "PurchasableItemId": 980, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 40000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 40000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2519, + "FriendlyName": "Aristocrat Jacket (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,LyVf9lnzdku1F4iPBGqk-g,xnxDimLvVUCI9tEJqWDDcA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 19, + "ItemSetFriendlyName": "Aristocrat (Green)" + }, + "PurchasableItemId": 978, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 80000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 80000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2498, + "FriendlyName": "Aristocrat Cuff (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 20, + "ItemSetFriendlyName": "Aristocrat (Red)" + }, + "PurchasableItemId": 972, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 40000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 40000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2501, + "FriendlyName": "Aristocrat Necklace (Ruby)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,0S7Nqm2kv0qMDTh-5Wy0MQ,StxMBEprVkqMa_IoDG-Q9A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 20, + "ItemSetFriendlyName": "Aristocrat (Red)" + }, + "PurchasableItemId": 963, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 80000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 80000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2496, + "FriendlyName": "Aristocrat Jacket (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 20, + "ItemSetFriendlyName": "Aristocrat (Red)" + }, + "PurchasableItemId": 971, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 120000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 120000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2509, + "FriendlyName": "Aristocrat Cuff (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,i5RKLo-RbEqisnGII1wpaQ,8kdJkmwNM0WQmMVrb8fK8A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 21, + "ItemSetFriendlyName": "Aristocrat (Black)" + }, + "PurchasableItemId": 975, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 80000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 80000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2503, + "FriendlyName": "Aristocrat Necklace (Topaz)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,F7luQ18ZXEaITFiCqqwkpg,StxMBEprVkqMa_IoDG-Q9A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 21, + "ItemSetFriendlyName": "Aristocrat (Black)" + }, + "PurchasableItemId": 965, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 160000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 160000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2505, + "FriendlyName": "Aristocrat Jacket (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,i5RKLo-RbEqisnGII1wpaQ,xnxDimLvVUCI9tEJqWDDcA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 21, + "ItemSetFriendlyName": "Aristocrat (Black)" + }, + "PurchasableItemId": 973, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 240000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 240000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2510, + "FriendlyName": "Aristocrat Cuff (Midnight)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "t9co2516nkuJO4LIJA1bJA,jwEB6LyEaEekqtPNRc6TVA,8kdJkmwNM0WQmMVrb8fK8A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 23, + "ItemSetFriendlyName": "Aristocrat (Midnight)" + }, + "PurchasableItemId": 976, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 200000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 200000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2502, + "FriendlyName": "Aristocrat Necklace (Amethyst)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "jiBzrzDPkkuL6DqIFKpcCw,poqpmOloPUKotHH9p0YAyw,StxMBEprVkqMa_IoDG-Q9A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 23, + "ItemSetFriendlyName": "Aristocrat (Midnight)" + }, + "PurchasableItemId": 964, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 400000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 400000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2506, + "FriendlyName": "Aristocrat Jacket (Midnight)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "QCtqxbvYOkan79YFugH_Cw,IzkFARlv6EenmrfcNkcOuw,xnxDimLvVUCI9tEJqWDDcA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 4204, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 23, + "ItemSetFriendlyName": "Aristocrat (Midnight)" + }, + "PurchasableItemId": 974, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 600000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 600000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2627, + "FriendlyName": "Red Apples", + "Tooltip": "A basket of apples to share with friends.", + "ConsumableItemDesc": "uMHrUPLYFk2rJOW_uop5Aw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 4205, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 25, + "ItemSetFriendlyName": "Dracula's Spoils" + }, + "PurchasableItemId": 1003, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2628, + "FriendlyName": "High Five Potion (Whip Crack)", + "Tooltip": "Give whip crack high fives for a time.", + "ConsumableItemDesc": "lag2tZyB90W04lQ7ol4vMw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 4205, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 25, + "ItemSetFriendlyName": "Dracula's Spoils" + }, + "PurchasableItemId": 1004, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2641, + "FriendlyName": "Film (Dracula)", + "Tooltip": "Take some photos with Dracula!", + "ConsumableItemDesc": "oG7CdvW7p0-S8sVe9w5vRw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 4205, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 25, + "ItemSetFriendlyName": "Dracula's Spoils" + }, + "PurchasableItemId": 1092, + "Type": 0, + "Prices": [ + { + "CurrencyType": 101, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 101, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + } + ], + "SubscriberDiscountPercent": 10, + "StorefrontType": 101, + "NextUpdate": "2222-02-22T22:22:00Z", + "NewUntil": null +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf102.json b/Server/RadicadeServer-Config/storefronts/sf102.json new file mode 100644 index 0000000..8281829 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf102.json @@ -0,0 +1,996 @@ +{ + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 3052, + "FriendlyName": "Knight Helmet (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,PMa9370LPEig_7pKwCceEQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 45, + "ItemSetFriendlyName": "Knight (Green)" + }, + "PurchasableItemId": 1559, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3068, + "FriendlyName": "Knight Gauntlet (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,PMa9370LPEig_7pKwCceEQ,fd67ec40-f898-4df6-b846-8bf350f362b8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 45, + "ItemSetFriendlyName": "Knight (Green)" + }, + "PurchasableItemId": 1560, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3062, + "FriendlyName": "Knight Armor (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,PMa9370LPEig_7pKwCceEQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 45, + "ItemSetFriendlyName": "Knight (Green)" + }, + "PurchasableItemId": 1563, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3058, + "FriendlyName": "Knight Cape (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,PMa9370LPEig_7pKwCceEQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 45, + "ItemSetFriendlyName": "Knight (Green)" + }, + "PurchasableItemId": 1564, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2270, + "FriendlyName": "Royal Crown (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 832, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-04T21:00:00Z", + "SaleEndDate": "2021-04-07T21:00:00Z" + } + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3054, + "FriendlyName": "Royal Crown (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,QkfA71-1z0qOiZAVrbzhtA,hCeaYHy1TE2s6rTPxAQP9A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1575, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3053, + "FriendlyName": "Royal Crown (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,wCe2yC2U1kyYvPd8Z1rX2w,hCeaYHy1TE2s6rTPxAQP9A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1566, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2957, + "FriendlyName": "Graphic Cap (Goblins)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1435, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2013, + "FriendlyName": "High Five Potion (Golden)", + "Tooltip": "Give golden high fives for a time.", + "ConsumableItemDesc": "-hy0qD-iUk-v4NHxNzanmg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4004, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 573, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 30, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 27, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3051, + "FriendlyName": "Archer Hat (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,61VZkFjB402brzWm4UsftA,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 46, + "ItemSetFriendlyName": "Archer (Grey)" + }, + "PurchasableItemId": 1561, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3067, + "FriendlyName": "Archer Gloves (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,8hUBfmq28EixYjv92Xcrgg,7b187c06-7ba2-4f3c-bca9-fdf98146f385,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 46, + "ItemSetFriendlyName": "Archer (Grey)" + }, + "PurchasableItemId": 1562, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3061, + "FriendlyName": "Archer Tunic (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,61VZkFjB402brzWm4UsftA,f88178b8-b204-4fa5-9dfd-6e438bf1e247,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 46, + "ItemSetFriendlyName": "Archer (Grey)" + }, + "PurchasableItemId": 1565, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3057, + "FriendlyName": "Archer Quiver (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,imDGL6dhrka-KliYlfpdgw,6094370d-5e28-473a-b422-f3f1c75d5db8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 46, + "ItemSetFriendlyName": "Archer (Grey)" + }, + "PurchasableItemId": 1567, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2271, + "FriendlyName": "Royal Cape (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 835, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2272, + "FriendlyName": "Royal Dress (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 50, + "ItemSetFriendlyName": "Royal Dresses" + }, + "PurchasableItemId": 833, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2538, + "FriendlyName": "Royal Dress (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,t1MeoroFOUioYmPfT9mCeA,PoMG28QAskagun1LSd4C_A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 50, + "ItemSetFriendlyName": "Royal Dresses" + }, + "PurchasableItemId": 1139, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2539, + "FriendlyName": "Royal Dress (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,0ps19D7b_U2iejdKQFORyA,PoMG28QAskagun1LSd4C_A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 50, + "ItemSetFriendlyName": "Royal Dresses" + }, + "PurchasableItemId": 1140, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3063, + "FriendlyName": "Royal Dress (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,pjkTGVHkIE-BzCVi1KAesQ,PoMG28QAskagun1LSd4C_A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 50, + "ItemSetFriendlyName": "Royal Dresses" + }, + "PurchasableItemId": 1569, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2273, + "FriendlyName": "Royal Tabard (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 49, + "ItemSetFriendlyName": "Royal Tabards" + }, + "PurchasableItemId": 834, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2536, + "FriendlyName": "Royal Tabard (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,rCeRk8Pdl0W5M47JAQPhpw,50NUFrDxbUKmXbyAHBenvQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 49, + "ItemSetFriendlyName": "Royal Tabards" + }, + "PurchasableItemId": 1141, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2537, + "FriendlyName": "Royal Tabard (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,5TJPDmvEBEOkP82ZWLFqtg,50NUFrDxbUKmXbyAHBenvQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 49, + "ItemSetFriendlyName": "Royal Tabards" + }, + "PurchasableItemId": 1142, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3064, + "FriendlyName": "Royal Tabard (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,s2kCppLsN0uY7Hy6qjQc-g,50NUFrDxbUKmXbyAHBenvQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 49, + "ItemSetFriendlyName": "Royal Tabards" + }, + "PurchasableItemId": 1570, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3474, + "FriendlyName": "Sus(picious) Goblin Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,4oywwjb3-kifr-um9x87Pw,11ZEZTFhy0ONpoXb3kiewQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1821, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3050, + "FriendlyName": "Winter Cloak (Nightwatch)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1592, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + } + ], + "SubscriberDiscountPercent": 10, + "StorefrontType": 102, + "NextUpdate": "2222-02-22T22:22:00Z", + "NewUntil": "2020-01-28T21:00:00Z" +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf103.json b/Server/RadicadeServer-Config/storefronts/sf103.json new file mode 100644 index 0000000..6dd9b87 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf103.json @@ -0,0 +1,750 @@ +{ + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 3065, + "FriendlyName": "Witch Hunter Torso (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,aBQ3R9boek-mYeSYSNvDbQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 47, + "ItemSetFriendlyName": "Witch Hunter (Blue)" + }, + "PurchasableItemId": 1571, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3069, + "FriendlyName": "Witch Hunter Gloves (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,aBQ3R9boek-mYeSYSNvDbQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 47, + "ItemSetFriendlyName": "Witch Hunter (Blue)" + }, + "PurchasableItemId": 1573, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3055, + "FriendlyName": "Witch Hunter Hat (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,aBQ3R9boek-mYeSYSNvDbQ,32d91d32-b102-4093-927a-6dc3ac2e9e86,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 47, + "ItemSetFriendlyName": "Witch Hunter (Blue)" + }, + "PurchasableItemId": 1576, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3073, + "FriendlyName": "Regal Crown (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,rmq1AOQvtUGOHvbPUximwg,XFuSiGupZ02Fut1Dxum_Iw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1574, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3071, + "FriendlyName": "Regal Crown (Gold)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1568, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3072, + "FriendlyName": "Regal Crown (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zljmtj5WqUaRvB3XQTgJXg,XFuSiGupZ02Fut1Dxum_Iw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1580, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3307, + "FriendlyName": "Tiny Crown", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "00f0b914-aa22-4d14-9af1-996e1af61f4d,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1769, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-09-05T21:00:00Z", + "SaleEndDate": "2021-09-06T21:00:00Z" + } + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2015, + "FriendlyName": "High Five Potion (Magic)", + "Tooltip": "Give magical high fives for a time.", + "ConsumableItemDesc": "VQSgL2pTLkWx4B3kwYG7UA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4014, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 575, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 30, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 27, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3066, + "FriendlyName": "Wizard Robes (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,d3KmqVuMQkCYK6sVTMOTOA,8768aa33-b014-4062-b323-b146dd7d78d2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 48, + "ItemSetFriendlyName": "Wizard (Purple)" + }, + "PurchasableItemId": 1572, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3056, + "FriendlyName": "Wizard Hat (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,d3KmqVuMQkCYK6sVTMOTOA,abdf525b-b871-41be-a1c8-810f49740ad4,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 48, + "ItemSetFriendlyName": "Wizard (Purple)" + }, + "PurchasableItemId": 1577, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3070, + "FriendlyName": "Wizard Bracers (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3KmqVuMQkCYK6sVTMOTOA,47514b9b-0d17-4727-97ed-dab67e7fe031,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 48, + "ItemSetFriendlyName": "Wizard (Purple)" + }, + "PurchasableItemId": 1578, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3306, + "FriendlyName": "Royal Cape (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,cKLBS6pbf0SFuavWyeZ7GA,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1770, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3060, + "FriendlyName": "Royal Cape (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,_9QlnTPXuEO5OQ3H0CwFkA,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1593, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3059, + "FriendlyName": "Royal Cape (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,MbusQ9bte0i9zirlxtNkSQ,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1579, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2572, + "FriendlyName": "Royal Cape (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,mqdncbYcuUSGFvFqEU0z4A,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 1138, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3087, + "FriendlyName": "Bog Monster Suit", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "dedd2e98-2721-4de6-92f2-9d399f34e8fe,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 51, + "ItemSetFriendlyName": "Bog Monster" + }, + "PurchasableItemId": 1607, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3088, + "FriendlyName": "Bog Monster Wrist", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4e42e02e-33fd-4ff9-b74e-9bde26253c35,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 51, + "ItemSetFriendlyName": "Bog Monster" + }, + "PurchasableItemId": 1605, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3089, + "FriendlyName": "Bog Monster Hood", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "20b424a9-861a-4d39-a583-22d7b7348c14,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 51, + "ItemSetFriendlyName": "Bog Monster" + }, + "PurchasableItemId": 1606, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + } + ], + "SubscriberDiscountPercent": 10, + "StorefrontType": 103, + "NextUpdate": "2222-02-22T22:22:00Z", + "NewUntil": "2020-01-28T21:00:00Z" +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf1100.json b/Server/RadicadeServer-Config/storefronts/sf1100.json new file mode 100644 index 0000000..37c9e6a --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1100.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1100, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1200.json b/Server/RadicadeServer-Config/storefronts/sf1200.json new file mode 100644 index 0000000..c42f828 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1200.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1200, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1300.json b/Server/RadicadeServer-Config/storefronts/sf1300.json new file mode 100644 index 0000000..993880e --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1300.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1300, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1400.json b/Server/RadicadeServer-Config/storefronts/sf1400.json new file mode 100644 index 0000000..f8cd20e --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1400.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1400, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1600.json b/Server/RadicadeServer-Config/storefronts/sf1600.json new file mode 100644 index 0000000..129a938 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1600.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1600, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1700.json b/Server/RadicadeServer-Config/storefronts/sf1700.json new file mode 100644 index 0000000..b7d273f --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1700.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1700, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1701.json b/Server/RadicadeServer-Config/storefronts/sf1701.json new file mode 100644 index 0000000..9640afe --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1701.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1701, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1702.json b/Server/RadicadeServer-Config/storefronts/sf1702.json new file mode 100644 index 0000000..0a8b8de --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1702.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1702, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1703.json b/Server/RadicadeServer-Config/storefronts/sf1703.json new file mode 100644 index 0000000..2ce3c3a --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1703.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1703, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1704.json b/Server/RadicadeServer-Config/storefronts/sf1704.json new file mode 100644 index 0000000..383f266 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1704.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1704, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1705.json b/Server/RadicadeServer-Config/storefronts/sf1705.json new file mode 100644 index 0000000..0bc4c6a --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1705.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1705, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1706.json b/Server/RadicadeServer-Config/storefronts/sf1706.json new file mode 100644 index 0000000..54e1230 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1706.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1706, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1707.json b/Server/RadicadeServer-Config/storefronts/sf1707.json new file mode 100644 index 0000000..19e8b24 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1707.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1707, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1708.json b/Server/RadicadeServer-Config/storefronts/sf1708.json new file mode 100644 index 0000000..9bd631e --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1708.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1708, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1709.json b/Server/RadicadeServer-Config/storefronts/sf1709.json new file mode 100644 index 0000000..7b7abdc --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1709.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1709, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1800.json b/Server/RadicadeServer-Config/storefronts/sf1800.json new file mode 100644 index 0000000..f760dd6 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1800.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1800, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf1900.json b/Server/RadicadeServer-Config/storefronts/sf1900.json new file mode 100644 index 0000000..925321c --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf1900.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 1900, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf2.json b/Server/RadicadeServer-Config/storefronts/sf2.json new file mode 100644 index 0000000..cfa6ba8 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf2.json @@ -0,0 +1,422 @@ +{ + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 3890, + "FriendlyName": "Gladiator Helmet", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "594a69fb-9f76-4f83-b2aa-94f0e72e996c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 923, + "ItemSetFriendlyName": "Gladiator Set" + }, + "PurchasableItemId": 2197, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": "2021-10-06T21:00:00Z" + }, + { + "GiftDrop": { + "GiftDropId": 3889, + "FriendlyName": "Gladiator Cape", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1633766e-fa77-48f3-8c6c-a233e56bf320,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 923, + "ItemSetFriendlyName": "Gladiator Set" + }, + "PurchasableItemId": 2196, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3600, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": "2021-10-06T21:00:00Z" + }, + { + "GiftDrop": { + "GiftDropId": 3888, + "FriendlyName": "Gladiator Armor", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 923, + "ItemSetFriendlyName": "Gladiator Set" + }, + "PurchasableItemId": 2195, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": "2021-10-06T21:00:00Z" + }, + { + "GiftDrop": { + "GiftDropId": 2079, + "FriendlyName": "Deep Sea Diver Gloves (Triton)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 945, + "ItemSetFriendlyName": "Deep Sea Diver Set (Triton)" + }, + "PurchasableItemId": 635, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-10-03T21:00:00Z", + "SaleEndDate": "2021-10-04T21:00:00Z" + } + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2222, + "FriendlyName": "Deep Sea Diver Tank (Triton)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 945, + "ItemSetFriendlyName": "Deep Sea Diver Set (Triton)" + }, + "PurchasableItemId": 778, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1978, + "FriendlyName": "4-Star Unique Box", + "Tooltip": "A random 4-star item that you don't have.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": true, + "QueryRedirectContext": 0, + "QueryRedirectRarity": 30, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 44, + "ItemSetFriendlyName": "Star Boxes" + }, + "PurchasableItemId": 539, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1980, + "FriendlyName": "2-Star Unique Box", + "Tooltip": "A random 2-star item that you don't have.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": true, + "QueryRedirectContext": 0, + "QueryRedirectRarity": 10, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 44, + "ItemSetFriendlyName": "Star Boxes" + }, + "PurchasableItemId": 541, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 1977, + "FriendlyName": "3-Star Unique Box", + "Tooltip": "A random 3-star item that you don't have.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": true, + "QueryRedirectContext": 0, + "QueryRedirectRarity": 20, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 44, + "ItemSetFriendlyName": "Star Boxes" + }, + "PurchasableItemId": 538, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 2078, + "FriendlyName": "Deep Sea Diver Suit (Triton)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 945, + "ItemSetFriendlyName": "Deep Sea Diver Set (Triton)" + }, + "PurchasableItemId": 634, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": null + }, + { + "GiftDrop": { + "GiftDropId": 3917, + "FriendlyName": "Wolf Amulet", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fb2a0e86-3363-4579-9f18-f41138dc85ba,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "PurchasableItemId": 2174, + "Type": 0, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "IsFeatured": false, + "NewUntil": "2021-10-06T21:00:00Z" + } + ], + "SubscriberDiscountPercent": 10, + "StorefrontType": 2, + "NextUpdate": "2021-10-06T21:00:00Z", + "NewUntil": null +} diff --git a/Server/RadicadeServer-Config/storefronts/sf200.json b/Server/RadicadeServer-Config/storefronts/sf200.json new file mode 100644 index 0000000..c049ebb --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf200.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 200, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf2000.json b/Server/RadicadeServer-Config/storefronts/sf2000.json new file mode 100644 index 0000000..9417fe2 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf2000.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 2000, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf2001.json b/Server/RadicadeServer-Config/storefronts/sf2001.json new file mode 100644 index 0000000..f4b308e --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf2001.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 2001, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf2002.json b/Server/RadicadeServer-Config/storefronts/sf2002.json new file mode 100644 index 0000000..a1f0a39 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf2002.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 2002, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf2003.json b/Server/RadicadeServer-Config/storefronts/sf2003.json new file mode 100644 index 0000000..07477e7 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf2003.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 2003, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf2004.json b/Server/RadicadeServer-Config/storefronts/sf2004.json new file mode 100644 index 0000000..75f85ea --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf2004.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 2004, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf3.json b/Server/RadicadeServer-Config/storefronts/sf3.json new file mode 100644 index 0000000..f5c65d3 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf3.json @@ -0,0 +1,41121 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 302, + "FriendlyName": "Doctor's Head Mirror", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5ac25e5a-8c8e-445c-82a4-7272baec1eb5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 303, + "FriendlyName": "Drum Major Hat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 3, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 304, + "FriendlyName": "Lacrosse Helmet (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7134c60b-0640-463a-8d97-caa9184bef93,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 4, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 305, + "FriendlyName": "Lacrosse Helmet (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7134c60b-0640-463a-8d97-caa9184bef93,e3454bc7-c88d-4958-8b81-db3fe5472a1e,7e688b30-ed13-4790-9ad8-c49885d1b7e8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 5, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 316, + "FriendlyName": "Flower Headband (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 6, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 317, + "FriendlyName": "Sun Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "703f1987-2b31-4b6f-bab7-1939b1105ac0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-06-27T21:00:00Z", + "SaleEndDate": "2021-06-28T21:00:00Z" + } + } + ], + "PurchasableItemId": 7, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 318, + "FriendlyName": "Visor", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8c51fc1a-b1ad-4002-a13c-4313abd92c7f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 8, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 331, + "FriendlyName": "Baseball Helmet (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 15, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 332, + "FriendlyName": "Beret (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 16, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 333, + "FriendlyName": "Bike Helmet (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 17, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 334, + "FriendlyName": "Bow (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 18, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 335, + "FriendlyName": "Boxing Helmet (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 19, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3600, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 336, + "FriendlyName": "Cat Ears (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-06-06T21:00:00Z", + "SaleEndDate": "2021-06-07T21:00:00Z" + } + } + ], + "PurchasableItemId": 20, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 337, + "FriendlyName": "Cloche (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 21, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 339, + "FriendlyName": "Cowboy Hat (Cherry)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,624c85e5-095a-48b6-85fb-c9bbade19a8d,54778d93-2cff-48db-b25b-65c08b0e5be8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 23, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 340, + "FriendlyName": "Cowboy Hat (Black, Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,192ba73d-6990-42da-8fe0-914c2a7181eb,54778d93-2cff-48db-b25b-65c08b0e5be8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 24, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 341, + "FriendlyName": "Cowboy Hat (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,54778d93-2cff-48db-b25b-65c08b0e5be8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 25, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 342, + "FriendlyName": "Equestrian Helmet (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 26, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 343, + "FriendlyName": "Firefighter Helmet (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 27, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 344, + "FriendlyName": "Flat Cap (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 28, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 345, + "FriendlyName": "Football Helmet (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 29, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 346, + "FriendlyName": "Football Helmet (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,d8d490aa-7088-4163-9305-1441367f490c,7660e11a-7ef0-43d3-bc02-76b3a4aab064,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 30, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 347, + "FriendlyName": "Football Helmet (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,61ff7a02-50ff-49ea-b95e-c31b95a30c8e,7660e11a-7ef0-43d3-bc02-76b3a4aab064,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 31, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 348, + "FriendlyName": "Flower Hair Clip (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 32, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 349, + "FriendlyName": "Hockey Helmet (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 33, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 355, + "FriendlyName": "Luchador Mask (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-23T21:00:00Z", + "SaleEndDate": "2021-05-24T21:00:00Z" + } + } + ], + "PurchasableItemId": 39, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 358, + "FriendlyName": "Captain Hat (White, Blue, Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 42, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 359, + "FriendlyName": "Captain Hat (White, Black, Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,f5403c89-9769-479b-8a05-c71607f9b189,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 43, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 360, + "FriendlyName": "Captain Hat (White, Black, Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,36e63797-5a42-4779-a5cc-ef5c2e7d17d4,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 44, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 361, + "FriendlyName": "Captain Hat (Black, Black, Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,70112887-dc8d-49d5-96ed-0845204e0b58,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 45, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 362, + "FriendlyName": "Small Brim Hat (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 46, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 364, + "FriendlyName": "Top Hat (Black, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 48, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 368, + "FriendlyName": "Side Pony Hair", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "49397e16-1027-4286-9bc4-b59eae565ff0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 50, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 369, + "FriendlyName": "Mohawk Hair", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e49d5d78-157e-4bc8-b878-ff7c4963fdc4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 51, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 376, + "FriendlyName": "Sunglasses (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 55, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 377, + "FriendlyName": "Paintball Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 56, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 379, + "FriendlyName": "Ranger Stache", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3e139dd9-4757-43a8-8546-8da70dc17f0a,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 58, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 380, + "FriendlyName": "Cowboy Stache", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "cca7ab3d-5609-4700-92fd-3280fcea9b77,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 59, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 381, + "FriendlyName": "Sea Captain Beard", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c161dc73-6ce3-49c4-8d15-45e6911f84e4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 60, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 383, + "FriendlyName": "Bowtie (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 62, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 385, + "FriendlyName": "Bandana (Flowers, Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 63, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 392, + "FriendlyName": "Luchador Cape (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 69, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 393, + "FriendlyName": "Doctor's Stethoscope", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "587ca2bb-dd42-42bf-833f-4e2a97349866,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 70, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 402, + "FriendlyName": "Tied Scarf (Striped, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 76, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 403, + "FriendlyName": "Astronaut Suit (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 77, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 404, + "FriendlyName": "Babydoll Dress (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 78, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 405, + "FriendlyName": "Babydoll Dress (Maroon)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,d6823e01-69f0-4f85-b94a-74894356a2cf,cb9e9e05-c481-4397-86e7-c2111bc0e817,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 79, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 406, + "FriendlyName": "Babydoll Dress (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c5deba2a-6e35-4b13-8e94-8ba5457f39df,cb9e9e05-c481-4397-86e7-c2111bc0e817,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 80, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 407, + "FriendlyName": "Babydoll Dress (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,e13cf33d-7588-4e2e-955e-f0a29d6380b7,cb9e9e05-c481-4397-86e7-c2111bc0e817,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 81, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 408, + "FriendlyName": "Baseball Jersey (White, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 82, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 409, + "FriendlyName": "Basketball Jersey (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 83, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 410, + "FriendlyName": "Bike Shirt (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 84, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 411, + "FriendlyName": "Cheerful Dress (Navy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 85, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 424, + "FriendlyName": "Collared Shirt (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,071d1bf3-86dc-4a31-b54a-e6579fab8649,d015cae7-a905-49e4-8823-6dec069689a6,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 87, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 433, + "FriendlyName": "Cowboy Vest (Black, Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,192ba73d-6990-42da-8fe0-914c2a7181eb,98553538-cafd-4df9-8f37-bb0a4bb478a5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 90, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6300, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 434, + "FriendlyName": "Cowboy Vest (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37,98553538-cafd-4df9-8f37-bb0a4bb478a5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 91, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6300, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 435, + "FriendlyName": "Cowboy Vest (Cherry)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,624c85e5-095a-48b6-85fb-c9bbade19a8d,98553538-cafd-4df9-8f37-bb0a4bb478a5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 92, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6300, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 440, + "FriendlyName": "Cheerleader Skirt (Pride)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 93, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 441, + "FriendlyName": "Field Hockey Uniform (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 94, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 442, + "FriendlyName": "Firefighter Jacket", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1b37d6cd-0a89-4604-a046-e5cff55b0cf6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 95, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 443, + "FriendlyName": "Flower Swimsuit (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 96, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 444, + "FriendlyName": "Football Shoulder Pads (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 97, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 445, + "FriendlyName": "Football Shoulder Pads (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,32dde6dc-e3dc-43ee-9b7c-987bc6778c17,e17481fc-b2b6-47a5-91e5-7205f45a3247,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 98, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 446, + "FriendlyName": "Football Shoulder Pads (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,88c3ed05-ce41-4a9d-a4a7-8a428d240b3f,e17481fc-b2b6-47a5-91e5-7205f45a3247,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 99, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 447, + "FriendlyName": "Fur Coat (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 100, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 449, + "FriendlyName": "Jazzercise Shirt (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 101, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 451, + "FriendlyName": "Luchador Singlet (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 103, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 452, + "FriendlyName": "Cheerleader Sweater (Pride)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 104, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 458, + "FriendlyName": "Paintball Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 106, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 463, + "FriendlyName": "Long Coat (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 107, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 464, + "FriendlyName": "Belted Dress (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 108, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 465, + "FriendlyName": "Belted Dress (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,5b58d879-2bc4-47ed-a996-1af2ace16425,3d971439-41d1-43a2-8434-704f9ec8d906,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 109, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 466, + "FriendlyName": "Belted Dress (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,d320ad9b-ba5c-45ea-b2b8-0d3e409a9db1,3d971439-41d1-43a2-8434-704f9ec8d906,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 110, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 467, + "FriendlyName": "Belted Dress (Cream)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,d61711fd-589a-4669-b991-9408a58fffd9,3d971439-41d1-43a2-8434-704f9ec8d906,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 111, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 468, + "FriendlyName": "Flowered Dress (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 112, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 469, + "FriendlyName": "Captain Jacket (Blue, White, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 113, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 470, + "FriendlyName": "Captain Jacket (Black, White, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b9f51861-ad10-4878-a9b1-d849bccf532a,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 114, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 471, + "FriendlyName": "Captain Jacket (Black, White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,b5d62931-e756-4a8a-8f3b-760beba49319,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 115, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 472, + "FriendlyName": "Captain Jacket (Black, White, Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,6145292a-9002-41c6-8cd5-43679b6f542f,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 116, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 473, + "FriendlyName": "Captain Jacket (Black, White, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,7c1aaa7e-503b-46ef-b1c5-0c61a75b916e,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 117, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 474, + "FriendlyName": "Captain Jacket (Blue, White, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,27af51bc-8226-4911-8d47-22d28ea8e378,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 118, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 475, + "FriendlyName": "Captain Jacket (Blue, White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,857fdcd8-704f-43d0-92b9-bca26619b153,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 119, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 476, + "FriendlyName": "Captain Jacket (Black, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,3df5d13b-ba77-4b51-90b2-7a946e4d0be7,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 120, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 483, + "FriendlyName": "Boxing Shirt (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 127, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 485, + "FriendlyName": "Doctor's Coat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "27e742bf-3081-4274-ba70-301f0edeb4f1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 129, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 486, + "FriendlyName": "Drum Major Shirt (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 130, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 491, + "FriendlyName": "Jersey (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,8377ab96-c908-457f-9fee-b784c9a759f3,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 131, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 492, + "FriendlyName": "Jersey (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,dee70c38-7a99-4c2b-9181-665f1bf75aca,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 132, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 498, + "FriendlyName": "Lacrosse Shirt (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "39278302-b52a-48a8-a566-ed756a5fa1a9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 138, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 499, + "FriendlyName": "Lacrosse Shirt (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "39278302-b52a-48a8-a566-ed756a5fa1a9,e3454bc7-c88d-4958-8b81-db3fe5472a1e,da36bbcc-2a9a-4ba2-9e4a-6f52c9b1ef74,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 139, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 503, + "FriendlyName": "Tracksuit (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 143, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 517, + "FriendlyName": "Astronaut Gloves (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 149, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 518, + "FriendlyName": "Bike Gloves (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 150, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 520, + "FriendlyName": "Boxing Gloves (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-30T21:00:00Z", + "SaleEndDate": "2021-05-31T21:00:00Z" + } + } + ], + "PurchasableItemId": 152, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 523, + "FriendlyName": "Cowboy Gloves (Cherry)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,17e44296-1cd9-4f8e-9aa7-3837dc69279a,8eef8837-9141-4088-835a-b2f47f4126f8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 155, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 524, + "FriendlyName": "Cowboy Gloves (Black, Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,c4ebfd1a-fcda-4b98-a151-a941b1801867,8eef8837-9141-4088-835a-b2f47f4126f8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 156, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 525, + "FriendlyName": "Cowboy Gloves (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,c0c33a35-494c-4a1a-aa53-d27d9b9ef5a4,8eef8837-9141-4088-835a-b2f47f4126f8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 157, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 526, + "FriendlyName": "Drum Major Gloves (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 158, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 532, + "FriendlyName": "Paintball Gloves", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "64ad8789-1645-45f7-819a-a413ff7c9622,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 164, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1600, + "FriendlyName": "Fighter Pilot Helmet (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,cea53c7f-24a9-4e3b-80b9-fbf95738f973,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 213, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1601, + "FriendlyName": "Fighter Pilot Helmet (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 214, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1603, + "FriendlyName": "Fighter Pilot Uniform (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,cea53c7f-24a9-4e3b-80b9-fbf95738f973,435eba0d-d8f9-43cc-91aa-2959d63c6fa7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 216, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1604, + "FriendlyName": "Fighter Pilot Uniform (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,e37b07d1-6b9d-49b9-94ca-1c728e228cfe,435eba0d-d8f9-43cc-91aa-2959d63c6fa7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 217, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1606, + "FriendlyName": "Fighter Pilot Gloves (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,18b52db5-0261-46a0-9214-ff510d455ed9,7869b2a5-cd89-4033-b9f7-4f1451ecb47b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 219, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1607, + "FriendlyName": "Fighter Pilot Gloves (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,55b4e661-43e1-4708-82d5-2dd2bebd4f45,7869b2a5-cd89-4033-b9f7-4f1451ecb47b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 220, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1616, + "FriendlyName": "Fighter Pilot Helmet (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 227, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1618, + "FriendlyName": "Fighter Pilot Uniform (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 228, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1619, + "FriendlyName": "Fighter Pilot Gloves (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 229, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1629, + "FriendlyName": "Mullet Hair", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2b7f4dae-51da-4af0-b009-12d6005c85c6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 238, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1630, + "FriendlyName": "Curly Mustache", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6b337a35-95d9-4b3a-9891-d9fbb06262aa,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 239, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1642, + "FriendlyName": "Punk Hawk Hair", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ce250e13-8137-4f62-9e52-2b1cebe7d0ad,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-18T21:00:00Z", + "SaleEndDate": "2021-04-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 250, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1643, + "FriendlyName": "Farmer Beard", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7553d851-1986-4325-870d-e425dfb7d007,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 251, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1644, + "FriendlyName": "Safari Belt Sash", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f47f81e7-fcdd-4687-9b99-013147bef30f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 252, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1646, + "FriendlyName": "Punk Jacket (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 254, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1647, + "FriendlyName": "Punk Jacket (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,4169bfed-8403-4590-a29d-4941275ac0b6,d738ad4c-c9b5-41e6-8979-d586c964fb6c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 255, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1648, + "FriendlyName": "Punk Jacket (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e4ba395e-f291-4e7b-8193-45f4422f41ce,0724b166-9ace-46ff-a191-b13f135b00a8,d738ad4c-c9b5-41e6-8979-d586c964fb6c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 256, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1649, + "FriendlyName": "Safari Shirt (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1041d745-c5e9-4218-a3e1-198f5c9c418c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 257, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1650, + "FriendlyName": "Punk Wristbands (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 258, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1651, + "FriendlyName": "Punk Wristbands (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,a6796264-9c7e-4605-830c-06db8d0dc780,dd7472ed-bdcb-4811-81ba-5ed682fb7675,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 259, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1652, + "FriendlyName": "Punk Wristbands (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,f3b2330a-b981-4c7e-902f-1f57382f2388,dd7472ed-bdcb-4811-81ba-5ed682fb7675,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 260, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1653, + "FriendlyName": "Aviator Hat (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 261, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1654, + "FriendlyName": "Safari Hat (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "418ed21e-766f-4c69-9d57-6f866dcc7feb,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 262, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1655, + "FriendlyName": "Aviator Goggles (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 263, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1656, + "FriendlyName": "Safari Monocle", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "02a5b48c-abba-4385-967b-ba900424829e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 264, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1657, + "FriendlyName": "Aviator Coat (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 265, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1658, + "FriendlyName": "Aviator Gloves (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 266, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1659, + "FriendlyName": "Safari Gloves (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "74365234-cb72-409d-8cf5-9a7209f707e1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 267, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1667, + "FriendlyName": "Barbershop Hat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 270, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1668, + "FriendlyName": "Barbershop Shirt (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 271, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1669, + "FriendlyName": "Barbershop Wrist (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ce81210f-8eb8-4cd2-95d5-046d4da229c8,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 272, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1671, + "FriendlyName": "Safari Hat (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "418ed21e-766f-4c69-9d57-6f866dcc7feb,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,0dabda0d-842c-4d39-a6b9-a8ca3ec20825,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 273, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1675, + "FriendlyName": "Safari Shirt (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1041d745-c5e9-4218-a3e1-198f5c9c418c,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,84c016ad-133f-45e3-a412-cac35612540b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 275, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1676, + "FriendlyName": "Punk Wristbands (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7,baffdefe-de21-4551-9330-b50873965bdd,dd7472ed-bdcb-4811-81ba-5ed682fb7675,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 276, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1677, + "FriendlyName": "Safari Gloves (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "74365234-cb72-409d-8cf5-9a7209f707e1,49bbd11a-e5c9-47ff-b04d-72eb321e8a57,fa9a3d8e-16c9-4a7e-9166-3226e0d13c39,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 277, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1681, + "FriendlyName": "Aviator Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,a8de80d3-286a-4fdc-bed2-75d52f8c0964,ff5ae41e-e446-4a96-8c11-49d85556fa81,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 281, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1682, + "FriendlyName": "Aviator Hat (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,c6cf096c-f0cb-4500-80a0-bc2916b5a426,ff5ae41e-e446-4a96-8c11-49d85556fa81,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 282, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1683, + "FriendlyName": "Aviator Goggles (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,7b82c3c5-fd1f-45ec-b83f-e32668aa8dce,556f876a-70da-4104-b792-fca8ad67be0c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 283, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1684, + "FriendlyName": "Aviator Goggles (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,23428ca9-be75-4146-b400-df01d1dadac9,556f876a-70da-4104-b792-fca8ad67be0c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 284, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1690, + "FriendlyName": "Aviator Coat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,da185fbb-ad2b-4f30-87a2-491f24b78ebb,9009bcdf-b6f7-4dae-b2ec-895511d118ff,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 290, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1691, + "FriendlyName": "Aviator Coat (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,bea795cc-aabd-4229-84dd-49ab4f91a445,9009bcdf-b6f7-4dae-b2ec-895511d118ff,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 291, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1692, + "FriendlyName": "Aviator Gloves (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,97bbb384-b37e-4ea1-902c-88fbd6854ab5,5659ce2c-9cb0-49da-8c39-4b7df0f8deec,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 292, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1693, + "FriendlyName": "Aviator Gloves (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7fb8496f-0e6c-4e48-b8fa-5709ebbf4820,5659ce2c-9cb0-49da-8c39-4b7df0f8deec,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 293, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1694, + "FriendlyName": "Barbershop Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,631c9015-2a00-47ea-bb35-465a2ddbca5d,83158223-e268-4036-8a6a-218a8430eb2c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 294, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1695, + "FriendlyName": "Barbershop Hat (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "dcf9012d-f9e9-4f15-872e-f130af3b2efa,7f9cb939-36a4-40da-8337-025e1d7bf8e5,83158223-e268-4036-8a6a-218a8430eb2c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 295, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1696, + "FriendlyName": "Barbershop Shirt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,a5925738-a451-4f25-b622-ccbfa12bd88c,15edb8d7-2ff6-4170-a7e6-acd9e965cab5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 296, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1697, + "FriendlyName": "Barbershop Shirt (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "18cd3470-2dd9-4a0a-98bd-eae2d5f5d522,c64ac7a9-7fd4-4587-8626-0413a190b706,15edb8d7-2ff6-4170-a7e6-acd9e965cab5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 297, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1698, + "FriendlyName": "Barbershop Wrist (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ce81210f-8eb8-4cd2-95d5-046d4da229c8,bb01b51e-a95f-441e-8c52-abc3aeed7145,f7c7d05f-5454-445a-af38-25f110bd204a,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 298, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1704, + "FriendlyName": "Drum Major Hat (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,5a614f97-ef7f-4b70-afff-9ed837a86407,a8a91216-0fcc-461e-a682-95ee82509cce,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 300, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1706, + "FriendlyName": "Drum Major Hat (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,8053b5e5-0f67-4329-a45d-1ee9fc830820,a8a91216-0fcc-461e-a682-95ee82509cce,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 302, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1707, + "FriendlyName": "Field Hockey Uniform (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a986ff33-9e16-4219-8ebb-2e7b4c772ca7,7b0d6661-e856-4ec1-ab90-d89c28a70826,1fede91a-434e-49a5-882e-e6db107112a8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 303, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1708, + "FriendlyName": "Drum Major Shirt (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,fe5288bc-0e02-4ea3-b17d-dee0e576979d,b0298c45-0dd1-453e-bad4-41e4656bb38e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 304, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1710, + "FriendlyName": "Drum Major Shirt (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,54d3c589-30ba-44c1-a77c-8f9e16eb0b20,b0298c45-0dd1-453e-bad4-41e4656bb38e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 306, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1711, + "FriendlyName": "Drum Major Gloves (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,fe5288bc-0e02-4ea3-b17d-dee0e576979d,6b38ab79-142b-446e-978c-ad829ade8823,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 307, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1712, + "FriendlyName": "Drum Major Gloves (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,b444ed0a-eab5-4bd6-af34-d080de74a149,6b38ab79-142b-446e-978c-ad829ade8823,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 308, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1713, + "FriendlyName": "Drum Major Gloves (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,8053b5e5-0f67-4329-a45d-1ee9fc830820,6b38ab79-142b-446e-978c-ad829ade8823,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 309, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1716, + "FriendlyName": "Boxing Helmet (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,92714eac-cc07-4f74-a6f5-cfbbadd07e06,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 312, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3600, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1717, + "FriendlyName": "Cheerful Dress (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,6e0f3af8-297f-4ea7-8f9e-4a1bc57d3e35,93987d32-7a6e-45df-af56-76f912969ce7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 313, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1718, + "FriendlyName": "Cheerful Dress (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,faedf215-289b-40f7-90cc-f5b98517d133,93987d32-7a6e-45df-af56-76f912969ce7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 314, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1719, + "FriendlyName": "Cheerful Dress (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,1773d5d8-4714-4721-b30c-97e25d6f2a5a,93987d32-7a6e-45df-af56-76f912969ce7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 315, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1720, + "FriendlyName": "Flower Swimsuit (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,cf515c87-3c0a-4443-9f63-5ea1bdbaa61a,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 316, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1721, + "FriendlyName": "Flower Swimsuit (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,27680277-aec1-41be-a476-9f51cf775580,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 317, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1722, + "FriendlyName": "Flower Swimsuit (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f41653f9-ae02-443c-aaf4-1c6f4e49035d,5a96ac4d-03c3-4214-9dcb-c2044ff28f2e,3248a3ec-50d1-4c0a-96ae-25eb9d8ed622,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 318, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1723, + "FriendlyName": "Boxing Shirt (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,0ec6628c-a106-4452-af82-105a3e3a9f2e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 319, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1724, + "FriendlyName": "Boxing Gloves (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-30T21:00:00Z", + "SaleEndDate": "2021-05-31T21:00:00Z" + } + } + ], + "PurchasableItemId": 320, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1728, + "FriendlyName": "Bowtie (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 324, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1729, + "FriendlyName": "Bowtie (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 325, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1730, + "FriendlyName": "Bowtie (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e69372eb-2fd8-4e17-a448-ee0be61a2c7a,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 326, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1731, + "FriendlyName": "Bandana (Dots, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 327, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1732, + "FriendlyName": "Bandana (Dots, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 328, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1733, + "FriendlyName": "Bandana (Dots, White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 329, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1734, + "FriendlyName": "Bandana (Flowers, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,27a5df72-4095-4837-bf24-cdd3d95a43e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 330, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1735, + "FriendlyName": "Bandana (Flowers, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,27a5df72-4095-4837-bf24-cdd3d95a43e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 331, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1736, + "FriendlyName": "Bandana (Flowers, White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,27a5df72-4095-4837-bf24-cdd3d95a43e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 332, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1737, + "FriendlyName": "Bandana (Plaid, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 333, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1738, + "FriendlyName": "Bandana (Plaid, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 334, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1739, + "FriendlyName": "Bandana (Plaid, White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 335, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1740, + "FriendlyName": "Business Tie (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,dee70c38-7a99-4c2b-9181-665f1bf75aca,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 336, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1741, + "FriendlyName": "Business Tie (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,0ecb8a2a-cffc-47db-aeda-fb0684aef1e5,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 337, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1742, + "FriendlyName": "Business Tie (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,8377ab96-c908-457f-9fee-b784c9a759f3,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 338, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1743, + "FriendlyName": "Business Tie (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,724c79a3-822c-422f-9462-a5374ee0211c,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 339, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1744, + "FriendlyName": "Business Tie (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c6c08eb5-381a-4193-9722-80da95d62abe,67bcca75-4ab1-4964-8688-9908c464d355,0b2395e1-ebcc-47e9-aaf1-faf9e9cec4cd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 340, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1748, + "FriendlyName": "Tied Scarf (Plaid, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 341, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1749, + "FriendlyName": "Tied Scarf (Plaid, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 342, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1750, + "FriendlyName": "Tied Scarf (Plaid, White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 343, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1751, + "FriendlyName": "Tied Scarf (Dots, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,827a1538-51bb-4fef-99c1-7f1bebd9866c,b292eb4b-07e3-4a48-99b5-3c6587a1e02e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 344, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1752, + "FriendlyName": "Tied Scarf (Dots, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,ad61c418-6d77-4a99-8ac5-9f10f5a3d42f,b292eb4b-07e3-4a48-99b5-3c6587a1e02e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 345, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1753, + "FriendlyName": "Tied Scarf (Dots, White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5d1c42a0-7e36-431a-8e78-a8b74046d153,6dd95046-acf8-42fe-ab78-80a334096a9d,b292eb4b-07e3-4a48-99b5-3c6587a1e02e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 346, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1756, + "FriendlyName": "Fur Coat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 347, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1757, + "FriendlyName": "Fur Coat (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "77c37481-a185-47ff-af55-65f9fdfcf9e5,643b5f2d-0666-4869-b699-03742de34d16,2c0d912d-c184-4eab-8ee4-73b90ba7b0a6,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 348, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1758, + "FriendlyName": "Long Coat (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,b42d903b-b393-4f49-af5e-38d0fb6c42b0,3ba69a39-a4e7-4505-bae7-8db31360ec1c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 349, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1759, + "FriendlyName": "Long Coat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,8cab8cc6-9a2d-434c-8b72-e66edbdd223c,3ba69a39-a4e7-4505-bae7-8db31360ec1c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 350, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1760, + "FriendlyName": "Long Coat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,657f28d5-672a-4f27-86c6-3cb00f25bb84,3ba69a39-a4e7-4505-bae7-8db31360ec1c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 351, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1761, + "FriendlyName": "Flowered Dress (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,1acb1f56-4509-449f-9c6a-065bdeaa9ee3,2d7361d2-31a2-404b-90da-10e89114e1f2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 352, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1762, + "FriendlyName": "Flowered Dress (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "515dfc36-7f7b-47bc-abee-380d68f41be6,e98c3f98-7844-4d6e-a21c-05027033c18c,2d7361d2-31a2-404b-90da-10e89114e1f2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 353, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1767, + "FriendlyName": "Flower Headband (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,c5884778-a87f-4612-9717-86fbb65d440f,7d040391-03f7-4b3d-bcfd-a1278ff459c9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 358, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1768, + "FriendlyName": "Flower Headband (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2b2eb9e5-d52e-4004-bdb2-0bcf6910992d,827a1538-51bb-4fef-99c1-7f1bebd9866c,7d040391-03f7-4b3d-bcfd-a1278ff459c9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 359, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1771, + "FriendlyName": "Bike Helmet (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,83522c12-9549-4d74-8c87-bae210bcd2bf,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 362, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1772, + "FriendlyName": "Bike Helmet (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 363, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1773, + "FriendlyName": "Flower Hair Clip (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,ce465d53-3807-423f-baa0-bdba2a75ceb3,65b89eae-347e-4eff-9ff3-7f25cace0125,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 364, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1774, + "FriendlyName": "Flower Hair Clip (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01,596097a8-d545-4256-959d-1d3cd96fc64c,65b89eae-347e-4eff-9ff3-7f25cace0125,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 365, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1775, + "FriendlyName": "Luchador Mask (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,916384ca-e209-42a0-bd64-c64f5a40b56f,96b36c98-cba1-4337-91dd-c74c00316f97,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-23T21:00:00Z", + "SaleEndDate": "2021-05-24T21:00:00Z" + } + } + ], + "PurchasableItemId": 366, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1776, + "FriendlyName": "Luchador Mask (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,96b36c98-cba1-4337-91dd-c74c00316f97,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-23T21:00:00Z", + "SaleEndDate": "2021-05-24T21:00:00Z" + } + } + ], + "PurchasableItemId": 367, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1777, + "FriendlyName": "Small Brim Hat (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,67fc9269-8dd8-4d6f-b594-c77e716f2482,65ed43ff-dfc4-4cab-b0b9-77dc94165e24,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 368, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1778, + "FriendlyName": "Small Brim Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,43501b0c-eaaa-4ab3-9d03-2ca61d2e8fc9,65ed43ff-dfc4-4cab-b0b9-77dc94165e24,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 369, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1779, + "FriendlyName": "Small Brim Hat (Maroon)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9cdb4965-6f00-4304-afed-e301e73a2b3a,d6823e01-69f0-4f85-b94a-74894356a2cf,65ed43ff-dfc4-4cab-b0b9-77dc94165e24,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 370, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1782, + "FriendlyName": "Bike Shirt (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,c5884778-a87f-4612-9717-86fbb65d440f,1695608b-e9cc-4a03-b56d-0f09d2804379,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 371, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1783, + "FriendlyName": "Bike Shirt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,83522c12-9549-4d74-8c87-bae210bcd2bf,1695608b-e9cc-4a03-b56d-0f09d2804379,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 372, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1787, + "FriendlyName": "Luchador Singlet (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,916384ca-e209-42a0-bd64-c64f5a40b56f,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 373, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1788, + "FriendlyName": "Luchador Singlet (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7ef7d0cf-99f1-4872-9d3d-8230cc9fa368,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 374, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1789, + "FriendlyName": "Tracksuit (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,026ca50e-3a81-43cf-87f5-950687b7bbdd,adae82eb-a4b9-4541-acf2-76cea839a593,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 375, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1790, + "FriendlyName": "Tracksuit (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,6584dada-499e-4d7c-958d-466955b20640,adae82eb-a4b9-4541-acf2-76cea839a593,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 376, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1813, + "FriendlyName": "Babydoll Dress (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,c9589974-c261-485e-b571-cb2f34fb178f,cb9e9e05-c481-4397-86e7-c2111bc0e817,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 398, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1864, + "FriendlyName": "Jersey (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,f8b0cfe8-e129-4578-8bb5-f60af5d38599,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 446, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1865, + "FriendlyName": "Jersey (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,d2b40639-5d34-45b6-904e-6dd29030ff44,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 447, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1866, + "FriendlyName": "Jersey (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,724c79a3-822c-422f-9462-a5374ee0211c,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 448, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1867, + "FriendlyName": "Jersey (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,c72911d0-453f-4732-b1bd-dad520220a06,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 449, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1868, + "FriendlyName": "Jersey (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,90bc2d31-1715-4d1a-813a-7c57e66cb017,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 450, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1869, + "FriendlyName": "Jersey (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,1b1d08f2-12ca-43dd-a44f-ea2820b919b4,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 451, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1874, + "FriendlyName": "Captain Hat (Pink, Black, Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,2738ab5f-8c94-46b3-bac6-4a8111c41c21,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 456, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1885, + "FriendlyName": "Romper (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e042433a-40a2-4758-8074-3b9d407835ec,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 463, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1887, + "FriendlyName": "Mobster Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 464, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1888, + "FriendlyName": "Mobster Suit (Pinstripe)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 465, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1889, + "FriendlyName": "Mobster Suit (Herringbone)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,531b4297-b38b-4362-9854-4f2139ea43be,33ee376a-586e-4aff-a7e4-4213445a3e92,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 466, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1890, + "FriendlyName": "Mobster Cuffs (Pinstripe)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 467, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1927, + "FriendlyName": "Flapper Headband (Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 493, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1929, + "FriendlyName": "Flapper Dress (Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 494, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1930, + "FriendlyName": "Flapper Gloves (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 495, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1977, + "FriendlyName": "3-Star Unique Box", + "Tooltip": "A random 3-star item that you don't have.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": true, + "QueryRedirectContext": 0, + "QueryRedirectRarity": 20, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 44, + "ItemSetFriendlyName": "Star Boxes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2201, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1978, + "FriendlyName": "4-Star Unique Box", + "Tooltip": "A random 4-star item that you don't have.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": true, + "QueryRedirectContext": 0, + "QueryRedirectRarity": 30, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 44, + "ItemSetFriendlyName": "Star Boxes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2202, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1980, + "FriendlyName": "2-Star Unique Box", + "Tooltip": "A random 2-star item that you don't have.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": true, + "QueryRedirectContext": 0, + "QueryRedirectRarity": 10, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 44, + "ItemSetFriendlyName": "Star Boxes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2203, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2007, + "FriendlyName": "Film (Black \u0026 White)", + "Tooltip": "Take some black and white photos.", + "ConsumableItemDesc": "frOMH6WxDEG1fBqC4_83vg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 20, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 568, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 18, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2008, + "FriendlyName": "Film (Dawn)", + "Tooltip": "Take some sun-soaked photos.", + "ConsumableItemDesc": "m0bVLwWGj0GuIBSb6wCk6Q", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 20, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 569, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 18, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2009, + "FriendlyName": "Film (Sepia)", + "Tooltip": "Take some old-timey photos.", + "ConsumableItemDesc": "A5M-yf9tgUihq1uab3v58g", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 20, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 570, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 18, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2010, + "FriendlyName": "Ego Potion", + "Tooltip": "Makes your head BIG for a time.", + "ConsumableItemDesc": "Tpxqe_lycUelySRHM8B0Vw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 571, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2011, + "FriendlyName": "Root Beer", + "Tooltip": "A sixer of root beer to share with friends.", + "ConsumableItemDesc": "JfnVXFmilU6ysv-VbTAe3A", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 50, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 572, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 45, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2013, + "FriendlyName": "High Five Potion (Golden)", + "Tooltip": "Give golden high fives for a time.", + "ConsumableItemDesc": "-hy0qD-iUk-v4NHxNzanmg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4004, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 30, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2204, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 27, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2014, + "FriendlyName": "Supreme Pizza", + "Tooltip": "A supreme pizza to share with friends.", + "ConsumableItemDesc": "wUCIKdJSvEmiQHYMyx4X4w", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 574, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2015, + "FriendlyName": "High Five Potion (Magic)", + "Tooltip": "Give magical high fives for a time.", + "ConsumableItemDesc": "VQSgL2pTLkWx4B3kwYG7UA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 4014, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 30, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2205, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 27, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2025, + "FriendlyName": "Cheese Pizza", + "Tooltip": "A cheese pizza to share with friends.", + "ConsumableItemDesc": "5hIAZ9wg5EyG1cILf4FS2A", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": { + "SalePercent": 30, + "SaleStartDate": "2021-09-05T07:00:00Z", + "SaleEndDate": "2021-09-06T07:00:00Z" + } + } + ], + "PurchasableItemId": 582, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 76, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2027, + "FriendlyName": "Pepperoni Pizza", + "Tooltip": "A pepperoni pizza to share with friends.", + "ConsumableItemDesc": "mq23W-RSP0G8iGNLdrcpUw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 584, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 81, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2039, + "FriendlyName": "Flapper Headband (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,565c17e0-496d-45bd-a2c3-564e118c06cc,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 596, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2040, + "FriendlyName": "Flapper Headband (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,d9be3a98-9da9-419e-9dce-fe58a3ad9c4e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 597, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2041, + "FriendlyName": "Mobster Hat (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,40283127-2ffc-4989-82f6-29c3381bf9e2,d759c88d-a159-4c26-86dd-b054bb05b3ed,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 598, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2042, + "FriendlyName": "Mobster Hat (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,1421caa9-9a0d-42c2-b847-1500ba2e483a,d759c88d-a159-4c26-86dd-b054bb05b3ed,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 599, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2043, + "FriendlyName": "Mobster Hat (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,390a476a-5545-48eb-955c-1bf1a16012bf,d759c88d-a159-4c26-86dd-b054bb05b3ed,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 600, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2044, + "FriendlyName": "Bow (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,c9589974-c261-485e-b571-cb2f34fb178f,e0186718-4a18-434b-b96e-0c9c912f0d1f,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 601, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2045, + "FriendlyName": "Bow (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1e504fee-b593-4037-bd8f-b88025147991,6564acf1-4d70-4f92-92ac-08e2b76dbb6b,e0186718-4a18-434b-b96e-0c9c912f0d1f,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 602, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2047, + "FriendlyName": "Equestrian Helmet (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,55901f12-d5b5-4fa8-b4c8-e479689ee39d,be6b3fa1-3388-48ac-8957-d7b8c85ff968,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 604, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2048, + "FriendlyName": "Equestrian Helmet (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,4828b50c-95b6-466a-bb25-514891d78202,be6b3fa1-3388-48ac-8957-d7b8c85ff968,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 605, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2049, + "FriendlyName": "Equestrian Helmet (Maroon)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a628d62c-fa30-4405-bcbc-3e646e3a3434,d6823e01-69f0-4f85-b94a-74894356a2cf,be6b3fa1-3388-48ac-8957-d7b8c85ff968,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 606, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2050, + "FriendlyName": "Firefighter Helmet (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,891d902e-3257-4a8c-9001-858046007997,c8ccb85e-52cd-4b3b-aadf-289819067125,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 607, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2051, + "FriendlyName": "Firefighter Helmet (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1,4c26d1e7-2382-4478-b9c4-a068ed01a607,c8ccb85e-52cd-4b3b-aadf-289819067125,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 608, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2052, + "FriendlyName": "Flat Cap (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,73fc2e2a-a036-410b-bafa-3a07658245b5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 609, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2053, + "FriendlyName": "Flat Cap (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,0ba92db2-690a-44ce-92f4-130e9503b6c1,73fc2e2a-a036-410b-bafa-3a07658245b5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 610, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2054, + "FriendlyName": "Hockey Helmet (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace,7c4d447c-6594-4bd8-9510-12ad683ab116,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 611, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2055, + "FriendlyName": "Hockey Helmet (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d703940-224a-4b0c-a04d-f365550071e1,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,7c4d447c-6594-4bd8-9510-12ad683ab116,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 612, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2056, + "FriendlyName": "Flapper Dress (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,565c17e0-496d-45bd-a2c3-564e118c06cc,a240d44f-963d-4d35-b22f-980938c7788d,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 613, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2057, + "FriendlyName": "Flapper Dress (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5f600892-aa67-484a-afc7-14c219d89c52,b6c25a3b-125b-4aab-bed0-e6e5ced2c21c,a240d44f-963d-4d35-b22f-980938c7788d,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 614, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2058, + "FriendlyName": "Flapper Gloves (Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,30c61e63-5e09-4a6f-8915-1e1fe4d55cf0,0e42108e-3b08-49b4-86c0-7984719e8380,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 615, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2059, + "FriendlyName": "Flapper Gloves (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "473c672d-2b79-48a2-a49d-fd68160c5bde,2cf2b644-3fa6-449c-9fe8-d0ab917f3106,0e42108e-3b08-49b4-86c0-7984719e8380,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 616, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2065, + "FriendlyName": "Basketball Jersey (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,59a62fa3-9e15-463e-9bd5-3aecdf604c1b,54a5c055-f757-4c00-a1e1-8b0b6552c608,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 622, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2066, + "FriendlyName": "Basketball Jersey (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,4effd67c-a52f-4b41-a5b0-04287f91a7bc,54a5c055-f757-4c00-a1e1-8b0b6552c608,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 623, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2067, + "FriendlyName": "Jazzercise Shirt (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,3b2cfc85-4e2a-4e0d-b738-c296ae8692f8,f6f26e2b-657b-454a-8663-55d6b9b6e50c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 624, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2068, + "FriendlyName": "Jazzercise Shirt (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5f492f8f-ab61-484a-9587-747132b54309,78eb0969-f246-4cac-acd6-295df7c092e4,f6f26e2b-657b-454a-8663-55d6b9b6e50c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 625, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2069, + "FriendlyName": "Paintball Vest (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 626, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2070, + "FriendlyName": "Paintball Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 627, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2072, + "FriendlyName": "Conductor Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 628, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2073, + "FriendlyName": "Conductor Jacket (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 629, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2074, + "FriendlyName": "Conductor Cuffs (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 630, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2075, + "FriendlyName": "KO Icon - Winged Skull", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "U38Qe6rhEk6mFvArHfYjng", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 631, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2076, + "FriendlyName": "Deep Sea Diver Helmet (Triton)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 632, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8550, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2077, + "FriendlyName": "Babydoll Dress (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf,da6d0ced-3d43-4878-a868-925f2ed6fd5b,cb9e9e05-c481-4397-86e7-c2111bc0e817,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 633, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2078, + "FriendlyName": "Deep Sea Diver Suit (Triton)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2206, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2079, + "FriendlyName": "Deep Sea Diver Gloves (Triton)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-10-03T21:00:00Z", + "SaleEndDate": "2021-10-04T21:00:00Z" + } + } + ], + "PurchasableItemId": 2207, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2080, + "FriendlyName": "KO Icon - Bear Claw", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "ZtZnYBpKkECJlhHmkj4MiA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 636, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2081, + "FriendlyName": "KO Icon - Sword \u0026 Shield", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "5AJin8T2iEG7BzOPOgx2HA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 637, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2102, + "FriendlyName": "Winner's Medal (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-07-25T21:00:00Z", + "SaleEndDate": "2021-07-26T21:00:00Z" + } + } + ], + "PurchasableItemId": 658, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2163, + "FriendlyName": "Baseball Jersey (White, Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,e5de2e45-207c-46af-9a5e-eb01a0c621fe,ca37823a-b8f3-4414-b38b-de0dc7fa4295,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 719, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2164, + "FriendlyName": "Baseball Jersey (White, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,de0a5a3a-8aff-4799-8269-9efa3aeb383a,ca37823a-b8f3-4414-b38b-de0dc7fa4295,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 720, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2168, + "FriendlyName": "Jersey (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,cbe29e9f-f2ac-47fb-97e1-8bad16abb89d,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 724, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2194, + "FriendlyName": "Jersey (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7b857a8c-92ad-4028-a2c2-b3c20cdab5f2,51ef8d39-2b94-4f9e-9620-07b6b0a913a5,d2a692e6-e1a9-4cfe-8154-10b52be7f8c8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 750, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2242, + "FriendlyName": "Ranger Hat (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f277af6a-0807-4716-8948-1bee236ffb74,d0bb468c-b78f-4e80-916d-65fec3ec0010,0272eb19-fc0e-4ad6-8db9-cc0cda2528b0,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 826, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2245, + "FriendlyName": "Ranger Uniform (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fb907eb2-91c8-45ff-84a5-462bb86ea7de,b415fe60-3e9d-4f4d-a53a-ca15eac6af7d,d21e1b6f-9c9e-42b4-bad5-201ec43161e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 825, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2222, + "FriendlyName": "Deep Sea Diver Tank (Triton)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2208, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2241, + "FriendlyName": "Beekeeper Veil (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,1919d319-6cce-4500-8766-15fed6a13fa8,e4b53a9a-ac95-49ae-bf68-615bb23fa075,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 822, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2244, + "FriendlyName": "Beekeeper Suit (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,1919d319-6cce-4500-8766-15fed6a13fa8,afd88a79-7f03-40f6-bcb4-c247dd86edc4,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 823, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2248, + "FriendlyName": "Beekeeper Gloves (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,1919d319-6cce-4500-8766-15fed6a13fa8,1f18670d-df52-4d2b-8009-a32cf7c67b40,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 824, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2032, + "FriendlyName": "Space Visor (Nova)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 589, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8550, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2274, + "FriendlyName": "Detective Cap (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 829, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2275, + "FriendlyName": "Detective Coat (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 830, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2279, + "FriendlyName": "RecFly Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 831, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2271, + "FriendlyName": "Royal Cape (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2209, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2289, + "FriendlyName": "Clubmaster Glasses", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "TcgHS_0aPkehrBg7ytMOMQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 851, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2290, + "FriendlyName": "Poindexter Shirt (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 852, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2270, + "FriendlyName": "Royal Crown (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-04T21:00:00Z", + "SaleEndDate": "2021-04-07T21:00:00Z" + } + } + ], + "PurchasableItemId": 2210, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2272, + "FriendlyName": "Royal Dress (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 50, + "ItemSetFriendlyName": "Royal Dresses" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2211, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2273, + "FriendlyName": "Royal Tabard (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 49, + "ItemSetFriendlyName": "Royal Tabards" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2212, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2263, + "FriendlyName": "Cat Ears (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,c1e430c5-fd8d-4032-9e3f-8d87cc8784da,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 854, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2307, + "FriendlyName": "Biker Helmet", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "VQnoLstCyEGDPy1c38-GvA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 865, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2310, + "FriendlyName": "Biker Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "_qXaNbm4yEeyalo372QbvA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 872, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2312, + "FriendlyName": "Biker Jacket", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "n52um7A-b0utGxixd1ESvA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 866, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2324, + "FriendlyName": "Glazed Donuts", + "Tooltip": "A dozen glazed donuts to share with friends.", + "ConsumableItemDesc": "7OZ5AE3uuUyqa0P-2W1ptg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 34, + "ItemSetFriendlyName": "Donuts by the Dozen" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 855, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 81, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2326, + "FriendlyName": "Assorted Donuts", + "Tooltip": "A dozen assorted donuts to share with friends.", + "ConsumableItemDesc": "ZuvkidodzkuOfGLDnTOFyg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 34, + "ItemSetFriendlyName": "Donuts by the Dozen" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 857, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2327, + "FriendlyName": "Chocolate Donuts", + "Tooltip": "A dozen chocolate donuts to share with friends.", + "ConsumableItemDesc": "mMCGPgK3tki5S_15q2Z81A", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 34, + "ItemSetFriendlyName": "Donuts by the Dozen" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 856, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2233, + "FriendlyName": "KO Icon - Star Power", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "yvqSbK2czkS2sUCRdrGaEw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 789, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2231, + "FriendlyName": "KO Icon - Grenade", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "EAhk3ZZdXEmH5wRAXXT24Q", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 787, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2321, + "FriendlyName": "Peacoat (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 863, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2265, + "FriendlyName": "Mobster Cuffs (Herringbone)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,531b4297-b38b-4362-9854-4f2139ea43be,c2a9c007-2748-4fa2-ab9b-0081eef792f1,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 878, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2319, + "FriendlyName": "Windbreaker (Teal)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 858, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2322, + "FriendlyName": "Windbreaker Cuffs (Teal)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 859, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2316, + "FriendlyName": "Shutter Shades (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-11T21:00:00Z", + "SaleEndDate": "2021-04-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 869, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2320, + "FriendlyName": "Letter Jacket (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 867, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2323, + "FriendlyName": "Letter Jacket Cuffs (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 879, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2318, + "FriendlyName": "Swing Dress (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 868, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2317, + "FriendlyName": "Plastic Sunglasses (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 871, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2409, + "FriendlyName": "Anime Tiara (Fuchsia)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 909, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2412, + "FriendlyName": "Anime Bow (Fuchsia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 898, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2418, + "FriendlyName": "Anime Skirt (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 908, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2422, + "FriendlyName": "Anime Glove (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 886, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2311, + "FriendlyName": "Classic Glasses", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "kghEf_iMKUSxPO-h5iRhSQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 870, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2261, + "FriendlyName": "Cat Ears (Brown)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,713781e5-ce47-41c6-a24f-94d42a565e30,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 913, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2470, + "FriendlyName": "Hawaiian Pizza", + "Tooltip": "A ham and pineapple pizza to share with friends.", + "ConsumableItemDesc": "_jnjYGBcyEWY5Ub4OezXcA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 922, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2471, + "FriendlyName": "Salted Pretzels", + "Tooltip": "A great snack to share with friends!", + "ConsumableItemDesc": "InQ25wQMGkG_bvuD5rf2Ag", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 52, + "ItemSetFriendlyName": "Snickety Snacks" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 75, + "StorefrontSaleData": { + "SalePercent": 25, + "SaleStartDate": "2021-04-26T07:00:00Z", + "SaleEndDate": "2021-04-27T07:00:00Z" + } + } + ], + "PurchasableItemId": 921, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 67, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2445, + "FriendlyName": "Rec Room Hoodie (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,u_Gzj9Nro0WS48xKnH9few,oHKhT-GTx0iFWrrlz9s6Tw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 891, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2446, + "FriendlyName": "Rec Room Hoodie (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,mvMeyStxGUGlNLSYNeSubQ,oHKhT-GTx0iFWrrlz9s6Tw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 894, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2452, + "FriendlyName": "Rec Room Hoodie (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,rNhOQoP3XkOBHV28ze2NpA,oHKhT-GTx0iFWrrlz9s6Tw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 893, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2455, + "FriendlyName": "Rec Room Hoodie (Grey)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,JTgXKeT7w0K2Mw4Jk2HCqg,oHKhT-GTx0iFWrrlz9s6Tw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 892, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2308, + "FriendlyName": "Detective Cap (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,fEPwvuJCWkGBe4IekXpmLw,axPBGoqorEe77Oqqjfzb5w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 880, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2314, + "FriendlyName": "Detective Coat (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,Z1Ib9r_fkUqgoZk0uid_Xw,9ZP617DQk0mZ10J3d5q9oA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 861, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2450, + "FriendlyName": "Peacoat (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,fvoaMN4M10WS6jiA6XlOJQ,AZnPWCN41kmNwFwDqWGFWQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 888, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2451, + "FriendlyName": "Peacoat (Tan)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,PwsLxCnzOkSW-RGz3tNb1w,AZnPWCN41kmNwFwDqWGFWQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 890, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2453, + "FriendlyName": "Peacoat (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,uiBklotTUEqRI2F67vyJyA,AZnPWCN41kmNwFwDqWGFWQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 889, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2481, + "FriendlyName": "Peacoat (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5,jXBi28cRV0CEQgKIrpD0_A,AZnPWCN41kmNwFwDqWGFWQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 953, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2379, + "FriendlyName": "D4 Skin (Turquoise)", + "Tooltip": "Reskin your dice.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D4]", + "EquipmentModificationGuid": "_2jxSTYSpEykZ_O91oDAzg", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 957, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2383, + "FriendlyName": "D6 Skin (Turquoise)", + "Tooltip": "Reskin your dice.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D6]", + "EquipmentModificationGuid": "HnVo08F-zk-YeikERFa1Yw", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 958, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2387, + "FriendlyName": "D8 Skin (Turquoise)", + "Tooltip": "Reskin your dice.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D8]", + "EquipmentModificationGuid": "f45tTZp4KUextmAsX8X9Hw", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 959, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2391, + "FriendlyName": "D10 Skin (Turquoise)", + "Tooltip": "Reskin your dice.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D10]", + "EquipmentModificationGuid": "SnSOf-GZ2kastpF5KWxT-Q", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 954, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2395, + "FriendlyName": "D12 Skin (Turquoise)", + "Tooltip": "Reskin your dice.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D12]", + "EquipmentModificationGuid": "ub6apg3zEUiCp3k81JjQGw", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 955, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2399, + "FriendlyName": "D20 Skin (Turquoise)", + "Tooltip": "Reskin your dice.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D20]", + "EquipmentModificationGuid": "DrG0ntsyPUSv8ra0m-Hhaw", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 956, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2458, + "FriendlyName": "Bike Shirt (Navy)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "843e3364-a743-4dfc-a990-0436e47b8c10,XNq4Cns4lEGydtktdsXlmQ,1695608b-e9cc-4a03-b56d-0f09d2804379,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 887, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2514, + "FriendlyName": "Bike Helmet (Navy)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "71014872-d879-4858-9d68-58da3c673d8b,XNq4Cns4lEGydtktdsXlmQ,5fd9e83a-cfaf-4c98-bd31-91a7484c26dd,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 988, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2447, + "FriendlyName": "Swing Dress (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,QzjakcO8tUaPeiI5KBLKtg,Rk18_7icF0Srgh7M1_G-hA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 884, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2448, + "FriendlyName": "Swing Dress (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,Yr0mF1EWzEaIyRMOWFiY6w,Rk18_7icF0Srgh7M1_G-hA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 883, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2456, + "FriendlyName": "Swing Dress (Pink)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,eb3SmJTIh0628LrZUkI4KQ,Rk18_7icF0Srgh7M1_G-hA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 885, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2466, + "FriendlyName": "Swing Dress (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee,BLgkgLeS-EyazgPQxGcavg,Rk18_7icF0Srgh7M1_G-hA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 882, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2473, + "FriendlyName": "Bowling Shirt (Hambone)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,-k9_-2onrEG407or8t1RFw,b-IM3eYMJUa40395O5XeLg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 942, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2474, + "FriendlyName": "Bowling Shirt (Turkey)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 943, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2475, + "FriendlyName": "Bowling Shirt (Flames, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,8xjU4GQ4OkaqGyad-ypwtw,5RUG6i2Mx0-55IO-IlhUdQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 944, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2476, + "FriendlyName": "Bowling Shirt (Deadwood)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,ghM-uFq5-0Ci38AOHa9zZA,IWg9wuda4kaCilJk8JQpTQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 941, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2570, + "FriendlyName": "Cowboy Vest (Black, Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,K5vPqD3BrEuODRDzFdC6Hw,98553538-cafd-4df9-8f37-bb0a4bb478a5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 992, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2589, + "FriendlyName": "Cowboy Hat (Black, Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,K5vPqD3BrEuODRDzFdC6Hw,54778d93-2cff-48db-b25b-65c08b0e5be8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 990, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2606, + "FriendlyName": "Cowboy Gloves (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,wat8f6fs80KptZbW39e8tA,8eef8837-9141-4088-835a-b2f47f4126f8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 994, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2607, + "FriendlyName": "Cowboy Gloves (Black, Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,wkqKPZmnoU-8TXSWzi2-Ow,8eef8837-9141-4088-835a-b2f47f4126f8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 993, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2571, + "FriendlyName": "Cowboy Vest (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,teVKVbVEsEyqOSuRdqHQzA,98553538-cafd-4df9-8f37-bb0a4bb478a5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 991, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2590, + "FriendlyName": "Cowboy Hat (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,teVKVbVEsEyqOSuRdqHQzA,54778d93-2cff-48db-b25b-65c08b0e5be8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 989, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2532, + "FriendlyName": "Windbreaker Cuffs (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,FvEjo9let0e_Sr-kWq5AvQ,MV1HW2x43kG4rrhyoC6mmw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1001, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2566, + "FriendlyName": "Windbreaker (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,JS7aa4QSUUOpUYPqRZEb5g,oSaMpc5rTE-U6yhjtPWQgg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 995, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2533, + "FriendlyName": "Windbreaker Cuffs (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,VDo682NvSUa8xYn8zTX-AQ,MV1HW2x43kG4rrhyoC6mmw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1002, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2534, + "FriendlyName": "Windbreaker Cuffs (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,42ZNGHXMvU29yo9s9tVpOQ,MV1HW2x43kG4rrhyoC6mmw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 999, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2535, + "FriendlyName": "Windbreaker Cuffs (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "52VsI_lKlkSUAOJlfDnyrQ,HncURj5yi0ykk2cVyCZ4Sw,MV1HW2x43kG4rrhyoC6mmw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1000, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2567, + "FriendlyName": "Windbreaker (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,C3ll7uPfUkeZ1QLX80_BpQ,oSaMpc5rTE-U6yhjtPWQgg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 996, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2568, + "FriendlyName": "Windbreaker (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,EbVDqEJQOk6j0UvX3t-8rQ,oSaMpc5rTE-U6yhjtPWQgg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 997, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2569, + "FriendlyName": "Windbreaker (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "AIpF1QUeMUWytpHYBDBAkg,grutFwJD_0Cb3t6fddNg6w,oSaMpc5rTE-U6yhjtPWQgg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 998, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2629, + "FriendlyName": "Ski Buddy Jacket (Summit)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1156, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2630, + "FriendlyName": "Ski Buddy Goggles (Summit)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1155, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2588, + "FriendlyName": "Top Hat (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,DzgN67HoikexE2rTlZLdYQ,vm1ESK9ls0SaxSwHplNALQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1163, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2544, + "FriendlyName": "Letter Jacket (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,iufpnteVCEiiZu_m-yPovQ,fz_0gZ6RXEC78NpYsx_eqA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1108, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2545, + "FriendlyName": "Letter Jacket (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,Z1F9rjKqCE-Hd2-Ip5g3gA,fz_0gZ6RXEC78NpYsx_eqA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1106, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2546, + "FriendlyName": "Letter Jacket (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,VpqHNpPiY0CfRJEjRRM-mw,fz_0gZ6RXEC78NpYsx_eqA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1109, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2547, + "FriendlyName": "Letter Jacket (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "C4uuztTiYUe20PACMARksA,apSQiQ1eO0mEzRsbugeu9w,fz_0gZ6RXEC78NpYsx_eqA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1107, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2610, + "FriendlyName": "Letter Jacket Cuffs (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,UUyjVU4mvECPhI9xTMYR1Q,4EHIyZkOOEqnM-PuD_a5xw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1111, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2611, + "FriendlyName": "Letter Jacket Cuffs (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,HWc25-d8dUmNN2F3n8mV5A,4EHIyZkOOEqnM-PuD_a5xw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1113, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2612, + "FriendlyName": "Letter Jacket Cuffs (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,Y0e7o0_BKUm7XJSLediN5w,4EHIyZkOOEqnM-PuD_a5xw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1110, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2613, + "FriendlyName": "Letter Jacket Cuffs (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "2dFMy4zc7ky_Fd5fzfgInw,MS7EPNLJEEyMLC6_yHQYtA,4EHIyZkOOEqnM-PuD_a5xw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1112, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2313, + "FriendlyName": "Rec Room Hoodie (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 864, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2579, + "FriendlyName": "Winner's Medal (Bronze)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,iaDL2im4lkCXUo0NWM-F3g,wzt-5C_LjEmIQWUVPcYWhA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-07-25T21:00:00Z", + "SaleEndDate": "2021-07-26T21:00:00Z" + } + } + ], + "PurchasableItemId": 1171, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2580, + "FriendlyName": "Winner's Medal (Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bf13fa1c-f991-4aaa-9402-aac109be9562,ERnUPsyT6kuEnIRZF8zPwg,wzt-5C_LjEmIQWUVPcYWhA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1250, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-07-25T21:00:00Z", + "SaleEndDate": "2021-07-26T21:00:00Z" + } + } + ], + "PurchasableItemId": 1172, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1125, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2667, + "FriendlyName": "Cheerful Ponytail", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "BJ2XrGvl40iIl28IbTfbKg,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1185, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2325, + "FriendlyName": "Treasure Hunter Hat (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "-SQLtL4rdEWTJdw2IroQCQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 907, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2414, + "FriendlyName": "Treasure Hunter Bandolier (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "t_66r_j5oUSh9awIPmdUKg,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 906, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2415, + "FriendlyName": "Treasure Hunter Belt (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ImwG0snB9ECc_gqRRBoz6A,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 895, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2420, + "FriendlyName": "Treasure Hunter Shirt (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 896, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2421, + "FriendlyName": "Treasure Hunter Tank Top (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 897, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2605, + "FriendlyName": "Boxing Glove (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b904304b-cce0-466f-8569-c621eeadd4f7,rzUd1IMTKUmW0HeMPSIOBQ,f79f8a46-ef1d-4d8c-a4fc-383e3a9eb10b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-30T21:00:00Z", + "SaleEndDate": "2021-05-31T21:00:00Z" + } + } + ], + "PurchasableItemId": 1054, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2549, + "FriendlyName": "Rec Room Hoodie (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,yM5xRrd9OkmDGyQddwOP6g,oHKhT-GTx0iFWrrlz9s6Tw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1134, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2585, + "FriendlyName": "Shutter Shades (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,7B7vpnMTfkGLcMdHCZ_kWA,tqyzKPdDwkWq3jfZ8jZdaA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-11T21:00:00Z", + "SaleEndDate": "2021-04-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 1152, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2402, + "FriendlyName": "Steampunk Goggles", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CdeM6RJmPUumXtdmdM3RXA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 903, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2403, + "FriendlyName": "Steampunk Corset", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "LK8jt9zCuUGwLRccgOOvjA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 917, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2462, + "FriendlyName": "Steampunk Pauldron", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "sOREgAGs-06CeGGsG-WIkw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 904, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2463, + "FriendlyName": "Steampunk Satchel", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "iBQ-v6xaqkipUlfzkSHEeQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 905, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2464, + "FriendlyName": "Steampunk Vest", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "E9VTiIVFPEa-OuQRto0e_Q,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 920, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2465, + "FriendlyName": "Steampunk Cuff", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "qxovV6Vym0ufL2cTCyKjOQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 902, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2573, + "FriendlyName": "Anime Bow (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,_uh3kj6hKUyBKOSR3R8dog,M5z2wk-GDkOAjvZzqsfcNQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1014, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2540, + "FriendlyName": "Anime Skirt (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,FE7_gQNBt0CXzMN6di5HWA,2UNd6eViFUO359xjSiuu2A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1021, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2592, + "FriendlyName": "Anime Tiara (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,EDLxuGjEVEGdOPXDqyN-xA,mG60EPIQIkaJNgOkonVmZg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1026, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2617, + "FriendlyName": "Anime Glove (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,N9CBuYaMtUOrCks5UcOycg,ZtCqMnFTQUWv2h0MfZ4m6A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1017, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2677, + "FriendlyName": "Baseball Helmet (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,FVdDQFOMQ06teQm2ANDPng,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1199, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2678, + "FriendlyName": "Baseball Helmet (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "293e7ad3-01c9-43af-96e3-06c34145ff12,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,FVdDQFOMQ06teQm2ANDPng,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1200, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2664, + "FriendlyName": "Traveler Hat", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1187, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2666, + "FriendlyName": "Traveler Shirt", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1189, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2668, + "FriendlyName": "Traveler Shades", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "cbN5e-t_jEKa4hzir5JAxQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1186, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2665, + "FriendlyName": "Fanny Pack", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "oYvOo47DJE-DEPlpn5xNUw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1188, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2559, + "FriendlyName": "Bowling Shirt (Brooklyn)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,Gtq8yRlt1UeFW8U-sc7FiA,IWg9wuda4kaCilJk8JQpTQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1045, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2563, + "FriendlyName": "Bowling Shirt (Runway)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,GDvaUU10-EKh3pKUQ9tnag,b-IM3eYMJUa40395O5XeLg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1050, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2556, + "FriendlyName": "Bowling Shirt (Lily)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yrQCLXiqRUSwnuQRYX2Rsw,VO_3cgqQqUOTz30TVUxrFA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1052, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2561, + "FriendlyName": "Bowling Shirt (Flames, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,032pE8s9Ik-2CImFVDrSqg,5RUG6i2Mx0-55IO-IlhUdQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1047, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2672, + "FriendlyName": "Permanent Hair Dye (Cauldron Crimson)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "XCwNFMT1i0i9Q9gJYTLPUQ", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1197, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2718, + "FriendlyName": "Permanent Hair Dye (Bucket Orange)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1I8AD333gku-iOQlS6WWlw", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1227, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2728, + "FriendlyName": "Permanent Hair Dye (Butterfly Blue)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "yTdx1nVVtECU8Vh7EC8dGA", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1228, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2726, + "FriendlyName": "Permanent Hair Dye (Circuit Cerulean)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "W5E2gDCKY0SXaKytP89-OA", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1229, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2723, + "FriendlyName": "Permanent Hair Dye (EDM Emerald)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "wXJZbYCkZE-NMbPVf2YuFA", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1230, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2722, + "FriendlyName": "Permanent Hair Dye (Frontier Forest)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "LBz3a9520kiOOMMOwsTExg", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1231, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2732, + "FriendlyName": "Permanent Hair Dye (Gizmo Grape)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "QmgjYhH_L0q58eF5SWeDhQ", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1232, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2721, + "FriendlyName": "Permanent Hair Dye (Goblin Green)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "TylV8M6zjUaadhxHPWAS-w", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1233, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2725, + "FriendlyName": "Permanent Hair Dye (H2Oasis Blue)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "Bjsgo9ddeUCk3zW1-DdJbw", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1234, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2724, + "FriendlyName": "Permanent Hair Dye (Jungle Jade)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "Sc8S3QiTIUuXNJZD6geY1Q", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1235, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2671, + "FriendlyName": "Permanent Hair Dye (KREQ Red)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "xyHGGFKNWEOTnXloDFahPQ", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1196, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2733, + "FriendlyName": "Permanent Hair Dye (Maker Mauve)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "G129V_kVXkSLN2OG8EjSqQ", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1237, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2734, + "FriendlyName": "Permanent Hair Dye (Mousebot Magenta)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8k_gZA_lrEyJxI19GrcAZg", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1238, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2727, + "FriendlyName": "Permanent Hair Dye (Paula's Periwinkle)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "_2QGtfHzuUSq0eyI84rImw", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1239, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2720, + "FriendlyName": "Permanent Hair Dye (Pirate Gold)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "pQNfh-3DsEGWfiIls6Qf6g", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1240, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2731, + "FriendlyName": "Permanent Hair Dye (Propulsion Purple)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "53y1prYATU2drZDS-jpaqg", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1241, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2673, + "FriendlyName": "Permanent Hair Dye (PVPeach)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0RUmjv2bL0ydbKU61dlqlg", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1198, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2729, + "FriendlyName": "Permanent Hair Dye (Ranger Roy-Al Blue)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3ksu_FLaukeJ-YE90jb4Ow", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1242, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2670, + "FriendlyName": "Permanent Hair Dye (Rebel Rose)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "aziIarpSREmcT6vihduPwA", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1195, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2674, + "FriendlyName": "Permanent Hair Dye (Recreational Raspberry)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "yejur5BbR0mevbH-rzgXYA", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1257, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2719, + "FriendlyName": "Permanent Hair Dye (Standee Sandy)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "H65ddsxAY0m2sccMrF48Aw", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1243, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2730, + "FriendlyName": "Permanent Hair Dye (Witch Attack Lilac)", + "Tooltip": "Permanently unlocks this hair color on your dorm room mirror.", + "ConsumableItemDesc": "", + "AvatarItemDesc": "qp_vRXZQPU2rlx_IU1Vr8Q", + "AvatarItemType": 1, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 20, + "SaleStartDate": "2021-07-21T03:00:00Z", + "SaleEndDate": "2021-07-26T06:59:00Z" + } + } + ], + "PurchasableItemId": 1236, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2602, + "FriendlyName": "Bike Gloves (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,4398ce1a-e6ee-4da4-ad0d-7abfab41020c,0XPd5RzNJEepHztqwAznHQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1044, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2603, + "FriendlyName": "Bike Gloves (Navy)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,XNq4Cns4lEGydtktdsXlmQ,0XPd5RzNJEepHztqwAznHQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1043, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2604, + "FriendlyName": "Bike Gloves (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7a6fe0e6-f930-4fb8-9b2f-4c857a273dd9,83522c12-9549-4d74-8c87-bae210bcd2bf,0XPd5RzNJEepHztqwAznHQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1042, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2552, + "FriendlyName": "Deep Sea Diver Suit (Leviathan)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,CahBzgN3D0y46AQsvGC14A,EiIow5mS80qpoS10r1ZdvQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1081, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2577, + "FriendlyName": "Deep Sea Diver Tank (Leviathan)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,CahBzgN3D0y46AQsvGC14A,ZpyoWbO6GkGglXlMuYUARw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1087, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2596, + "FriendlyName": "Deep Sea Diver Helmet (Leviathan)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,CahBzgN3D0y46AQsvGC14A,a3Er_BCApUiPtBaGkSH9LA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1084, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8550, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2609, + "FriendlyName": "Deep Sea Diver Gloves (Leviathan)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,CahBzgN3D0y46AQsvGC14A,0p5N9U5qokKdQhW07yYLGQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1082, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2581, + "FriendlyName": "Space Visor (Nebula)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,FFIpxInE-EKbk2GY-pIkyg,uB4gFxfrh0W5OtkmZdFN1Q,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1162, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8550, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2551, + "FriendlyName": "Rec Room Hoodie (Pink)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,TEgugZRs80iJbhL87RdYwg,oHKhT-GTx0iFWrrlz9s6Tw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1132, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2586, + "FriendlyName": "Shutter Shades (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,ZpcM5H2Q1UCt0hh82nSu9w,tqyzKPdDwkWq3jfZ8jZdaA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-11T21:00:00Z", + "SaleEndDate": "2021-04-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 1151, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2572, + "FriendlyName": "Royal Cape (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,mqdncbYcuUSGFvFqEU0z4A,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2213, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2536, + "FriendlyName": "Royal Tabard (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,rCeRk8Pdl0W5M47JAQPhpw,50NUFrDxbUKmXbyAHBenvQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 49, + "ItemSetFriendlyName": "Royal Tabards" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2214, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2538, + "FriendlyName": "Royal Dress (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,t1MeoroFOUioYmPfT9mCeA,PoMG28QAskagun1LSd4C_A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 50, + "ItemSetFriendlyName": "Royal Dresses" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2215, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2378, + "FriendlyName": "D4 Skin (Bone)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D4]", + "EquipmentModificationGuid": "x3Cv42hIkkaNIy7fHZiWYg", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1072, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2382, + "FriendlyName": "D6 Skin (Bone)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D6]", + "EquipmentModificationGuid": "Fn0gcj17c06tA9s98CKwAg", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1075, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2386, + "FriendlyName": "D8 Skin (Bone)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D8]", + "EquipmentModificationGuid": "d_CbCG3DmUCtWGWYHioghg", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1078, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2390, + "FriendlyName": "D10 Skin (Bone)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D10]", + "EquipmentModificationGuid": "GjQwBmNnK0ukv0hEEHK8tw", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1063, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2394, + "FriendlyName": "D12 Skin (Bone)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D12]", + "EquipmentModificationGuid": "heFLfpPNd0-nhLOFpUCnpA", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1066, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2398, + "FriendlyName": "D20 Skin (Bone)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D20]", + "EquipmentModificationGuid": "gXOOKrQEYE-FmS3uXWj1hA", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1069, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2760, + "FriendlyName": "Grillmaster Apron (Tan)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1278, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2771, + "FriendlyName": "Grillmaster Mitt (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1284, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2739, + "FriendlyName": "Notes Headphones (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1296, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2748, + "FriendlyName": "Notes Headphones (Techno)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,ns5tIRishUOp-nYqLedcwA,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1300, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2753, + "FriendlyName": "Notes Headphones (Neon Void)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,Uz2hrptCa0erjqyPoiZxew,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1304, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2543, + "FriendlyName": "Anime Skirt (Teal)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,VQjkyb5aoEKHKo_eZsq9ug,2UNd6eViFUO359xjSiuu2A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1022, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2595, + "FriendlyName": "Anime Tiara (Teal)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,lhfOKGDAgEKWucerCmH7Fg,mG60EPIQIkaJNgOkonVmZg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1023, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2614, + "FriendlyName": "Anime Glove (Teal)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,zaSTWOUy_k6cnoVVYzypWg,ZtCqMnFTQUWv2h0MfZ4m6A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1018, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2576, + "FriendlyName": "Anime Bow (Teal)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,Ot9EG-FfKEauT0u6pSZuWg,M5z2wk-GDkOAjvZzqsfcNQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1012, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2560, + "FriendlyName": "Bowling Shirt (Flames, Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,M4765UHzsUeNTwH-Pcxp1g,5RUG6i2Mx0-55IO-IlhUdQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-09T21:00:00Z", + "SaleEndDate": "2021-05-10T21:00:00Z" + } + } + ], + "PurchasableItemId": 1048, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2555, + "FriendlyName": "Bowling Shirt (Split)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yZR5aX3gQ0WpRUT6cfK1cA,VO_3cgqQqUOTz30TVUxrFA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1053, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2562, + "FriendlyName": "Bowling Shirt (Spared)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,kLyM-6otSUy2ql5SEmnF9Q,b-IM3eYMJUa40395O5XeLg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1049, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2558, + "FriendlyName": "Bowling Shirt (Sleeper)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,XrSL-v2MbkK0md-FMOweOA,IWg9wuda4kaCilJk8JQpTQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1046, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2763, + "FriendlyName": "Lifeguard Uniform", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "rJ9zPPGdlk2i24sxLfkqng,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1279, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2758, + "FriendlyName": "Snorkel", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "22pyI3Kz-UullhcebC6DLQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-07-11T21:00:00Z", + "SaleEndDate": "2021-07-12T21:00:00Z" + } + } + ], + "PurchasableItemId": 1280, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2565, + "FriendlyName": "Beekeeper Suit (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,lBe3yxuE40eBmTrnEHBqQQ,afd88a79-7f03-40f6-bcb4-c247dd86edc4,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1034, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2599, + "FriendlyName": "Beekeeper Veil (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,lBe3yxuE40eBmTrnEHBqQQ,e4b53a9a-ac95-49ae-bf68-615bb23fa075,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1039, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2600, + "FriendlyName": "Beekeeper Gloves (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,lBe3yxuE40eBmTrnEHBqQQ,1f18670d-df52-4d2b-8009-a32cf7c67b40,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1035, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2756, + "FriendlyName": "Unicorn Horn (Harmony)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,FNe9LkWJMUeC3frYJMyEaQ,nW_fBsuZA0OfIjTMXm6ZCA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1311, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8550, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2754, + "FriendlyName": "Soda Clerk Hat (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1285, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2759, + "FriendlyName": "Soda Clerk Bow Tie (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "83be5ba4-525a-4781-a5f6-839c22e4d1d3,e4IDo-fuH0C9YByzcX5k-A,qCIoeLQw-Uq-SKIetIgIpA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1286, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2764, + "FriendlyName": "Soda Clerk Uniform (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1277, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2550, + "FriendlyName": "Rec Room Hoodie (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,ytmSzjhCr0Sm58MSNbwJUA,oHKhT-GTx0iFWrrlz9s6Tw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1133, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2584, + "FriendlyName": "Shutter Shades (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,nO-YacKU1kmLjTFAAqNeYQ,tqyzKPdDwkWq3jfZ8jZdaA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-11T21:00:00Z", + "SaleEndDate": "2021-04-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 1153, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1766, + "FriendlyName": "Baseball Jersey (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "2f8518fc-c989-40de-98b4-c6f09b304166,9967ce54-c2b7-437a-946e-11f8ee6d6fdd,ca37823a-b8f3-4414-b38b-de0dc7fa4295,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 357, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2842, + "FriendlyName": "Stunt Shades (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d57cf97e-b5d2-4e47-8231-a226a161f6e3,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-15T21:00:00Z", + "SaleEndDate": "2021-08-16T21:00:00Z" + } + } + ], + "PurchasableItemId": 1379, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2846, + "FriendlyName": "Stunt Shades (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-15T21:00:00Z", + "SaleEndDate": "2021-08-16T21:00:00Z" + } + } + ], + "PurchasableItemId": 1378, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2876, + "FriendlyName": "Runner Wrist Guard (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,sX0ZZDcCUkyFSJISfvq0vg,rgzG7sfrlkqd9EO13LNmpQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1385, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2849, + "FriendlyName": "Runner Harness (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,RfZNDPUok0WyQEa3Ko08ow,ovy_3j_qwE-7mmks1Z47ig,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1381, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2852, + "FriendlyName": "Runner Harness (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,sX0ZZDcCUkyFSJISfvq0vg,ovy_3j_qwE-7mmks1Z47ig,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1380, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2858, + "FriendlyName": "Runner Helmet (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Euf1h1qsZ02evDsyoHYbPQ,PZHohDVX0U2frX13N4KYDQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1376, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2855, + "FriendlyName": "Runner Helmet (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,zlqLrGEa8E2WW9ey_szREA,PZHohDVX0U2frX13N4KYDQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1377, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2879, + "FriendlyName": "Runner Wrist Guard (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,RfZNDPUok0WyQEa3Ko08ow,rgzG7sfrlkqd9EO13LNmpQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1384, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2786, + "FriendlyName": "RecFly Vest (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,kkvNbVRjcEKtChpy3W1bqQ,nbhzQy_Hdkeohh4S1IezwA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1316, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2675, + "FriendlyName": "Trucker Hat (Pickup)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ReqOfohe6UamzaiHG1x3Ow,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1264, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2743, + "FriendlyName": "Notes Headphones (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,JWBK5v78Y0uRVEXlD-Qk6Q,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-01T21:00:00Z", + "SaleEndDate": "2021-08-02T21:00:00Z" + } + } + ], + "PurchasableItemId": 1298, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2747, + "FriendlyName": "Notes Headphones (Lounge)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,nX7krD8e2km_ZlOLWcoxdQ,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-01T21:00:00Z", + "SaleEndDate": "2021-08-02T21:00:00Z" + } + } + ], + "PurchasableItemId": 1302, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2752, + "FriendlyName": "Notes Headphones (Neon Dawn)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,d9mwowRelE2lDKEA0UQMZQ,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 10000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-01T21:00:00Z", + "SaleEndDate": "2021-08-02T21:00:00Z" + } + } + ], + "PurchasableItemId": 1306, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2790, + "FriendlyName": "Big Head Bow (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1320, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2791, + "FriendlyName": "Big Head Bow (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,8iZO162KNUiMC8l_z3OC_A,3DdSzSqV1UG5AtPr8ro3Kg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1321, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2792, + "FriendlyName": "Big Head Bow (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,f_c50saStkaGJuhLjJ9lfA,3DdSzSqV1UG5AtPr8ro3Kg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1322, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2793, + "FriendlyName": "Big Head Bow (Pink)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,FUP4iRnoc0ikNmjrFD06lg,3DdSzSqV1UG5AtPr8ro3Kg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1323, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2537, + "FriendlyName": "Royal Tabard (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,5TJPDmvEBEOkP82ZWLFqtg,50NUFrDxbUKmXbyAHBenvQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 49, + "ItemSetFriendlyName": "Royal Tabards" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2216, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2539, + "FriendlyName": "Royal Dress (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,0ps19D7b_U2iejdKQFORyA,PoMG28QAskagun1LSd4C_A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 50, + "ItemSetFriendlyName": "Royal Dresses" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2217, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2957, + "FriendlyName": "Graphic Cap (Goblins)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,pqLkLbri9Eu48eryQIFGMg,_yznwUwL80ejrS3vki0IKg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2218, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2958, + "FriendlyName": "Graphic Cap (High Fashion)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1432, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2960, + "FriendlyName": "Graphic Cap (Hyper Ramen)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "474db08a-d4dc-4625-9f5a-1a6ae0b70827,cpTKHXjj0kulivZCKVQqiA,Mrf17aidXEeCF-fFURUUcQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-06-20T21:00:00Z", + "SaleEndDate": "2021-06-21T21:00:00Z" + } + } + ], + "PurchasableItemId": 1434, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2956, + "FriendlyName": "Baseball Cap (White, Red, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,nZor-clxlkm8EO9Gco0Otg,r_GLekt6mkWxXw8zPPR8Mg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1436, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2962, + "FriendlyName": "Round Glasses", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bc7299ac-be2b-404f-b8e6-402e2c0660f0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1431, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2932, + "FriendlyName": "Bowler Hat", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "bfd9c70b-9ad8-4f47-a840-c03231f9ad41,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1437, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2946, + "FriendlyName": "Trucker Jacket (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1443, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2950, + "FriendlyName": "Trucker Jacket (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1446, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2949, + "FriendlyName": "Trucker Jacket (Tan)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,o2WfRyjJy0CKoUg8YIxNjQ,sxWOOK1RwUOp5YqZ7a36xg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1447, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2947, + "FriendlyName": "Trucker Jacket (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1445, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2843, + "FriendlyName": "Chef Hat (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1460, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2847, + "FriendlyName": "Chef Coat (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1459, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2933, + "FriendlyName": "Turtleneck (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1448, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2934, + "FriendlyName": "Turtleneck (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,9b9t8_aKH0-gQXMhZycaOA,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1450, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2936, + "FriendlyName": "Turtleneck (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Jt0QqNrAp0qzUR_RQoivVg,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1449, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2937, + "FriendlyName": "Turtleneck (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,lBePHNL9LkyaAjEFyFE8Sw,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1451, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2938, + "FriendlyName": "Turtleneck (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G4HIPUjekUigiU0-Kr9mdA,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1453, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2951, + "FriendlyName": "Trucker Cuff (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1454, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2952, + "FriendlyName": "Trucker Cuff (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,yA0KhVg19kWqG1UkyxQogQ,yBNsYcZXzEiuR0u3tWnnWQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1456, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2954, + "FriendlyName": "Trucker Cuff (Tan)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,o2WfRyjJy0CKoUg8YIxNjQ,sHyNs-dpc0-_Ah-HJVczcw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1458, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2955, + "FriendlyName": "Trucker Cuff (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,bgRKH1wfqkWVHcuSnFYnlQ,sHyNs-dpc0-_Ah-HJVczcw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1457, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3025, + "FriendlyName": "Fancy Bubbly", + "Tooltip": null, + "ConsumableItemDesc": "iiGTvhOCHkOTNJhb16Zbyw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1554, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3014, + "FriendlyName": "Formal Necklace (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5e9bbda6-d681-42b1-9cc4-5758acf34bc0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-06-13T21:00:00Z", + "SaleEndDate": "2021-06-16T21:00:00Z" + } + } + ], + "PurchasableItemId": 1518, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3013, + "FriendlyName": "Formal Necklace (Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b95678aa-351a-4929-8a0e-0274a183eb18,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-06-13T21:00:00Z", + "SaleEndDate": "2021-06-16T21:00:00Z" + } + } + ], + "PurchasableItemId": 1519, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3018, + "FriendlyName": "Party Hat (Shindig)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1520, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3006, + "FriendlyName": "Formal Dress (Gold Sequins)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9a366f8b-ea5b-49ae-a98c-f6b282e9bf14,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-16T21:00:00Z", + "SaleEndDate": "2021-05-17T21:00:00Z" + } + } + ], + "PurchasableItemId": 1511, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3007, + "FriendlyName": "Formal Dress (Silver Sequins)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "73c0e2ba-cba1-482a-89d5-1508997aceeb,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1513, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3011, + "FriendlyName": "Business Tie (Gold Sequins)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "05ec4e87-088f-4281-a118-2e63c2585200,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1516, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3009, + "FriendlyName": "Business Tie (Silver Sequins)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0d4e7ebe-efd9-4e68-9900-5645916e7596,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-09-12T21:00:00Z", + "SaleEndDate": "2021-09-13T21:00:00Z" + } + } + ], + "PurchasableItemId": 1517, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3012, + "FriendlyName": "Bow Tie (Gold Sequins)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-16T21:00:00Z", + "SaleEndDate": "2021-05-17T21:00:00Z" + } + } + ], + "PurchasableItemId": 1514, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3010, + "FriendlyName": "Bow Tie (Silver Sequins)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "988e4069-4341-428b-bbf5-023309891385,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-16T21:00:00Z", + "SaleEndDate": "2021-05-17T21:00:00Z" + } + } + ], + "PurchasableItemId": 1515, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3020, + "FriendlyName": "Ski Buddy Goggles (Chill)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "5fwjFQoC_Uu9ChvF6uNGZw,fXNkMWFMV0aXalK_y_8SHw,SSY8ZYMgaE2Bl4oFmMjKQA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1529, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3022, + "FriendlyName": "Ski Buddy Jacket (Chill)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "6RhwWExXekaYpK0NGHSXGQ,fXNkMWFMV0aXalK_y_8SHw,HQQWccdyakaEmntPhvC6CQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1530, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2593, + "FriendlyName": "Anime Tiara (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,f0pjsV0XMUWoE_z2kqOlSA,mG60EPIQIkaJNgOkonVmZg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1025, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2541, + "FriendlyName": "Anime Skirt (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,hYu9itGe1Ee5Neanhj9VtQ,2UNd6eViFUO359xjSiuu2A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1020, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2574, + "FriendlyName": "Anime Bow (Dark Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,S9uymuvraUCOM8gmB7bnEA,M5z2wk-GDkOAjvZzqsfcNQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1011, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2616, + "FriendlyName": "Anime Glove (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,xpkEQ6yjhU-WfFPSjvPdHg,ZtCqMnFTQUWv2h0MfZ4m6A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1016, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2835, + "FriendlyName": "Mobster Hat (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "76369aef-aeda-46d2-996a-cd00594d0543,PZx7Iae0pkiDaxT9t3-vgg,d759c88d-a159-4c26-86dd-b054bb05b3ed,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1365, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2837, + "FriendlyName": "Mobster Suit (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "4308da04-af84-40bb-845a-7a9e1a2726ec,newGzX_zA0eGUX3Iayq5Ag,6Jv4AJGBYUGP_n4sDcz2Kw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1367, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2838, + "FriendlyName": "Mobster Cuffs (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "08d56627-4500-4724-8692-a23c96ddb3ec,newGzX_zA0eGUX3Iayq5Ag,QBzMp7MdAUOLIofvddui1A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1368, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3028, + "FriendlyName": "Bear Hug Hat (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ef7771eb-c5f3-44fd-8c27-f581b035ef10,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1556, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3052, + "FriendlyName": "Knight Helmet (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e614df5d-3cfe-4bca-b9c2-30c267b9c94e,PMa9370LPEig_7pKwCceEQ,0dd6596c-b4ea-4d55-a219-e1f44e4a376d,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 45, + "ItemSetFriendlyName": "Knight (Green)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2219, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3068, + "FriendlyName": "Knight Gauntlet (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9bf9d502-8a3c-4b24-9565-0a2c2d9864fd,PMa9370LPEig_7pKwCceEQ,fd67ec40-f898-4df6-b846-8bf350f362b8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 45, + "ItemSetFriendlyName": "Knight (Green)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2220, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3058, + "FriendlyName": "Knight Cape (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2e5afc40-7e25-418c-8b1b-316b8ef479a7,PMa9370LPEig_7pKwCceEQ,15099be7-38b7-4b0b-ac13-1bacedbf5fb2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 45, + "ItemSetFriendlyName": "Knight (Green)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2221, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3051, + "FriendlyName": "Archer Hat (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d2d3264b-fbf6-40b5-9f10-c85ec737d112,61VZkFjB402brzWm4UsftA,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 46, + "ItemSetFriendlyName": "Archer (Grey)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2222, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3067, + "FriendlyName": "Archer Gloves (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5b01eaa3-0cac-40c0-b72a-b3f6a868ae0c,8hUBfmq28EixYjv92Xcrgg,7b187c06-7ba2-4f3c-bca9-fdf98146f385,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 46, + "ItemSetFriendlyName": "Archer (Grey)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2223, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3053, + "FriendlyName": "Royal Crown (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,wCe2yC2U1kyYvPd8Z1rX2w,hCeaYHy1TE2s6rTPxAQP9A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2224, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3072, + "FriendlyName": "Regal Crown (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,Zljmtj5WqUaRvB3XQTgJXg,XFuSiGupZ02Fut1Dxum_Iw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2225, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3062, + "FriendlyName": "Knight Armor (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "739fd42a-8a8c-44a5-afa3-e0974802c6ae,PMa9370LPEig_7pKwCceEQ,51a1809d-becd-42e0-8dfb-188d6f07ba1c,f5270130-6634-4052-aa3e-9849ddf5314e", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 45, + "ItemSetFriendlyName": "Knight (Green)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2226, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3061, + "FriendlyName": "Archer Tunic (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b,61VZkFjB402brzWm4UsftA,f88178b8-b204-4fa5-9dfd-6e438bf1e247,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 46, + "ItemSetFriendlyName": "Archer (Grey)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2227, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3057, + "FriendlyName": "Archer Quiver (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5b6535f0-86cd-417a-bcce-a1ace9a5f260,imDGL6dhrka-KliYlfpdgw,6094370d-5e28-473a-b422-f3f1c75d5db8,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 46, + "ItemSetFriendlyName": "Archer (Grey)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2228, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3060, + "FriendlyName": "Royal Cape (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,_9QlnTPXuEO5OQ3H0CwFkA,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2229, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3059, + "FriendlyName": "Royal Cape (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,MbusQ9bte0i9zirlxtNkSQ,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2230, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3055, + "FriendlyName": "Witch Hunter Hat (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6d36091e-7083-4e3d-8729-94612602c8cb,aBQ3R9boek-mYeSYSNvDbQ,32d91d32-b102-4093-927a-6dc3ac2e9e86,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 47, + "ItemSetFriendlyName": "Witch Hunter (Blue)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2231, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3056, + "FriendlyName": "Wizard Hat (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "01689baf-405e-4b38-9f09-b624e03865b8,d3KmqVuMQkCYK6sVTMOTOA,abdf525b-b871-41be-a1c8-810f49740ad4,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 48, + "ItemSetFriendlyName": "Wizard (Purple)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2232, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3070, + "FriendlyName": "Wizard Bracers (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "22ef1089-d073-4fe8-b038-6f34adf0b85f,d3KmqVuMQkCYK6sVTMOTOA,47514b9b-0d17-4727-97ed-dab67e7fe031,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 48, + "ItemSetFriendlyName": "Wizard (Purple)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2233, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3073, + "FriendlyName": "Regal Crown (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,rmq1AOQvtUGOHvbPUximwg,XFuSiGupZ02Fut1Dxum_Iw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2234, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3054, + "FriendlyName": "Royal Crown (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "40734c4d-e1fe-426c-8921-930f7463d8e8,QkfA71-1z0qOiZAVrbzhtA,hCeaYHy1TE2s6rTPxAQP9A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2235, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3071, + "FriendlyName": "Regal Crown (Gold)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "9d395a84-4342-4b1c-940c-213ec2eb56f9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2236, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3065, + "FriendlyName": "Witch Hunter Torso (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "418db0e0-c7af-40c2-9cac-d461ce41e210,aBQ3R9boek-mYeSYSNvDbQ,0f49ac81-63bd-4845-8bdc-1944181a8aa3,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 47, + "ItemSetFriendlyName": "Witch Hunter (Blue)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2237, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3066, + "FriendlyName": "Wizard Robes (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "97eae086-cfea-45ff-9b77-1d184256d493,d3KmqVuMQkCYK6sVTMOTOA,8768aa33-b014-4062-b323-b146dd7d78d2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 48, + "ItemSetFriendlyName": "Wizard (Purple)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2238, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3069, + "FriendlyName": "Witch Hunter Gloves (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1974dd48-aa44-434f-879a-eaff0c7c06e6,aBQ3R9boek-mYeSYSNvDbQ,5b8d9490-e5bc-480e-91bc-65d1797b52ad,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 47, + "ItemSetFriendlyName": "Witch Hunter (Blue)" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2239, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3063, + "FriendlyName": "Royal Dress (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab6c95c-e157-48bd-a172-2642896b4ffc,pjkTGVHkIE-BzCVi1KAesQ,PoMG28QAskagun1LSd4C_A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 50, + "ItemSetFriendlyName": "Royal Dresses" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2240, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3064, + "FriendlyName": "Royal Tabard (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e13f6558-d49c-4314-820c-962ef48eb0c6,s2kCppLsN0uY7Hy6qjQc-g,50NUFrDxbUKmXbyAHBenvQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 49, + "ItemSetFriendlyName": "Royal Tabards" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2241, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2380, + "FriendlyName": "D4 Skin (Pewter)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D4]", + "EquipmentModificationGuid": "WxWaXY61mUuNe_f6SgECnA", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1073, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2384, + "FriendlyName": "D6 Skin (Pewter)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D6]", + "EquipmentModificationGuid": "5U_KiCi_90-eWucQ68VdZQ", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1076, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2388, + "FriendlyName": "D8 Skin (Pewter)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D8]", + "EquipmentModificationGuid": "77cRA0O4GUqd6nr9QLefQA", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1079, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2396, + "FriendlyName": "D12 Skin (Pewter)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D12]", + "EquipmentModificationGuid": "2gYYjbVGqkiOefY2xjOxiQ", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1067, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2400, + "FriendlyName": "D20 Skin (Pewter)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D20]", + "EquipmentModificationGuid": "H3vrSi45AU6lshChLnr19Q", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1070, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3050, + "FriendlyName": "Winter Cloak (Nightwatch)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2242, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3090, + "FriendlyName": "Server Apron (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1612, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3091, + "FriendlyName": "Server Apron (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,HdwZhjrqxE6wuf68MnDhDw,f5z_jhdcqEGjo-1WUmldaw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1613, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3094, + "FriendlyName": "Server Dress (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1616, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3095, + "FriendlyName": "Server Dress (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,GQSpXmD0B02_gwzpq6StFA,ORmS9HkfoUeFuO4RhqcW7g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1617, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3026, + "FriendlyName": "Popcorn", + "Tooltip": "", + "ConsumableItemDesc": "QRx0aSTT9keMFdAJMQHdTg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 52, + "ItemSetFriendlyName": "Snickety Snacks" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1594, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3102, + "FriendlyName": "Paintball Belt", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1599, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3033, + "FriendlyName": "Froggy Headband", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "cec9cfac-a45b-4edb-b26d-7453cc2bfaf2,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1585, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3109, + "FriendlyName": "Top Hat (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "d3b8bd7b-ca9a-4f5d-b115-3659dc294a03,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1622, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3107, + "FriendlyName": "Bow Tie (Green Sequins)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "86af3a97-18d8-4dda-a2f2-cfd3b4224254,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1620, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3108, + "FriendlyName": "Big Head Bow (Green Sequins)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "66f8f55d-cad4-4afb-93e8-b6bdf04b37b3,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1621, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2557, + "FriendlyName": "Bowling Shirt (Striker)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,KqjQ1xRpokGLKp9GVN1EFA,VO_3cgqQqUOTz30TVUxrFA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1051, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2935, + "FriendlyName": "Turtleneck (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,Zz4XatKfUEapReRAQHxKKA,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1452, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2703, + "FriendlyName": "Collared Shirt (Plaid)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343,ojvIFCTpJkeegestF065wA,10fYSIBytk-yUjveDa407w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1218, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2948, + "FriendlyName": "Trucker Jacket (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1444, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3115, + "FriendlyName": "Butterfly Wings (Cool Mint)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fc96ea5d-b49b-479b-a963-92f1eab44b2c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-29T21:00:00Z", + "SaleEndDate": "2021-09-01T21:00:00Z" + } + } + ], + "PurchasableItemId": 1631, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3116, + "FriendlyName": "Butterfly Wings (The Blue One)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "27c2eb7f-9fac-4c42-90c7-44e7a33d5303,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1630, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3117, + "FriendlyName": "Butterfly Wings (Monarch)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "c506feae-be73-437b-b18a-1152bb9906e9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1629, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3110, + "FriendlyName": "Raincoat (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1627, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3111, + "FriendlyName": "Raincoat (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,GH9q8_0O9EqjGdz7lGWD2g,T7zwcIIc8Ee3ud8a2JCuwQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1628, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3112, + "FriendlyName": "Rain Hat (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1625, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3113, + "FriendlyName": "Rain Hat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2bd53190-46f8-4956-9388-87c5a04efc4f,v3af60p1Q02NpdCqIRO9lw,1hVOI7IuY0mKvtr2PZXLww,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1626, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2750, + "FriendlyName": "Notes Headphones (Neon Sky)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,2sf-FU4WckGBDWUkVz4z1g,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 10000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-02T21:00:00Z", + "SaleEndDate": "2021-05-03T21:00:00Z" + } + } + ], + "PurchasableItemId": 1305, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2745, + "FriendlyName": "Notes Headphones (Chillout)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,4scG_UBlzE-5DWRhSrtm6g,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-02T21:00:00Z", + "SaleEndDate": "2021-05-03T21:00:00Z" + } + } + ], + "PurchasableItemId": 1301, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2741, + "FriendlyName": "Notes Headphones (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,8ANbOhNnO0yLQOCIBsDwfg,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-02T21:00:00Z", + "SaleEndDate": "2021-05-03T21:00:00Z" + } + } + ], + "PurchasableItemId": 1297, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3046, + "FriendlyName": "Quilted Vest (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1588, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3047, + "FriendlyName": "Quilted Vest (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,QBglgj80zkuvUkk55Y5Feg,ACrBLiAIgEur9V6l_B0CvA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1589, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3048, + "FriendlyName": "Quilted Vest (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,-HYrY2A5Tk2UWMi4_hmYbg,ACrBLiAIgEur9V6l_B0CvA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1590, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3049, + "FriendlyName": "Quilted Vest (Purple)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "0ab5790f-a537-4306-ba66-5eb87c02ec7d,IjRMopsYlUqSE6327I9TIQ,ACrBLiAIgEur9V6l_B0CvA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1591, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2972, + "FriendlyName": "Formal Vest (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1537, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2973, + "FriendlyName": "Formal Vest (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,ffHKr5EcIU24PbAtWSnalw,bVrkIyADCUiGNYWdmP_HPw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1538, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3004, + "FriendlyName": "Formal Dress (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1525, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3005, + "FriendlyName": "Formal Dress (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "30380607-8b54-4b63-81d3-cb3f08e452c1,rcdwgNvO30Cvx2ycE1GfHw,JmFzNL5v_0q1p0bgoOlBpQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1526, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2953, + "FriendlyName": "Trucker Cuff (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "973d84ee-5b88-475c-b579-a76c34a7c533,P9bBSL0YLEyjN8PxMfgbKQ,yBNsYcZXzEiuR0u3tWnnWQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1455, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3131, + "FriendlyName": "Rec Room Hoodie (Galaxy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5362d685-fc55-4b43-85a5-cc3d6cad3f69,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1649, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3170, + "FriendlyName": "Nightcap (Zonked)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1724, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3171, + "FriendlyName": "Nightcap (Zzz)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,2vawoQvJPUegnClmr2RNAQ,RR69IGV4j0Sd2xtmyN5wFg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-25T21:00:00Z", + "SaleEndDate": "2021-04-26T21:00:00Z" + } + } + ], + "PurchasableItemId": 1726, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3172, + "FriendlyName": "Nightcap (Dozy)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,kIcd-yEnsEqxoeogquvmRw,RR69IGV4j0Sd2xtmyN5wFg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1725, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3173, + "FriendlyName": "Nightcap (Sandman)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "250bbb53-1fae-440d-b25a-46a16c271367,qHyKfCsrbUqSWomjLXf7Hw,RR69IGV4j0Sd2xtmyN5wFg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1727, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3166, + "FriendlyName": "Night Coat (Zzz)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-25T21:00:00Z", + "SaleEndDate": "2021-04-26T21:00:00Z" + } + } + ], + "PurchasableItemId": 1728, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3167, + "FriendlyName": "Night Coat (Dozy)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,uDJ5ZqU1f0CiOAH8WY22Yw,KOCzRE4a6EKb3_NVOI_lcg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1730, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3169, + "FriendlyName": "Night Coat (Zonked)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,_jkPDSXFUkSU_yOfutQ7BA,KOCzRE4a6EKb3_NVOI_lcg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1731, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3168, + "FriendlyName": "Night Coat (Sandman)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "64fddd4e-3d59-43df-bb39-671e18953c0f,j66We9SYr0SNQ2ufZqIqhA,KOCzRE4a6EKb3_NVOI_lcg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1729, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2582, + "FriendlyName": "Space Visor (Quasar)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "b89be768-a169-4b24-8022-751e7b817865,SXG9rdEkxkOR31Nk0aTOww,uB4gFxfrh0W5OtkmZdFN1Q,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1159, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8550, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3133, + "FriendlyName": "International Thief Coat (Mastermind)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1650, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3134, + "FriendlyName": "International Thief Coat (Spy)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,YB7kXMyLJ0mF_5WidhmH8A,SeA8s77hr0K6DRZyVlRTCg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1651, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3135, + "FriendlyName": "International Thief Coat (Rogue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,bQyNgFWAe0SPttPri6hxyg,SeA8s77hr0K6DRZyVlRTCg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1652, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3136, + "FriendlyName": "International Thief Coat (Outlaw)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "097a3266-4aab-4b23-8f56-373c9b1bc837,wRPoHTsXFESK5pjkxVMqGA,SeA8s77hr0K6DRZyVlRTCg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1653, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3137, + "FriendlyName": "International Thief Hat (Mastermind)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1640, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3138, + "FriendlyName": "International Thief Hat (Spy)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,RBRFHN8_rkuImbyDAfcHWw,BOB_mAXdA0u2khOH5qjqXQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1641, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3139, + "FriendlyName": "International Thief Hat (Rogue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,QJSPgq2KXEOWNynY8uqQvA,BOB_mAXdA0u2khOH5qjqXQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1642, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3140, + "FriendlyName": "International Thief Hat (Outlaw)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "bc3f8ab4-cdb1-4e63-bda5-ba5a4b1f4a43,4RH8_Z1mqU-JIVvx31wOsA,BOB_mAXdA0u2khOH5qjqXQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1643, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3141, + "FriendlyName": "International Thief Gloves (Mastermind)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1654, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3142, + "FriendlyName": "International Thief Gloves (Spy)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,DtFYrQwaCky5C1ZycqZbiQ,egpzBs77Q0CpDndZHqXOog,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1655, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3143, + "FriendlyName": "International Thief Gloves (Rogue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,9SAPupKy_EO6YeG0SMkI5A,egpzBs77Q0CpDndZHqXOog,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1656, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3144, + "FriendlyName": "International Thief Gloves (Outlaw)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "1014dded-3bbc-479b-a6cb-afa7413de2da,hiMYlmk5sEqh-sElOY3A9g,egpzBs77Q0CpDndZHqXOog,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1657, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3149, + "FriendlyName": "Beanie (Gray)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,SeVmHtrMS0y7gKCYkHcrOA,uWXtz0WaO0qJ1W4TfWnLqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1717, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3150, + "FriendlyName": "Beanie (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,uDcJZClcFkiNdkEEh9IIyA,uWXtz0WaO0qJ1W4TfWnLqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1715, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3153, + "FriendlyName": "Beanie (Pink)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,F5vJzMfNB0Ob6Abjadho7Q,uWXtz0WaO0qJ1W4TfWnLqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1720, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3154, + "FriendlyName": "Beanie (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,vbOUaJsGHEaSfQCivNcMWg,uWXtz0WaO0qJ1W4TfWnLqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1723, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3163, + "FriendlyName": "Brimmed Beanie (Gray)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,t2bByt5oUEG90PMMePCP6A,7nBHNMZhAE-t9rLCYO_STA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1706, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3158, + "FriendlyName": "Brimmed Beanie (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,-XSM6ZcSjE-vrWL7jyF4fQ,7nBHNMZhAE-t9rLCYO_STA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1705, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3164, + "FriendlyName": "Brimmed Beanie (Pink)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,m7KXVnULz0iHvKfCFzFoyw,7nBHNMZhAE-t9rLCYO_STA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1709, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3160, + "FriendlyName": "Brimmed Beanie (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,9tTuREkoQkW0ycqOjWXsiA,7nBHNMZhAE-t9rLCYO_STA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1712, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3217, + "FriendlyName": "Folded Bandana (Red) ", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "49800740-33d6-4280-aefb-3497597c6366,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1735, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3214, + "FriendlyName": "Gathered Shirt (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "cf1a6f8f-1649-423b-b513-b8d40d242b1f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1736, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3180, + "FriendlyName": "Fanny Pack (Cosmic)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4ee2133d-c419-4fbc-9117-746d756a250d,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1668, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3092, + "FriendlyName": "Server Apron (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,b7oobDcDtE-69fhM-5g5AQ,f5z_jhdcqEGjo-1WUmldaw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1614, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3096, + "FriendlyName": "Server Dress (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,YvPeCbQS2UOcLjPfoIML_g,ORmS9HkfoUeFuO4RhqcW7g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1618, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3093, + "FriendlyName": "Server Apron (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7,Blev_VFjRUysY_nymGWAVg,f5z_jhdcqEGjo-1WUmldaw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1615, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3097, + "FriendlyName": "Server Dress (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "f6caeb89-dbb4-4471-92fe-100bf48cd5ce,zGOD45zNAk2i6P_zPsNPfg,ORmS9HkfoUeFuO4RhqcW7g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1619, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3078, + "FriendlyName": "Soda Clerk Hat (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,MxnAq_XOw0uTzQ8IIxqb_Q,sV_ByCug30G1Gw-77KUe1Q,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1609, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3081, + "FriendlyName": "Soda Clerk Uniform (Yellow)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,MxnAq_XOw0uTzQ8IIxqb_Q,MBuxBAIkpkWI26d-arAKQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1611, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3077, + "FriendlyName": "Soda Clerk Hat (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,oKxmH-1s2EyIDkZoOQSQQw,sV_ByCug30G1Gw-77KUe1Q,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1608, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3203, + "FriendlyName": "Flowing Hair", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5d13a7a2-8213-40e6-90a6-efdd76a3fdcb,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1676, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3118, + "FriendlyName": "Trucker Hat (Flatbed)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ReqOfohe6UamzaiHG1x3Ow,lDXambcP60CeY2C8qeGDFw,uiAwVt3HK0mRVtWp9B-gTg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1648, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2941, + "FriendlyName": "Trucker Jacket (Sleeveless, Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1438, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2942, + "FriendlyName": "Trucker Jacket (Sleeveless, White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,yA0KhVg19kWqG1UkyxQogQ,PMGQ8fLc2UuJXMy42Ati-w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1440, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2944, + "FriendlyName": "Trucker Jacket (Sleeveless, Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,bgRKH1wfqkWVHcuSnFYnlQ,sxWOOK1RwUOp5YqZ7a36xg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1442, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2943, + "FriendlyName": "Trucker Jacket (Sleeveless, Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,P9bBSL0YLEyjN8PxMfgbKQ,PMGQ8fLc2UuJXMy42Ati-w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1439, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2772, + "FriendlyName": "Grillmaster Mitt (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,90qQe0AR3kOou0MsuoTmPA,ojCeJ1VwtUelyVqFnUwK5A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1288, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2773, + "FriendlyName": "Grillmaster Mitt (Black)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,hZKPr4sg10SRj9DLuEgOrw,ojCeJ1VwtUelyVqFnUwK5A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1287, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2761, + "FriendlyName": "Grillmaster Apron (Denim)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,eV3Nv6SWS0yl3f-fgjPb-w,t-ZEWJCgFEyGP2PjNblQMA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1294, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2762, + "FriendlyName": "Grillmaster Apron (White)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,Lxx3q6JRnUW_cPG_kGG4ag,t-ZEWJCgFEyGP2PjNblQMA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1295, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3132, + "FriendlyName": "Dino Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4abc2ac4-e60c-49b1-9ac8-4d4751ed78ae,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1639, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2977, + "FriendlyName": "Scuba Wetsuit (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1535, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2978, + "FriendlyName": "Scuba Wetsuit (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "57552652-d902-43b7-a68e-5c7da87582bc,VqtOYBiKRkaSW4xAlBxQgg,xTqtLPQVyEKjAQZYl6HGow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1536, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2980, + "FriendlyName": "Scuba Tank (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1533, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2981, + "FriendlyName": "Scuba Tank (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "5b3dc472-773e-4714-a9d0-7e7bbf6bc695,V_duN2oWaUmmao1xHhzCJA,Uvarm_CwTUWWQYZvzI8OaQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1534, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2983, + "FriendlyName": "Scuba Goggles (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1531, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2984, + "FriendlyName": "Scuba Goggles (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "9c28e450-8e12-455c-a4c8-c0a2160d4190,DRS8xu3AqUOy2L0RKF3f7g,A7Y52vGIN0ijEKsWR_GQLg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1532, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3015, + "FriendlyName": "Pearl Necklace (Freshwater)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7be2eafc-21ea-4266-ae64-8888c06798d6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1523, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3016, + "FriendlyName": "Pearl Necklace (Saltwater)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "7be2eafc-21ea-4266-ae64-8888c06798d6,sqPMH0LhLkeG-4XkxpU-sw,URWHffwjGUKgXeV01upePA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1524, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3252, + "FriendlyName": "Mermaid Dress", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fd7774bb-b29c-40b5-84b1-5c2bbe574255,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-22T21:00:00Z", + "SaleEndDate": "2021-08-23T21:00:00Z" + } + } + ], + "PurchasableItemId": 1746, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3254, + "FriendlyName": "Mermaid Crown", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "16359b51-96c4-4be0-b598-319c7549e18a,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-22T21:00:00Z", + "SaleEndDate": "2021-08-23T21:00:00Z" + } + } + ], + "PurchasableItemId": 1744, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3260, + "FriendlyName": "Crab Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3ae0eb82-13ce-43a2-bbc2-99a1cdbec63f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-08T21:00:00Z", + "SaleEndDate": "2021-08-09T21:00:00Z" + } + } + ], + "PurchasableItemId": 1745, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3253, + "FriendlyName": "Shell Necklace", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c8c00394-4cd9-4bad-9774-a825ded78f80,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-22T21:00:00Z", + "SaleEndDate": "2021-08-23T21:00:00Z" + } + } + ], + "PurchasableItemId": 1747, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2794, + "FriendlyName": "Big Head Bow (Teal, Polka Dot)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,GmS0wKwzVEqdf3NoTMHG5A,3DdSzSqV1UG5AtPr8ro3Kg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-07-18T21:00:00Z", + "SaleEndDate": "2021-07-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 1324, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2795, + "FriendlyName": "Big Head Bow (Red, Polka Dot)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,ZNKGkou_gU-1cxBFy7pDKw,3DdSzSqV1UG5AtPr8ro3Kg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-07-18T21:00:00Z", + "SaleEndDate": "2021-07-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 1325, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2796, + "FriendlyName": "Big Head Bow (White, Polka Dot)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "JTrfBXhIT02o2-ssP94zew,-Iqmonry4EKoqAULCKzRPA,3DdSzSqV1UG5AtPr8ro3Kg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-07-18T21:00:00Z", + "SaleEndDate": "2021-07-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 1326, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3276, + "FriendlyName": "Sundress (Seafoam)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,wxvSDMommkenIl5hTY2fdQ,thwvF0DFRk-mV99ruqYFoQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1748, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3277, + "FriendlyName": "Sundress (Sunrise)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,-sqfCQpzpUyjEShEXH9JYQ,thwvF0DFRk-mV99ruqYFoQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1749, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3278, + "FriendlyName": "Sundress (Sunset)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,as9fjqDWiUi1w7yYavKZXA,thwvF0DFRk-mV99ruqYFoQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1750, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2583, + "FriendlyName": "Shutter Shades (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,76CA_KCr806y1FA35m-YTg,tqyzKPdDwkWq3jfZ8jZdaA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-11T21:00:00Z", + "SaleEndDate": "2021-04-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 1150, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3177, + "FriendlyName": "Luau Dress (Primrose)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1680, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3189, + "FriendlyName": "Luau Shirt (Primrose)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,RZFWcTzxPkOfUuTIQ1mzkg,ytfIq0TiYU6EUdd0ruaGAQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1702, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3195, + "FriendlyName": "Luau Hat (Primrose)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,SqHAYcqBXU2PtF2urWGruQ,sIP83lqv3kG31TH5Rw4gqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1696, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3206, + "FriendlyName": "Luau Headband (Primrose)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1686, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3182, + "FriendlyName": "Luau Dress (Begonia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,wyKAZSZwT0u9cuSaQ_-IHg,vUgpsaXrO0ak7huhkHqlUA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1685, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3187, + "FriendlyName": "Luau Shirt (Begonia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,4ijeE3LqUUuSplKrMto6Kg,ytfIq0TiYU6EUdd0ruaGAQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1703, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3210, + "FriendlyName": "Luau Headband (Begonia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,J5VsmblfWU2MeUjNo0fz1Q,5ekjxLQZwUm7-iXJ0bYSiQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1691, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3156, + "FriendlyName": "Beanie (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,_fBrHCkTKUe82QgxCyI-Kw,uWXtz0WaO0qJ1W4TfWnLqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": true, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1714, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3148, + "FriendlyName": "Beanie (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,c24f1DNZjEmqkt-FibKUjQ,uWXtz0WaO0qJ1W4TfWnLqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1716, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3146, + "FriendlyName": "Beanie (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1713, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3193, + "FriendlyName": "Luau Hat (Begonia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,B6qNcZQ770eJel13RskuIg,sIP83lqv3kG31TH5Rw4gqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1697, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2945, + "FriendlyName": "Trucker Jacket (Sleeveless, Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3e3f5ed5-d810-4054-b648-d93167b90033,4Eck_dNL1UKlIZWsA1YppQ,WeyKw_WS70Cx3NZdOaxGeA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1441, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3248, + "FriendlyName": "Retro Sunglasses", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "767684e7-0eec-4f6e-ad58-3d5ae715206e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-03-21T21:00:00Z", + "SaleEndDate": "2021-03-22T21:00:00Z" + } + } + ], + "PurchasableItemId": 1760, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2740, + "FriendlyName": "Notes Headphones (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,viL4CPch2ECwG-RNaWcyZw,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1270, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2744, + "FriendlyName": "Notes Headphones (Psy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,hf0gkFVS8ESvceRsC3LAXQ,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1271, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2749, + "FriendlyName": "Notes Headphones (Neon Heat)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,I1J-xY4YNU2dB_od4P_7Dg,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1272, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3294, + "FriendlyName": "Silly Shorts (Pink Flamingo)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,9P8ARRUYdk6hCoGMKhix4w,o__JORNtYEiJEyyoBIimPA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1761, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3296, + "FriendlyName": "Silly Shorts (Sour Popsicle)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,56qchovVVEaVwL9S52kneA,BAjMoTHbD0mSCjoG9jmzjA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1764, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3297, + "FriendlyName": "Silly Shorts (Pina Colada)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,sJioq5ARDUyMHFji_6OxFw,bjXO36Nu6EWD2dU1UC4Uwg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1762, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3298, + "FriendlyName": "Silly Shorts (Watermelon Rain)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,nu_UGtkJOUK-5MWaW83MUg,pgBFpOtl00K2zsTJcfrefQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1763, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2377, + "FriendlyName": "D4 Skin (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D4]", + "EquipmentModificationGuid": "KiVWBp5_J0qCLuizAJGcAg", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1074, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2381, + "FriendlyName": "D6 Skin (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D6]", + "EquipmentModificationGuid": "TxIu8CUjykGDMr5RIJcNVQ", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1077, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2385, + "FriendlyName": "D8 Skin (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D8]", + "EquipmentModificationGuid": "AQXXckjNZ0uzPMTcP1C3Kg", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1080, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2389, + "FriendlyName": "D10 Skin (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D10]", + "EquipmentModificationGuid": "ZM543HEyl0G6n7P5rDMEWw", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1065, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2393, + "FriendlyName": "D12 Skin (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D12]", + "EquipmentModificationGuid": "EoV2ZoLR2UWY898p8j7qGQ", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1068, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2397, + "FriendlyName": "D20 Skin (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D20]", + "EquipmentModificationGuid": "Des7txtX0EO-7NiooR0-ow", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1071, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3268, + "FriendlyName": "Pegasus Wings", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4c673a35-c10f-4a8c-b8e0-96a50fe5b97f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1768, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2757, + "FriendlyName": "Unicorn Horn (Joy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,mudt-Z5-5USdnIq-xaDgOw,nW_fBsuZA0OfIjTMXm6ZCA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 12000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1310, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 10800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3312, + "FriendlyName": "Chocolate Cake", + "Tooltip": "Eight slices of tasty cake to share with friends.", + "ConsumableItemDesc": "BbJb1_0g4EGJP_CeNrjpew", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 32, + "ItemSetFriendlyName": "Coach's Cakes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1765, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3313, + "FriendlyName": "Red Velvet Cake", + "Tooltip": "Eight slices of tasty cake to share with friends.", + "ConsumableItemDesc": "LwaotjVEBUir0-w8126n_g", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 32, + "ItemSetFriendlyName": "Coach's Cakes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1766, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2755, + "FriendlyName": "Unicorn Horn (Spirit)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2caHvVQ1vECrPv-YcdxdyA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1273, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8550, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3307, + "FriendlyName": "Tiny Crown", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "00f0b914-aa22-4d14-9af1-996e1af61f4d,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-09-05T21:00:00Z", + "SaleEndDate": "2021-09-06T21:00:00Z" + } + } + ], + "PurchasableItemId": 2243, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3306, + "FriendlyName": "Royal Cape (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,cKLBS6pbf0SFuavWyeZ7GA,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2244, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3308, + "FriendlyName": "I Heart Rec Room Shirt (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1771, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3161, + "FriendlyName": "Brimmed Beanie (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,MVkY4sU5BkGC4QhweRiyIA,7nBHNMZhAE-t9rLCYO_STA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1708, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3162, + "FriendlyName": "Brimmed Beanie (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,ux9DQVSRyU-_5GA-t197OA,7nBHNMZhAE-t9rLCYO_STA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1710, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3159, + "FriendlyName": "Brimmed Beanie (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "48ec7a6a-0c31-40d4-9179-66e3ba6505a1,RL6HkufaJ0yubTpCGVI9eQ,7nBHNMZhAE-t9rLCYO_STA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1707, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3262, + "FriendlyName": "Flannel (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,sscD2O3LEUOAKkqjGpGe8g,uZjxYOMa10GzJ9wQkRge2w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1773, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3263, + "FriendlyName": "Flannel (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,hRxyLsKfsESFAsXAyZQjoA,uZjxYOMa10GzJ9wQkRge2w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1774, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3264, + "FriendlyName": "Flannel (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,kgCKjtSjFEmFq9ez2llxKQ,uZjxYOMa10GzJ9wQkRge2w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1775, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3265, + "FriendlyName": "Flannel (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,KaZqNx5VKkarRrOJcTuD9A,uZjxYOMa10GzJ9wQkRge2w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1776, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3317, + "FriendlyName": "Moons \u0026 Stars Dress ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "936d7126-a3b7-48a2-bceb-6cad9221b267,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1778, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3273, + "FriendlyName": "Sundress (Lodge)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,OJYpuFw810Sg4bTw0XQf-g,NqU5Mw75BE6kPaD8t11Qow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1780, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3274, + "FriendlyName": "Sundress (Yacht)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,t4gMAKp4L0WBaTGf_rtqFw,NqU5Mw75BE6kPaD8t11Qow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1779, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3275, + "FriendlyName": "Sundress (Veranda)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,rGMFdPKmK02_OvIIFYlAxA,NqU5Mw75BE6kPaD8t11Qow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1781, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3318, + "FriendlyName": "Starship Helm", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "380a5e35-b4d0-4119-9721-8ce4c0840fbd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1785, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3319, + "FriendlyName": "Luchador Mask (Green Lightning)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,5_UnQ7vZ9E2T-EhJ7M-mQw,jL-LAsd03EeN1J-Zp8opew,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1782, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3320, + "FriendlyName": "Luchador Cape (Green Lightning)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Dtis2HpVzkelZb6M9mW5TQ,iqGv6iRJ-UGbofv8-aHKVA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1783, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3321, + "FriendlyName": "Luchador Singlet (Green Lightning)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,7Jx8mrxoi0aQtZEb03CdRA,-IgxYllxkEyr7V1gosWXUA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1784, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3328, + "FriendlyName": "87 Flavor Cake", + "Tooltip": "Eight slices of tasty cake to share with friends.", + "ConsumableItemDesc": "xwQWBB_fekmTqRc2LB92cg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 32, + "ItemSetFriendlyName": "Coach's Cakes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-06-01T21:00:00Z", + "SaleEndDate": "2021-06-07T21:00:00Z" + } + } + ], + "PurchasableItemId": 1772, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3314, + "FriendlyName": "Celebration Cake", + "Tooltip": "Eight slices of tasty cake to share with friends.", + "ConsumableItemDesc": "Sk_1sm88ZU2zpWn01Lv7hw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 32, + "ItemSetFriendlyName": "Coach's Cakes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1767, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2554, + "FriendlyName": "Boxing Shirt (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff15e25d-d467-47a8-9ecc-e415d5acb90c,rzUd1IMTKUmW0HeMPSIOBQ,0ec6628c-a106-4452-af82-105a3e3a9f2e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1056, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2591, + "FriendlyName": "Boxing Helmet (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,rzUd1IMTKUmW0HeMPSIOBQ,92714eac-cc07-4f74-a6f5-cfbbadd07e06,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1055, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3600, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3357, + "FriendlyName": "Winged Hat (Kingfisher)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,tXhgU1o_wkaNQ-7P3KXidQ,njEWOjNEQEWCEqmytodpow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1805, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3372, + "FriendlyName": "Winged Hat (Raven)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,6547pqf6vUm2L89AG6D91A,njEWOjNEQEWCEqmytodpow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1806, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3373, + "FriendlyName": "Winged Hat (Grackle)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,oxi26T5a4U6L73pF-Pgrhg,njEWOjNEQEWCEqmytodpow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1807, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3360, + "FriendlyName": "Winged Helm", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "16707919-756d-45ae-95fc-cfb6715b8ae6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1808, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2309, + "FriendlyName": "Detective Cap (Grey)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "5bde836f-9343-4934-b01a-606f707020ef,MQOxmUk-mUG-Ee9m9nX31Q,axPBGoqorEe77Oqqjfzb5w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 860, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2315, + "FriendlyName": "Detective Coat (Grey)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "033e328f-d9d2-4e53-af20-44f18e3ea208,msOFHoFN9UORkeXXtdqBsw,9ZP617DQk0mZ10J3d5q9oA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 862, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3354, + "FriendlyName": "Turtleneck (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,R0UCacpdhUS906Xf7l142A,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1811, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3355, + "FriendlyName": "Turtleneck (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,G-VKgepBkkeATcwCaS6Rwg,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1810, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3356, + "FriendlyName": "Turtleneck (Navy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,1LonkCbt4kykeAScYKpwyg,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1809, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1709, + "FriendlyName": "Drum Major Shirt (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a6f1825e-1647-4d06-85d7-ac2d139612c0,82de1ca3-67d6-454d-84fb-0eeece71fcbc,b0298c45-0dd1-453e-bad4-41e4656bb38e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 305, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1705, + "FriendlyName": "Drum Major Hat (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1a028850-64ee-40ab-b547-ebbcfa1b04b3,b444ed0a-eab5-4bd6-af34-d080de74a149,a8a91216-0fcc-461e-a682-95ee82509cce,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 301, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1712, + "FriendlyName": "Drum Major Gloves (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4d533b3a-b5fa-446a-842d-92798ad5b493,b444ed0a-eab5-4bd6-af34-d080de74a149,6b38ab79-142b-446e-978c-ad829ade8823,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2245, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3362, + "FriendlyName": "Bounty Hunter Helmet", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2fadddc1-babd-4cb3-8f5a-0e7c889c3785,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1812, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3363, + "FriendlyName": "Bounty Hunter Belt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f2c1c5e3-b201-4d34-9ce6-3082ff7e5d42,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1816, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3364, + "FriendlyName": "Bounty Hunter Cape", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e773fddc-ad9f-408c-82c5-2637f75b3214,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1813, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3365, + "FriendlyName": "Bounty Hunter Armor ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9ffa6e06-5200-4e53-ac13-f8491aecc864,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1815, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3366, + "FriendlyName": "Bounty Hunter Glove", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2ddd2c2b-596c-4022-a488-655b0db1c265,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1814, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3374, + "FriendlyName": "Conductor Cuffs (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,HBt3ToOkUUereoPKwyvzuw,awWjleyNP0akWNYJoehCLA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1818, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3375, + "FriendlyName": "Conductor Jacket (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,2axtF2iOGkez1kfRRGUbYw,cQUquXMk5EGt0VUCuPKBgg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1819, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3376, + "FriendlyName": "Conductor Hat (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,LdKaVAveokmp3qRwVBxnFA,YJGsYfwkik2lp7qWqXyXTw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1817, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2645, + "FriendlyName": "Figure Skater Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9o0gWLJjREG552gxRreT8A,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1096, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2646, + "FriendlyName": "Figure Skater Skirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5PUja-WX00Sme9yBwvDq-g,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1097, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3383, + "FriendlyName": "Nineties Outfit (Da Bomb)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1820, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3474, + "FriendlyName": "Sus(picious) Goblin Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,4oywwjb3-kifr-um9x87Pw,11ZEZTFhy0ONpoXb3kiewQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2246, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3181, + "FriendlyName": "Luau Dress (Dahlia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,_OM7UNZENEejgO5h7H1ztg,vUgpsaXrO0ak7huhkHqlUA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1684, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3188, + "FriendlyName": "Luau Shirt (Dahlia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,d27e5m1BLUWI42Pxnj6F4A,ytfIq0TiYU6EUdd0ruaGAQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1701, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3194, + "FriendlyName": "Luau Hat (Dahlia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,p3g8YAOhTUKAiE-Wh-X-aA,sIP83lqv3kG31TH5Rw4gqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1695, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3211, + "FriendlyName": "Luau Headband (Dahlia)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,Asfe0ZRi50eryD5wlJzgzA,5ekjxLQZwUm7-iXJ0bYSiQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1689, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1602, + "FriendlyName": "Fighter Pilot Helmet (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ec836d2a-b1b8-4566-b8eb-123ae2845956,8c74ee27-3d56-401b-8a8c-7868a80770e8,9d6ac0b7-bbef-40bc-9173-effe65d7c0ff,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 215, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1605, + "FriendlyName": "Fighter Pilot Uniform (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff7e6d41-5c27-4516-930c-ad0e9a23cce2,8c74ee27-3d56-401b-8a8c-7868a80770e8,435eba0d-d8f9-43cc-91aa-2959d63c6fa7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 218, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1608, + "FriendlyName": "Fighter Pilot Gloves (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8,8c74ee27-3d56-401b-8a8c-7868a80770e8,7869b2a5-cd89-4033-b9f7-4f1451ecb47b,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 221, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2553, + "FriendlyName": "Deep Sea Diver Suit (Kraken)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c3745cee-d87e-4a23-b6e2-64ebf05c8b11,Su7kiW24d0KapNns96JoFQ,EiIow5mS80qpoS10r1ZdvQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1086, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2578, + "FriendlyName": "Deep Sea Diver Tank (Kraken)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8d97303c-5138-4da1-a74d-378da19d11cb,Su7kiW24d0KapNns96JoFQ,ZpyoWbO6GkGglXlMuYUARw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1088, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2597, + "FriendlyName": "Deep Sea Diver Helmet (Kraken)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4fcb5b84-13a9-43e6-9680-72b17874ae53,Su7kiW24d0KapNns96JoFQ,a3Er_BCApUiPtBaGkSH9LA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1085, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8550, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2608, + "FriendlyName": "Deep Sea Diver Gloves (Kraken)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b1c3ccc8-91d7-434e-b9cc-bd632fffc01b,Su7kiW24d0KapNns96JoFQ,0p5N9U5qokKdQhW07yYLGQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1083, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3472, + "FriendlyName": "Fancy Cape (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1825, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3473, + "FriendlyName": "Fancy Cape (Gilded)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "46cde502-86d6-4061-a80c-67458ffd4d60,crl8VlZ59kGxG01noGEWmA,EckOEU0mJkGIVDvYoKBN9g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1826, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3388, + "FriendlyName": "Chef Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "af33bdb1-b7fa-4fe6-b37a-b1fe6a420879,PR5dh2jf2kqGLO0lXgA69A,-lYK7LhYyEy6wqL1tIE9cg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1823, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3389, + "FriendlyName": "Chef Coat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ff5c62b3-4f41-4acd-bfd8-8db00de4f36c,PR5dh2jf2kqGLO0lXgA69A,VDn1hO6ahky0GDJt5dRmyg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1824, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2974, + "FriendlyName": "Formal Vest (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3d9d52cb-6a3d-436b-90e7-e76f238329ee,m4W5D0Oa9kaWa-8I96YYzA,WF_fg80f30qvHit0vGBwCA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1539, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3008, + "FriendlyName": "Formal Dress (Red Sequins)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "73a119ab-d2c8-4da9-98b2-29edbb38e2c2,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1512, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2262, + "FriendlyName": "Cat Ears (Grey)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3d5184e1-0201-4476-bf4b-4eb28190de62,96e42c7a-2881-4299-a2fe-80c0d4d5ca26,1e3195ab-f7f6-41f4-a5ec-378d09ecdf69,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 914, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3080, + "FriendlyName": "Soda Clerk Uniform (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,oKxmH-1s2EyIDkZoOQSQQw,MBuxBAIkpkWI26d-arAKQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1610, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3076, + "FriendlyName": "Soda Clerk Hat (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,XFCpIdeDXUqeYCIgh4fGtg,sV_ByCug30G1Gw-77KUe1Q,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1600, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3079, + "FriendlyName": "Soda Clerk Uniform (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,XFCpIdeDXUqeYCIgh4fGtg,MBuxBAIkpkWI26d-arAKQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1602, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3152, + "FriendlyName": "Beanie (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,fLWfBSEO6U6aHrDm3nKFEQ,uWXtz0WaO0qJ1W4TfWnLqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1718, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3155, + "FriendlyName": "Beanie (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "41bece0e-9b5d-4151-bf5c-d0e786a803b4,wbFPXIg5gUK3QK6YycZexg,uWXtz0WaO0qJ1W4TfWnLqg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1722, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2542, + "FriendlyName": "Anime Skirt (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "MJkDYDH510izAab13ZfXQw,kTP3xKhzBkuQF3SqvmYFKA,2UNd6eViFUO359xjSiuu2A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1019, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2575, + "FriendlyName": "Anime Bow (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "WuShlrJnHUKuscn8e6Rt2g,i3rD_hnQG0a-5f6Ve4AfWg,M5z2wk-GDkOAjvZzqsfcNQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1013, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2594, + "FriendlyName": "Anime Tiara (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "hT7nSD7Ts06F_1SNlOMLwg,kLv_wsbbGUmlfIUO1QLYLw,mG60EPIQIkaJNgOkonVmZg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1024, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2615, + "FriendlyName": "Anime Glove (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "eS_or4AnMka9M_bD7YN28g,1FcyrVGyGEC9dmOq_51nFQ,ZtCqMnFTQUWv2h0MfZ4m6A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1015, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3476, + "FriendlyName": "Jacket Dress (Red) ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1854, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3477, + "FriendlyName": "Jacket Dress (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,v_5qFi-vAUSDd1fLNL7pvA,X0_GvGrZGkSP9Ub81CHgqQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1855, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3478, + "FriendlyName": "Jacket Dress (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6fccd476-c621-4f3f-b2e3-45c95be912b5,y5O8zRJy4kuhnXzATSWKew,X0_GvGrZGkSP9Ub81CHgqQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1856, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 338, + "FriendlyName": "Cowboy Hat (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ad0bdec5-f767-42e7-99e0-221473464a85,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 22, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 363, + "FriendlyName": "Viking Helmet", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c87239ca-a255-4b13-bbc5-1b38236fb4cc,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 47, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 432, + "FriendlyName": "Cowboy Vest (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ba84b23e-01ee-4668-a7ef-2e3e0eca2f23,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 89, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6300, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 522, + "FriendlyName": "Cowboy Gloves (Brown)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9eb7f505-5afd-4b28-9886-7996d38c59c6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 154, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2840, + "FriendlyName": "Squid Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "899f1d17-eee1-4b7d-a112-fbbba597e738,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1370, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3379, + "FriendlyName": "Party Hat (Celebration)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8df990d9-141a-4757-b4f2-138d5e68ab74,RROvzwtshUGyRudVIBn8bw,MrrsmY0J0EKduH-f5wUj6g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1863, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3625, + "FriendlyName": "Business Tie (Red Sequins)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "05ec4e87-088f-4281-a118-2e63c2585200,Wm30oV5BtEuFQZFk3dPK_w,tfiSfpRez0y_R4QjAhO0bQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1861, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3626, + "FriendlyName": "Bow Tie (Red Sequins)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6dd261d8-9806-4dac-81c4-a58f6d6a7f9a,Wm30oV5BtEuFQZFk3dPK_w,0R3Mh96tS0631EZYs03dmw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1862, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3255, + "FriendlyName": "Hero Guy Armor", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ffad1160-8383-4b4a-a5b9-223476b49b92,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1758, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3256, + "FriendlyName": "Hero Gal Armor", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1988692c-aae3-450b-acd6-e9015b831f5c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1757, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3257, + "FriendlyName": "Hero Bracer ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fa0e701c-5e80-46f4-8817-6e2dbf904734,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1756, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3258, + "FriendlyName": "Hero Crown", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b4e48239-a3b0-49ed-b78a-86f4e3aaca1e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1755, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3386, + "FriendlyName": "Martial Arts Gi (Strength) ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "25769d1f-4ecb-4d92-b692-322b38c013b5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1871, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3387, + "FriendlyName": "Martial Arts Gi (Speed)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e29d044e-79fb-4280-bca9-aa1e1257e968,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1872, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3636, + "FriendlyName": "Martial Arts Gi (Cunning) ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6423f23e-dde3-4428-96f5-6091027c92cb,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1873, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3216, + "FriendlyName": "Fox Ears", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "31bb82db-cd30-4988-a8bf-3ce55a12a4c9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1874, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3623, + "FriendlyName": "Rec Room Hoodie (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b6rLwzD4NkKV7xKn9ZYVkA,LuYelZY8NkqmOp1pnxPXBg,oHKhT-GTx0iFWrrlz9s6Tw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1875, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3003, + "FriendlyName": "Disco Ball Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a5b747b8-c1a5-4bce-999f-e13dbe77bde6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1527, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3669, + "FriendlyName": "Star Captain Hat ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34f45c5b-fea7-40b4-add7-bc7a37000ab5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1876, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3465, + "FriendlyName": "Bandana (Dots, Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,b292eb4b-07e3-4a48-99b5-3c6587a1e02e,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1878, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3468, + "FriendlyName": "Bandana (Flowers, Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,27a5df72-4095-4837-bf24-cdd3d95a43e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1877, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3471, + "FriendlyName": "Bandana (Plaid, Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,40ldoL7MAUKnb5JA68CMBA,cf119781-5bd9-4b85-9a0b-12e82e988c23,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1879, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2862, + "FriendlyName": "Stunt Singlet (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,i1XbjBqTu0CILIuJClWimA,B-sIpWNOfEigMeQF-WVR1g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1383, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2865, + "FriendlyName": "Stunt Singlet (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1382, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3674, + "FriendlyName": "Snowy Owl Cape", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9441142e-981c-4f35-8f9d-b4b3604da9a4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1881, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3675, + "FriendlyName": "Snowy Owl Mask", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "771863b6-70d1-4f48-9c64-c621ce0fea46,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1880, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3640, + "FriendlyName": "Cheerleader Skirt (Victory)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "657efe89-620a-46a2-8b58-cafd855c7cd5,eWf2c62cqEOkRukLzhl_wQ,KFq8d1VsO0KDqBWUj4qRFg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1882, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3659, + "FriendlyName": "Cheerleader Sweater (Victory)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9073d6a0-3bc1-4c82-aeb3-d4ee195538b4,6secgNPvmUyFnI3NC5dIYw,5V_1JEDrRUa1UPKhtrSB2w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1883, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 375, + "FriendlyName": "Square Glasses (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 54, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2061, + "FriendlyName": "Square Glasses (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,e0186718-4a18-434b-b96e-0c9c912f0d1f,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 618, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2062, + "FriendlyName": "Square Glasses (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,c5884778-a87f-4612-9717-86fbb65d440f,e0186718-4a18-434b-b96e-0c9c912f0d1f,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 619, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3670, + "FriendlyName": "Hoodie Jacket", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "85e03c16-4288-45f4-a579-c911437d27d4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1884, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 330, + "FriendlyName": "Astronaut Helmet (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 14, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3087, + "FriendlyName": "Bog Monster Suit", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "dedd2e98-2721-4de6-92f2-9d399f34e8fe,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 51, + "ItemSetFriendlyName": "Bog Monster" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2247, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3088, + "FriendlyName": "Bog Monster Wrist", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4e42e02e-33fd-4ff9-b74e-9bde26253c35,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 51, + "ItemSetFriendlyName": "Bog Monster" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2248, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3089, + "FriendlyName": "Bog Monster Hood", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "20b424a9-861a-4d39-a583-22d7b7348c14,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 51, + "ItemSetFriendlyName": "Bog Monster" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2249, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 8100, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3671, + "FriendlyName": "Championship Wrestling Belt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9231a9c6-7638-4fe9-bd2f-eac6c943009b,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1892, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3295, + "FriendlyName": "Silly Shorts (Lonely Island)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,zJMD-_HR80WCAUqYGe7_NA,rAzhaLh1nEWMd9v1D-HWcQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1890, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3299, + "FriendlyName": "Silly Shorts (Donut Dreams)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ae180fb1-3e74-4232-9a9d-388fd488eba1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-07-04T21:00:00Z", + "SaleEndDate": "2021-07-05T21:00:00Z" + } + } + ], + "PurchasableItemId": 1891, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3620, + "FriendlyName": "Luchador Cape (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,xNjX_6-aREm0cYZObVLiAg,OE-PtL0-0kWcJPwn1wvuVg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1893, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3621, + "FriendlyName": "Luchador Cape (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,Kcyp8sv0202Puym1Wf86xg,OE-PtL0-0kWcJPwn1wvuVg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1894, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2444, + "FriendlyName": "Astronaut Helmet (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,JFalLDUvm0aMm3wh1UVfVg,iQjkS1xYP0S63ltvKpkz5g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 911, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2457, + "FriendlyName": "Astronaut Suit (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,9GhnX4Pj4E-wxbPlD8GB-g,YSrGwXybGEqaqHNCS64qOA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 912, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2460, + "FriendlyName": "Astronaut Gloves (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,y_gTbnuYS0GKJmnCs2retw,hjk9OO8CW0KNGR7A6_p84A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 910, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3678, + "FriendlyName": "Assorted Chocolates", + "Tooltip": "Assorted chocolates to share with friends!", + "ConsumableItemDesc": "K9RbXo-4U0q6NbGu8VL1Sw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 52, + "ItemSetFriendlyName": "Snickety Snacks" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1896, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3672, + "FriendlyName": "Animal Sweater (Ferret) ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1899, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2564, + "FriendlyName": "Beekeeper Suit (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7,oZ8k0Qv3SkG-DkY0_dP7UA,afd88a79-7f03-40f6-bcb4-c247dd86edc4,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1036, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2598, + "FriendlyName": "Beekeeper Veil (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "96bc2f9f-ce3a-4c4c-b92c-cfed4ed4594f,oZ8k0Qv3SkG-DkY0_dP7UA,e4b53a9a-ac95-49ae-bf68-615bb23fa075,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1040, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2601, + "FriendlyName": "Beekeeper Gloves (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4025e4c4-abaa-4729-a2a3-a7175313e5ed,oZ8k0Qv3SkG-DkY0_dP7UA,1f18670d-df52-4d2b-8009-a32cf7c67b40,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1037, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3695, + "FriendlyName": "Barbarian Tunic", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e48955ae-50e0-4f55-93a1-611150142331,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-09-19T21:00:00Z", + "SaleEndDate": "2021-09-20T21:00:00Z" + } + } + ], + "PurchasableItemId": 1906, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3696, + "FriendlyName": "Barbarian Helm ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "07023692-97e1-4a65-a881-64ca6b4ae08e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1905, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3627, + "FriendlyName": "Winter Cloak (Hearthstone)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b9d08f9a-b23d-4d7e-978b-a0cd8adbfe9c,pXrZhtzjV02_qvIQoWEFWA,Hj8WnsdidE6rJeNyOmWUfg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1907, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1674, + "FriendlyName": "Mirrored Glasses (Silver)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,f841cdfa-45ba-4f09-a117-503eef34cb4b,9c55b609-b7dc-4070-a3ad-6351a0054a06,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 274, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2155, + "FriendlyName": "Mirrored Glasses (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7db6b49f-3e2a-4da8-bdfa-e9ad9ebc5ba6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 711, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3694, + "FriendlyName": "Puffer Vest ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "be75b46c-ce22-4758-8810-c409c5aa1c50,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1908, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3697, + "FriendlyName": "Dragon Wings ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "84efabec-fe50-4c92-845f-b99b1d84ca82,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-04-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1909, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3617, + "FriendlyName": "Aviator Hat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7a7d8a06-7982-4bcb-b7bd-4d03b48889b4,FjnZ-D1hcESLOWL4Ukv7kw,ff5ae41e-e446-4a96-8c11-49d85556fa81,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1911, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3622, + "FriendlyName": "Aviator Coat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "42aa7888-f0b9-4584-9cc1-966585c6b36e,2APfqVnnQUaDYKi-fQg8jg,9009bcdf-b6f7-4dae-b2ec-895511d118ff,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1913, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3624, + "FriendlyName": "Aviator Gloves (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "125a3a62-1005-47dc-9fd8-ee09ea803e9f,7nRz9Jmnn066_PzngkhfFA,5659ce2c-9cb0-49da-8c39-4b7df0f8deec,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1912, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3643, + "FriendlyName": "Aviator Goggles (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "47629ed1-5378-427c-ad61-4b44d2fe90cc,qJGX62O_HUiZWN61NAc1Lg,556f876a-70da-4104-b792-fca8ad67be0c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1910, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3082, + "FriendlyName": "KO Icon - Tire Track", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "J1WqFNUWo0OBi4LGKPDHWw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1601, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2443, + "FriendlyName": "Steampunk Topper", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "gwLiwmqBIk6glFxvaTzxeA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 919, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2401, + "FriendlyName": "Steampunk Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "JWanxvOHyESg9F_HlmTOfA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 918, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3644, + "FriendlyName": "Graphic Cap (Buckaneer)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,5iCrYb8MBkqN6IBEe3IipQ,SroEVC6vBkG42ILwCtKB9w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T20:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1914, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3353, + "FriendlyName": "Turtleneck (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,5mcp7-2ZEE6n3-zN3_CoWw,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1916, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3664, + "FriendlyName": "Turtleneck (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285,IXWd396grEOWHt32L7fCpQ,rzoH8P44a0esN-HnYd3aQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1917, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2631, + "FriendlyName": "Ski Buddy Ear Muffs (Summit)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1154, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3021, + "FriendlyName": "Ski Buddy Ear Muffs (Chill)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6QT99hx6DE6t9cKAs61i3w,KwCEye0nsESFs-yj4h9sjQ,c_t9XDbn-Eqml1O-wC5zJQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1528, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3642, + "FriendlyName": "Long Coat (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "745c5110-104e-4b37-aabf-7c690fd54811,w_aK2K9bHEeU6oSQ7BK0qg,3ba69a39-a4e7-4505-bae7-8db31360ec1c,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-03-19T21:00:00Z", + "SaleEndDate": "2021-03-22T21:00:00Z" + } + } + ], + "PurchasableItemId": 1920, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3613, + "FriendlyName": "Beret (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ef84cb35-5e25-4aa6-82c9-17f01a5040c7,TKbjEeTWfUyA1u4laoULMg,FWL8ZeMe9EerdB0_Fya3AA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-03-19T21:00:00Z", + "SaleEndDate": "2021-03-22T21:00:00Z" + } + } + ], + "PurchasableItemId": 1919, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3714, + "FriendlyName": "Birthstone Earrings (January Garnet)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,FgLAEMh1e0GyHO8J8_xj5Q,iM-04HpG6k2N8n-P3XS22w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1921, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3715, + "FriendlyName": "Birthstone Earrings (February Amethyst)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,C9CZUwEZeUiuOue4CK4luA,gUxl7WNXlUqFYHZ9eGq1cg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1922, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3705, + "FriendlyName": "Birthstone Earrings (March Aquamarine)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,mM7I9kDTx0ut243UkWKXvw,YKaJjmMHo02FWwem8nw1yw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1925, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3706, + "FriendlyName": "Birthstone Earrings (April Diamond)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "308171c3-cfd1-4ff2-8593-856e8de7071a,46f7Ewe99E6tF2qFaMtQ0g,8IK1U23XlUOV-2ibVznkDQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1924, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3661, + "FriendlyName": "Poindexter Shirt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "pQqNvrhDJ0uuipKHA2qUOA,lq59KZwElEStrviDEQLLcA,olyS074RHU-ojE0GzBBYJw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-03-26T21:00:00Z", + "SaleEndDate": "2021-03-29T21:00:00Z" + } + } + ], + "PurchasableItemId": 1926, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3738, + "FriendlyName": "Cat Hat (Calico)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "2c7f90ab-53f7-4579-bf5b-a1c0a6d49e37,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2053, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3750, + "FriendlyName": "Basketball Jersey (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,RU-B71GUF0WPduoSOI1XMg,54a5c055-f757-4c00-a1e1-8b0b6552c608,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-04-04T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-02T21:00:00Z", + "SaleEndDate": "2021-04-05T21:00:00Z" + } + } + ], + "PurchasableItemId": 2054, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3751, + "FriendlyName": "Basketball Jersey (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,se0-2ylLD0u6sqCVPkeP6A,54a5c055-f757-4c00-a1e1-8b0b6552c608,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-04-04T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-02T21:00:00Z", + "SaleEndDate": "2021-04-05T21:00:00Z" + } + } + ], + "PurchasableItemId": 2055, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3752, + "FriendlyName": "Basketball Jersey (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "348ac48b-986e-4d42-a0f7-1aea16b88271,ojTIShRZHEOV1qQRfhwvTA,54a5c055-f757-4c00-a1e1-8b0b6552c608,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-04-04T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-02T21:00:00Z", + "SaleEndDate": "2021-04-05T21:00:00Z" + } + } + ], + "PurchasableItemId": 2056, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3743, + "FriendlyName": "Robot Gauntlets", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d33c2f3a-6831-4deb-82b2-062d1c4224e4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-05T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2059, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3673, + "FriendlyName": "Animal Sweater (Axolotl)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,azbt5pgvrUS4LQwhndFeJg,cHZ4lDnSoUypIXCiRRowzg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-05T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2058, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3382, + "FriendlyName": "Nineties Outfit (Aiight)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,ulOBBvFEJ0ioFA1qmL390A,u9yHtj_tfkuowGp0h_YpFg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-05T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-09T21:00:00Z", + "SaleEndDate": "2021-04-12T21:00:00Z" + } + } + ], + "PurchasableItemId": 2061, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3381, + "FriendlyName": "Nineties Outfit (Booyah)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ca218759-7eed-440e-b5dc-dba15f81e598,V4CVhCB8eU-nrfYgticVOA,u9yHtj_tfkuowGp0h_YpFg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-05T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-09T21:00:00Z", + "SaleEndDate": "2021-04-12T21:00:00Z" + } + } + ], + "PurchasableItemId": 2060, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3757, + "FriendlyName": "Maker Pen Skin (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[MakerPen]", + "EquipmentModificationGuid": "VYF9NGZBgU6TOo9ayjgBEQ", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2062, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3759, + "FriendlyName": "Maker Pen Skin (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[MakerPen]", + "EquipmentModificationGuid": "iRtlLKI-X0S9oYUWLenAKQ", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2063, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3760, + "FriendlyName": "Maker Pen Skin (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[MakerPen]", + "EquipmentModificationGuid": "tvjYGeRdwEqnr9mf84WgcQ", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-05-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2064, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3761, + "FriendlyName": "Maker Pen Skin (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[MakerPen]", + "EquipmentModificationGuid": "Q7Tr_0DXAkOdr61UXiocow", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2065, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3720, + "FriendlyName": "Treasure Hunter Shirt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "QK6lBlX_wUCiSbJ9RkIwdA,H0sqS2_VPEieVrHSyMNo7g,9HVoTIf90Ea8_czICVg9AA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-16T21:00:00Z", + "SaleEndDate": "2021-04-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 2068, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3726, + "FriendlyName": "Treasure Hunter Hat (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "-SQLtL4rdEWTJdw2IroQCQ,CoSVCyrI-0qg-EkqB2R0hg,JO9lJOQSREWbPyDBZH_cyg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-16T21:00:00Z", + "SaleEndDate": "2021-04-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 2066, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3727, + "FriendlyName": "Treasure Hunter Bandolier (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "t_66r_j5oUSh9awIPmdUKg,YTlnBuEjhUG1hSfAs8qwuw,IcR9Z_MxKkS9uh4uFYuFzQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-16T21:00:00Z", + "SaleEndDate": "2021-04-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 2067, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3728, + "FriendlyName": "Treasure Hunter Belt (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ImwG0snB9ECc_gqRRBoz6A,lAjUeUtTLU2-3bG2OXBukg,LFB_YqTym0-tQmUIj-YcYA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-16T21:00:00Z", + "SaleEndDate": "2021-04-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 2069, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3784, + "FriendlyName": "Chicken Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fc6a560a-5ff1-46f6-a593-320c50ee397e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-21T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2073, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3729, + "FriendlyName": "Graphic Cap (Minitron)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,Vk1n941ZgkKrhrL9mh18oA,lxmw7EbzsUuoy0roRy5McQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-21T20:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1915, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3702, + "FriendlyName": "Resistance Trench Coat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "c4622dca-80dc-423b-ae22-2065174e817b,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-28T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2075, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3703, + "FriendlyName": "Resistance Shades", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d4f86b2b-0fa3-4264-9eac-cf15b62463bd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-09-26T21:00:00Z", + "SaleEndDate": "2021-09-27T21:00:00Z" + } + } + ], + "PurchasableItemId": 2074, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3746, + "FriendlyName": "Flannel (Black)\t", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8b065e2a-b106-4f11-b415-4c47ce87993e,eOJkmpSL4EypK-AQz7j8gQ,uZjxYOMa10GzJ9wQkRge2w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-30T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-04-30T21:00:00Z", + "SaleEndDate": "2021-05-03T21:00:00Z" + } + } + ], + "PurchasableItemId": 2076, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3780, + "FriendlyName": "Overcoat (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "35d67043-ca27-4b8a-a893-2d42674e6e68,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-05T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2081, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3676, + "FriendlyName": "Shield Skin (Electric Bloom)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[QuestShield]", + "EquipmentModificationGuid": "L9usPszLFkiA6xs32es2Lw", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-06-02T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2082, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3648, + "FriendlyName": "Cloche (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,w9I-I0GWRE-3RgOUMMlHjg,WWmx0nBo3k2nXUPPRJVJLQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-07T21:00:00Z", + "SaleEndDate": "2021-05-10T21:00:00Z" + } + } + ], + "PurchasableItemId": 2077, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3649, + "FriendlyName": "Cloche (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,bmi_S166JEGWhf_7woVL0A,WWmx0nBo3k2nXUPPRJVJLQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-07T21:00:00Z", + "SaleEndDate": "2021-05-10T21:00:00Z" + } + } + ], + "PurchasableItemId": 2080, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3650, + "FriendlyName": "Cloche (Teal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,dSEiNUAZnUmErO5Vfv51zg,WWmx0nBo3k2nXUPPRJVJLQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-07T21:00:00Z", + "SaleEndDate": "2021-05-10T21:00:00Z" + } + } + ], + "PurchasableItemId": 2078, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3651, + "FriendlyName": "Cloche (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a16d59da-caa5-4388-af67-315bfcb16f64,jTG-jnKn0k-Etg_-4Gb9gw,WWmx0nBo3k2nXUPPRJVJLQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-07T21:00:00Z", + "SaleEndDate": "2021-05-10T21:00:00Z" + } + } + ], + "PurchasableItemId": 2079, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3794, + "FriendlyName": "Gunslinger Poncho", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "62a28dea-adcc-40b3-9045-917ae4f38c63,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2085, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3795, + "FriendlyName": "Gunslinger Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a471995f-1fe7-4b77-a05f-8eea4e0970ab,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2084, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3796, + "FriendlyName": "Gunslinger Vest", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b0c8d3fe-d3de-4883-b63f-8cc8890537cb,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 7500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2086, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 6750, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2392, + "FriendlyName": "D10 Skin (Pewter)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "[Sandbox_D10]", + "EquipmentModificationGuid": "I161QAym2kqPi5wXIHCt8A", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": -1, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2020-02-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1064, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3716, + "FriendlyName": "I Heart Rec Room Shirt (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "97253092-90ba-4ff2-b137-6324ab244d11,HQOKcDCvVUCItkAKo2ygGg,RmjxL1O9bEGOuYxY9kzHEA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-12T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-14T21:00:00Z", + "SaleEndDate": "2021-05-17T21:00:00Z" + } + } + ], + "PurchasableItemId": 2087, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3798, + "FriendlyName": "Tray of Lattes", + "Tooltip": "Four hot lattes to share with friends.", + "ConsumableItemDesc": "P15H1ONBhk-5DYYjid1ttg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-19T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2089, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3756, + "FriendlyName": "Top Hat (Patchwork)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "102c625b-b988-4bf8-a2aa-a31ad7029cdc,xeK_acj2hE-sYGhsMRwjhw,HwhHShudGkGjXagJxCN2uA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-19T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 6500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2088, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5850, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3609, + "FriendlyName": "Cheerful Dress (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,05K7dmjigkCByO2ufw9jtw,93987d32-7a6e-45df-af56-76f912969ce7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-26T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-21T21:00:00Z", + "SaleEndDate": "2021-05-22T21:00:00Z" + } + } + ], + "PurchasableItemId": 2090, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3610, + "FriendlyName": "Cheerful Dress (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,yOw1WqEN0EWqR9t53EsQpw,93987d32-7a6e-45df-af56-76f912969ce7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-26T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-21T21:00:00Z", + "SaleEndDate": "2021-05-22T21:00:00Z" + } + } + ], + "PurchasableItemId": 2091, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3611, + "FriendlyName": "Cheerful Dress (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b02053b3-013f-42b0-8881-85f5be2b8cda,qoVZuGa5NkOmyfikHjDhXw,93987d32-7a6e-45df-af56-76f912969ce7,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-05-26T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-21T21:00:00Z", + "SaleEndDate": "2021-05-22T21:00:00Z" + } + } + ], + "PurchasableItemId": 2092, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3828, + "FriendlyName": "Red Panda Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f61eb9cc-c0d3-4b45-a7b7-b6c69e5e6db4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2093, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3662, + "FriendlyName": "Tracksuit (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,uUOO-k5KS0mU7B5BLaOpnA,adae82eb-a4b9-4541-acf2-76cea839a593,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-28T21:00:00Z", + "SaleEndDate": "2021-05-29T21:00:00Z" + } + } + ], + "PurchasableItemId": 2094, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3663, + "FriendlyName": "Tracksuit (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d766c8d3-76e8-4021-b0a4-24b78fb7eaba,Ijf-j7EgEkGYOKo9SQNLRw,adae82eb-a4b9-4541-acf2-76cea839a593,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-05-28T21:00:00Z", + "SaleEndDate": "2021-05-29T21:00:00Z" + } + } + ], + "PurchasableItemId": 2095, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3803, + "FriendlyName": "Winged Headphones", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "cbf478e1-a9d7-4b3f-9f52-c59271a924f6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-07T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2099, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3718, + "FriendlyName": "Raincoat (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,q_HcScqHgEG-x70g1wtfLA,T7zwcIIc8Ee3ud8a2JCuwQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-09T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-04T21:00:00Z", + "SaleEndDate": "2021-06-05T21:00:00Z" + } + } + ], + "PurchasableItemId": 2098, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3719, + "FriendlyName": "Raincoat (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9e6397dd-b7b0-4d32-8c86-1910106a8478,M-N9vQt0CEefqgmeLp3l4g,T7zwcIIc8Ee3ud8a2JCuwQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-09T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-04T21:00:00Z", + "SaleEndDate": "2021-06-05T21:00:00Z" + } + } + ], + "PurchasableItemId": 2097, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3833, + "FriendlyName": "Wedding Dress", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "74aae830-a46e-4ea7-83dd-1fc7c3a50b1f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-09T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2103, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3368, + "FriendlyName": "Conductor Jacket (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "e60fccda-8711-463d-b8a3-7cd5e76903b2,Chiz8maLoEGDpZOJZEtdQg,cQUquXMk5EGt0VUCuPKBgg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-16T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-11T21:00:00Z", + "SaleEndDate": "2021-06-12T21:00:00Z" + } + } + ], + "PurchasableItemId": 2104, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3367, + "FriendlyName": "Conductor Hat (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "07205057-0804-41de-abe4-bd4027fee1df,jwXdjDbEmkOFyKZHT_RYOw,YJGsYfwkik2lp7qWqXyXTw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-16T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-11T21:00:00Z", + "SaleEndDate": "2021-06-12T21:00:00Z" + } + } + ], + "PurchasableItemId": 2101, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3369, + "FriendlyName": "Conductor Cuffs (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8fc7aaef-adec-4534-8f91-445e4e7095d5,5Dbc8qCaY0OXEuwykM4naA,awWjleyNP0akWNYJoehCLA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-16T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-11T21:00:00Z", + "SaleEndDate": "2021-06-12T21:00:00Z" + } + } + ], + "PurchasableItemId": 2102, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3835, + "FriendlyName": "Animal Sweater (Toucan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "20eb23e8-ab3b-44fc-8038-3714946c1a09,yXVeN9SzQUKE_6u_2KPzgw,sWgRczvpUU6ZHeoMrMD_yw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-16T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2108, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3652, + "FriendlyName": "Flat Cap (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,9aYT6E9sd0OSavlmkhtkGw,73fc2e2a-a036-410b-bafa-3a07658245b5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-25T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-18T21:00:00Z", + "SaleEndDate": "2021-06-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 2105, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3653, + "FriendlyName": "Flat Cap (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,H-ypJNyVSka9as7rDFRZ_A,73fc2e2a-a036-410b-bafa-3a07658245b5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-25T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-18T21:00:00Z", + "SaleEndDate": "2021-06-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 2106, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3654, + "FriendlyName": "Flat Cap (Tan)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5e3929d3-d291-4e91-a0ba-2ef203e407d3,-7uPJmO-wUu3cUhqo0caPg,73fc2e2a-a036-410b-bafa-3a07658245b5,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-25T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-18T21:00:00Z", + "SaleEndDate": "2021-06-19T21:00:00Z" + } + } + ], + "PurchasableItemId": 2107, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3842, + "FriendlyName": "Apocalypse Helmet", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "17f275d7-c5b1-4a0c-857f-8fb7a5d7c57a,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-23T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2112, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3844, + "FriendlyName": "Apocalypse Scarf", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3e307eca-0850-4d85-8cc1-f5c176bfa7e9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-23T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2113, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3847, + "FriendlyName": "Apocalypse Gloves", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1fed823a-22bb-4225-bdc4-491da6eba875,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-23T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2114, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3848, + "FriendlyName": "Apocalypse Jacket", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "23944856-b567-4075-815f-261cb4655cff,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-23T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2115, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3279, + "FriendlyName": "Sundress (Meadow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,sz-J7RjesE-ke3bKwumzZQ,mkNzDvyGfEijbdzAxKYrPQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-30T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-25T21:00:00Z", + "SaleEndDate": "2021-06-26T21:00:00Z" + } + } + ], + "PurchasableItemId": 2109, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3280, + "FriendlyName": "Sundress (Estate)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,6PyJEQxyd0K9TPRnvt_86Q,mkNzDvyGfEijbdzAxKYrPQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-30T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-25T21:00:00Z", + "SaleEndDate": "2021-06-26T21:00:00Z" + } + } + ], + "PurchasableItemId": 2110, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3300, + "FriendlyName": "Sundress (Vineyard)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "adf2aa89-61c4-4b7a-9307-e39e72db23d3,QlZt51mdn0WuiqKoJaIRyA,mkNzDvyGfEijbdzAxKYrPQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-30T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-06-25T21:00:00Z", + "SaleEndDate": "2021-06-26T21:00:00Z" + } + } + ], + "PurchasableItemId": 2111, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3797, + "FriendlyName": "Backsword (Leathered Iron)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2116, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3804, + "FriendlyName": "Grillmaster Apron (Charcoal)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "tTKv7eRODka2KC_hsY2O3Q,vek37vRROUKnSJKVP0CH6w,t-ZEWJCgFEyGP2PjNblQMA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-09T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-02T21:00:00Z", + "SaleEndDate": "2021-07-03T21:00:00Z" + } + } + ], + "PurchasableItemId": 2117, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3818, + "FriendlyName": "Grillmaster Mitt (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "q7pSsRMNHkqEJmYHuhhG6g,x3Q0vVeG_E6juqGM1o0SOQ,ojCeJ1VwtUelyVqFnUwK5A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-09T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-02T21:00:00Z", + "SaleEndDate": "2021-07-03T21:00:00Z" + } + } + ], + "PurchasableItemId": 2118, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3764, + "FriendlyName": "Luchador Singlet (Pouncing Tiger)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "9c34c2c1-07d7-4180-94a2-be8c9caaae84,TLrw3oncCE2MeuVifXSWTg,-_ViYCHHRUip7oN_AnH0Og,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2121, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3765, + "FriendlyName": "Luchador Cape (Pouncing Tiger)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "67117b3d-0669-4cf3-8a4f-066cabd4d8b1,_VfdBTi7PU276qzsYKayow,qSsEcadcm0-twR1TvVcgYQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2122, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3766, + "FriendlyName": "Luchador Mask (Pouncing Tiger)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,RA7cizxxYE6J8yV_0XwPxA,VSLcLM0TJU2kJU5gS4KZGw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2123, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3846, + "FriendlyName": "Monkey Backpack", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5f6a7dee-0b44-4138-b3a7-a079dbc63e83,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-08-07T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2124, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3723, + "FriendlyName": "Treasure Hunter Tank Top (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,3qu7mxSNpkKRrYMhsWHVGg,K5NXVecrqk6Nb6SkBu08ww,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-09T21:00:00Z", + "SaleEndDate": "2021-07-10T21:00:00Z" + } + } + ], + "PurchasableItemId": 2070, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3724, + "FriendlyName": "Treasure Hunter Tank Top (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,Ij8MXrl1Okip4M7y-gF23Q,K5NXVecrqk6Nb6SkBu08ww,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-09T21:00:00Z", + "SaleEndDate": "2021-07-10T21:00:00Z" + } + } + ], + "PurchasableItemId": 2119, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3725, + "FriendlyName": "Treasure Hunter Tank Top (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "TjrFi6TFhUC5tQXWkZiK9A,qEojxjSXm06CX52hRDJAew,K5NXVecrqk6Nb6SkBu08ww,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-14T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-09T21:00:00Z", + "SaleEndDate": "2021-07-10T21:00:00Z" + } + } + ], + "PurchasableItemId": 2120, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3837, + "FriendlyName": "Towel Cape", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "281937b2-f994-45e9-a6d3-03d097247ce6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-21T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2125, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3861, + "FriendlyName": "Sarong Swimsuit", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6ada01c9-3a82-4e21-be3f-70d6eeb24a8d,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-28T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2128, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3874, + "FriendlyName": "Lifeguard Whistle", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "affdc9ad-8312-4421-8f56-e35dfdaeb63a,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-28T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2129, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3179, + "FriendlyName": "Luau Dress (Poppy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "7aa994d2-6499-4918-ab2a-2531c17e27a7,IzdUC8x0F02i3BhPfEUOYw,CAqT_iLfQ0qQ-GbIdAsZow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-16T21:00:00Z", + "SaleEndDate": "2021-07-17T21:00:00Z" + } + } + ], + "PurchasableItemId": 1681, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3186, + "FriendlyName": "Luau Shirt (Poppy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,adJVA26PyUSVvmZgxkCAmQ,0WOBGm34H0ySBC10VToa9g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-16T21:00:00Z", + "SaleEndDate": "2021-07-17T21:00:00Z" + } + } + ], + "PurchasableItemId": 1698, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3192, + "FriendlyName": "Luau Hat (Poppy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,cSFt9MWLj06CDzIzATqrTg,znNiHfNdTE-szOIAnIWFQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-16T21:00:00Z", + "SaleEndDate": "2021-07-17T21:00:00Z" + } + } + ], + "PurchasableItemId": 1692, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3208, + "FriendlyName": "Luau Headband (Poppy)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3eb14fcf-5d20-47d3-b991-97808798a225,B9DJPqTeYUuPnPHRbKzEJA,5ekjxLQZwUm7-iXJ0bYSiQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-16T21:00:00Z", + "SaleEndDate": "2021-07-17T21:00:00Z" + } + } + ], + "PurchasableItemId": 1687, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3747, + "FriendlyName": "Traveler Shirt (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "vDSlMnayzEqWXwT-lkcKSA,gwdtq3NiEEmMKYtn1VPoiw,M-mi8QUHwEeIewXE8uAlrQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-28T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 400, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-23T21:00:00Z", + "SaleEndDate": "2021-07-24T21:00:00Z" + } + } + ], + "PurchasableItemId": 2126, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 360, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3753, + "FriendlyName": "Traveler Hat (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0QOlT9c5xUuinpfZaoEe8Q,aiBfwCNwpUyJ0rYKCMa7qw,Cjy-bE0PikWLCRrL4U2NGQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-07-28T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-07-23T21:00:00Z", + "SaleEndDate": "2021-07-24T21:00:00Z" + } + } + ], + "PurchasableItemId": 2127, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3679, + "FriendlyName": "Football Helmet (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,X24EvVokeUqyDNdqevV2EA,7660e11a-7ef0-43d3-bc02-76b3a4aab064,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-01-29T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1886, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3680, + "FriendlyName": "Football Helmet (Gray)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5a38f864-7d1c-4989-8558-40b9668f78ef,UfhOpLemQ0ypYh7CIC3W1Q,7660e11a-7ef0-43d3-bc02-76b3a4aab064,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-01-29T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1885, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3681, + "FriendlyName": "Football Shoulder Pads (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,vMqhT19x302aYEo6E9HhOA,e17481fc-b2b6-47a5-91e5-7205f45a3247,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-01-29T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1888, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3682, + "FriendlyName": "Football Shoulder Pads (Gray)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34d45669-b319-4c7d-bd05-078961d70d0b,kCqthBeVvUKZA9b76kxuVQ,e17481fc-b2b6-47a5-91e5-7205f45a3247,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-01-29T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1889, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3885, + "FriendlyName": "Prankster Jacket", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5b3cae00-88bd-4179-b75a-9444c5a52fa5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-08T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2131, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3886, + "FriendlyName": "Prankster Hair ", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "76a73866-5b60-424d-b751-a105e030f46e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-08T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2132, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3791, + "FriendlyName": "Royal Cape (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a44766dc-19ad-4476-849c-5113a5faabd0,DZyW-gOG9U-QLGOMFBhOoA,HTU3Wp_ri0GNYGFSezFHSg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-08T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2130, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3655, + "FriendlyName": "Plastic Sunglasses (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,z-yaXP9ImkysLOGuCQiblw,S21TW8NwMEWLV9zqHxWfFw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-15T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-06T21:00:00Z", + "SaleEndDate": "2021-08-07T21:00:00Z" + } + } + ], + "PurchasableItemId": 2133, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3656, + "FriendlyName": "Plastic Sunglasses (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,SmRcrycMZ0qYNsHuQQOORA,S21TW8NwMEWLV9zqHxWfFw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-15T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-06T21:00:00Z", + "SaleEndDate": "2021-08-07T21:00:00Z" + } + } + ], + "PurchasableItemId": 2134, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3657, + "FriendlyName": "Plastic Sunglasses (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,0svDrGn2NkmhUjC1VJUgRg,S21TW8NwMEWLV9zqHxWfFw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-15T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-06T21:00:00Z", + "SaleEndDate": "2021-08-07T21:00:00Z" + } + } + ], + "PurchasableItemId": 2135, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3658, + "FriendlyName": "Plastic Sunglasses (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "exQz9Zr3HEyOR9nDDrimlQ,Aq4fjEW5gEyzetXMoG2tdQ,S21TW8NwMEWLV9zqHxWfFw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-15T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 300, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-06T21:00:00Z", + "SaleEndDate": "2021-08-07T21:00:00Z" + } + } + ], + "PurchasableItemId": 2136, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 270, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2839, + "FriendlyName": "Shark Hat", + "Tooltip": "Doo doo doo doo doo doo", + "ConsumableItemDesc": "", + "AvatarItemDesc": "13e70c80-69a4-4ad5-90e5-4dd5e03da677,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1369, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3466, + "FriendlyName": "Bandana (Flowers, Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,Z9H3wUn3_kS6pPehL74aHg,27a5df72-4095-4837-bf24-cdd3d95a43e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-22T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-13T21:00:00Z", + "SaleEndDate": "2021-08-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 2141, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3467, + "FriendlyName": "Bandana (Flowers, Black)\t", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "5accb7dc-90a3-46ee-bad2-8fd1861d50f1,J1bmomo_3Ume0KR2Yh46Uw,27a5df72-4095-4837-bf24-cdd3d95a43e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-22T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-13T21:00:00Z", + "SaleEndDate": "2021-08-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 2142, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3734, + "FriendlyName": "Robo Logo Cap (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ee9a7c8a-7383-4a09-b44c-474b66f9008a,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-22T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2143, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3735, + "FriendlyName": "Robo Logo Cap (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ee9a7c8a-7383-4a09-b44c-474b66f9008a,W-xMu8n2CUuHSXfHvEL4yw,-apO3Y7om0GKhC4V_hVYdQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-22T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2144, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3880, + "FriendlyName": "Sushi", + "Tooltip": "15 piece premium sushi set to share with friends", + "ConsumableItemDesc": "4My42w1D1Uu3-98pXB-x_Q", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-08-25T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2145, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3910, + "FriendlyName": "Wristbands (Plaid)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,j367nSYyg06rOMZ1GmfmFA,FS7kgc4DpkKuar24J9dW8g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-25T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-20T21:00:00Z", + "SaleEndDate": "2021-08-21T21:00:00Z" + } + } + ], + "PurchasableItemId": 2146, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3911, + "FriendlyName": "Wristbands (Moons \u0026 Stars)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,2qYs9IkE90eppx1DggNlsA,6Y43EyTpiEejGp0ODWTSjA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-25T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-20T21:00:00Z", + "SaleEndDate": "2021-08-21T21:00:00Z" + } + } + ], + "PurchasableItemId": 2147, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3912, + "FriendlyName": "Wristbands (Polka Dot)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ecc1dbe6-ca06-4564-b2a6-30956194d1e9,JV60jZGerUWuzQ2rRnwquQ,gSb8iLTEZ0mi3g5-jZXifQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-25T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-20T21:00:00Z", + "SaleEndDate": "2021-08-21T21:00:00Z" + } + } + ], + "PurchasableItemId": 2148, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3891, + "FriendlyName": "Poodle Skirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "08046fdc-aafc-4b22-9917-3e7a9a7ffcd3,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-27T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2149, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3859, + "FriendlyName": "Graphic Cap (Holographic)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "14ef6b00-debf-4a85-9755-b4d37df496d3,mdSSJyipq0KvjDNQ2NyUaw,LQheTa-EvUaAonSyoWNmXg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-08-27T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 8000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2150, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7200, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3920, + "FriendlyName": "Sunglasses (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,0Ytm7G4_lkmZWPA_YfaIeQ,KfM7veOVlE-dNZmsbDNZBA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-09-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-27T21:00:00Z", + "SaleEndDate": "2021-08-29T21:00:00Z" + } + } + ], + "PurchasableItemId": 2151, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3921, + "FriendlyName": "Sunglasses (White)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "6427bd57-fcb2-420f-8b3e-d1da43470b84,dZM1bs6_MU2F6Vz1mH0QKg,KfM7veOVlE-dNZmsbDNZBA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-09-01T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-08-27T21:00:00Z", + "SaleEndDate": "2021-08-29T21:00:00Z" + } + } + ], + "PurchasableItemId": 2152, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3831, + "FriendlyName": "Elven Crown", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "8ff44820-2c4a-4a0b-b409-5b46b6c9e21c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2154, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2700, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3832, + "FriendlyName": "Elven Cape", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "a37c3161-1476-44b0-a9d1-d6eb854db2d9,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2155, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3836, + "FriendlyName": "Elven Armor", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "485bbda2-37e8-45a7-90db-7c35518dcdd0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-09-03T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 2500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2156, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2250, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3838, + "FriendlyName": "Elven Bracer", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "74042b5f-f29e-4e72-b3b1-04e6abaed4a4,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2157, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3774, + "FriendlyName": "Leather Necklace (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,SDbpnjVYmEGQneK5fhZ0Cw,FwjsTo0nb0uNU6l8XvllNA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-09-03T21:00:00Z", + "SaleEndDate": "2021-09-04T21:00:00Z" + } + } + ], + "PurchasableItemId": 2158, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3777, + "FriendlyName": "Leather Necklace (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,uwoWPY4YjEmZSQcNCrx1Gg,FwjsTo0nb0uNU6l8XvllNA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-09-03T21:00:00Z", + "SaleEndDate": "2021-09-04T21:00:00Z" + } + } + ], + "PurchasableItemId": 2159, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3778, + "FriendlyName": "Leather Necklace (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "48fdfbc6-fca4-424c-aa57-3fcea41e23dc,COZbkLDc20mdUlWSkqMX-w,FwjsTo0nb0uNU6l8XvllNA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-09-03T21:00:00Z", + "SaleEndDate": "2021-09-04T21:00:00Z" + } + } + ], + "PurchasableItemId": 2160, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3914, + "FriendlyName": "Rogue Gloves", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4b72736e-6fca-4961-98f2-1d4e199eb1e0,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-09-15T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2161, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3916, + "FriendlyName": "Rogue Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "155c13b9-3c72-4314-85ac-6fbac19cdc76,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-09-15T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2162, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3918, + "FriendlyName": "Rogue Cape", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "63b5196e-881a-4b96-82c8-9df54de4a506,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2163, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3600, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3618, + "FriendlyName": "Belted Dress (Light Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,oJFfs62tR0WVRry2Smt8yA,3d971439-41d1-43a2-8434-704f9ec8d906,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-09-10T21:00:00Z", + "SaleEndDate": "2021-09-11T21:00:00Z" + } + } + ], + "PurchasableItemId": 2164, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3619, + "FriendlyName": "Belted Dress (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f51a7ed5-52ea-485f-a430-303697f6fec9,MIfCkcrFOEasJqfDmxThdw,3d971439-41d1-43a2-8434-704f9ec8d906,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-09-15T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-09-10T21:00:00Z", + "SaleEndDate": "2021-09-11T21:00:00Z" + } + } + ], + "PurchasableItemId": 2165, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3930, + "FriendlyName": "Bard's Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b17591ba-3c2d-4309-ab01-b6743424e07e,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-09-22T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2175, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3932, + "FriendlyName": "Bard's Vest", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f23e86eb-8b59-4b32-a72a-5d382568e399,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-09-22T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 1250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2176, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1125, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3917, + "FriendlyName": "Wolf Amulet", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fb2a0e86-3363-4579-9f18-f41138dc85ba,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": true, + "Rarity": 50, + "Context": 21, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2250, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3933, + "FriendlyName": "Bard's Cuffs", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "4b4d239a-ddf3-4f8c-a58a-b8f4c031ae26,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-09-22T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2177, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3931, + "FriendlyName": "Backsword (Wolf Steel)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "23b97189-b49f-477b-8a22-0903b3d236f4,GfC01_o8QkGTbnGNjv_DSw,7mLb5jMJMUug7Nzwxv6D4A,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-09-17T21:00:00Z", + "SaleEndDate": "2021-09-18T21:00:00Z" + } + } + ], + "PurchasableItemId": 2179, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3939, + "FriendlyName": "Cleric Gloves", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b1ca1dc4-6867-48b6-a332-4efff06a5db1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2181, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3940, + "FriendlyName": "Cleric Headpiece", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b73ae23b-c911-4b09-a803-40999bdef6b8,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2400, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2182, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 2160, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3941, + "FriendlyName": "Cleric Armor", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1bd891b1-6ec0-4227-9ed8-73c59af4f819,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2183, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3690, + "FriendlyName": "RecFly Vest (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,GVoh3mdWf0WNA9lfqL1GEw,w9qG5gHlAUqh8f9ZgnezsQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-09-29T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-09-24T21:00:00Z", + "SaleEndDate": "2021-09-25T21:00:00Z" + } + } + ], + "PurchasableItemId": 2184, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3691, + "FriendlyName": "RecFly Vest (Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "74c65554-0d15-409b-b9a8-0b2620a7d7e2,x4PPvuhg6USE6GSFgZZnCg,w9qG5gHlAUqh8f9ZgnezsQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-09-29T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-09-24T21:00:00Z", + "SaleEndDate": "2021-09-25T21:00:00Z" + } + } + ], + "PurchasableItemId": 2185, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3888, + "FriendlyName": "Gladiator Armor", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "52268f02-b8ba-4a8e-ab3a-7d5928f637a6,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 923, + "ItemSetFriendlyName": "Gladiator Set" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 3500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2251, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3150, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3889, + "FriendlyName": "Gladiator Cape", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1633766e-fa77-48f3-8c6c-a233e56bf320,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 923, + "ItemSetFriendlyName": "Gladiator Set" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2252, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3600, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3890, + "FriendlyName": "Gladiator Helmet", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "594a69fb-9f76-4f83-b2aa-94f0e72e996c,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 923, + "ItemSetFriendlyName": "Gladiator Set" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2253, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3358, + "FriendlyName": "Winged Hat (Vintage)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "fe15ca53-c5b8-4acf-9309-ff3f4e610fc9,nn86ATWxekWdIGlPePPgGQ,njEWOjNEQEWCEqmytodpow,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-08T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": { + "SalePercent": 10, + "SaleStartDate": "2021-10-01T21:00:00Z", + "SaleEndDate": "2021-10-02T21:00:00Z" + } + } + ], + "PurchasableItemId": 2198, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 69, + "FriendlyName": "Class of 2016", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,5864d23d-1a86-40a6-a52c-46ee670e4508,2ee3944d-e9fb-4959-973e-570a7fc6cc57,167ba0d4-4fac-4143-b5ae-1dbd8e02dc76", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 73, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 70, + "FriendlyName": "Birthday Shirt", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,922f229c-124f-4f07-b4e3-19138420ca19,2ee3944d-e9fb-4959-973e-570a7fc6cc57,ceb07e4f-50a9-4ed0-b50b-c59d92ba66b5", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 74, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2740, + "FriendlyName": "Note Headphones (Gold)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "akAhnN0cFkecj8q3spixkw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2254, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2751, + "FriendlyName": "Notes Headphones (Neon Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,3PfP661Fg0CruzRxis9EiA,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1303, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2752, + "FriendlyName": "Notes Headphones (Two Xbox Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,k_ZYy0SjtU2TB7IFdq2vDA,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2255, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2753, + "FriendlyName": "Notes Headphones (Xbox Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,thG3Dc1PFEqAvT9xCVshuw,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2256, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2754, + "FriendlyName": "Notes Headphones (Purple Lounge)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,OqsbZ8Xls0i8uA6ha7H3qQ,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2257, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2755, + "FriendlyName": "Notes Headphones (Purple)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "UcPo9SsF3EGTCBB2J-Efdw,a5nUvJZgTEGBUvdHx0Y6Ng,_Yq00H-NWUyQNvGxRunHbg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2199, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2755, + "FriendlyName": "Captain Hat (Contest)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f5c9743f-e77c-42f2-ba63-9843342cbf8e,hhs9GY3XZkWb-8TXODvo6A,a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1307, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 477, + "FriendlyName": "Captain Jacket (Contest)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,hhs9GY3XZkWb-8TXODvo6A,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2258, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2755, + "FriendlyName": "Captain Jacket (Contest)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "0bc6bd1b-8b7e-4945-a1d6-c26b9c043318,hhs9GY3XZkWb-8TXODvo6A,8ec5de40-73fc-4f09-9bf3-ddf4701107bc,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2200, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3119, + "FriendlyName": "Bee Wings", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "37668d3d-c9f4-4bd6-81d4-a4f64ba2d61b,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1632, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1172, + "FriendlyName": "Winter Sweater (Trees Green)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "3757149b-f478-4975-81e5-d808a1bed8d5,i33RmJJCwUem-ADkOwMNQQ,Yn1uv3Y8JE6VSxsf0LOBvw,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": true, + "NewUntil": "2021-10-06T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 1175, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4050, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 405, + "FriendlyName": "Astronaut Suit (Stranded)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300,EBT5-2K2tEyln18XS9waqg,l4NwuwgEtE6b6Qi6lAk2iA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 8500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2259, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 7650, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 333, + "FriendlyName": "Astronaut Helmet (Stranded)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f218e2de-46fd-45b9-9e0e-670b6bb905e7,QlBsZP9HRUS3eug-em6MlQ,n2L0XJTYb0qN4zoUT2fV4w,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 10000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2260, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 9000, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 520, + "FriendlyName": "Astronaut Gloves (Stranded)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "88739d79-aeb9-471a-b25b-776f46bba9f6,QlBsZP9HRUS3eug-em6MlQ,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2261, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 3, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf300.json b/Server/RadicadeServer-Config/storefronts/sf300.json new file mode 100644 index 0000000..b3b9e59 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf300.json @@ -0,0 +1,758 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 3880, + "FriendlyName": "Sushi", + "Tooltip": "15 piece premium sushi set to share with friends", + "ConsumableItemDesc": "4My42w1D1Uu3-98pXB-x_Q", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-08-25T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 750, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2262, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 675, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3798, + "FriendlyName": "Tray of Lattes", + "Tooltip": "Four hot lattes to share with friends.", + "ConsumableItemDesc": "P15H1ONBhk-5DYYjid1ttg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": "2021-06-19T21:00:00Z", + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2263, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2027, + "FriendlyName": "Pepperoni Pizza", + "Tooltip": "A pepperoni pizza to share with friends.", + "ConsumableItemDesc": "mq23W-RSP0G8iGNLdrcpUw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2264, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 81, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2025, + "FriendlyName": "Cheese Pizza", + "Tooltip": "A cheese pizza to share with friends.", + "ConsumableItemDesc": "5hIAZ9wg5EyG1cILf4FS2A", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": { + "SalePercent": 30, + "SaleStartDate": "2021-09-05T07:00:00Z", + "SaleEndDate": "2021-09-06T07:00:00Z" + } + } + ], + "PurchasableItemId": 2265, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 76, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2014, + "FriendlyName": "Supreme Pizza", + "Tooltip": "A supreme pizza to share with friends.", + "ConsumableItemDesc": "wUCIKdJSvEmiQHYMyx4X4w", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2266, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2470, + "FriendlyName": "Hawaiian Pizza", + "Tooltip": "A ham and pineapple pizza to share with friends.", + "ConsumableItemDesc": "_jnjYGBcyEWY5Ub4OezXcA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2267, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2326, + "FriendlyName": "Assorted Donuts", + "Tooltip": "A dozen assorted donuts to share with friends.", + "ConsumableItemDesc": "ZuvkidodzkuOfGLDnTOFyg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 34, + "ItemSetFriendlyName": "Donuts by the Dozen" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2268, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2324, + "FriendlyName": "Glazed Donuts", + "Tooltip": "A dozen glazed donuts to share with friends.", + "ConsumableItemDesc": "7OZ5AE3uuUyqa0P-2W1ptg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 34, + "ItemSetFriendlyName": "Donuts by the Dozen" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2269, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 81, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2327, + "FriendlyName": "Chocolate Donuts", + "Tooltip": "A dozen chocolate donuts to share with friends.", + "ConsumableItemDesc": "mMCGPgK3tki5S_15q2Z81A", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 34, + "ItemSetFriendlyName": "Donuts by the Dozen" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2270, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3312, + "FriendlyName": "Chocolate Cake", + "Tooltip": "Eight slices of tasty cake to share with friends.", + "ConsumableItemDesc": "BbJb1_0g4EGJP_CeNrjpew", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 32, + "ItemSetFriendlyName": "Coach's Cakes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2271, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3313, + "FriendlyName": "Red Velvet Cake", + "Tooltip": "Eight slices of tasty cake to share with friends.", + "ConsumableItemDesc": "LwaotjVEBUir0-w8126n_g", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 32, + "ItemSetFriendlyName": "Coach's Cakes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2272, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3328, + "FriendlyName": "87 Flavor Cake", + "Tooltip": "Eight slices of tasty cake to share with friends.", + "ConsumableItemDesc": "xwQWBB_fekmTqRc2LB92cg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 32, + "ItemSetFriendlyName": "Coach's Cakes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-06-01T21:00:00Z", + "SaleEndDate": "2021-06-07T21:00:00Z" + } + } + ], + "PurchasableItemId": 2273, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3314, + "FriendlyName": "Celebration Cake", + "Tooltip": "Eight slices of tasty cake to share with friends.", + "ConsumableItemDesc": "Sk_1sm88ZU2zpWn01Lv7hw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 32, + "ItemSetFriendlyName": "Coach's Cakes" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2274, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3026, + "FriendlyName": "Popcorn", + "Tooltip": "", + "ConsumableItemDesc": "QRx0aSTT9keMFdAJMQHdTg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 52, + "ItemSetFriendlyName": "Snickety Snacks" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2275, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2471, + "FriendlyName": "Salted Pretzels", + "Tooltip": "A great snack to share with friends!", + "ConsumableItemDesc": "InQ25wQMGkG_bvuD5rf2Ag", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 52, + "ItemSetFriendlyName": "Snickety Snacks" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 75, + "StorefrontSaleData": { + "SalePercent": 25, + "SaleStartDate": "2021-04-26T07:00:00Z", + "SaleEndDate": "2021-04-27T07:00:00Z" + } + } + ], + "PurchasableItemId": 2276, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 67, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3678, + "FriendlyName": "Assorted Chocolates", + "Tooltip": "Assorted chocolates to share with friends!", + "ConsumableItemDesc": "K9RbXo-4U0q6NbGu8VL1Sw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 52, + "ItemSetFriendlyName": "Snickety Snacks" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 250, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2277, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 225, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2011, + "FriendlyName": "Root Beer", + "Tooltip": "A sixer of root beer to share with friends.", + "ConsumableItemDesc": "JfnVXFmilU6ysv-VbTAe3A", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 50, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2278, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 45, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3025, + "FriendlyName": "Fancy Bubbly", + "Tooltip": null, + "ConsumableItemDesc": "iiGTvhOCHkOTNJhb16Zbyw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2279, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 300, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf4.json b/Server/RadicadeServer-Config/storefronts/sf4.json new file mode 100644 index 0000000..1d2a728 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf4.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 4, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf400.json b/Server/RadicadeServer-Config/storefronts/sf400.json new file mode 100644 index 0000000..a7c2c91 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf400.json @@ -0,0 +1,336 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 2075, + "FriendlyName": "KO Icon - Winged Skull", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "U38Qe6rhEk6mFvArHfYjng", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2280, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2081, + "FriendlyName": "KO Icon - Sword \u0026 Shield", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "5AJin8T2iEG7BzOPOgx2HA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2281, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2231, + "FriendlyName": "KO Icon - Grenade", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "EAhk3ZZdXEmH5wRAXXT24Q", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2282, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2080, + "FriendlyName": "KO Icon - Bear Claw", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "ZtZnYBpKkECJlhHmkj4MiA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2283, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 377, + "FriendlyName": "Paintball Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2284, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2069, + "FriendlyName": "Paintball Vest (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2285, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2070, + "FriendlyName": "Paintball Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2286, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 458, + "FriendlyName": "Paintball Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2287, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 400, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf401.json b/Server/RadicadeServer-Config/storefronts/sf401.json new file mode 100644 index 0000000..92f82b3 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf401.json @@ -0,0 +1,336 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 377, + "FriendlyName": "Paintball Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2288, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2839, + "FriendlyName": "Shark Hat", + "Tooltip": "Doo doo doo doo doo doo", + "ConsumableItemDesc": "", + "AvatarItemDesc": "13e70c80-69a4-4ad5-90e5-4dd5e03da677,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2289, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2840, + "FriendlyName": "Squid Hat", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "899f1d17-eee1-4b7d-a112-fbbba597e738,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2290, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4500, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2081, + "FriendlyName": "KO Icon - Sword \u0026 Shield", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "5AJin8T2iEG7BzOPOgx2HA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2291, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 458, + "FriendlyName": "Paintball Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2292, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2069, + "FriendlyName": "Paintball Vest (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2293, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2070, + "FriendlyName": "Paintball Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2294, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3102, + "FriendlyName": "Paintball Belt", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2295, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 401, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf402.json b/Server/RadicadeServer-Config/storefronts/sf402.json new file mode 100644 index 0000000..ebd20a0 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf402.json @@ -0,0 +1,336 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 377, + "FriendlyName": "Paintball Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2296, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2062, + "FriendlyName": "Square Glasses (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,c5884778-a87f-4612-9717-86fbb65d440f,e0186718-4a18-434b-b96e-0c9c912f0d1f,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2297, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2061, + "FriendlyName": "Square Glasses (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f527ffaf-da03-4519-82ed-46a9cb981dc3,a1c51d1e-1d5f-4f8a-bc0f-6a20eddfe58a,e0186718-4a18-434b-b96e-0c9c912f0d1f,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 5500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2298, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 4950, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2075, + "FriendlyName": "KO Icon - Winged Skull", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "U38Qe6rhEk6mFvArHfYjng", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2299, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 458, + "FriendlyName": "Paintball Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2300, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2069, + "FriendlyName": "Paintball Vest (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2301, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2070, + "FriendlyName": "Paintball Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2302, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3102, + "FriendlyName": "Paintball Belt", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2303, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 402, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf403.json b/Server/RadicadeServer-Config/storefronts/sf403.json new file mode 100644 index 0000000..4d5b4aa --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf403.json @@ -0,0 +1,344 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 377, + "FriendlyName": "Paintball Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2304, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1776, + "FriendlyName": "Luchador Mask (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250,96b36c98-cba1-4337-91dd-c74c00316f97,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-23T21:00:00Z", + "SaleEndDate": "2021-05-24T21:00:00Z" + } + } + ], + "PurchasableItemId": 2305, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1775, + "FriendlyName": "Luchador Mask (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1596c113-aa3a-4f5c-b398-e5c3575548e7,916384ca-e209-42a0-bd64-c64f5a40b56f,96b36c98-cba1-4337-91dd-c74c00316f97,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-05-23T21:00:00Z", + "SaleEndDate": "2021-05-24T21:00:00Z" + } + } + ], + "PurchasableItemId": 2306, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2231, + "FriendlyName": "KO Icon - Grenade", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "EAhk3ZZdXEmH5wRAXXT24Q", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2307, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 458, + "FriendlyName": "Paintball Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2308, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2069, + "FriendlyName": "Paintball Vest (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2309, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2070, + "FriendlyName": "Paintball Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2310, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3102, + "FriendlyName": "Paintball Belt", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2311, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 403, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf404.json b/Server/RadicadeServer-Config/storefronts/sf404.json new file mode 100644 index 0000000..82d7845 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf404.json @@ -0,0 +1,336 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 377, + "FriendlyName": "Paintball Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2312, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 335, + "FriendlyName": "Boxing Helmet (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2313, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3600, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 1716, + "FriendlyName": "Boxing Helmet (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "b99a89c7-38c5-4196-9862-4e1bf9cb0ce1,8c7b09f2-6213-4ef9-87ab-a2df72d07a22,92714eac-cc07-4f74-a6f5-cfbbadd07e06,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 4000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2314, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 3600, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2231, + "FriendlyName": "KO Icon - Grenade", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "EAhk3ZZdXEmH5wRAXXT24Q", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 150, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2315, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 135, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 458, + "FriendlyName": "Paintball Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2316, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2069, + "FriendlyName": "Paintball Vest (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2317, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2070, + "FriendlyName": "Paintball Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2318, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3102, + "FriendlyName": "Paintball Belt", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2319, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 404, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf405.json b/Server/RadicadeServer-Config/storefronts/sf405.json new file mode 100644 index 0000000..fab8258 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf405.json @@ -0,0 +1,344 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 377, + "FriendlyName": "Paintball Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2320, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2316, + "FriendlyName": "Shutter Shades (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-11T21:00:00Z", + "SaleEndDate": "2021-04-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 2321, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2585, + "FriendlyName": "Shutter Shades (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "bu9tb-K7NEiocvZY3o7ukw,7B7vpnMTfkGLcMdHCZ_kWA,tqyzKPdDwkWq3jfZ8jZdaA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 6000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-04-11T21:00:00Z", + "SaleEndDate": "2021-04-14T21:00:00Z" + } + } + ], + "PurchasableItemId": 2322, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 5400, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2233, + "FriendlyName": "KO Icon - Star Power", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "yvqSbK2czkS2sUCRdrGaEw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2323, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 458, + "FriendlyName": "Paintball Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2324, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2069, + "FriendlyName": "Paintball Vest (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,178c6beb-c737-434c-a53b-d14f438f6a98,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2325, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2070, + "FriendlyName": "Paintball Vest (Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,2913bd93-bf35-4bdf-b83a-d40a10213adc,0189ec4c-0389-4ebc-b9ba-32303a6341c2,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2326, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3102, + "FriendlyName": "Paintball Belt", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "eda29bce-5a07-4439-a2a7-cafa11e2ed62,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2327, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 405, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf406.json b/Server/RadicadeServer-Config/storefronts/sf406.json new file mode 100644 index 0000000..9d3c86e --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf406.json @@ -0,0 +1,336 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 377, + "FriendlyName": "Paintball Goggles", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "34144f4c-f6ef-49e5-a286-1781e82e6782,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2328, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3076, + "FriendlyName": "Soda Clerk Hat (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,XFCpIdeDXUqeYCIgh4fGtg,sV_ByCug30G1Gw-77KUe1Q,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2329, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2754, + "FriendlyName": "Soda Clerk Hat (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "Wua43ED71UqL71ATtyS0iQ,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2330, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3082, + "FriendlyName": "KO Icon - Tire Track", + "Tooltip": "When you KO someone this will appear over their head.", + "ConsumableItemDesc": "J1WqFNUWo0OBi4LGKPDHWw", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 200, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2331, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 180, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 458, + "FriendlyName": "Paintball Vest (Black)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d8f408f0-78f5-4e8a-b42a-e7b09632d1fd,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 550, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2332, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 495, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3079, + "FriendlyName": "Soda Clerk Uniform (Blue)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,XFCpIdeDXUqeYCIgh4fGtg,MBuxBAIkpkWI26d-arAKQg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2333, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2764, + "FriendlyName": "Soda Clerk Uniform (Red)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "iai6P7dDTUq8S4hqQmY1YA,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 700, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2334, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 630, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 3026, + "FriendlyName": "Popcorn", + "Tooltip": "", + "ConsumableItemDesc": "QRx0aSTT9keMFdAJMQHdTg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 52, + "ItemSetFriendlyName": "Snickety Snacks" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2335, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 406, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf500.json b/Server/RadicadeServer-Config/storefronts/sf500.json new file mode 100644 index 0000000..673c620 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf500.json @@ -0,0 +1,340 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 2011, + "FriendlyName": "Root Beer", + "Tooltip": "A sixer of root beer to share with friends.", + "ConsumableItemDesc": "JfnVXFmilU6ysv-VbTAe3A", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 50, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2336, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 45, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2471, + "FriendlyName": "Salted Pretzels", + "Tooltip": "A great snack to share with friends!", + "ConsumableItemDesc": "InQ25wQMGkG_bvuD5rf2Ag", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 0, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 52, + "ItemSetFriendlyName": "Snickety Snacks" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 75, + "StorefrontSaleData": { + "SalePercent": 25, + "SaleStartDate": "2021-04-26T07:00:00Z", + "SaleEndDate": "2021-04-27T07:00:00Z" + } + } + ], + "PurchasableItemId": 2337, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 67, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2470, + "FriendlyName": "Hawaiian Pizza", + "Tooltip": "A ham and pineapple pizza to share with friends.", + "ConsumableItemDesc": "_jnjYGBcyEWY5Ub4OezXcA", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 10, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 33, + "ItemSetFriendlyName": "Pizza Aplenty" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 95, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2338, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 85, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2326, + "FriendlyName": "Assorted Donuts", + "Tooltip": "A dozen assorted donuts to share with friends.", + "ConsumableItemDesc": "ZuvkidodzkuOfGLDnTOFyg", + "AvatarItemDesc": "", + "AvatarItemType": null, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 110000, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": 34, + "ItemSetFriendlyName": "Donuts by the Dozen" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 100, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2339, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 90, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2563, + "FriendlyName": "Bowling Shirt (Runway)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,GDvaUU10-EKh3pKUQ9tnag,b-IM3eYMJUa40395O5XeLg,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 600, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2340, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 540, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2556, + "FriendlyName": "Bowling Shirt (Lily)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,yrQCLXiqRUSwnuQRYX2Rsw,VO_3cgqQqUOTz30TVUxrFA,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 800, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2341, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 720, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2561, + "FriendlyName": "Bowling Shirt (Flames, Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,032pE8s9Ik-2CImFVDrSqg,5RUG6i2Mx0-55IO-IlhUdQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2342, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2475, + "FriendlyName": "Bowling Shirt (Flames, Red)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "CQuKKNpan0O7rhR1fr14-Q,8xjU4GQ4OkaqGyad-ypwtw,5RUG6i2Mx0-55IO-IlhUdQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1000, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2343, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 500, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf600.json b/Server/RadicadeServer-Config/storefronts/sf600.json new file mode 100644 index 0000000..6ef81b2 --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf600.json @@ -0,0 +1,426 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [ + { + "GiftDrop": { + "GiftDropId": 2858, + "FriendlyName": "Runner Helmet (Orange)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,Euf1h1qsZ02evDsyoHYbPQ,PZHohDVX0U2frX13N4KYDQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2344, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2855, + "FriendlyName": "Runner Helmet (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "459ec62c-67db-4b72-9e29-0ad3ecd0b737,zlqLrGEa8E2WW9ey_szREA,PZHohDVX0U2frX13N4KYDQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 850, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2345, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 765, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2846, + "FriendlyName": "Stunt Shades (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "f684d86f-4a75-4a9a-bc0a-cf58f36c91c5,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-15T21:00:00Z", + "SaleEndDate": "2021-08-16T21:00:00Z" + } + } + ], + "PurchasableItemId": 2346, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2842, + "FriendlyName": "Stunt Shades (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "d57cf97e-b5d2-4e47-8231-a226a161f6e3,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 2000, + "StorefrontSaleData": { + "SalePercent": 50, + "SaleStartDate": "2021-08-15T21:00:00Z", + "SaleEndDate": "2021-08-16T21:00:00Z" + } + } + ], + "PurchasableItemId": 2347, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1800, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2879, + "FriendlyName": "Runner Wrist Guard (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,RfZNDPUok0WyQEa3Ko08ow,rgzG7sfrlkqd9EO13LNmpQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2348, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2852, + "FriendlyName": "Runner Harness (Orange)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,sX0ZZDcCUkyFSJISfvq0vg,ovy_3j_qwE-7mmks1Z47ig,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2349, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2849, + "FriendlyName": "Runner Harness (Blue)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af,RfZNDPUok0WyQEa3Ko08ow,ovy_3j_qwE-7mmks1Z47ig,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 30, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 900, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2350, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 810, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2865, + "FriendlyName": "Stunt Singlet (Yellow)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,,,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2351, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2862, + "FriendlyName": "Stunt Singlet (Pink)", + "Tooltip": "", + "ConsumableItemDesc": "", + "AvatarItemDesc": "17236d76-e3b6-42f6-9179-6fcf0479436f,i1XbjBqTu0CILIuJClWimA,B-sIpWNOfEigMeQF-WVR1g,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 50, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 1500, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2352, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 1350, + "StorefrontSaleData": null + } + ], + "Type": 0 + }, + { + "GiftDrop": { + "GiftDropId": 2876, + "FriendlyName": "Runner Wrist Guard (Green)", + "Tooltip": null, + "ConsumableItemDesc": "", + "AvatarItemDesc": "1beaf288-b5a5-475c-8149-c8f3d1146083,sX0ZZDcCUkyFSJISfvq0vg,rgzG7sfrlkqd9EO13LNmpQ,", + "AvatarItemType": 0, + "EquipmentPrefabName": "", + "EquipmentModificationGuid": "", + "IsQuery": false, + "QueryRedirectContext": null, + "QueryRedirectRarity": null, + "Unique": false, + "SubscribersOnly": false, + "Rarity": 20, + "Context": 0, + "Currency": 0, + "CurrencyType": 0, + "ItemSetId": null, + "ItemSetFriendlyName": "" + }, + "IsFeatured": false, + "NewUntil": null, + "Prices": [ + { + "CurrencyType": 2, + "Price": 450, + "StorefrontSaleData": null + } + ], + "PurchasableItemId": 2353, + "SubscriberPrices": [ + { + "CurrencyType": 2, + "Price": 405, + "StorefrontSaleData": null + } + ], + "Type": 0 + } + ], + "StorefrontType": 600, + "SubscriberDiscountPercent": 10 +} \ No newline at end of file diff --git a/Server/RadicadeServer-Config/storefronts/sf700.json b/Server/RadicadeServer-Config/storefronts/sf700.json new file mode 100644 index 0000000..5d46d1f --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf700.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 700, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf800.json b/Server/RadicadeServer-Config/storefronts/sf800.json new file mode 100644 index 0000000..150016b --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf800.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 800, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RadicadeServer-Config/storefronts/sf900.json b/Server/RadicadeServer-Config/storefronts/sf900.json new file mode 100644 index 0000000..cbf5def --- /dev/null +++ b/Server/RadicadeServer-Config/storefronts/sf900.json @@ -0,0 +1,7 @@ +{ + "NewUntil": null, + "NextUpdate": "2222-02-22T22:22:00Z", + "StoreItems": [], + "StorefrontType": 900, + "SubscriberDiscountPercent": 10 +} diff --git a/Server/RecNet/DB/PlaylistDB.cs b/Server/RecNet/DB/PlaylistDB.cs new file mode 100644 index 0000000..8399c55 --- /dev/null +++ b/Server/RecNet/DB/PlaylistDB.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.DB +{ + public class PlaylistDB + { + [Key] + public long PlaylistId { get; set; } + + public string Keywords { get; set; } = string.Empty; + + public string[] GetKeywords() + { + return Keywords.Split(',', StringSplitOptions.TrimEntries); + } + } + + public class TagDB + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long TagId { get; set; } + + public string Tag { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + } +} diff --git a/Server/RecNet/Enums/Accessibility.cs b/Server/RecNet/Enums/Accessibility.cs new file mode 100644 index 0000000..6b467e1 --- /dev/null +++ b/Server/RecNet/Enums/Accessibility.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Enums +{ + public enum Accessibility + { + Private, + Public, + Unlised + } +} diff --git a/Server/RecNet/Enums/EventResponses.cs b/Server/RecNet/Enums/EventResponses.cs new file mode 100644 index 0000000..91e1699 --- /dev/null +++ b/Server/RecNet/Enums/EventResponses.cs @@ -0,0 +1,11 @@ +namespace RadicadeServer.RecNet.Enums +{ + public enum EventResponses + { + None = -1, + Yes, + Interested, + No, + Pending + } +} diff --git a/Server/RecNet/Enums/InventionTagResult.cs b/Server/RecNet/Enums/InventionTagResult.cs new file mode 100644 index 0000000..ac894ff --- /dev/null +++ b/Server/RecNet/Enums/InventionTagResult.cs @@ -0,0 +1,20 @@ +namespace RadicadeServer.RecNet.Enums +{ + public enum InventionTagResult + { + Success, + TooManyTags, + TagUseRestricted, + InvalidTag, + InappropriateTag, + TagTooLong, + TagNotFound, + TagAlreadyExists, + NoChange, + TagRepeated, + LacksPermission, + RoomDoesNotExist, + InventionDoesNotExist, + RoomPlaylistDoesNotExist + } +} diff --git a/Server/RecNet/Enums/Messages/MessageType.cs b/Server/RecNet/Enums/Messages/MessageType.cs new file mode 100644 index 0000000..a105594 --- /dev/null +++ b/Server/RecNet/Enums/Messages/MessageType.cs @@ -0,0 +1,39 @@ +namespace RadicadeServer.RecNet.Enums.Messages +{ + public enum MessageType + { + GameInvite, + GameInviteDeclined, + GameJoinFailed, + PartyActivitySwitch, + FriendInvite, + VoteToKick, + GameInviteV2, + PartyActivitySwitchV2, + RequestGameInvite = 10, + RequestGameInviteDeclined, + FriendStatusOnline = 20, + TextMessage = 30, + FriendRequestAccepted = 40, + PlayerCheer = 50, + PlayerCheerAnonymous, + RoomCoOwnerAdded = 60, + RoomCoOwnerRemoved, + RoomCoOwnerInvited, + CreatorPublishedNewRoom = 70, + PlayerAttendingEvent = 80, + PlayerEventInvitation, + DEPRECATED_GroupInvitation = 90, + DEPRECATED_PlayerJoinedGroup, + CoachMessage = 100, + NewRoomComments = 110, + PartyUpRequest = 120, + ClubMemberInvited = 200, + ClubModeratorInvited, + ClubCoownerInvited, + VirtualCreatorAnnouncementRoomPublished = 100000, + VirtualCreatorAnnouncementInventionPublished, + VirtualCreatorAnnouncementGeneric, + VirtualCreatorAnnouncementPlayerEventPublished + } +} diff --git a/Server/RecNet/Enums/Messages/NotificationType.cs b/Server/RecNet/Enums/Messages/NotificationType.cs new file mode 100644 index 0000000..d55db7d --- /dev/null +++ b/Server/RecNet/Enums/Messages/NotificationType.cs @@ -0,0 +1,49 @@ +namespace RadicadeServer.RecNet.Enums.Messages +{ + public enum NotificationType + { + RelationshipChanged = 1, + MessageReceived, + MessageDeleted, + PresenceHeartbeatResponse, + RefreshLogin, + Logout, + SubscriptionUpdateProfile = 11, + SubscriptionUpdatePresence, + SubscriptionUpdateGameSession, + SubscriptionUpdateRoom = 15, + SubscriptionUpdateRoomPlaylist, + ModerationQuitGame = 20, + ModerationUpdateRequired, + ModerationKick, + ModerationKickAttemptFailed, + ModerationRoomBan, + ServerMaintenance, + AnnouncementText, + AnnouncementGameUpdate, + GiftPackageReceived = 30, + GiftPackageReceivedImmediate, + GiftPackageRewardSelectionReceived, + ProfileJuniorStatusUpdate = 40, + RelationshipsInvalid = 50, + StorefrontBalanceAdd = 60, + StorefrontBalanceUpdate, + StorefrontBalancePurchase, + ConsumableMappingAdded = 70, + ConsumableMappingRemoved, + PlayerEventCreated = 80, + PlayerEventUpdated, + PlayerEventDeleted, + PlayerEventResponseChanged, + PlayerEventResponseDeleted, + PlayerEventStateChanged, + ChatMessageReceived = 90, + CommunityBoardUpdate = 95, + CommunityBoardAnnouncementUpdate, + InventionModerationStateChanged = 100, + FreeGiftButtonItemsAdded = 110, + LocalRoomKeyCreated = 120, + LocalRoomKeyDeleted + } + +} diff --git a/Server/RecNet/Enums/Messages/Notifications.cs b/Server/RecNet/Enums/Messages/Notifications.cs new file mode 100644 index 0000000..b349d6c --- /dev/null +++ b/Server/RecNet/Enums/Messages/Notifications.cs @@ -0,0 +1,46 @@ +namespace RadicadeServer.RecNet.Enums.Messages +{ + public enum Notifs + { + RelationshipChanged = 1, + MessageReceived, + MessageDeleted, + PresenceHeartbeatResponse, + RefreshLogin, + Logout, + SubscriptionUpdateProfile = 11, + SubscriptionUpdatePresence, + SubscriptionUpdateGameSession, + SubscriptionUpdateRoom = 15, + SubscriptionUpdateRoomPlaylist, + ModerationQuitGame = 20, + ModerationUpdateRequired, + ModerationKick, + ModerationKickAttemptFailed, + ModerationRoomBan, + ServerMaintenance, + GiftPackageReceived = 30, + GiftPackageReceivedImmediate, + GiftPackageRewardSelectionReceived, + ProfileJuniorStatusUpdate = 40, + RelationshipsInvalid = 50, + StorefrontBalanceAdd = 60, + StorefrontBalanceUpdate, + StorefrontBalancePurchase, + ConsumableMappingAdded = 70, + ConsumableMappingRemoved, + PlayerEventCreated = 80, + PlayerEventUpdated, + PlayerEventDeleted, + PlayerEventResponseChanged, + PlayerEventResponseDeleted, + PlayerEventStateChanged, + ChatMessageReceived = 90, + CommunityBoardUpdate = 95, + CommunityBoardAnnouncementUpdate, + InventionModerationStateChanged = 100, + FreeGiftButtonItemsAdded = 110, + LocalRoomKeyCreated = 120, + LocalRoomKeyDeleted + } +} diff --git a/Server/RecNet/Enums/Moderation/CheerCategory.cs b/Server/RecNet/Enums/Moderation/CheerCategory.cs new file mode 100644 index 0000000..ba53dad --- /dev/null +++ b/Server/RecNet/Enums/Moderation/CheerCategory.cs @@ -0,0 +1,15 @@ +namespace RadicadeServer.RecNet.Enums.Moderation +{ + public enum CheerCategory + { + General = 0, + Helpful = 10, + Sportsman = 20, + GreatHost = 30, + Creative = 40, + + RecRoomDeveloper = 9000, + + Credit = 50 + } +} \ No newline at end of file diff --git a/Server/RecNet/Enums/Moderation/ReportCategory.cs b/Server/RecNet/Enums/Moderation/ReportCategory.cs new file mode 100644 index 0000000..0dda393 --- /dev/null +++ b/Server/RecNet/Enums/Moderation/ReportCategory.cs @@ -0,0 +1,24 @@ +namespace RadicadeServer.RecNet.Enums.Moderation +{ + public enum ReportCategory + { + Moderator = -1, + Unknown, + DEPRECATED_MicrophoneAbuse, + Harassment, + Cheating, + DEPRECATED_ImmatureBehavior, + AFK, + Misc, + Underage, + VoteKick = 10, + MisleadingPurchases, + CoC_Underage = 100, + CoC_Sexual, + CoC_Discrimination, + CoC_Trolling, + CoC_NameOrProfile, + InappropriateClothing = 200, + IssuingInaccurateReports = 1000 + } +} diff --git a/Server/RecNet/Enums/Players/Avatar/AvatarItemType.cs b/Server/RecNet/Enums/Players/Avatar/AvatarItemType.cs new file mode 100644 index 0000000..2350486 --- /dev/null +++ b/Server/RecNet/Enums/Players/Avatar/AvatarItemType.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Enums.Players.Avatar +{ + public enum AvatarItemType + { + Outfit, + HairDye + } +} diff --git a/Server/RecNet/Enums/Players/DeviceClass.cs b/Server/RecNet/Enums/Players/DeviceClass.cs new file mode 100644 index 0000000..e44787d --- /dev/null +++ b/Server/RecNet/Enums/Players/DeviceClass.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Enums.Players +{ + public enum DeviceClass + { + Unknown, + VR, + Screen, + Mobile, + VRLow, + Quest2 + } +} diff --git a/Server/RecNet/Enums/Players/JuniorState.cs b/Server/RecNet/Enums/Players/JuniorState.cs new file mode 100644 index 0000000..52aea84 --- /dev/null +++ b/Server/RecNet/Enums/Players/JuniorState.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Enums.Players +{ + public enum JuniorState + { + BasedOnBirthday, + ModerationOverride, + PendingOverride + } +} diff --git a/Server/RecNet/Enums/Players/Platforms.cs b/Server/RecNet/Enums/Players/Platforms.cs new file mode 100644 index 0000000..e1a638a --- /dev/null +++ b/Server/RecNet/Enums/Players/Platforms.cs @@ -0,0 +1,15 @@ +namespace RadicadeServer.RecNet.Enums.Players +{ + public enum Platforms + { + All = -1, + Steam, + Oculus, + PlayStation, + Xbox, + HeadlessBot, + IOS, + GooglePlay, + Standalone = 7 + } +} diff --git a/Server/RecNet/Enums/Players/StatusVisibility.cs b/Server/RecNet/Enums/Players/StatusVisibility.cs new file mode 100644 index 0000000..bf2bc65 --- /dev/null +++ b/Server/RecNet/Enums/Players/StatusVisibility.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Enums.Players +{ + public enum StatusVisibility + { + Public, + FriendsOnly, + FavoriteFriendsOnly, + Offline + } +} diff --git a/Server/RecNet/Enums/Players/VRMovementModes.cs b/Server/RecNet/Enums/Players/VRMovementModes.cs new file mode 100644 index 0000000..697a2ee --- /dev/null +++ b/Server/RecNet/Enums/Players/VRMovementModes.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Enums.Players +{ + public enum VRMovementModes + { + TELEPORT, + WALK + } +} diff --git a/Server/RecNet/Enums/Rooms/JoinMode.cs b/Server/RecNet/Enums/Rooms/JoinMode.cs new file mode 100644 index 0000000..3c1ce07 --- /dev/null +++ b/Server/RecNet/Enums/Rooms/JoinMode.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Enums.Rooms +{ + public enum JoinMode + { + PublicMatchmaking, + PublicNewInstance, + PrivateNewInstance + } +} diff --git a/Server/RecNet/Enums/Rooms/JoinStatus.cs b/Server/RecNet/Enums/Rooms/JoinStatus.cs new file mode 100644 index 0000000..9581e94 --- /dev/null +++ b/Server/RecNet/Enums/Rooms/JoinStatus.cs @@ -0,0 +1,54 @@ +namespace RadicadeServer.RecNet.Enums.Rooms +{ + public enum JoinStatus + { + UnknownError = -1, + Success, + NoSuchGame, + PlayerNotOnline, + InsufficientSpace, + EventNotStarted, + EventAlreadyFinished, + BlockedFromRoom = 7, + JuniorNotAllowed = 11, + Banned, + AlreadyInBestInstance, + InsufficientRelationship, + UpdateRequired = 16, + AlreadyInTargetInstance, + UGCNotAllowed = 19, + NoSuchRoom, + RoomIsNotActive = 22, + RoomBlockedByCreator, + RoomIsPrivate = 25, + RoomInstanceIsPrivate, + DeviceClassNotSupported = 30, + DeviceClassNotSupportedByRoomOwner, + MovementModeNotSupportedByRoomOwner, + EventIsPrivate = 35, + EventIsFull, + RoomInviteExpired = 40, + NoAvailableRegion = 45, + NotorietyTooPoor = 50, + BannedFromRoom = 55, + NoSuchRoomPlaylist = 60, + RoomPlaylistIsNotActive, + RoomPlaylistIsPrivate, + NoSuchClub = 70, + ClubHasNoClubhouse, + ClubIsNotActive = 73, + NotAMemberOfClub, + BannedFromClub, + InstanceJoinNotPermitted, + LevelTooLow, + + ChatPartyInviteNotFound = 78, + ChatPartyInviteModerated, + ChatMessageNotAnInvite, + DeveloperOnly, + RRPlusRequired, + MetaJuniorAccountRestriction, + NotExclusivelyLoggedIn, + AccountDoesNotExist + } +} diff --git a/Server/RecNet/Enums/Rooms/MaxPlayerMode.cs b/Server/RecNet/Enums/Rooms/MaxPlayerMode.cs new file mode 100644 index 0000000..1619d21 --- /dev/null +++ b/Server/RecNet/Enums/Rooms/MaxPlayerMode.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Enums.Rooms +{ + public enum MaxPlayerMode + { + AllSubrooms, + OnlyEntrySubrooms + } +} diff --git a/Server/RecNet/Enums/Rooms/RolePermissions.cs b/Server/RecNet/Enums/Rooms/RolePermissions.cs new file mode 100644 index 0000000..d999ee5 --- /dev/null +++ b/Server/RecNet/Enums/Rooms/RolePermissions.cs @@ -0,0 +1,25 @@ +namespace RadicadeServer.RecNet.Enums.Rooms +{ + public enum RolePermissions + { + INVALID = -1, + CAN_EDIT_ROOM_ROLES = 1, + VOTE_KICK_PERMISSION = 3, + CAN_INVITE = 4, + CAN_TALK = 6, + CAN_PRINT_PHOTOS = 7, + CAN_START_GAMES = 8, + CAN_SELF_REVIVE = 9, + AUTO_ASSIGNED_GAME_ROLES = 11, + CAN_CHANGE_GAME_MODE = 12, + CAN_USE_MAKER_PEN = 13, + CAN_USE_DELETE_ALL_BUTTON = 14, + CAN_SAVE_INVENTIONS = 15, + DISABLE_MIC_AUTO_MUTE = 16, + CAN_END_GAMES_EARLY = 17, + CAN_USE_SHARE_CAM = 18, + DEPRECATED_CAN_EDIT_CIRCUITS = 19, + CAN_SPAWN_INVENTIONS = 20, + CAN_SPAWN_CONSUMABLES = 21 + } +} diff --git a/Server/RecNet/Enums/Rooms/RoleType.cs b/Server/RecNet/Enums/Rooms/RoleType.cs new file mode 100644 index 0000000..b8e1d21 --- /dev/null +++ b/Server/RecNet/Enums/Rooms/RoleType.cs @@ -0,0 +1,13 @@ +namespace RadicadeServer.RecNet.Enums.Rooms +{ + public enum RoleType + { + None, + Banned, + Host = 10, + Moderator = 20, + CoOwner = 30, + TemporaryCoOwner, + Creator = 255 + } +} diff --git a/Server/RecNet/Enums/Rooms/RoomInstanceType.cs b/Server/RecNet/Enums/Rooms/RoomInstanceType.cs new file mode 100644 index 0000000..1395611 --- /dev/null +++ b/Server/RecNet/Enums/Rooms/RoomInstanceType.cs @@ -0,0 +1,14 @@ +namespace RadicadeServer.RecNet.Enums.Rooms +{ + public enum RoomInstanceType + { + Public, + Private, + Dormroom, + Event, + Meetup, + Clubhouse, + + MultiInstanceEvent = 6 + } +} diff --git a/Server/RecNet/Enums/Rooms/RoomState.cs b/Server/RecNet/Enums/Rooms/RoomState.cs new file mode 100644 index 0000000..17b028f --- /dev/null +++ b/Server/RecNet/Enums/Rooms/RoomState.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Enums.Rooms +{ + public enum RoomState + { + Active, + Junior_Pending = 11, + Moderator_Pending = 100, + Moderator_Closed, + Moderator_Banned, + MarkedForDelete = 255 + } +} diff --git a/Server/RecNet/Enums/Rooms/WarningMask.cs b/Server/RecNet/Enums/Rooms/WarningMask.cs new file mode 100644 index 0000000..c4dad6e --- /dev/null +++ b/Server/RecNet/Enums/Rooms/WarningMask.cs @@ -0,0 +1,15 @@ +namespace RadicadeServer.RecNet.Enums.Rooms +{ + [Flags] + public enum WarningMask + { + None, + Scary, + Mature, + FlashingLights = 4, + IntenseMotion = 8, + Violence = 16, + Custom = 32, + Reports = 64 + } +} diff --git a/Server/RecNet/Enums/Store/ConsumableCategories.cs b/Server/RecNet/Enums/Store/ConsumableCategories.cs new file mode 100644 index 0000000..84e6033 --- /dev/null +++ b/Server/RecNet/Enums/Store/ConsumableCategories.cs @@ -0,0 +1,14 @@ +namespace RadicadeServer.RecNet.Enums.Store +{ + public enum ConsumableCategories + { + Invalid = -1, + CameraFilter, + Potion_Head = 2, + Potion_Hand, + Food, + KO_Customization, + Potion_HairDye, + Coupon + } +} diff --git a/Server/RecNet/Enums/Store/CurrencyType.cs b/Server/RecNet/Enums/Store/CurrencyType.cs new file mode 100644 index 0000000..98b9c30 --- /dev/null +++ b/Server/RecNet/Enums/Store/CurrencyType.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Enums.Store +{ + public enum CurrencyType + { + Invalid, + LaserTagTickets, + RecCenterTokens, + LostSkullsGold = 100, + DraculaSilver, + RecRoyale_Season1 = 200 + } +} diff --git a/Server/RecNet/Enums/Store/GiftContext.cs b/Server/RecNet/Enums/Store/GiftContext.cs new file mode 100644 index 0000000..e5f6b9d --- /dev/null +++ b/Server/RecNet/Enums/Store/GiftContext.cs @@ -0,0 +1,123 @@ +namespace RadicadeServer.RecNet.Enums.Store +{ + public enum RewardType + { + FirstActivityOfDay, + DailyChallengesComplete, + PostGameActivity, + PostQuestActivity, + WeeklyCompleteButHaveSkin, + Levelup, + ReferralReward + } + public enum GiftContext + { + None = -1, + Default, + First_Activity, + Game_Drop, + All_Daily_Challenges_Complete, + All_Weekly_Challenge_Complete, + Daily_Challenge_Complete, + Weekly_Challenge_Complete, + Unassigned_Equipment = 10, + Unassigned_Avatar, + Unassigned_Consumable, + Reacquisition = 20, + Membership, + NUX_TokensAndDressUp = 30, + NUX_Experiment1, + NUX_Experiment2, + NUX_Experiment3, + NUX_Experiment4, + NUX_Experiment5, + GameRewards = 50, + GameRewards_Tokens, + LevelUp = 100, + Purchased_Gift_A = 500, + Purchased_Gift_B, + Purchased_Gift_C, + Purchased_Gift_D, + Holiday = 1000, + Contest, + Promotion, + SubscribersOnly, + Deprecated = 1100, + RecRoyale = 1200, + + + Friendotron_Gift = 1300, + DEPRECATED_Paintball_ClearCut = 2000, + DEPRECATED_Paintball_Homestead, + DEPRECATED_Paintball_Quarry, + DEPRECATED_Paintball_River, + DEPRECATED_Paintball_Dam, + DEPRECATED_Paintball_DriveIn, + Paintball_ClearCut = 2010, + Paintball_Homestead, + Paintball_Quarry, + Paintball_River, + Paintball_Dam, + Paintball_DriveIn, + DEPRECATED_Discgolf_Propulsion = 3000, + DEPRECATED_Discgolf_Lake, + Discgolf_Propulsion = 3010, + Discgolf_Lake, + Discgolf_Mode_CoopCatch = 3500, + Quest_Goblin_A = 4000, + Quest_Goblin_B, + Quest_Goblin_C, + Quest_Goblin_S, + Quest_Goblin_Consumable, + Quest_Cauldron_A = 4010, + Quest_Cauldron_B, + Quest_Cauldron_C, + Quest_Cauldron_S, + Quest_Cauldron_Consumable, + Quest_Pirate1_A = 4100, + Quest_Pirate1_B, + Quest_Pirate1_C, + Quest_Pirate1_S, + Quest_Pirate1_X, + Quest_Pirate1_Consumable, + Quest_Dracula1_A = 4200, + Quest_Dracula1_B, + Quest_Dracula1_C, + Quest_Dracula1_S, + Quest_Dracula1_X, + Quest_Dracula1_Consumable, + Quest_Dracula1_SS, + Quest_SciFi_A = 4500, + Quest_SciFi_B, + Quest_SciFi_C, + Quest_SciFi_S, + Quest_Scifi_Consumable, + DEPRECATED_Charades = 5000, + Charades, + DEPRECATED_Soccer = 6000, + Soccer, + DEPRECATED_Paddleball = 7000, + Paddleball, + DEPRECATED_Dodgeball = 8000, + Dodgeball, + DEPRECATED_Lasertag = 9000, + Lasertag, + DEPRECATED_Bowling = 10000, + Bowling, + StuntRunner_TheMainEvent_A = 11000, + StuntRunner_TheMainEvent_B, + StuntRunner_TheMainEvent_C, + StuntRunner_TheMainEvent_D, + StuntRunner_TheMainEvent_S, + StuntRunner_TheMainEvent_X, + StuntRunner_TheMainEvent_Consumable, + StuntRunner_TheMainEvent_SS, + Store_LaserTag = 100000, + Store_RecCenter = 100010, + Consumable = 110000, + Token = 110100, + Punchcard_Challenge_Complete = 110200, + All_Punchcard_Challenges_Complete, + Commerce_Purchase = 200000 + } +} diff --git a/Server/RecNet/Enums/Store/Rarity.cs b/Server/RecNet/Enums/Store/Rarity.cs new file mode 100644 index 0000000..d598da7 --- /dev/null +++ b/Server/RecNet/Enums/Store/Rarity.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Enums.Store +{ + public enum Rarity + { + None = -1, + Common, + Uncommon = 10, + Rare = 20, + Epic = 30, + Legendary = 50 + } +} diff --git a/Server/RecNet/Enums/Store/Referrals.cs b/Server/RecNet/Enums/Store/Referrals.cs new file mode 100644 index 0000000..891d28e --- /dev/null +++ b/Server/RecNet/Enums/Store/Referrals.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Enums.Store +{ + public enum ReferralStatus + { + Locked, + Unlocked, + Claimed + } +} diff --git a/Server/RecNet/Enums/Store/SKU/CampusCard.cs b/Server/RecNet/Enums/Store/SKU/CampusCard.cs new file mode 100644 index 0000000..8d7673b --- /dev/null +++ b/Server/RecNet/Enums/Store/SKU/CampusCard.cs @@ -0,0 +1,27 @@ +namespace RadicadeServer.RecNet.Enums.Store.SKU +{ + public enum CampusCardStatus + { + Started, + Reactivated, + AutoRenewed, + UserCancelledAutoRenewals, + UserReactivatedAutoRenewalsBeforeExpiration = 10, + ManuallyRenewed = 5, + ExpiredAutoRenewalFailed = 4, + Expired = 6, + Upgrade, + Downgrade, + Rollback + } + public enum CampusCardLevel + { + Gold, + Platinum + } + public enum CampusCardPeriod + { + Month, + Year + } +} diff --git a/Server/RecNet/Enums/Store/SKU/PurchaseType.cs b/Server/RecNet/Enums/Store/SKU/PurchaseType.cs new file mode 100644 index 0000000..9d1887c --- /dev/null +++ b/Server/RecNet/Enums/Store/SKU/PurchaseType.cs @@ -0,0 +1,27 @@ +namespace RadicadeServer.RecNet.Enums.Store.SKU +{ + public enum PurchaseType + { + NonPurchasedNotUsableInP2P = -2, + + NonPurchasedDefault, + + SteamPurchased, + + OculusPurchased, + + PlayStationPurchased, + + MicrosoftPurchased, + + IOSPurchased = 5, + + [Obsolete] + PlayStationNonPurchasedP2P = 100, + + [Obsolete] + NonPlayStationNonPurchasedP2P, + + NonPurchasedEarnedByP2P = 1000 + } +} diff --git a/Server/RecNet/Enums/Store/StorefrontBalanceAddTypes.cs b/Server/RecNet/Enums/Store/StorefrontBalanceAddTypes.cs new file mode 100644 index 0000000..888ff3c --- /dev/null +++ b/Server/RecNet/Enums/Store/StorefrontBalanceAddTypes.cs @@ -0,0 +1,28 @@ +namespace RadicadeServer.RecNet.Enums.Store +{ + public enum StorefrontBalanceAddTypes + { + Invalid, + DirectBalanceWithMultiplier, + FromGiftBox, + NUXChallenge = 10, + AllNUXChallenges, + DailyChallenge = 100, + AllDailyChallenges, + FinishActivity = 200, + RecRoyaleMatchFinished = 250, + ChecklistCredit = 303, + WonGame = 1000, + LostGame, + WonGameRateLimited, + WonGamePartial, + LevelUp = 1100, + Registered = 1200, + CreatorReward = 1300, + CommercePurchase = 1400, + CommercePurchaseRevoked, + Manual_Refund = 2000, + Manual_Thanks = 2010, + Manual_Apology = 2020 + } +} diff --git a/Server/RecNet/Enums/Store/StorefrontTypes.cs b/Server/RecNet/Enums/Store/StorefrontTypes.cs new file mode 100644 index 0000000..10c9b9a --- /dev/null +++ b/Server/RecNet/Enums/Store/StorefrontTypes.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Enums.Store +{ + + + + + + +} diff --git a/Server/RecNet/Enums/TagType.cs b/Server/RecNet/Enums/TagType.cs new file mode 100644 index 0000000..3ee5f0e --- /dev/null +++ b/Server/RecNet/Enums/TagType.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Enums +{ + public enum TagType + { + General, + Auto, + AGOnly, + Banned + } +} diff --git a/Server/RecNet/Enums/UpdateResponseTypes.cs b/Server/RecNet/Enums/UpdateResponseTypes.cs new file mode 100644 index 0000000..f08a0f3 --- /dev/null +++ b/Server/RecNet/Enums/UpdateResponseTypes.cs @@ -0,0 +1,13 @@ +namespace RadicadeServer.RecNet.Enums +{ + public enum UpdateResponseTypes + { + OK = 0, + TooManyRequests = 1, + NotEnoughCredit = 2, + AlreadyOwned = 3, + NoItemAvailable = 4, + CouponNotApplicable = 5, + RequestedPriceDoesNotMatch = 6 + } +} diff --git a/Server/RecNet/Generators.cs b/Server/RecNet/Generators.cs new file mode 100644 index 0000000..0dfffcc --- /dev/null +++ b/Server/RecNet/Generators.cs @@ -0,0 +1,87 @@ +namespace RadicadeServer.RecNet +{ + public class Generators + { + public readonly static string[] Adjectives = [ + "Brilliant", + "Calm", + "Curious", + "Delightful", + "Elegant", + "Fierce", + "Gentle", + "Honest", + "Imaginative", + "Joyful", + "Keen", + "Lively", + "Mysterious", + "Nimble", + "Optimistic", + "Playful", + "Quiet", + "Resilient", + "Thoughtful", + "Vivid", + "Adventurous", + "Bold", + "Charming", + "Confident", + "Creative", + "Daring", + "Energetic", + "Friendly", + "Graceful", + "Humble", + "Insightful", + "Loyal", + "Motivated", + "Patient", + "Reliable", + "Smart", + "Strong", + "Versatile", + "Warm", + "Wise" + ]; + public readonly static string[] Animals = [ + "Bear", + "Cat", + "Dolphin", + "Eagle", + "Fox", + "Giraffe", + "Horse", + "Kangaroo", + "Lion", + "Monkey", + "Narwhal", + "Owl", + "Panda", + "Rabbit", + "Shark", + "Tiger", + "Vulture", + "Wolf", + "Zebra", + "Alligator", + "Cheetah", + "Elephant", + "Falcon", + "Goat", + "Koala", + "Leopard", + "Penguin", + "Raccoon", + "Seal", + "Turtle" + ]; + + public static string GenerateUsername() + { + string adjective = Adjectives[Random.Shared.Next(0, Adjectives.Length - 1)]; + string animal = Animals[Random.Shared.Next(0, Animals.Length - 1)]; + return adjective + animal + Random.Shared.Next(0, 9999).ToString(); + } + } +} diff --git a/Server/RecNet/GenericData.cs b/Server/RecNet/GenericData.cs new file mode 100644 index 0000000..aa4a6df --- /dev/null +++ b/Server/RecNet/GenericData.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet +{ + public class GenericData + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long AccountId { get; set; } + public string Data { get; set; } = null!; + } +} diff --git a/Server/RecNet/ItemTag.cs b/Server/RecNet/ItemTag.cs new file mode 100644 index 0000000..04ca4e0 --- /dev/null +++ b/Server/RecNet/ItemTag.cs @@ -0,0 +1,10 @@ +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet +{ + public class ItemTag + { + public string Tag { get; set; } = null!; + public TagType Type { get; set; } + } +} diff --git a/Server/RecNet/Maintenance/ServerMaintenance.cs b/Server/RecNet/Maintenance/ServerMaintenance.cs new file mode 100644 index 0000000..785a143 --- /dev/null +++ b/Server/RecNet/Maintenance/ServerMaintenance.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Maintenance +{ + public class ServerMaintenance + { + public int StartsInMinutes { get; set; } + } +} diff --git a/Server/RecNet/Responses/AllEvents.cs b/Server/RecNet/Responses/AllEvents.cs new file mode 100644 index 0000000..3d69e2e --- /dev/null +++ b/Server/RecNet/Responses/AllEvents.cs @@ -0,0 +1,10 @@ +using RadicadeServer.RecNet.Events; + +namespace RadicadeServer.RecNet.Responses +{ + public class AllEvents + { + public List Created { get; set; } = new(); + public List Responses { get; set; } = new(); + } +} diff --git a/Server/RecNet/Responses/AuthResponse.cs b/Server/RecNet/Responses/AuthResponse.cs new file mode 100644 index 0000000..97557d7 --- /dev/null +++ b/Server/RecNet/Responses/AuthResponse.cs @@ -0,0 +1,26 @@ +namespace RadicadeServer.RecNet.Responses +{ + public class AuthResponse + { + public string error { get; set; } = string.Empty; + public string error_description { get; set; } = string.Empty; + public string access_token { get; set; } = null!; + public string refresh_token { get; set; } = null!; + public string key { get; set; } = null!; + + + + + + + + public string eac_nonce { get; set; } = null!; + + public AuthResponse(string Error, string ErrorDescription) + { + error = Error; + error_description = ErrorDescription; + } + public AuthResponse() {} + } +} diff --git a/Server/RecNet/Responses/BalanceResponse.cs b/Server/RecNet/Responses/BalanceResponse.cs new file mode 100644 index 0000000..55c1bdd --- /dev/null +++ b/Server/RecNet/Responses/BalanceResponse.cs @@ -0,0 +1,12 @@ +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Responses +{ + public class BalanceResponse + { + public long Balance { get; set; } + public CurrencyType CurrencyType { get; set; } + + public int BalanceType { get; set; } = 0; + } +} diff --git a/Server/RecNet/Responses/ClientAction.cs b/Server/RecNet/Responses/ClientAction.cs new file mode 100644 index 0000000..0374697 --- /dev/null +++ b/Server/RecNet/Responses/ClientAction.cs @@ -0,0 +1,13 @@ +namespace RadicadeServer.RecNet.Responses +{ + public class ClientAction + { + public bool ALOCDGMHFKA { get; set; } + + public string FBILGKFKFJE { get; set; } = string.Empty; + + public static ClientAction Ok() => new() { ALOCDGMHFKA = true }; + + public static ClientAction Failure(string why) => new() { ALOCDGMHFKA = false, FBILGKFKFJE = why }; + } +} diff --git a/Server/RecNet/Responses/Failure.cs b/Server/RecNet/Responses/Failure.cs new file mode 100644 index 0000000..9730fa2 --- /dev/null +++ b/Server/RecNet/Responses/Failure.cs @@ -0,0 +1,15 @@ +namespace RadicadeServer.RecNet.Responses +{ + public class Failure + { + public bool success { get; set; } = false; + + public string error { get; set; } = string.Empty; + + public Failure(string why) + { + success = false; + error = string.IsNullOrWhiteSpace(why) ? "request failed" : why; + } + } +} diff --git a/Server/RecNet/Responses/MatchResponse.cs b/Server/RecNet/Responses/MatchResponse.cs new file mode 100644 index 0000000..cbe8e88 --- /dev/null +++ b/Server/RecNet/Responses/MatchResponse.cs @@ -0,0 +1,26 @@ +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.RecNet.Responses +{ + public class MatchResponse + { + public JoinStatus errorCode { get; set; } + public RoomInstance? roomInstance { get; set; } + + public static MatchResponse Error(JoinStatus status) + { + return new MatchResponse { errorCode = status, roomInstance = null }; + } + public MatchResponse() + { + + } + public MatchResponse(JoinStatus status, RoomInstance instance) + { + this.errorCode = status; + this.roomInstance = instance; + } + } +} diff --git a/Server/RecNet/Responses/ProgressionResponse.cs b/Server/RecNet/Responses/ProgressionResponse.cs new file mode 100644 index 0000000..9efb9fe --- /dev/null +++ b/Server/RecNet/Responses/ProgressionResponse.cs @@ -0,0 +1,22 @@ +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.Utils; + +namespace RadicadeServer.RecNet.Responses +{ + public class ProgressionResponse + { + public long Id { get; set; } + public long PlayerId { get; set; } + public int Level { get; set; } + public int XP { get; set; } + + public ProgressionResponse() {} + public ProgressionResponse(ProgressionData data) { + this.Id = data.PlayerId; + this.PlayerId = data.PlayerId; + this.Level = LevelUpGiftService.DisplayLevel(data.Level); + this.XP = LevelUpGiftService.XpIntoCurrentLevel(data); + } + } +} diff --git a/Server/RecNet/Responses/Success.cs b/Server/RecNet/Responses/Success.cs new file mode 100644 index 0000000..7bfbfe5 --- /dev/null +++ b/Server/RecNet/Responses/Success.cs @@ -0,0 +1,20 @@ +namespace RadicadeServer.RecNet.Responses +{ + public class Success + { + internal static readonly Success Ok = new Success(true, null, null); + internal static Success Error(string why) => new Success(false, why, null); + + public bool success { get; set; } + + public string error { get; set; } = string.Empty; + + public object? value { get; set; } + + public Success(bool success, string? error, object? value) { + this.success = success; + this.error = error ?? string.Empty; + this.value = value; + } + } +} diff --git a/Server/RecNet/Responses/TagResponse.cs b/Server/RecNet/Responses/TagResponse.cs new file mode 100644 index 0000000..1a4b9d1 --- /dev/null +++ b/Server/RecNet/Responses/TagResponse.cs @@ -0,0 +1,15 @@ +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.Responses +{ + public class TagResponse + { + public int Results { get { return Tags.Count; } } + public List Tags { get; set; } = null!; + } + public class InventionTagResponse + { + public InventionTagResult Result { get; set; } + public List Tags { get; set; } = new(); + } +} diff --git a/Server/Relationships/RealRelationship.cs b/Server/Relationships/RealRelationship.cs new file mode 100644 index 0000000..1fd19de --- /dev/null +++ b/Server/Relationships/RealRelationship.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Relationships +{ + public class RealRelationship + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long RelationshipId { get; set; } + public long AccountId { get; set; } + public long PlayerId { get; set; } + public RelationshipType RelationshipType { get; set; } + public ReciprocalStatus Muted { get; set; } + public ReciprocalStatus Ignored { get; set; } + public ReciprocalStatus Favorited { get; set; } + } +} diff --git a/Server/Relationships/ReciprocalStatus.cs b/Server/Relationships/ReciprocalStatus.cs new file mode 100644 index 0000000..e26257a --- /dev/null +++ b/Server/Relationships/ReciprocalStatus.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Relationships +{ + public enum ReciprocalStatus + { + None, + Local, + Remote, + Mutual + } +} diff --git a/Server/Relationships/Relationship.cs b/Server/Relationships/Relationship.cs new file mode 100644 index 0000000..237874f --- /dev/null +++ b/Server/Relationships/Relationship.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Relationships +{ + public class Relationship + { + [JsonPropertyName("Id")] + public long Id { get; set; } + + [JsonPropertyName("PlayerID")] + public long PlayerID { get; set; } + + [JsonPropertyName("RelationshipType")] + public RelationshipType RelationshipType { get; set; } + + [JsonPropertyName("Favorited")] + public ReciprocalStatus Favorited { get; set; } + + [JsonPropertyName("Muted")] + public ReciprocalStatus Muted { get; set; } + + [JsonPropertyName("Ignored")] + public ReciprocalStatus Ignored { get; set; } + + public Relationship() { } + + public Relationship(RealRelationship realRelationship) + { + Id = realRelationship.RelationshipId; + PlayerID = realRelationship.PlayerId; + RelationshipType = realRelationship.RelationshipType; + Muted = realRelationship.Muted; + Ignored = realRelationship.Ignored; + Favorited = realRelationship.Favorited; + } + } +} diff --git a/Server/Relationships/RelationshipType.cs b/Server/Relationships/RelationshipType.cs new file mode 100644 index 0000000..ffd229c --- /dev/null +++ b/Server/Relationships/RelationshipType.cs @@ -0,0 +1,10 @@ +namespace RadicadeServer.RecNet.Relationships +{ + public enum RelationshipType + { + None, + FriendRequestSent, + FriendRequestReceived, + Friend + } +} diff --git a/Server/Relationships/RelationshipsShared.cs b/Server/Relationships/RelationshipsShared.cs new file mode 100644 index 0000000..8787da9 --- /dev/null +++ b/Server/Relationships/RelationshipsShared.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Runtime.CompilerServices; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Relationships; + +namespace RadicadeServer.API.Relationships +{ + public static class RelationshipsShared + { + internal static IDbContextFactory DbFactory = null!; + + internal static async Task GetOrCreate(RecDBContext db, long accountId, long playerId) + { + var rel = await db.Relationships.FirstOrDefaultAsync(x => x.AccountId == accountId && x.PlayerId == playerId); + if (rel == null) + { + rel = new RealRelationship { AccountId = accountId, PlayerId = playerId }; + await db.Relationships.AddAsync(rel); + } + + await db.SaveChangesAsync(); + + return rel; + } + + internal static void SetReciprocalStatus( + RealRelationship mine, RealRelationship theirs, + Func getter, + Action setter, + bool enabling) + { + if (enabling) + { + var theirStatus = getter(theirs); + bool theyHaveIt = theirStatus == ReciprocalStatus.Local || theirStatus == ReciprocalStatus.Mutual; + setter(mine, theyHaveIt ? ReciprocalStatus.Mutual : ReciprocalStatus.Local); + setter(theirs, theyHaveIt ? ReciprocalStatus.Mutual : ReciprocalStatus.Remote); + } + else + { + var theirStatus = getter(theirs); + setter(mine, ReciprocalStatus.None); + setter(theirs, theirStatus == ReciprocalStatus.Mutual ? ReciprocalStatus.Remote : ReciprocalStatus.None); + } + } + } +} diff --git a/Server/Relationships/RelationshipsV1.cs b/Server/Relationships/RelationshipsV1.cs new file mode 100644 index 0000000..69f5b95 --- /dev/null +++ b/Server/Relationships/RelationshipsV1.cs @@ -0,0 +1,278 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Text.Json.Nodes; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Relationships; + +namespace RadicadeServer.API.Relationships.V1; + +[Route("api/relationships/v1")] +public class RelationshipsV1(RecDBContext db) : Controller +{ + + [HttpPost("bulkignoreplatformusers")] + public IActionResult BulkIgnorePlatformUsers() + { + return Ok(Array.Empty()); + } + + [HttpPost("~/api/relationships/sendfriendintroductions"), Authorize] + public async Task SendFriendIntroductions() + { + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(); + + var (toPlayerIds, aboutPlayerId) = await ReadIntroductionFieldsAsync(); + aboutPlayerId = aboutPlayerId > 0 ? aboutPlayerId : userId.Value; + + _ = toPlayerIds; + return Ok(new OkResponse { Success = true }); + } + + private async Task<(List ToPlayerIds, long AboutPlayerId)> ReadIntroductionFieldsAsync() + { + var toPlayerIds = new List(); + long aboutPlayerId = 0; + + void AddIds(JsonNode? node) + { + if (node is JsonValue value && long.TryParse(value.ToString(), out var single)) + { + toPlayerIds.Add(single); + return; + } + if (node is not JsonArray array) return; + foreach (var entry in array) + if (long.TryParse(entry?.ToString(), out var id)) + toPlayerIds.Add(id); + } + + void ReadObject(JsonObject body) + { + foreach (var pair in body) + { + if (pair.Key.Equals("ToPlayerIds", StringComparison.OrdinalIgnoreCase) || + pair.Key.Equals("PlayerIds", StringComparison.OrdinalIgnoreCase) || + pair.Key.Equals("PlayerId", StringComparison.OrdinalIgnoreCase) || + pair.Key.Equals("Id", StringComparison.OrdinalIgnoreCase)) + AddIds(pair.Value); + else if (pair.Key.Equals("AboutPlayerId", StringComparison.OrdinalIgnoreCase) || + pair.Key.Equals("AccountId", StringComparison.OrdinalIgnoreCase)) + long.TryParse(pair.Value?.ToString(), out aboutPlayerId); + } + } + + var request = Request; + if (request != null) + { + foreach (var key in new[] { "toPlayerIds", "ToPlayerIds", "playerIds", "PlayerIds", "playerId", "PlayerId", "id", "Id" }) + if (request.Query.TryGetValue(key, out var query)) + foreach (var raw in query) + if (long.TryParse(raw, out var queryId)) + toPlayerIds.Add(queryId); + + foreach (var key in new[] { "aboutPlayerId", "AboutPlayerId", "accountId", "AccountId" }) + if (aboutPlayerId <= 0 && request.Query.TryGetValue(key, out var queryAbout) && + long.TryParse(queryAbout.FirstOrDefault(), out var parsedAbout)) + aboutPlayerId = parsedAbout; + + if (request.ContentLength is > 0 && request.Body != null) + { + try + { + request.EnableBuffering(); + var node = await JsonNode.ParseAsync(request.Body); + if (node is JsonObject bodyObject) ReadObject(bodyObject); + else AddIds(node); + request.Body.Position = 0; + } + catch + { + try { request.Body.Position = 0; } catch { } + } + } + } + + return (toPlayerIds, aboutPlayerId); + } + + [HttpGet("relationshipwith/{playerId:long}"), Authorize] + public async Task RelationshipWith(long playerId) + { + var userId = User.GetUserId(); + if (userId == null) + return Unauthorized(); + + var row = await db.Relationships.AsNoTracking() + .FirstOrDefaultAsync(x => x.AccountId == userId && x.PlayerId == playerId); + if (row != null) + return Ok(new Relationship(row)); + + return Ok(new Relationship(new RealRelationship { AccountId = (long)userId, PlayerId = playerId })); + } + + [HttpGet("mutualfriends"), Authorize] + [HttpGet("~/api/relationships/mutualfriends"), Authorize] + public async Task> MutualFriends([FromQuery] long id) + { + var userId = User.GetUserId(); + if (userId == null || id <= 0 || id == userId) + return new List(); + + var mine = await db.Relationships.AsNoTracking() + .Where(x => x.AccountId == userId && x.RelationshipType == RelationshipType.Friend) + .Select(x => x.PlayerId) + .ToListAsync(); + var theirs = await db.Relationships.AsNoTracking() + .Where(x => x.AccountId == id && x.RelationshipType == RelationshipType.Friend) + .Select(x => x.PlayerId) + .ToListAsync(); + + var mutual = mine.Intersect(theirs).OrderBy(x => x).Take(50).ToList(); + if (mutual.Count == 0) + return new List(); + + var accounts = await db.Profiles.AsNoTracking() + .Where(p => mutual.Contains(p.accountId)) + .ToDictionaryAsync(p => p.accountId); + + return mutual + .Where(accounts.ContainsKey) + .Select(accountId => (object)new + { + AccountId = accountId, + Username = accounts[accountId].username, + DisplayName = accounts[accountId].displayName, + ProfileImage = accounts[accountId].profileImage ?? "" + }) + .ToList(); + } + + [HttpGet("favorite"), Authorize] + [HttpPost("favorite"), Authorize] + [HttpGet("favorite/{id:long}"), Authorize] + [HttpPost("favorite/{id:long}"), Authorize] + public async Task Favorite() + => await ToggleFlag(await ReadTargetPlayerIdAsync(), r => r.Favorited, (r, v) => r.Favorited = v, true); + + [HttpGet("unfavorite"), Authorize] + [HttpPost("unfavorite"), Authorize] + [HttpGet("unfavorite/{id:long}"), Authorize] + [HttpPost("unfavorite/{id:long}"), Authorize] + public async Task Unfavorite() + => await ToggleFlag(await ReadTargetPlayerIdAsync(), r => r.Favorited, (r, v) => r.Favorited = v, false); + + private static readonly string[] TargetIdKeys = + { + "id", "Id", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId", + "accountId", "AccountId", "otherPlayerId", "OtherPlayerId" + }; + + private async Task ReadTargetPlayerIdAsync() + { + var routeData = RouteData; + if (routeData != null && + routeData.Values.TryGetValue("id", out var route) && + long.TryParse(route?.ToString(), out var routeId) && routeId > 0) + return routeId; + + var request = Request; + if (request == null) return 0; + + foreach (var key in TargetIdKeys) + if (request.Query.TryGetValue(key, out var query) && + long.TryParse(query.FirstOrDefault(), out var queryId) && queryId > 0) + return queryId; + + if (request.ContentLength is > 0 && request.Body != null) + { + try + { + request.EnableBuffering(); + if (await JsonNode.ParseAsync(request.Body) is JsonObject body) + foreach (var pair in body) + if (TargetIdKeys.Contains(pair.Key, StringComparer.OrdinalIgnoreCase) && + long.TryParse(pair.Value?.ToString(), out var bodyId) && bodyId > 0) + { + request.Body.Position = 0; + return bodyId; + } + + request.Body.Position = 0; + } + catch + { + try { request.Body.Position = 0; } catch { } + } + } + + if (request.HasFormContentType) + { + var form = await request.ReadFormAsync(); + foreach (var key in TargetIdKeys) + if (form.TryGetValue(key, out var formValue) && + long.TryParse(formValue.FirstOrDefault(), out var formId) && formId > 0) + return formId; + } + + return 0; + } + + [HttpPost("mute"), Authorize] + [HttpPost("mute/{id:long}"), Authorize] + [HttpGet("mute"), Authorize] + [HttpGet("mute/{id:long}"), Authorize] + public async Task Mute() + => await ToggleFlagActionResult(await ReadTargetPlayerIdAsync(), r => r.Muted, (r, v) => r.Muted = v, true); + + [HttpPost("unmute"), Authorize] + [HttpPost("unmute/{id:long}"), Authorize] + [HttpGet("unmute"), Authorize] + [HttpGet("unmute/{id:long}"), Authorize] + public async Task Unmute() + => await ToggleFlagActionResult(await ReadTargetPlayerIdAsync(), r => r.Muted, (r, v) => r.Muted = v, false); + + [HttpPost("ignore"), Authorize] + [HttpPost("ignore/{id:long}"), Authorize] + [HttpGet("ignore"), Authorize] + [HttpGet("ignore/{id:long}"), Authorize] + public async Task Ignore() + => await ToggleFlagActionResult(await ReadTargetPlayerIdAsync(), r => r.Ignored, (r, v) => r.Ignored = v, true); + + [HttpPost("unignore"), Authorize] + [HttpPost("unignore/{id:long}"), Authorize] + [HttpGet("unignore"), Authorize] + [HttpGet("unignore/{id:long}"), Authorize] + public async Task Unignore() + => await ToggleFlagActionResult(await ReadTargetPlayerIdAsync(), r => r.Ignored, (r, v) => r.Ignored = v, false); + + private async Task ToggleFlag(long id, Func getter, Action setter, bool enabling) + { + var userId = (long)User.GetUserId()!; + + var mine = await RelationshipsShared.GetOrCreate(db, userId, id); + var theirs = await RelationshipsShared.GetOrCreate(db, id, userId); + + RelationshipsShared.SetReciprocalStatus(mine, theirs, getter, setter, enabling); + await db.SaveChangesAsync(); + + await Notify.SendNotify(userId, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(mine) }); + await Notify.SendNotify(id, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(theirs) }); + + return new Relationship(mine); + } + + private async Task ToggleFlagActionResult(long id, Func getter, Action setter, bool enabling) + { + if (id <= 0) + return BadRequest(new { error = "invalid player id" }); + return Ok(await ToggleFlag(id, getter, setter, enabling)); + } +} diff --git a/Server/Relationships/RelationshipsV2.cs b/Server/Relationships/RelationshipsV2.cs new file mode 100644 index 0000000..d4a7a05 --- /dev/null +++ b/Server/Relationships/RelationshipsV2.cs @@ -0,0 +1,152 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Relationships; + +namespace RadicadeServer.API.Relationships.V2; + +[Route("api/relationships/v2")] +public class RelationshipsV2(RecDBContext db) : Controller +{ + [HttpGet("get"), Authorize] + public async Task> GetRelationships() + { + var userId = User.GetUserId(); + + var rows = await db.Relationships.AsNoTracking() + .Where(x => x.AccountId == userId) + .ToListAsync(); + + return rows.Select(relationship => new Relationship(relationship)).ToList(); + } + + [HttpGet("friends"), Authorize] + public async Task> GetFriends() + { + var userId = User.GetUserId(); + + var rows = await db.Relationships.AsNoTracking() + .Where(x => x.AccountId == userId && x.RelationshipType == RelationshipType.Friend) + .OrderBy(x => x.RelationshipId) + .ToListAsync(); + + return rows.Select(relationship => new Relationship(relationship)).ToList(); + } + + [HttpGet("sendfriendrequest"), Authorize] + [HttpPost("sendfriendrequest"), Authorize] + public async Task SendFriendRequest(long id) + { + var userId = User.GetUserId(); + if (userId == null || id <= 0 || id == userId) + return BadRequest(new { error = "invalid player id" }); + + var mine = await RelationshipsShared.GetOrCreate(db, (long)userId, id); + var theirs = await RelationshipsShared.GetOrCreate(db, id, (long)userId); + + if (mine.RelationshipType != RelationshipType.None) + return Ok(new Relationship(mine)); + + mine.RelationshipType = RelationshipType.FriendRequestSent; + theirs.RelationshipType = RelationshipType.FriendRequestReceived; + await db.SaveChangesAsync(); + + await Notify.SendNotify((long)userId, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(mine) }); + await Notify.SendNotify(id, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(theirs) }); + + await Notify.SendMessage(id, new Message { FromPlayerId = (long)userId, Type = MessageType.FriendInvite }); + + return Ok(new Relationship(mine)); + } + + [HttpGet("acceptfriendrequest"), Authorize] + [HttpPost("acceptfriendrequest"), Authorize] + public async Task AcceptFriendRequest(long id) + { + var userId = User.GetUserId(); + if (userId == null || id <= 0 || id == userId) + return BadRequest(new { error = "invalid player id" }); + + var mine = await db.Relationships + .FirstOrDefaultAsync(x => x.AccountId == userId && x.PlayerId == id); + var theirs = await db.Relationships + .FirstOrDefaultAsync(x => x.AccountId == id && x.PlayerId == userId); + + if (mine == null || theirs == null || + mine.RelationshipType != RelationshipType.FriendRequestReceived || + theirs.RelationshipType != RelationshipType.FriendRequestSent) + { + return Ok(mine != null ? new Relationship(mine) : new Relationship()); + } + + mine.RelationshipType = RelationshipType.Friend; + theirs.RelationshipType = RelationshipType.Friend; + await db.SaveChangesAsync(); + + await Notify.SendNotify((long)userId, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(mine) }); + await Notify.SendNotify(id, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(theirs) }); + + await Notify.SendMessage(id, new Message { FromPlayerId = (long)userId, Type = MessageType.FriendRequestAccepted }); + + return Ok(new Relationship(mine)); + } + + [HttpGet("addfriend"), Authorize] + [HttpPost("addfriend"), Authorize] + public async Task AddFriend(long id) + { + var userId = User.GetUserId(); + if (userId == null || id <= 0 || id == userId) + return BadRequest(new { error = "invalid player id" }); + + var mine = await RelationshipsShared.GetOrCreate(db, (long)userId, id); + var theirs = await RelationshipsShared.GetOrCreate(db, id, (long)userId); + + if (mine.RelationshipType == RelationshipType.FriendRequestReceived || theirs.RelationshipType == RelationshipType.FriendRequestReceived) + { + mine.RelationshipType = RelationshipType.Friend; + theirs.RelationshipType = RelationshipType.Friend; + } + else if (mine.RelationshipType == RelationshipType.None && theirs.RelationshipType == RelationshipType.None) + { + mine.RelationshipType = RelationshipType.FriendRequestSent; + theirs.RelationshipType = RelationshipType.FriendRequestReceived; + } + + await db.SaveChangesAsync(); + + await Notify.SendNotify((long)userId, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(mine) }); + await Notify.SendNotify(id, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(theirs) }); + + await Notify.SendMessage(id, new Message { FromPlayerId = (long)userId, Type = MessageType.FriendInvite }); + + return Ok(new Relationship(mine)); + } + + [HttpGet("removefriend"), Authorize] + [HttpPost("removefriend"), Authorize] + public async Task RemoveFriend(long id) + { + var userId = User.GetUserId(); + if (userId == null || id <= 0 || id == userId) + return BadRequest(new { error = "invalid player id" }); + + var mine = await RelationshipsShared.GetOrCreate(db, (long)userId, id); + var theirs = await RelationshipsShared.GetOrCreate(db, id, (long)userId); + + mine.RelationshipType = RelationshipType.None; + theirs.RelationshipType = RelationshipType.None; + await db.SaveChangesAsync(); + + await Notify.SendNotify((long)userId, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(mine) }); + await Notify.SendNotify(id, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(theirs) }); + + return Ok(new Relationship(mine)); + } +} diff --git a/Server/Relationships/RelationshipsV3.cs b/Server/Relationships/RelationshipsV3.cs new file mode 100644 index 0000000..e5d9426 --- /dev/null +++ b/Server/Relationships/RelationshipsV3.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Relationships; + +namespace RadicadeServer.API.Relationships.V3; + +[Route("api/relationships/v3")] +public class RelationshipsV3(RecDBContext db) : Controller +{ + [HttpPost("{id:long}"), Authorize] + public async Task SendFriendRequest(long id) + { + var userId = User.GetUserId(); + if (userId == null || id <= 0 || id == userId) + return BadRequest(new { error = "invalid player id" }); + + var mine = await RelationshipsShared.GetOrCreate(db, (long)userId, id); + var theirs = await RelationshipsShared.GetOrCreate(db, id, (long)userId); + + if (mine.RelationshipType == RelationshipType.None && + theirs.RelationshipType == RelationshipType.None) + { + mine.RelationshipType = RelationshipType.FriendRequestSent; + theirs.RelationshipType = RelationshipType.FriendRequestReceived; + await db.SaveChangesAsync(); + + await Notify.SendNotify((long)userId, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(mine) }); + await Notify.SendNotify(id, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(theirs) }); + await Notify.SendMessage(id, new Message { FromPlayerId = (long)userId, Type = MessageType.FriendInvite }); + } + + return Ok(new Relationship(mine)); + } + + [HttpDelete("{id:long}"), Authorize] + [HttpPost("{id:long}/remove"), Authorize] + public async Task RemoveFriend(long id) + { + var userId = User.GetUserId(); + if (userId == null || id <= 0 || id == userId) + return BadRequest(new { error = "invalid player id" }); + + var mine = await RelationshipsShared.GetOrCreate(db, (long)userId, id); + var theirs = await RelationshipsShared.GetOrCreate(db, id, (long)userId); + + mine.RelationshipType = RelationshipType.None; + theirs.RelationshipType = RelationshipType.None; + await db.SaveChangesAsync(); + + await Notify.SendNotify((long)userId, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(mine) }); + await Notify.SendNotify(id, new Notification { Id = Notifs.RelationshipChanged, Msg = new Relationship(theirs) }); + + return Ok(new Relationship(mine)); + } +} diff --git a/Server/Rooms/2018-CloneRequest.cs b/Server/Rooms/2018-CloneRequest.cs new file mode 100644 index 0000000..37571b7 --- /dev/null +++ b/Server/Rooms/2018-CloneRequest.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class CloneRequest + { + public string Name { get; set; } = null!; + public long RoomId { get; set; } + } +} diff --git a/Server/Rooms/ActualRoomRole.cs b/Server/Rooms/ActualRoomRole.cs new file mode 100644 index 0000000..4d1a49b --- /dev/null +++ b/Server/Rooms/ActualRoomRole.cs @@ -0,0 +1,18 @@ +using RadicadeServer.RecNet.Enums.Rooms; + +namespace RadicadeServer.RecNet.Rooms +{ + public class ActualRoomRole + { + public long AccountId { get; set; } + public RoleType InvitedRole { get; set; } + public RoleType Role { get; set; } + + public ActualRoomRole(RoomRole role) + { + AccountId = role.AccountId; + InvitedRole = role.InvitedRole; + Role = role.Role; + } + } +} diff --git a/Server/Rooms/DefaultRooms.cs b/Server/Rooms/DefaultRooms.cs new file mode 100644 index 0000000..508fe8c --- /dev/null +++ b/Server/Rooms/DefaultRooms.cs @@ -0,0 +1,828 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Utils +{ + public static class DefaultRooms + { + private const string AlignmentKey = "DefaultRooms.AlignmentVersion"; + + private const int AlignmentVersion = 1; + + private const string RroCorrectionKey = "DefaultRooms.RroCorrectionVersion"; + + private const string RecCenterRepairKey = "DefaultRooms.RecCenterRepairVersion"; + + private const int RecCenterRepairVersion = 1; + + private const string RecCenterRepairRoom = "RecCenter"; + + private const string CloningCorrectionKey = "DefaultRooms.NoCloneVersion"; + + private const int CloningCorrectionVersion = 1; + + private const int RroCorrectionVersion = 2; + + private const string ShippedRoomResetKey = "DefaultRooms.ShippedRoomResetVersion"; + + private const int ShippedRoomResetVersion = 3; + + public static IReadOnlyList All { get; } = new List + { + new DefaultRoom + { + Name = "Orientation", Description = "An introductory tour of Rec Room!", + MaxPlayers = 1, SceneId = "c79709d8-a31b-48aa-9eb8-cc31ba9505e8", + Tags = Rro(), CloningAllowed = false, DisableRoomComments = true + }, + new DefaultRoom + { + Name = "RecCenter", Description = "A social hub to meet and mingle with friends new and old.", + MaxPlayers = 12, SceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6", + Tags = Rro(), CloningAllowed = false + }, + new DefaultRoom + { + Name = "TheRiseofJumbotron", Description = "Robot invaders threaten the galaxy! Team up with your friends and bring the laser heat!", + MaxPlayers = 4, SceneId = "acc06e66-c2d0-4361-b0cd-46246a4c455c", + Tags = RroWith("quest") + }, + new DefaultRoom + { + Name = "GoldenTrophy", Description = "The goblin king stole Coach's Golden Trophy. Team up and embark on an epic quest to recover it!", + MaxPlayers = 4, SceneId = "91e16e35-f48f-4700-ab8a-a1b79e50e51b", + Tags = RroWith("quest") + }, + new DefaultRoom + { + Name = "Crescendo", Description = "Gather your vampire hunting crew, conquer a legendary castle, and restore peace to Rec Room!", + MaxPlayers = 3, SceneId = "49cb8993-a956-43e2-86f4-1318f279b22a", + Tags = RroWith("quest") + }, + new DefaultRoom + { + Name = "CrimsonCauldron", Description = "Can your band of adventurers brave the enchanted wilds, and lift the curse of the crimson cauldron?", + MaxPlayers = 4, SceneId = "949fa41f-4347-45c0-b7ac-489129174045", + Tags = RroWith("quest") + }, + new DefaultRoom + { + Name = "IsleOfLostSkulls", Description = "Can your pirate crew get to the Isle, defeat its fearsome guardian, and escape with the gold?", + MaxPlayers = 3, SceneId = "7e01cfe0-820a-406f-b1b3-0a5bf575235c", + Tags = RroWith("quest") + }, + new DefaultRoom + { + Name = "StuntRunner", Description = "Sprint, climb, and wall jump in this high-speed obstacle course where every second counts!", + MaxPlayers = 12, SceneId = "b7281665-a715-4051-826b-8e08e69c6172", SubRoomName = "StuntRunner", + Tags = RroWith("sports"), CloningAllowed = false, + ExtraSubRooms = new List + { + new() { Name = "TheMainEvent", SceneId = "3a636bd2-f896-424c-9225-c184522c0d87", MaxPlayers = 4 } + } + }, + new DefaultRoom + { + Name = "3DCharades", Description = "Take turns drawing, acting, and guessing funny phrases with your friends!", + MaxPlayers = 8, SceneId = "a673712c-877f-4749-b69a-4a4c6310d545", SubRoomName = "InkSpaceHome", + Tags = Rro(), DisableRoomComments = true + }, + new DefaultRoom + { + Name = "Legacy3DCharades", Description = "Take turns drawing, acting, and guessing funny phrases with your friends!", + MaxPlayers = 8, SceneId = "4078dfed-24bb-4db7-863f-578ba48d726b", + Tags = Rro(), DisableRoomComments = true + }, + new DefaultRoom + { + Name = "TheInkSpace", Description = "A cozy event venue with a stage. The entire room is made of props, so customize to your heart's content!", + MaxPlayers = 8, SceneId = "1fa06e3c-c307-4c11-a91b-1fabcddb8a96", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true, DisableRoomComments = true + }, + new DefaultRoom + { + Name = "Dodgeball", Description = "Throw dodgeballs to knock out your friends in this gym classic!", + MaxPlayers = 6, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f", + Tags = RroWith("sports") + }, + new DefaultRoom + { + Name = "DodgeballVR", Description = "Throw dodgeballs to knock out your friends in this gym classic!", + MaxPlayers = 6, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f" + }, + new DefaultRoom + { + Name = "Bowling", Description = "Hang out with friends, bowl a few games, eat snacks!", + MaxPlayers = 8, SceneId = "ae929543-9a07-41d5-8ee9-dbbee8c36800", + Tags = RroWith("sports") + }, + new DefaultRoom + { + Name = "BowlingAlley", Description = "Lone Shoe Lanes is the hottest hangout in town.", + MaxPlayers = 8, SceneId = "ae929543-9a07-41d5-8ee9-dbbee8c36800", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Paddleball", Description = "A simple rally game between two players in a plexiglass tube with a zero-g ball.", + MaxPlayers = 2, SceneId = "d89f74fa-d51e-477a-a425-025a891dd499", + Tags = RroWith("sports") + }, + new DefaultRoom + { + Name = "Soccer", Description = "Teams of three run around slamming themselves into an over-sized soccer ball. Goal!", + MaxPlayers = 6, SceneId = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", + Tags = RroWith("sports") + }, + new DefaultRoom + { + Name = "DiscGolfLake", Description = "A leisurely stroll through the grass. Throw your disc into the goal. Sounds easy, right?", + MaxPlayers = 4, SceneId = "f6f7256c-e438-4299-b99e-d20bef8cf7e0", + Tags = RroWith("sports") + }, + new DefaultRoom + { + Name = "DiscGolfPropulsion", Description = "Throw your disc through hazards and around wind machines on this challenging course!", + MaxPlayers = 4, SceneId = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", + Tags = RroWith("sports") + }, + new DefaultRoom + { + Name = "Paintball", Description = "Red and Blue teams splat each other in capture the flag and team battle.", + MaxPlayers = 8, SceneId = "e122fe98-e7db-49e8-a1b1-105424b6e1f0", SubRoomName = "River", + Tags = RroWith("sports"), + ExtraSubRooms = new List + { + new() { Name = "Homestead", SceneId = "a785267d-c579-42ea-be43-fec1992d1ca7" }, + new() { Name = "Quarry", SceneId = "ff4c6427-7079-4f59-b22a-69b089420827" }, + new() { Name = "Clearcut", SceneId = "380d18b5-de9c-49f3-80f7-f4a95c1de161" }, + new() { Name = "Spillway", SceneId = "58763055-2dfb-4814-80b8-16fac5c85709" }, + new() { Name = "Drive-in", SceneId = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3" } + } + }, + new DefaultRoom + { + Name = "LaserTag", Description = "Teams battle each other and waves of robots.", + MaxPlayers = 8, SceneId = "239e676c-f12f-489f-bf3a-d4c383d692c3", SubRoomName = "Hangar", + Tags = RroWith("pvp"), + ExtraSubRooms = new List + { + new() { Name = "CyberJunkCity", SceneId = "9d6456ce-6264-48b4-808d-2d96b3d91038" } + } + }, + new DefaultRoom + { + Name = "RecRoyaleSquads", Description = "Squads of three battle it out on Frontier Island. Last squad standing wins!", + MaxPlayers = 18, SceneId = "253fa009-6e65-4c90-91a1-7137a56a267f", ImageName = "RecRoyaleSquads", + Tags = RroWith("pvp"), CloningAllowed = false + }, + new DefaultRoom + { + Name = "RecRoyaleSolos", Description = "Battle it out on Frontier Island. Last person standing wins!", + MaxPlayers = 16, SceneId = "b010171f-4875-4e89-baba-61e878cd41e1", ImageName = "RecRoyaleSolos", + Tags = RroWith("pvp"), CloningAllowed = false, Aliases = new[] { "RecRoyaleSolo" } + }, + new DefaultRoom + { + Name = "Showdown", Description = "Showdown", + MaxPlayers = 16, SceneId = SceneLocations.MakerRoom, ImageName = "Showdown", + Tags = new List + { + new() { Tag = "rro", Type = TagType.AGOnly }, + new() { Tag = "games", Type = TagType.General }, + new() { Tag = "beta", Type = TagType.Auto } + }, + CloningAllowed = true, + SaveName = "Showdown.room", BundleName = "Showdown_0_1.assetbundle" + }, + new DefaultRoom + { + Name = "Lounge", Description = "A low-key lounge to chill with your friends. Great for private parties!", + MaxPlayers = 20, SceneId = "a067557f-ca32-43e6-b6e5-daaec60b4f5a", + Tags = Rro(), SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Park", Description = "A sprawling park with amphitheater, play fields, and a cave.", + MaxPlayers = 20, SceneId = "0a864c86-5a71-4e18-8041-8124e4dc9d98", + Tags = Rro(), SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "MakerRoom", Description = "This room is a blank canvas. Make it into whatever you like!", + MaxPlayers = 20, SceneId = "a75f7547-79eb-47c6-8986-6767abcb4f92", ImageName = "MakerRoom", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + + new DefaultRoom + { + Name = "Gym", Description = "A school gymnasium for smaller sporting events.", + MaxPlayers = 20, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Stadium", Description = "A professional stadium for larger sporting events.", + MaxPlayers = 20, SceneId = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "River", Description = "The original outdoor paintball course. Simple, balanced, classic.", + MaxPlayers = 20, SceneId = "e122fe98-e7db-49e8-a1b1-105424b6e1f0", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Homestead", Description = "A day on the farm. Great for asymmetrical battles!", + MaxPlayers = 20, SceneId = "a785267d-c579-42ea-be43-fec1992d1ca7", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Quarry", Description = "The sun sets on this construction site in the desert. Great for sniping battles!", + MaxPlayers = 20, SceneId = "ff4c6427-7079-4f59-b22a-69b089420827", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Clearcut", Description = "The sun rises on this logging camp in the forest. Great for mid-range splatting!", + MaxPlayers = 20, SceneId = "380d18b5-de9c-49f3-80f7-f4a95c1de161", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Spillway", Description = "Night shift at the hydroelectric plant. Great for stealthy splatting!", + MaxPlayers = 20, SceneId = "58763055-2dfb-4814-80b8-16fac5c85709", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Lake", Description = "A leisurely trail to the lake and beyond.", + MaxPlayers = 20, SceneId = "f6f7256c-e438-4299-b99e-d20bef8cf7e0", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "PropulsionTestRange", Description = "The science department left some of their equipment laying around. What will you do with it?", + MaxPlayers = 20, SceneId = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Hangar", Description = "A late-eighties-style laser tag battle arcade.", + MaxPlayers = 20, SceneId = "239e676c-f12f-489f-bf3a-d4c383d692c3", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "CyberJunkCity", Description = "A late-2080s-style cyberpunk dystopian laser tag battle arcade", + MaxPlayers = 20, SceneId = "9d6456ce-6264-48b4-808d-2d96b3d91038", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "StuntRunnerBaseRoom", Description = "A soundstage warehouse used to film Stunt Runner and other RecTV shows.", + MaxPlayers = 20, SceneId = "882e9b96-7115-4b03-86f6-c0c9d8e22e00", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + new DefaultRoom + { + Name = "Registration", Description = "Registration", + MaxPlayers = 1, SceneId = "cf61556d-68fd-4288-9ae5-7a512621e569", + IsRRO = false, Accessibility = Accessibility.Unlised, CloningAllowed = false + }, + new DefaultRoom + { + Name = "ARRoom", Description = "Your real room!", + MaxPlayers = 4, SceneId = "bf268f5f-b55b-41af-8628-32fa4b5d70b6", + IsRRO = false, Accessibility = Accessibility.Unlised, CloningAllowed = false, SubRoomSandbox = true, + WarningMask = WarningMask.Custom + }, + new DefaultRoom + { + Name = "DriveIn", Description = "A drive-in theater, diner, and truck stop make this a multi-area paint arena", + MaxPlayers = 20, SceneId = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3", + IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true + }, + + new DefaultRoom + { + Name = "RecRally", Description = "A six-player off-road rally race where you boost, bump, and powerslide your way to the finish line!", + MaxPlayers = 6, SceneId = "56193568-9ae0-498c-8a77-4df79dec91f5", + Tags = RroWith("racing", "driving", "recrally"), + WarningMask = WarningMask.IntenseMotion, SupportsTeleportVR = false + }, + new DefaultRoom + { + Name = "FrontierUGC", Description = "A public copyable room featuring the Rec Royale map!", + MaxPlayers = 40, SceneId = "a16bfd31-ffb9-46ac-a199-362c163130c0", ImageName = "RecRoyale", + IsRRO = false, Accessibility = Accessibility.Unlised + } + }; + + public static async Task EnsureAsync(RecDBContext db) + { + try + { + await ResetRoomsAsync(db); + await EnsureCatalogAsync(db); + await RemoveStrayRroAsync(db); + await DisableCloningAsync(db); + await RepairRecCenterAsync(db); + } + catch (Exception) + { + db.ChangeTracker.Clear(); + } + } + + private static async Task RepairRecCenterAsync(RecDBContext db) + { + try + { + if (await HasRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion)) return; + + var reference = All.FirstOrDefault(room => Matches(RecCenterRepairRoom, room)); + if (reference == null) + { + await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion); + return; + } + + var rooms = await db.Rooms.ToListAsync(); + var existing = rooms.FirstOrDefault(room => Matches(room.Name, reference)); + if (existing == null) + { + await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion); + return; + } + + var subRooms = await db.SubRooms.ToListAsync(); + var changes = await AlignAsync(db, existing, subRooms, reference); + + foreach (var sub in subRooms.Where(sub => sub.RoomId == existing.RoomId)) + { + if (string.IsNullOrEmpty(sub.DataBlob)) continue; + changes.Add($"subroom {sub.Name} content reset to the stock scene ({sub.DataBlob.Length} chars discarded)"); + sub.DataBlob = string.Empty; + sub.UnityAssetId = string.Empty; + } + + await db.SaveChangesAsync(); + await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion); + + } + catch (Exception) + { + db.ChangeTracker.Clear(); + } + } + + private static async Task ResetRoomsAsync(RecDBContext db) + { + if (await HasRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion)) return; + + var rooms = await db.Rooms.ToListAsync(); + if (rooms.Count == 0) + { + await MarkRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion); + return; + } + + var ids = rooms.Select(room => room.RoomId).ToList(); + var subRoomIds = await db.SubRooms.Where(sub => ids.Contains(sub.RoomId)).Select(sub => sub.SubRoomId).ToListAsync(); + + db.SubRoomPermissions.RemoveRange(await db.SubRoomPermissions.Where(permission => subRoomIds.Contains(permission.subRoomId)).ToListAsync()); + db.SubRoomHistory.RemoveRange(await db.SubRoomHistory.Where(history => ids.Contains(history.RoomId)).ToListAsync()); + db.SubRooms.RemoveRange(await db.SubRooms.Where(sub => ids.Contains(sub.RoomId)).ToListAsync()); + db.RoomStats.RemoveRange(await db.RoomStats.Where(stats => ids.Contains(stats.RoomId)).ToListAsync()); + db.RoomRoles.RemoveRange(await db.RoomRoles.Where(role => ids.Contains(role.RoomId)).ToListAsync()); + db.RoomKeys.RemoveRange(await db.RoomKeys.Where(key => ids.Contains(key.RoomId)).ToListAsync()); + db.RoomBans.RemoveRange(await db.RoomBans.Where(ban => ids.Contains(ban.roomId)).ToListAsync()); + db.RoomComments.RemoveRange(await db.RoomComments.Where(comment => ids.Contains(comment.RoomId)).ToListAsync()); + db.RoomInteractions.RemoveRange(await db.RoomInteractions.Where(interaction => ids.Contains(interaction.RoomId)).ToListAsync()); + db.RoomInstances.RemoveRange(await db.RoomInstances.Where(instance => ids.Contains(instance.roomId) || subRoomIds.Contains(instance.subRoomId)).ToListAsync()); + db.LeaderboardScores.RemoveRange(await db.LeaderboardScores.Where(score => ids.Contains(score.RoomId)).ToListAsync()); + + var events = await db.PlayerEvents.Where(playerEvent => ids.Contains(playerEvent.RoomId)).ToListAsync(); + if (events.Count > 0) + { + var eventIds = events.Select(playerEvent => playerEvent.PlayerEventId).ToList(); + db.PlayerEventResponses.RemoveRange(await db.PlayerEventResponses.Where(response => eventIds.Contains(response.PlayerEventId)).ToListAsync()); + db.PlayerEvents.RemoveRange(events); + } + + foreach (var image in await db.Images.Where(image => image.RoomId.HasValue && ids.Contains(image.RoomId.Value)).ToListAsync()) + image.RoomId = null; + foreach (var message in await db.Messages.Where(message => message.RoomId.HasValue && ids.Contains(message.RoomId.Value)).ToListAsync()) + message.RoomId = null; + + db.Rooms.RemoveRange(rooms); + await db.SaveChangesAsync(); + + await db.AddDormRoom(); + await ClearCatalogMarkerAsync(db); + await MarkRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion); + + var dorm = await db.Rooms.FirstOrDefaultAsync(room => room.IsDorm && room.CreatorAccountId == 1); + } + + private static async Task ClearCatalogMarkerAsync(RecDBContext db) + { + var row = await db.GameConfig.FirstOrDefaultAsync(config => config.Key == AlignmentKey); + if (row == null) + await db.GameConfig.AddAsync(new ConfigVal(AlignmentKey, "0")); + else + row.Value = "0"; + + await db.SaveChangesAsync(); + } + + private static async Task EnsureCatalogAsync(RecDBContext db) + { + try + { + if (await IsAlignedAsync(db)) return; + + var rooms = await db.Rooms.ToListAsync(); + var subRooms = await db.SubRooms.ToListAsync(); + var statsRoomIds = await db.RoomStats.Select(x => x.RoomId).ToListAsync(); + + var created = new List(); + var aligned = new List(); + + foreach (var reference in All) + { + var existing = rooms.FirstOrDefault(room => Matches(room.Name, reference)); + if (existing == null) + { + var room = await CreateAsync(db, reference, statsRoomIds); + rooms.Add(room); + created.Add($"{reference.Name} ({reference.MaxPlayers} players)"); + continue; + } + + var changes = await AlignAsync(db, existing, subRooms, reference); + if (changes.Count > 0) + aligned.Add($"{existing.Name}: {string.Join(", ", changes)}"); + } + + await db.SaveChangesAsync(); + await MarkAlignedAsync(db); + + foreach (var line in created) ; foreach (var line in aligned) ; } + catch (Exception) + { + db.ChangeTracker.Clear(); + } + } + + private static async Task RemoveStrayRroAsync(RecDBContext db) + { + if (await HasRunAsync(db, RroCorrectionKey, RroCorrectionVersion)) return; + + var rooms = await db.Rooms.ToListAsync(); + var corrected = new List(); + + foreach (var room in rooms) + { + var entry = All.FirstOrDefault(candidate => !candidate.IsRRO && Matches(room.Name, candidate)); + if (entry == null) continue; + + var tags = StringTools.ReadTags(room.Tags); + var strayTags = tags.Where(tag => string.Equals(tag.Tag, "rro", StringComparison.OrdinalIgnoreCase)).ToList(); + if (strayTags.Count == 0 && !room.IsRRO) continue; + + foreach (var tag in strayTags) + tags.Remove(tag); + + room.Tags = StringTools.CreateTagString(tags); + room.IsRRO = false; + corrected.Add(room.Name); + } + + if (corrected.Count > 0) + await db.SaveChangesAsync(); + await MarkRunAsync(db, RroCorrectionKey, RroCorrectionVersion); + } + + private static async Task DisableCloningAsync(RecDBContext db) + { + if (await HasRunAsync(db, CloningCorrectionKey, CloningCorrectionVersion)) return; + + var rooms = await db.Rooms.ToListAsync(); + var off = new List(); + var on = new List(); + + foreach (var room in rooms) + { + var entry = All.FirstOrDefault(candidate => Matches(room.Name, candidate)); + if (entry == null || entry.CloningAllowed == room.CloningAllowed) continue; + + room.CloningAllowed = entry.CloningAllowed; + (entry.CloningAllowed ? on : off).Add(room.Name); + } + + if (off.Count > 0 || on.Count > 0) + await db.SaveChangesAsync(); + await MarkRunAsync(db, CloningCorrectionKey, CloningCorrectionVersion); + + if (off.Count == 0 && on.Count == 0) + { + return; + } + } + + + private static async Task CreateAsync(RecDBContext db, DefaultRoom reference, List statsRoomIds) + { + var room = new RoomData + { + Name = reference.Name, + Description = reference.Description, + ImageName = reference.ImageName ?? reference.Name, + MaxPlayers = reference.MaxPlayers, + MaxPlayerCalculationMode = MaxPlayerMode.OnlyEntrySubrooms, + CreatorAccountId = 1, + IsRRO = reference.IsRRO, + IsDorm = false, + Accessibility = reference.Accessibility, + CreatedAt = DateTime.Now, + State = RoomState.Active, + CloningAllowed = reference.CloningAllowed, + DisableRoomComments = reference.DisableRoomComments, + WarningMask = reference.WarningMask, + SupportsTeleportVR = reference.SupportsTeleportVR, + Tags = StringTools.CreateTagString(reference.Tags.ToList()) + }; + + await db.Rooms.AddAsync(room); + await db.SaveChangesAsync(); + + await AddSubRoomAsync(db, room.RoomId, reference.SubRoomName, reference.SceneId, reference.MaxPlayers, + reference.Accessibility, reference.SubRoomSandbox, reference.SaveName, reference.BundleName); + foreach (var extra in reference.ExtraSubRooms) + await AddSubRoomAsync(db, room.RoomId, extra.Name, extra.SceneId, ExtraSubRoomSize(extra, reference), + reference.Accessibility, reference.SubRoomSandbox); + + if (!statsRoomIds.Contains(room.RoomId)) + { + await db.RoomStats.AddAsync(new RoomStats { RoomId = room.RoomId }); + statsRoomIds.Add(room.RoomId); + } + + await db.RoomRoles.AddAsync(new RoomRole + { + RoomId = room.RoomId, + AccountId = 1, + Role = RoleType.Creator, + InvitedRole = RoleType.None + }); + + await db.SaveChangesAsync(); + return room; + } + + private static async Task> AlignAsync(RecDBContext db, RoomData room, List subRooms, DefaultRoom reference) + { + var changes = new List(); + var image = reference.ImageName ?? reference.Name; + + if (room.MaxPlayers != reference.MaxPlayers) + { + changes.Add($"max players {room.MaxPlayers} -> {reference.MaxPlayers}"); + room.MaxPlayers = reference.MaxPlayers; + } + if (!string.Equals(room.ImageName, image, StringComparison.Ordinal)) + { + changes.Add($"image '{room.ImageName}' -> '{image}'"); + room.ImageName = image; + } + if (room.Accessibility != reference.Accessibility) + { + changes.Add($"accessibility {room.Accessibility} -> {reference.Accessibility}"); + room.Accessibility = reference.Accessibility; + } + if (room.CloningAllowed != reference.CloningAllowed) + { + changes.Add($"cloning {room.CloningAllowed} -> {reference.CloningAllowed}"); + room.CloningAllowed = reference.CloningAllowed; + } + if (room.DisableRoomComments != reference.DisableRoomComments) + { + changes.Add($"room comments {(reference.DisableRoomComments ? "off" : "on")}"); + room.DisableRoomComments = reference.DisableRoomComments; + } + if (room.WarningMask != reference.WarningMask) + { + changes.Add($"warning mask {room.WarningMask} -> {reference.WarningMask}"); + room.WarningMask = reference.WarningMask; + } + if (room.SupportsTeleportVR != reference.SupportsTeleportVR) + { + changes.Add($"teleport VR {reference.SupportsTeleportVR}"); + room.SupportsTeleportVR = reference.SupportsTeleportVR; + } + if (room.IsRRO != reference.IsRRO) + { + changes.Add(reference.IsRRO ? "marked IsRRO" : "no longer marked IsRRO"); + room.IsRRO = reference.IsRRO; + } + + var mine = subRooms.Where(sub => sub.RoomId == room.RoomId).OrderBy(sub => sub.SubRoomId).ToList(); + var entry = mine.FirstOrDefault(); + if (entry == null) + { + subRooms.Add(await AddSubRoomAsync(db, room.RoomId, reference.SubRoomName, reference.SceneId, + reference.MaxPlayers, reference.Accessibility, reference.SubRoomSandbox, reference.SaveName, + reference.BundleName)); + changes.Add($"added the {reference.SubRoomName} subroom"); + } + else + { + AlignSubRoom(changes, entry, reference.SubRoomName, reference.SceneId, reference.MaxPlayers, + reference.Accessibility, reference.SubRoomSandbox); + } + + foreach (var extra in reference.ExtraSubRooms) + { + var match = subRooms.FirstOrDefault(sub => sub.RoomId == room.RoomId + && string.Equals(sub.UnitySceneId, extra.SceneId, StringComparison.OrdinalIgnoreCase)); + if (match == null) + { + subRooms.Add(await AddSubRoomAsync(db, room.RoomId, extra.Name, extra.SceneId, + ExtraSubRoomSize(extra, reference), reference.Accessibility, reference.SubRoomSandbox)); + changes.Add($"added the {extra.Name} subroom"); + } + else + { + AlignSubRoom(changes, match, extra.Name, extra.SceneId, ExtraSubRoomSize(extra, reference), + reference.Accessibility, reference.SubRoomSandbox); + } + } + + return changes; + } + + private static void AlignSubRoom(List changes, SubRoom sub, string name, string sceneId, int maxPlayers, Accessibility accessibility, bool sandbox, string? saveName = null, string? bundleName = null) + { + var what = $"subroom {sub.Name}"; + + if (!string.Equals(sub.Name, name, StringComparison.Ordinal)) + { + changes.Add($"{what} renamed to {name}"); + sub.Name = name; + } + if (!string.Equals(sub.UnitySceneId, sceneId, StringComparison.OrdinalIgnoreCase)) + { + changes.Add($"{what} scene {sub.UnitySceneId} -> {sceneId}"); + sub.UnitySceneId = sceneId; + } + if (sub.MaxPlayers != maxPlayers) + { + changes.Add($"{what} max players {sub.MaxPlayers} -> {maxPlayers}"); + sub.MaxPlayers = maxPlayers; + } + if (sub.Accessibility != accessibility) + { + changes.Add($"{what} accessibility {sub.Accessibility} -> {accessibility}"); + sub.Accessibility = accessibility; + } + if (sub.IsSandbox != sandbox) + { + changes.Add($"{what} sandbox {sandbox}"); + sub.IsSandbox = sandbox; + } + if (saveName != null && !string.Equals(sub.DataBlob, saveName, StringComparison.Ordinal)) + { + changes.Add($"{what} room file '{(sub.DataBlob.Length == 0 ? "none" : sub.DataBlob)}' -> '{saveName}'"); + sub.DataBlob = saveName; + } + if (bundleName != null && !string.Equals(sub.UnityAssetId, bundleName, StringComparison.Ordinal)) + { + changes.Add($"{what} bundle '{(sub.UnityAssetId.Length == 0 ? "none" : sub.UnityAssetId)}' -> '{bundleName}'"); + sub.UnityAssetId = bundleName; + } + } + + private static async Task AddSubRoomAsync(RecDBContext db, long roomId, string name, string sceneId, int maxPlayers, Accessibility accessibility, bool sandbox, string? saveName = null, string? bundleName = null) + { + var subRoom = new SubRoom + { + RoomId = roomId, + SavedByAccountId = 1, + Name = name, + DataBlob = saveName ?? "", + UnityAssetId = bundleName ?? "", + IsSandbox = sandbox, + MaxPlayers = maxPlayers, + Accessibility = accessibility, + UnitySceneId = sceneId, + CreatedAt = DateTime.Now + }; + + await db.SubRooms.AddAsync(subRoom); + return subRoom; + } + + private static int ExtraSubRoomSize(DefaultSubRoom subRoom, DefaultRoom room) => + subRoom.MaxPlayers > 0 ? subRoom.MaxPlayers : room.MaxPlayers; + + + private static Task IsAlignedAsync(RecDBContext db) => HasRunAsync(db, AlignmentKey, AlignmentVersion); + + private static Task MarkAlignedAsync(RecDBContext db) => MarkRunAsync(db, AlignmentKey, AlignmentVersion); + + private static async Task HasRunAsync(RecDBContext db, string key, int version) + { + var row = await db.GameConfig.FirstOrDefaultAsync(x => x.Key == key); + return int.TryParse(row?.Value, out var recorded) && recorded >= version; + } + + private static async Task MarkRunAsync(RecDBContext db, string key, int version) + { + var row = await db.GameConfig.FirstOrDefaultAsync(x => x.Key == key); + if (row == null) + await db.GameConfig.AddAsync(new ConfigVal(key, version.ToString())); + else + row.Value = version.ToString(); + + await db.SaveChangesAsync(); + } + + private static bool Matches(string? existingName, DefaultRoom reference) + { + var key = NormalizeName(existingName); + if (key.Length == 0) return false; + + return key == NormalizeName(reference.Name) + || reference.Aliases.Any(alias => key == NormalizeName(alias)); + } + + private static string NormalizeName(string? name) => + string.Concat((name ?? "").Where(char.IsLetterOrDigit)).ToLowerInvariant(); + + private static List Rro() => new() { new ItemTag { Tag = "rro", Type = TagType.AGOnly } }; + + private static List RroWith(params string[] tags) + { + var list = Rro(); + list.AddRange(tags.Select(tag => new ItemTag { Tag = tag, Type = TagType.General })); + return list; + } + } + + public sealed class DefaultRoom + { + public string Name { get; init; } = ""; + public string Description { get; init; } = ""; + + public int MaxPlayers { get; init; } + + public string SceneId { get; init; } = ""; + + public string SubRoomName { get; init; } = "Home"; + + public string? ImageName { get; init; } + + public string? SaveName { get; init; } + + public string? BundleName { get; init; } + + public IReadOnlyList Tags { get; init; } = Array.Empty(); + + public IReadOnlyList ExtraSubRooms { get; init; } = Array.Empty(); + + public IReadOnlyList Aliases { get; init; } = Array.Empty(); + + public Accessibility Accessibility { get; init; } = Accessibility.Public; + + public bool CloningAllowed + { + get => cloningAllowedOverride ?? Accessibility == Accessibility.Unlised; + init => cloningAllowedOverride = value; + } + + private bool? cloningAllowedOverride; + public bool IsRRO { get; init; } = true; + + public bool DisableRoomComments { get; init; } + public bool SubRoomSandbox { get; init; } + public WarningMask WarningMask { get; init; } = WarningMask.None; + public bool SupportsTeleportVR { get; init; } = true; + } + + public sealed class DefaultSubRoom + { + public string Name { get; init; } = ""; + public string SceneId { get; init; } = ""; + + public int MaxPlayers { get; init; } + } +} diff --git a/Server/Rooms/ExternalContent.cs b/Server/Rooms/ExternalContent.cs new file mode 100644 index 0000000..6ac2990 --- /dev/null +++ b/Server/Rooms/ExternalContent.cs @@ -0,0 +1,10 @@ +using RadicadeServer.RecNet.Enums.Board; + +namespace RadicadeServer.RecNet.Rooms +{ + public class ExternalContent + { + public ExternalContentType Type { get; set; } + public string Reference { get; set; } = null!; + } +} diff --git a/Server/Rooms/FeaturedRoomGroup.cs b/Server/Rooms/FeaturedRoomGroup.cs new file mode 100644 index 0000000..d6465ca --- /dev/null +++ b/Server/Rooms/FeaturedRoomGroup.cs @@ -0,0 +1,15 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class FeaturedRoomGroup + { + public long FeaturedRoomGroupId { get; set; } + public string Name { get; set; } = null!; + public List Rooms { get; set; } = null!; + } + public class FeaturedRoom + { + public long RoomId { get; set; } + public string RoomName { get; set; } = null!; + public string ImageName { get; set; } = "default.png"; + } +} diff --git a/Server/Rooms/FullRoom.cs b/Server/Rooms/FullRoom.cs new file mode 100644 index 0000000..18d4df3 --- /dev/null +++ b/Server/Rooms/FullRoom.cs @@ -0,0 +1,59 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class FullRoom : RoomDetail + { + public List SubRooms { get; set; } = null!; + public List Roles { get; set; } = null!; + public List LoadScreens { get; set; } = new(); + public List PromoImages { get; set; } = new(); + public List PromoExternalContent { get; set; } = new(); + public string? DataBlob { get; set; } + public List Tags { get; set; } = new(); + + + public FullRoom CopyTo(FullRoom target) + { + target.RoomId = RoomId; + target.IsDorm = IsDorm; + target.MaxPlayers = MaxPlayers; + target.MaxPlayerCalculationMode = MaxPlayerCalculationMode; + target.CloningAllowed = CloningAllowed; + target.DisableMicAutoMute = DisableMicAutoMute; + target.DisableRoomComments = DisableRoomComments; + target.EncryptVoiceChat = EncryptVoiceChat; + + target.Name = Name; + target.Description = Description; + target.ImageName = ImageName; + target.WarningMask = WarningMask; + target.CustomWarning = CustomWarning; + target.CreatorAccountId = CreatorAccountId; + target.State = State; + target.Accessibility = Accessibility; + target.SupportsLevelVoting = SupportsLevelVoting; + target.IsRRO = IsRRO; + target.IsDeveloperOwned = IsDeveloperOwned; + target.SupportsScreens = SupportsScreens; + target.SupportsWalkVR = SupportsWalkVR; + target.SupportsTeleportVR = SupportsTeleportVR; + target.SupportsVRLow = SupportsVRLow; + target.SupportsQuest2 = SupportsQuest2; + target.SupportsMobile = SupportsMobile; + target.SupportsJuniors = SupportsJuniors; + target.MinLevel = MinLevel; + target.CreatedAt = CreatedAt; + target.Stats = Stats; + target.Version = Version; + + target.SubRooms = SubRooms; + target.Roles = Roles; + target.LoadScreens = LoadScreens; + target.PromoImages = PromoImages; + target.PromoExternalContent = PromoExternalContent; + target.DataBlob = DataBlob; + target.Tags = Tags; + + return target; + } + } +} diff --git a/Server/Rooms/Lists/AlgorithmicListsAPI.cs b/Server/Rooms/Lists/AlgorithmicListsAPI.cs new file mode 100644 index 0000000..fa0d124 --- /dev/null +++ b/Server/Rooms/Lists/AlgorithmicListsAPI.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Lists +{ + [Route("lists/algorithmiclists")] + public class AlgorithmicListsAPI : Controller + { + private const int Rooms = 1; + + private const int RowSize = 20; + + RecDBContext db; + + public AlgorithmicListsAPI(RecDBContext context) + { + this.db = context; + } + + [HttpGet("{list}")] + public async Task GetAlgorithmicList(string list, [FromQuery] int? type) + { + var echoed = type is >= 0 and <= 255 ? type.Value : Rooms; + + return Ok(new AlgorithmicList + { + Type = echoed, + Entities = (await GetRowIds(list)).Select(roomId => new AlgorithmicListEntity + { + Id = roomId.ToString(), + Context = null, + }).ToList(), + }); + } + + private async Task> GetRowIds(string? list) + { + var key = (list ?? string.Empty).Trim().ToLowerInvariant(); + + var rooms = await db.Rooms + .Where(x => x.Accessibility == Accessibility.Public + && x.State == RoomState.Active + && !x.IsDorm) + .ToListAsync(); + + if (key == "new") + { + return rooms + .OrderByDescending(x => x.PublishedAt) + .ThenBy(x => x.RoomId) + .Take(RowSize) + .Select(x => x.RoomId) + .ToList(); + } + + var visits = await db.RoomStats + .Select(x => new { x.RoomId, x.VisitCount }) + .ToDictionaryAsync(x => x.RoomId, x => x.VisitCount); + + return rooms + .OrderByDescending(x => visits.TryGetValue(x.RoomId, out var count) ? count : 0) + .ThenBy(x => x.RoomId) + .Take(RowSize) + .Select(x => x.RoomId) + .ToList(); + } + } + + public class AlgorithmicList + { + public int Type { get; set; } + + public List Entities { get; set; } = new(); + } + + public class AlgorithmicListEntity + { + public string Id { get; set; } = ""; + + public string? Context { get; set; } + } +} diff --git a/Server/Rooms/Lists/CuratedListsAPI.cs b/Server/Rooms/Lists/CuratedListsAPI.cs new file mode 100644 index 0000000..6989622 --- /dev/null +++ b/Server/Rooms/Lists/CuratedListsAPI.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.Lists +{ + [Route("lists/curatedlists")] + public class CuratedListsAPI : Controller + { + [HttpGet("bulk")] + public IActionResult GetBulk() + { + return Ok(Array.Empty()); + } + } +} diff --git a/Server/Rooms/PlayerInteraction.cs b/Server/Rooms/PlayerInteraction.cs new file mode 100644 index 0000000..f259d86 --- /dev/null +++ b/Server/Rooms/PlayerInteraction.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Rooms +{ + public class PlayerInteraction + { + public bool Cheered { get; set; } + public bool Favorited { get; set; } + + public PlayerInteraction(PlayerInteractionDB db) { Cheered = db.Cheered; Favorited = db.Favorited; } + public PlayerInteraction() { } + } + public class PlayerInteractionDB + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long InteractionId { get; set; } + public long PlayerId { get; set; } + public long RoomId { get; set; } + + public bool Cheered { get; set; } + public bool Favorited { get; set; } + + public DateTime CheeredAt { get; set; } = DateTime.Now; + public DateTime FavoritedAt { get; set; } = DateTime.Now; + + public int VisitCount { get; set; } + public bool Visited { get; set; } + public DateTime LastVisted { get; set; } = DateTime.Now; + } +} diff --git a/Server/Rooms/PlaylistRoom.cs b/Server/Rooms/PlaylistRoom.cs new file mode 100644 index 0000000..eb714a7 --- /dev/null +++ b/Server/Rooms/PlaylistRoom.cs @@ -0,0 +1,16 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class PlaylistRoom : FullRoom + { + public List Rooms { get; set; } = new(); + public PlaylistRoom(FullRoom room) + { + room.CopyTo(this); + } + } + public class SeededRoom + { + public FullRoom SeedRoom { get; set; } = null!; + public List Rooms { get; set; } = []; + } +} diff --git a/Server/Rooms/PublicRoomData.cs b/Server/Rooms/PublicRoomData.cs new file mode 100644 index 0000000..7ac9610 --- /dev/null +++ b/Server/Rooms/PublicRoomData.cs @@ -0,0 +1,13 @@ +namespace RadicadeServer.Radicade.Public +{ + public class PublicRoomData + { + public string ImageCdnUrl { get; set; } = DBConfig.DefaultRoomImage; + + public string RoomName { get; set; } = "^Nowhere"; + public string OwnerName { get; set; } = "@Noone"; + + public int Cheers { get; set; } = 0; + public int Favorites { get; set; } = 0; + } +} diff --git a/Server/Rooms/RestorableSubRoom.cs b/Server/Rooms/RestorableSubRoom.cs new file mode 100644 index 0000000..5879dc5 --- /dev/null +++ b/Server/Rooms/RestorableSubRoom.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RestorableSubRoom + { + [JsonIgnore, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long HistoryId { get; set; } + + public long SubRoomId { get; set; } + + [JsonIgnore] + public long RoomId { get; set; } + + public long SavedByAccountId { get; set; } + + public string DataBlob { get; set; } = string.Empty; + + public DateTime CreatedAt { get; set; } + + public RestorableSubRoom() { } + public RestorableSubRoom(SubRoom subRoom) + { + this.RoomId = subRoom.RoomId; + this.SubRoomId = subRoom.SubRoomId; + this.DataBlob = subRoom.DataBlob; + this.CreatedAt = subRoom.CreatedAt; + this.SavedByAccountId = subRoom.SavedByAccountId; + } + } +} diff --git a/Server/Rooms/Room.cs b/Server/Rooms/Room.cs new file mode 100644 index 0000000..7c1cef5 --- /dev/null +++ b/Server/Rooms/Room.cs @@ -0,0 +1,43 @@ +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; + +namespace RadicadeServer.RecNet.Rooms +{ + public class Room + { + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + + public string ImageName { get; set; } = null!; + + public WarningMask WarningMask { get; set; } + public string CustomWarning { get; set; } = null!; + + public long CreatorAccountId { get; set; } + + public RoomState State { get; set; } + public Accessibility Accessibility { get; set; } + + public bool SupportsLevelVoting { get; set; } + + public bool IsRRO { get; set; } + + public bool IsDeveloperOwned { get; set; } + + public bool SupportsScreens { get; set; } + public bool SupportsWalkVR { get; set; } + public bool SupportsTeleportVR { get; set; } + public bool SupportsVRLow { get; set; } + public bool SupportsQuest2 { get; set; } + public bool SupportsMobile { get; set; } + public bool SupportsJuniors { get; set; } + + public int MinLevel { get; set; } + + public DateTime CreatedAt { get; set; } + + public RoomStats Stats { get; set; } = null!; + + public int Version { get; set; } = 1; + } +} diff --git a/Server/Rooms/RoomAssetStore.cs b/Server/Rooms/RoomAssetStore.cs new file mode 100644 index 0000000..c8f28a0 --- /dev/null +++ b/Server/Rooms/RoomAssetStore.cs @@ -0,0 +1,95 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using System.Text.Json; + +namespace RadicadeServer.API.Rooms.V1; + +internal sealed class RoomAssetRecord +{ + public string Id { get; set; } = ""; + public long RoomId { get; set; } + public string Kind { get; set; } = ""; + public Dictionary Fields { get; set; } = new(StringComparer.OrdinalIgnoreCase); +} + +internal static class RoomAssetStore +{ + private const string ConfigKey = "RoomAssets"; + private static readonly SemaphoreSlim Gate = new(1, 1); + + public static async Task> LoadAsync(RecDBContext db) + { + var config = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(row => row.Key == ConfigKey); + if (string.IsNullOrWhiteSpace(config?.Value)) return new List(); + + try + { + return JsonSerializer.Deserialize>(config.Value, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List(); + } + catch (JsonException) + { + return new List(); + } + } + + public static async Task> ForRoomAsync(RecDBContext db, long roomId, string kind) + { + var records = await LoadAsync(db); + return records.Where(record => record.RoomId == roomId && + string.Equals(record.Kind, kind, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + public static async Task SaveAsync(RecDBContext db, RoomAssetRecord record) + { + await Gate.WaitAsync(); + try + { + var config = await db.ServerConfig.FirstOrDefaultAsync(row => row.Key == ConfigKey); + List records; + try + { + records = string.IsNullOrWhiteSpace(config?.Value) + ? new List() + : JsonSerializer.Deserialize>(config.Value, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List(); + } + catch (JsonException) + { + records = new List(); + } + + records.RemoveAll(existing => existing.RoomId == record.RoomId && + string.Equals(existing.Kind, record.Kind, StringComparison.OrdinalIgnoreCase) && + existing.Id == record.Id); + records.Add(record); + if (config == null) + { + config = new ServerConfigVal(ConfigKey, JsonSerializer.Serialize(records)); + await db.ServerConfig.AddAsync(config); + } + else + { + config.Value = JsonSerializer.Serialize(records); + } + await db.SaveChangesAsync(); + } + finally + { + Gate.Release(); + } + } + + public static async Task>> GetDtosAsync(RecDBContext db, long roomId, string kind) + { + var records = await ForRoomAsync(db, roomId, kind); + return records.Select(record => + { + var dto = new Dictionary(record.Fields, StringComparer.OrdinalIgnoreCase); + dto[kind.Equals("currency", StringComparison.OrdinalIgnoreCase) ? "CurrencyId" : "ConsumableId"] = record.Id; + dto["RoomId"] = record.RoomId; + return dto; + }).ToList(); + } +} diff --git a/Server/Rooms/RoomBan.cs b/Server/Rooms/RoomBan.cs new file mode 100644 index 0000000..c3f49ce --- /dev/null +++ b/Server/Rooms/RoomBan.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Rooms +{ + + + + + + + + public class RoomBan + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long BanId { get; set; } + + public long roomId { get; set; } + + + public long actorId { get; set; } + + + public long bannedPlayerId { get; set; } + + + public string bannedByName { get; set; } = ""; + + public string reason { get; set; } = ""; + + public DateTime bannedAt { get; set; } = DateTime.UtcNow; + + + + public DateTime? expiresAt { get; set; } + } +} diff --git a/Server/Rooms/RoomComment.cs b/Server/Rooms/RoomComment.cs new file mode 100644 index 0000000..d2a1c4c --- /dev/null +++ b/Server/Rooms/RoomComment.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomComment + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long RoomId { get; set; } + + public long CreatorAccountId { get; set; } + + public string Comment { get; set; } = string.Empty; + + public string CommentType { get; set; } = "Feedback"; + + public float? PositionX { get; set; } + public float? PositionY { get; set; } + public float? PositionZ { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Server/Rooms/RoomConsumablesAPI.cs b/Server/Rooms/RoomConsumablesAPI.cs new file mode 100644 index 0000000..796d668 --- /dev/null +++ b/Server/Rooms/RoomConsumablesAPI.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.API.Rooms; +using RadicadeServer.API.Rooms.V1; +using RadicadeServer.DB; + +namespace RadicadeServer.API.Rooms +{ + [Route("api/roomconsumables/v1")] + [Route("econ/api/roomconsumables/v1")] + public class RoomConsumablesAPI(RecDBContext db) : Controller + { + [HttpGet("roomConsumable/room/{roomId:long}")] + public async Task GetRoomConsumablesInRoom(long roomId) + { + return Ok(await RoomAssetStore.GetDtosAsync(db, roomId, "consumable")); + } + + [HttpGet("roomConsumable/room/{roomId:long}/me")] + public IActionResult GetOwnedRoomConsumablesInRoom(long roomId) + { + return Ok(Array.Empty()); + } + + [HttpPut("roomConsumable")] + [HttpPost("roomConsumable")] + public async Task CreateRoomConsumable() + { + var body = await RoomEconomyPayload.ReadBodyAsync(Request); + var roomId = await RoomEconomyPayload.RoomIdAsync(db, Request, body, "roomId", "RoomId"); + if (roomId is not > 0) return BadRequest(new { Message = "roomId is required." }); + var consumableId = RoomEconomyPayload.String(body, "consumableId", "ConsumableId") ?? RoomEconomyPayload.NewId(); + + var consumable = RoomEconomyPayload.Fields(body); + consumable["ConsumableId"] = consumableId; + consumable["RoomId"] = roomId.Value; + consumable["Name"] = RoomEconomyPayload.String(body, "name", "Name") ?? "Consumable"; + consumable["Description"] = RoomEconomyPayload.String(body, "description", "Description") ?? string.Empty; + + await RoomAssetStore.SaveAsync(db, new RoomAssetRecord + { + Id = consumableId, + RoomId = roomId.Value, + Kind = "consumable", + Fields = consumable + }); + + return Ok(consumable); + } + } +} diff --git a/Server/Rooms/RoomCurrencyAPI.cs b/Server/Rooms/RoomCurrencyAPI.cs new file mode 100644 index 0000000..e9e08ad --- /dev/null +++ b/Server/Rooms/RoomCurrencyAPI.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.API.Rooms; +using RadicadeServer.API.Rooms.V1; +using RadicadeServer.DB; +using RadicadeServer.Extensions; + +namespace RadicadeServer.API.Rooms.Currency.V1 +{ + [Route("api/roomcurrencies/v1")] + [Route("econ/api/roomcurrencies/v1")] + public class RoomCurrencyAPI(RecDBContext db) : Controller + { + [HttpGet("betaEnabled")] + public bool IsBetaEnabled() + { + return true; + } + + [HttpGet("currencies")] + public async Task GetRoomCurrencies([FromQuery] long roomId) + { + return Ok(await RoomAssetStore.GetDtosAsync(db, roomId, "currency")); + } + + [HttpGet("getAllBalances")] + public List GetAllBalances([FromQuery] long roomId) + { + return []; + } + + [HttpGet("getBalance")] + public IActionResult GetBalance( + [FromQuery] string? currencyId, + [FromQuery] long? accountId, + [FromQuery] long? roomId) + { + return Ok(new + { + CurrencyId = currencyId ?? string.Empty, + AccountId = accountId ?? User.GetUserId() ?? 0, + RoomId = roomId ?? 0, + Balance = 0 + }); + } + + [HttpPost("createCurrency")] + public async Task CreateCurrency() + { + var body = await RoomEconomyPayload.ReadBodyAsync(Request); + var roomId = await RoomEconomyPayload.RoomIdAsync(db, Request, body, "roomId", "RoomId"); + if (roomId is not > 0) return BadRequest(new { Message = "roomId is required." }); + var currencyId = RoomEconomyPayload.String(body, "currencyId", "CurrencyId") ?? RoomEconomyPayload.NewId(); + + var currency = RoomEconomyPayload.Fields(body); + currency["CurrencyId"] = currencyId; + currency["RoomId"] = roomId.Value; + currency["CurrencyType"] = currency.TryGetValue("CurrencyType", out var type) && type != null ? type : "RoomCurrency"; + currency["Name"] = RoomEconomyPayload.String(body, "name", "Name") ?? "Currency"; + currency["Description"] = RoomEconomyPayload.String(body, "description", "Description") ?? string.Empty; + currency["InitialBalance"] = RoomEconomyPayload.Long(body, "initialBalance", "InitialBalance") ?? 0; + + await RoomAssetStore.SaveAsync(db, new RoomAssetRecord + { + Id = currencyId, + RoomId = roomId.Value, + Kind = "currency", + Fields = currency + }); + + return Ok(currency); + } + } +} diff --git a/Server/Rooms/RoomData.cs b/Server/Rooms/RoomData.cs new file mode 100644 index 0000000..ad0fb17 --- /dev/null +++ b/Server/Rooms/RoomData.cs @@ -0,0 +1,92 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Radicade.Gifts.Enums; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomData + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long RoomId { get; set; } + + public string Tags { get; set; } = string.Empty; + + public bool IsDorm { get; set; } = false; + + public int MaxPlayers { get; set; } = 10; + + public MaxPlayerMode MaxPlayerCalculationMode { get; set; } = MaxPlayerMode.OnlyEntrySubrooms; + + public bool CloningAllowed { get; set; } = false; + + public bool DisableMicAutoMute { get; set; } = false; + public bool DisableRoomComments { get; set; } = false; + + public bool EncryptVoiceChat { get; set; } = false; + + public string Name { get; set; } = ""; + public string? Description { get; set; } = ""; + public string ImageName { get; set; } = "default.png"; + + public WarningMask WarningMask { get; set; } + public string? CustomWarning { get; set; } = null; + + public long CreatorAccountId { get; set; } = 1; + + public RoomState State { get; set; } = RoomState.Active; + public Accessibility Accessibility { get; set; } = Accessibility.Private; + + public bool SupportsLevelVoting { get; set; } = false; + + public bool IsRRO { get; set; } = false; + + public bool IsDeveloperOwned { get; set; } = false; + + public bool SupportsScreens { get; set; } = true; + public bool SupportsWalkVR { get; set; } = true; + public bool SupportsTeleportVR { get; set; } = true; + public bool SupportsVRLow { get; set; } = true; + public bool SupportsQuest2 { get; set; } = true; + public bool SupportsMobile { get; set; } = true; + public bool SupportsJuniors { get; set; } = true; + + public int MinLevel { get; set; } = 0; + + public DateTime CreatedAt { get; set; } = DateTime.MinValue; + + [JsonIgnore] public DateTime PublishedAt { get; set; } = DateTime.Now; + [JsonIgnore] public bool WasPublished = false; + + public string loadScreen { get; set; } = "[]"; + + public int Version { get; set; } = 1; + + public bool JoinGiftEnabled { get; set; } = false; + public bool JoinGiftOncePerPlayer { get; set; } = true; + public int JoinGiftVersion { get; set; } = 1; + public string JoinGiftTitle { get; set; } = "Room Visit Gift"; + public string JoinGiftMessage { get; set; } = "Thanks for visiting!"; + public string JoinGiftAvatarItem { get; set; } = ""; + public string JoinGiftEquipmentItem { get; set; } = ""; + public string JoinGiftConsumableItem { get; set; } = ""; + public int JoinGiftCurrencyAmount { get; set; } = 0; + public CurrencyType JoinGiftCurrencyType { get; set; } = CurrencyType.RecCenterTokens; + public int JoinGiftXp { get; set; } = 0; + public GiftContext JoinGiftContext { get; set; } = GiftContext.Game_Drop; + public Rarity JoinGiftRarity { get; set; } = Rarity.Common; + public AvatarItemType JoinGiftAvatarItemType { get; set; } = AvatarItemType.Outfit; + public string JoinGiftEquipmentModificationGuid { get; set; } = ""; + + /// + /// The account the join box is from, which the client shows on the box and reads off + /// RecNet's Gift.FromPlayerId. Zero means the room names no sender, as it did before this + /// was configurable. + /// + public long JoinGiftFromPlayerId { get; set; } = 0; + } +} diff --git a/Server/Rooms/RoomDetail.cs b/Server/Rooms/RoomDetail.cs new file mode 100644 index 0000000..1665819 --- /dev/null +++ b/Server/Rooms/RoomDetail.cs @@ -0,0 +1,22 @@ +using RadicadeServer.RecNet.Enums.Rooms; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomDetail : Room + { + public long RoomId { get; set; } + + public bool IsDorm { get; set; } + + public int MaxPlayers { get; set; } + + public MaxPlayerMode MaxPlayerCalculationMode { get; set; } + + public bool CloningAllowed { get; set; } + + public bool DisableMicAutoMute { get; set; } + public bool DisableRoomComments { get; set; } + + public bool EncryptVoiceChat { get; set; } + } +} diff --git a/Server/Rooms/RoomEarningsAPI.cs b/Server/Rooms/RoomEarningsAPI.cs new file mode 100644 index 0000000..4cb89f4 --- /dev/null +++ b/Server/Rooms/RoomEarningsAPI.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.API.Rooms.Earnings.V1 +{ + [Route("api/roomEarningsDistributions/v1")] + [Route("econ/api/roomEarningsDistributions/v1")] + public class RoomEarningsAPI : Controller + { + [HttpGet("earningsDistribution/{roomId}")] + public IActionResult GetEarningsDistribution(long roomId) + { + + return Ok(new + { + RoomId = roomId, + CurrencyId = string.Empty, + TotalEarnings = 0, + TotalSpent = 0, + UnclaimedEarnings = 0, + LifetimeEarnings = 0, + IsAffiliatedWithCreator = false, + LastPaidAt = (string?)null, + Distributions = Array.Empty() + }); + } + } +} diff --git a/Server/Rooms/RoomEconomyPayload.cs b/Server/Rooms/RoomEconomyPayload.cs new file mode 100644 index 0000000..9d18688 --- /dev/null +++ b/Server/Rooms/RoomEconomyPayload.cs @@ -0,0 +1,228 @@ +using System.Security.Cryptography; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; + +namespace RadicadeServer.API.Rooms +{ + internal static class RoomEconomyPayload + { + private const string RawKey = "RoomEconomyPayload.Raw"; + + public static async Task ReadBodyAsync(HttpRequest request) + { + if (request.HasFormContentType) + { + try + { + var form = await request.ReadFormAsync(); + if (form.Count > 0) + { + var fields = new Dictionary(); + foreach (var field in form) + fields[field.Key] = field.Value.ToString(); + foreach (var file in form.Files) + fields[file.Name] = file.FileName; + return JsonSerializer.SerializeToElement(fields); + } + } + catch (Exception ex) + { + request.HttpContext.Items[RawKey] = $"
"; + } + } + + string raw; + using (var reader = new StreamReader(request.Body)) + raw = await reader.ReadToEndAsync(); + + var text = raw.TrimStart('\uFEFF', ' ', '\t', '\r', '\n'); + if (text.Length == 0) return null; + + if (text[0] is '{' or '[') + { + try + { + using var doc = JsonDocument.Parse(text); + return doc.RootElement.Clone(); + } + catch (JsonException) + { + } + } + + var pairs = ParsePairs(raw); + if (pairs.Count > 0) + { + request.HttpContext.Items[RawKey] = raw; + return JsonSerializer.SerializeToElement(pairs); + } + + request.HttpContext.Items[RawKey] = raw; + return null; + } + + public static string RawBody(HttpRequest request) => + request.HttpContext.Items.TryGetValue(RawKey, out var raw) ? raw as string ?? "" : ""; + + public static JsonElement? Value(JsonElement? body, params string[] names) + { + if (body == null) return null; + + var stack = new Stack(); + stack.Push(body.Value); + var visited = 0; + + while (stack.Count > 0 && visited++ < 2000) + { + var element = stack.Pop(); + if (element.ValueKind == JsonValueKind.Array) + { + foreach (var item in element.EnumerateArray()) stack.Push(item); + continue; + } + if (element.ValueKind != JsonValueKind.Object) continue; + + foreach (var property in element.EnumerateObject()) + { + if (property.Value.ValueKind is JsonValueKind.Object or JsonValueKind.Array) + { + stack.Push(property.Value); + continue; + } + if (Matches(property.Name, names)) return property.Value; + } + } + + return null; + } + + public static string? String(JsonElement? body, params string[] names) + { + var value = Value(body, names); + if (value == null) return null; + if (value.Value.ValueKind == JsonValueKind.String) return value.Value.GetString(); + if (value.Value.ValueKind is JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False) + return value.Value.ToString(); + return null; + } + + public static long? Long(JsonElement? body, params string[] names) + { + var value = Value(body, names); + if (value == null) return null; + if (value.Value.ValueKind == JsonValueKind.Number) + { + if (value.Value.TryGetInt64(out var number)) return number; + if (value.Value.TryGetDouble(out var real)) return (long)real; + } + if (value.Value.ValueKind == JsonValueKind.String && long.TryParse(value.Value.GetString(), out var parsed)) + return parsed; + return null; + } + + public static bool? Bool(JsonElement? body, params string[] names) + { + var value = Value(body, names); + if (value == null) return null; + if (value.Value.ValueKind == JsonValueKind.True) return true; + if (value.Value.ValueKind == JsonValueKind.False) return false; + return null; + } + + public static long? LongFromRequest(HttpRequest request, params string[] names) + { + foreach (var pair in request.Query) + { + if (!Matches(pair.Key, names)) continue; + if (long.TryParse(pair.Value.FirstOrDefault(), out var fromQuery)) return fromQuery; + } + foreach (var pair in request.RouteValues) + { + if (!Matches(pair.Key, names)) continue; + if (long.TryParse(pair.Value?.ToString(), out var fromRoute)) return fromRoute; + } + return null; + } + + public static Dictionary Fields(JsonElement? body) + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (body == null || body.Value.ValueKind != JsonValueKind.Object) return fields; + + foreach (var property in body.Value.EnumerateObject()) + { + fields[property.Name] = property.Value.ValueKind switch + { + JsonValueKind.String => property.Value.GetString(), + JsonValueKind.Number => property.Value.TryGetInt64(out var number) ? number : property.Value.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => property.Value.Clone() + }; + } + + return fields; + } + + public static async Task RoomIdAsync(RecDBContext db, HttpRequest request, JsonElement? body, params string[] names) + { + var roomId = Long(body, names) ?? LongFromRequest(request, names); + if (roomId is > 0) return roomId; + + var accountId = request.HttpContext.User.GetUserId(); + if (accountId is not > 0) return null; + var presence = await db.Presences.AsNoTracking() + .FirstOrDefaultAsync(item => item.playerId == accountId.Value); + if (presence?.roomInstance is not > 0) return null; + return await db.RoomInstances.AsNoTracking() + .Where(instance => instance.roomInstanceId == presence.roomInstance.Value) + .Select(instance => (long?)instance.roomId) + .FirstOrDefaultAsync(); + } + + public static string NewId() + { + var bytes = RandomNumberGenerator.GetBytes(16); + return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('='); + } + + private static Dictionary ParsePairs(string raw) + { + var pairs = new Dictionary(); + foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var at = pair.IndexOf('='); + var key = at < 0 ? pair : pair[..at]; + var value = at < 0 ? "" : pair[(at + 1)..]; + key = key.Trim(); + if (key.Length == 0) continue; + + try + { + pairs[Uri.UnescapeDataString(key.Replace('+', ' '))] = + Uri.UnescapeDataString(value.Replace('+', ' ')); + } + catch (UriFormatException) + { + pairs[key] = value; + } + } + return pairs; + } + + private static bool Matches(string name, string[] names) + { + foreach (var wanted in names) + { + if (string.Equals(Normalize(name), Normalize(wanted), StringComparison.Ordinal)) return true; + } + return false; + } + + private static string Normalize(string name) => + string.Concat(name.Where(char.IsLetterOrDigit)).ToLowerInvariant(); + } +} diff --git a/Server/Rooms/RoomFilters.cs b/Server/Rooms/RoomFilters.cs new file mode 100644 index 0000000..d5193c3 --- /dev/null +++ b/Server/Rooms/RoomFilters.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomFilters + { + public List PinnedFilters { get; set; } = null!; + public List PopularFilters { get; set; } = null!; + public List? TrendingFilters { get; set; } + } +} diff --git a/Server/Rooms/RoomInstance.cs b/Server/Rooms/RoomInstance.cs new file mode 100644 index 0000000..00566a9 --- /dev/null +++ b/Server/Rooms/RoomInstance.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums.Rooms; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomInstance + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long roomInstanceId { get; set; } + + public long roomId { get; set; } = 1; + public long subRoomId { get; set; } = 1; + + public RoomInstanceType roomInstanceType { get; set; } = RoomInstanceType.Dormroom; + + public string? location { get; set; } = ""; + + public string dataBlob { get; set; } = ""; + + public long? eventId { get; set; } = 0; + + public long? clubId { get; set; } = 0; + + public string? roomCode { get; set; } = ""; + + [NotMapped] + public string photonRegion { get; set; } = "us"; + public string photonRegionId { get; set; } = "us"; + public string photonRoomId { get; set; } = $"Radicade{Random.Shared.Next(232)}-{Guid.NewGuid().ToString().Replace("-", "")}"; + + public string name { get; set; } = "DormRoom"; + + public int maxCapacity { get; set; } = 4; + + public bool isFull { get; set; } = false; + public bool isPrivate { get; set; } = false; + public bool isInProgress { get; set; } = false; + public bool EncryptVoiceChat { get; set; } = false; + + public RoomInstance Copy() => (RoomInstance)MemberwiseClone(); + } + + public class FullRoomInstance : RoomInstance + { + public List playerIds { get; set; } = new(); + } +} + diff --git a/Server/Rooms/RoomJoinGift.cs b/Server/Rooms/RoomJoinGift.cs new file mode 100644 index 0000000..045c76e --- /dev/null +++ b/Server/Rooms/RoomJoinGift.cs @@ -0,0 +1,59 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Radicade.Gifts.Enums; + +namespace RadicadeServer.RecNet.Rooms +{ + /// + /// One box a room hands a player when they join it. A room used to be able to describe + /// exactly one box in its own Rooms row; a row here is one more box, and the panel shows + /// them as a list. A room with no rows here still has its one box - see + /// - so a room configured before this table existed + /// keeps handing out what it always did. + /// + public class RoomJoinGift + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long RoomJoinGiftId { get; set; } + + public long RoomId { get; set; } + + /// Where the box sits in the room's list. Boxes are delivered in this order. + public int SortOrder { get; set; } + + /// + /// Bumped whenever this box changes. The version is part of the box's gift drop id and + /// of a once-per-player claim, so a changed box is handed out again rather than being + /// suppressed by the claim a player already made on the old one. + /// + public int Version { get; set; } = 1; + + public bool Enabled { get; set; } = true; + + public bool OncePerPlayer { get; set; } = true; + + public string Title { get; set; } = "Room Visit Gift"; + public string Message { get; set; } = "Thanks for visiting!"; + + public string AvatarItem { get; set; } = ""; + public AvatarItemType AvatarItemType { get; set; } = AvatarItemType.Outfit; + + public string EquipmentItem { get; set; } = ""; + public string EquipmentModificationGuid { get; set; } = ""; + + public string ConsumableItem { get; set; } = ""; + + public CurrencyType CurrencyType { get; set; } = CurrencyType.RecCenterTokens; + public int CurrencyAmount { get; set; } = 0; + + public int Xp { get; set; } = 0; + + public GiftContext Context { get; set; } = GiftContext.Game_Drop; + public Rarity Rarity { get; set; } = Rarity.Common; + + /// The account the box is from, or zero for no sender. + public long FromPlayerId { get; set; } = 0; + } +} diff --git a/Server/Rooms/RoomKey.cs b/Server/Rooms/RoomKey.cs new file mode 100644 index 0000000..c9143cd --- /dev/null +++ b/Server/Rooms/RoomKey.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomKey + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long RoomKeyId { get; set; } + + public string ReplicationId { get; set; } = null!; + + public long RoomId { get; set; } + + public string Name { get; set; } = null!; + + public string Description { get; set; } = null!; + + public int Price { get; set; } + } +} diff --git a/Server/Rooms/RoomKeysAPI.cs b/Server/Rooms/RoomKeysAPI.cs new file mode 100644 index 0000000..8ccc1cb --- /dev/null +++ b/Server/Rooms/RoomKeysAPI.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Rooms; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.API.Rooms.Keys.V1 +{ + [Route("api/roomkeys/v1")] + [Route("econ/api/roomkeys/v1")] + public class RoomKeysAPI : Controller + { + private const int DefaultMaxKeysPerRoom = 500; + + RecDBContext db; + public RoomKeysAPI(RecDBContext context) { + this.db = context; + } + + [HttpGet("room")] + public IActionResult GetRoomKeysForRoom(long? roomId) + { + if (roomId is not > 0) + return Ok(Array.Empty()); + + return Ok( + db.RoomKeys.Where(roomKey => roomKey.RoomId == roomId.Value).ToList() + ); + } + + [HttpGet("mine")] + public async Task GetOwnedRoomkeys([FromQuery] long? roomId) + { + var userId = User.GetUserId(); + if (roomId == null || userId == null) return Ok(Array.Empty()); + var ownedIds = (await db.Unlocks.AsNoTracking() + .FirstOrDefaultAsync(unlocks => unlocks.AccountId == userId.Value))?.RoomKeys + .FromJson>() ?? new List(); + var keys = await db.RoomKeys.AsNoTracking() + .Where(key => key.RoomId == roomId.Value && ownedIds.Contains(key.RoomKeyId)) + .ToListAsync(); + return Ok(keys); + } + + [HttpGet("purchased/{roomKeyId:long}")] + public async Task IsRoomKeyPurchased(long roomKeyId) + { + var userId = User.GetUserId(); + if (userId == null || roomKeyId <= 0) return Ok(false); + + var roomKey = await db.RoomKeys.AsNoTracking() + .FirstOrDefaultAsync(key => key.RoomKeyId == roomKeyId); + if (roomKey == null) return Ok(false); + + var unlocks = await db.Unlocks.AsNoTracking() + .FirstOrDefaultAsync(row => row.AccountId == userId.Value); + var ownedIds = unlocks?.RoomKeys.FromJson>() ?? new List(); + return Ok(ownedIds.Contains(roomKeyId)); + } + + [HttpPost("create")] + [HttpPost("create/{roomId}")] + public async Task CreateRoomKey() + { + var body = await RoomEconomyPayload.ReadBodyAsync(Request); + + var roomId = await RoomEconomyPayload.RoomIdAsync(db, Request, body, "roomId", "roomKeyRoomId") + ?? ReadRoomIdFromReferrer(); + if (roomId is not > 0) return BadRequest(new { Message = "roomId is required." }); + + var key = new RoomKey + { + RoomId = roomId.Value, + Name = RoomEconomyPayload.String(body, "name", "keyName") ?? "Room Key", + Description = RoomEconomyPayload.String(body, "description") ?? string.Empty, + Price = (int)(RoomEconomyPayload.Long(body, "price", "tokenPrice", "cost") ?? 0), + ReplicationId = RoomEconomyPayload.String(body, "replicationId") ?? Guid.NewGuid().ToString() + }; + + var maxKeys = await MaxKeysPerRoomAsync(); + var existing = await db.RoomKeys.CountAsync(existing => existing.RoomId == roomId.Value); + if (existing >= maxKeys) + { + return BadRequest(new { Message = $"A room can have at most {maxKeys} keys." }); + } + + await db.RoomKeys.AddAsync(key); + await db.SaveChangesAsync(); + return Ok(key); + } + + private long? ReadRoomIdFromReferrer() + { + var referer = Request.Headers.Referer.FirstOrDefault(); + if (!Uri.TryCreate(referer, UriKind.Absolute, out var uri)) return null; + var parts = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries); + for (var index = parts.Length - 1; index >= 0; index--) + if (long.TryParse(parts[index], out var roomId) && roomId > 0) return roomId; + return null; + } + + private string Describe(System.Text.Json.JsonElement? body) + { + var raw = RoomEconomyPayload.RawBody(Request); + if (raw.Length == 0 && body == null) return "(empty)"; + if (raw.Length == 0) raw = body?.ToString() ?? ""; + return raw.Length <= 1000 ? raw : raw[..1000] + "..."; + } + + private async Task MaxKeysPerRoomAsync() + { + var config = await db.Config.FirstOrDefaultAsync(); + var limit = config?.RoomKeyConfig?.FromJson()?.MaxKeysPerRoom ?? 0; + return limit > 0 ? limit : DefaultMaxKeysPerRoom; + } + + } +} diff --git a/Server/Rooms/RoomLoadScreen.cs b/Server/Rooms/RoomLoadScreen.cs new file mode 100644 index 0000000..52a2f22 --- /dev/null +++ b/Server/Rooms/RoomLoadScreen.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomLoadScreen + { + public long RoomId { get; set; } + public string ImageName { get; set; } = null!; + public string Title { get; set; } = null!; + public string Subtitle { get; set; } = null!; + } +} diff --git a/Server/Rooms/RoomPlayerData.cs b/Server/Rooms/RoomPlayerData.cs new file mode 100644 index 0000000..707fe90 --- /dev/null +++ b/Server/Rooms/RoomPlayerData.cs @@ -0,0 +1,7 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomPlayerData + { + public string data { get; set; } = null!; + } +} diff --git a/Server/Rooms/RoomPlaylistAPI.cs b/Server/Rooms/RoomPlaylistAPI.cs new file mode 100644 index 0000000..dc719dd --- /dev/null +++ b/Server/Rooms/RoomPlaylistAPI.cs @@ -0,0 +1,206 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.DB; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace RadicadeServer.RoomServer +{ + [Route("rooms")] + public class RoomPlaylistAPI(RecDBContext db) : Controller + { + private async Task GetBestOfPlaylist() + { + var topRooms = await db.Rooms + .Select(r => new + { + r.RoomId, + r.CreatorAccountId, + Engagement = db.RoomInteractions + .Where(i => i.RoomId == r.RoomId) + .Sum(i => (i.Favorited ? 1 : 0) + (i.Cheered ? 1 : 0)) + }) + .OrderByDescending(x => x.Engagement) + .ToListAsync(); + + var convertedRooms = new List(); + var tags = new List(); + foreach (var foundRoom in topRooms) + { + var room = await RoomUtils.GetRoom(foundRoom.RoomId); + + if (room == null || room.Accessibility != RecNet.Enums.Accessibility.Public || room.IsDorm) continue; + + convertedRooms.Add(room); + foreach (var tag in room.Tags) + tags.Add(tag); + } + + if (convertedRooms.Count <= 0) + return null; + + var mainRoom = new PlaylistRoom(convertedRooms[0]); + + return mainRoom; + } + + [HttpGet("playlists/{playlistId}")] + public async Task GetPlaylistRooms(long playlistId) + { + List convertedRooms; + var tags = new List(); + + if (playlistId == 24226488) + { + var mainRoom = await GetBestOfPlaylist(); + if (mainRoom == null) return null; + convertedRooms = mainRoom.Rooms; + tags = mainRoom.Tags; + } + else + { + var playlist = await db.Playlists.FirstOrDefaultAsync(x => x.PlaylistId == playlistId); + if (playlist == null) return null; + + var keywords = playlist.GetKeywords(); + var foundRooms = await db.Rooms + .Where(room => room.Accessibility == RecNet.Enums.Accessibility.Public + && keywords.Any(k => room.Name.Contains(k) || room.Tags.Contains(k))) + .ToListAsync(); + + convertedRooms = new List(); + foreach (var foundRoom in foundRooms) + { + var room = await RoomUtils.GetRoom(foundRoom.RoomId); + if (room != null) + { + convertedRooms.Add(room); + foreach (var tag in room.Tags) + tags.Add(tag); + } + } + } + + var roomDtos = convertedRooms.Select(room => ToPlaylistRoomDict(room, playlistId)).ToArray(); + + var tagDtos = tags + .Select(t => new Dictionary { ["Tag"] = t.Tag, ["Type"] = (int)t.Type }) + .ToArray(); + + var root = ToPlaylistRoomDict(convertedRooms.FirstOrDefault(), playlistId); + + root["Rooms"] = roomDtos; + root["Tags"] = tagDtos; + root["community"] = Array.Empty(); + root["junior"] = Array.Empty(); + root["pickup"] = Array.Empty(); + root["screen"] = Array.Empty(); + root["teleportvr"] = Array.Empty(); + root["walkvr"] = Array.Empty(); + root["PinnedFilters"] = Array.Empty(); + root["PopularFilters"] = Array.Empty(); + root["TrendingFilters"] = Array.Empty(); + + return root; + } + + private static Dictionary ToPlaylistRoomDict(FullRoom? room, long playlistId) + { + var dict = new Dictionary + { + ["AnnouncementId"] = 0L, + ["AnnouncementType"] = 0, + ["Body"] = "", + ["LinkName"] = "", + ["LinkType"] = 0, + ["LinkUri"] = "", + ["Platform"] = 0, + ["Title"] = "", + ["PlaylistId"] = playlistId, + ["RoomId"] = 0L, + ["IsDorm"] = false, + ["MaxPlayers"] = 0, + ["MaxPlayerCalculationMode"] = 0, + ["CloningAllowed"] = false, + ["DisableMicAutoMute"] = false, + ["DisableRoomComments"] = false, + ["EncryptVoiceChat"] = false, + ["Name"] = "", + ["Description"] = "", + ["ImageName"] = "", + ["WarningMask"] = 0, + ["CustomWarning"] = "", + ["CreatorAccountId"] = 0L, + ["State"] = 0, + ["Accessibility"] = 1, + ["SupportsLevelVoting"] = false, + ["IsRRO"] = false, + ["SupportsScreens"] = false, + ["SupportsWalkVR"] = false, + ["SupportsTeleportVR"] = false, + ["SupportsVRLow"] = false, + ["SupportsQuest2"] = false, + ["SupportsMobile"] = false, + ["SupportsJuniors"] = false, + ["MinLevel"] = 0, + ["CreatedAt"] = DateTime.UnixEpoch, + ["Stats"] = new Dictionary + { + ["RoomId"] = 0L, + ["CheerCount"] = 0, + ["FavoriteCount"] = 0, + ["VisitorCount"] = 0, + ["VisitCount"] = 0 + }, + ["Version"] = 1, + ["SubRooms"] = Array.Empty(), + ["Roles"] = Array.Empty(), + ["LoadScreens"] = Array.Empty(), + ["PromoImages"] = Array.Empty(), + ["PromoExternalContent"] = Array.Empty(), + ["DataBlob"] = "", + ["Tags"] = Array.Empty() + }; + + if (room != null) + { + var element = System.Text.Json.JsonSerializer.SerializeToElement(room, room.GetType()); + var roomDict = System.Text.Json.JsonSerializer.Deserialize>(element) ?? new Dictionary(); + foreach (var kv in roomDict) + dict[kv.Key] = kv.Value; + } + + return dict; + } + + [HttpGet("rooms/recommendations")] + public async Task?> GetRecommendedRooms() + { + var topRooms = await db.Rooms + .Select(r => new + { + r.RoomId, + r.CreatorAccountId, + Engagement = db.RoomInteractions + .Where(i => i.RoomId == r.RoomId) + .Sum(i => (i.Favorited ? 1 : 0) + (i.Cheered ? 1 : 0)) + }) + .OrderByDescending(x => x.Engagement) + .Take(10) + .ToListAsync(); + + var convertedRooms = new List(); + foreach (var room in topRooms) + { + var converted = await RoomUtils.GetRoom(room.RoomId); + if (converted != null && converted.Accessibility == RecNet.Enums.Accessibility.Public && !converted.IsDorm) + convertedRooms.Add(converted); + } + + return convertedRooms; + } + } +} diff --git a/Server/Rooms/RoomProgressionService.cs b/Server/Rooms/RoomProgressionService.cs new file mode 100644 index 0000000..4c1ecab --- /dev/null +++ b/Server/Rooms/RoomProgressionService.cs @@ -0,0 +1,203 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Progression; + +namespace RadicadeServer.Utils +{ + public static class RoomProgressionService + { + public const int XpPerInterval = 10; + + public static readonly TimeSpan Interval = TimeSpan.FromMinutes(5); + + public const int RoomXpCap = 60; + + public const int DailyXpCap = 150; + + public static readonly TimeSpan DailyCapCooldown = TimeSpan.FromHours(24); + + public const string ProgressionLevelUpdateNotificationId = "PlayerProgressionLevelUpdate"; + + public static async Task AwardRoomXpAsync(RecDBContext db, long playerId, long roomId, bool isPublicRoom) + { + if (playerId <= 0 || roomId <= 0 || !isPublicRoom) return 0; + + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + if (progression == null) + { + progression = new ProgressionData { PlayerId = playerId }; + await db.Progression.AddAsync(progression); + } + + var json = progression.GetProgressionJson() ?? new ProgressionJson(); + var now = DateTime.UtcNow; + + if (json.XpEarnedTodayDate != now.Date) + { + json.XpEarnedToday = 0; + json.XpEarnedTodayDate = now.Date; + json.XpDailyCapReachedAt = null; + } + + if (json.XpEarnedToday >= DailyXpCap) + { + if (json.XpDailyCapReachedAt is { } reachedAt && now >= reachedAt.Add(DailyCapCooldown)) + { + json.XpEarnedToday = 0; + json.XpEarnedTodayDate = now.Date; + json.XpDailyCapReachedAt = null; + } + else + { + json.XpDailyCapReachedAt ??= now; + progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + return 0; + } + } + + if (json.RoomXpRoomId != roomId) + { + json.RoomXpRoomId = roomId; + json.RoomXpEarned = 0; + json.RoomXpLastGrantAt = now; + progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + return 0; + } + + if (json.RoomXpEarned >= RoomXpCap) + { + progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + return 0; + } + + json.RoomXpLastGrantAt ??= now; + + int granted = 0; + while (now - json.RoomXpLastGrantAt.Value >= Interval) + { + int roomHeadroom = RoomXpCap - (json.RoomXpEarned + granted); + int dailyHeadroom = DailyXpCap - (json.XpEarnedToday + granted); + int instalment = Math.Min(XpPerInterval, Math.Min(roomHeadroom, dailyHeadroom)); + if (instalment <= 0) break; + + granted += instalment; + json.RoomXpLastGrantAt = json.RoomXpLastGrantAt.Value.Add(Interval); + } + + if (granted == 0 && now - json.RoomXpLastGrantAt.Value >= Interval) + json.RoomXpLastGrantAt = now; + + if (granted == 0) + { + progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + return 0; + } + + int applied = (int)Math.Min(granted, int.MaxValue); + + json.RoomXpEarned += applied; + json.XpEarnedToday += applied; + json.XpEarnedTodayDate = now.Date; + json.LastXpEarnedAt = now; + if (json.XpEarnedToday >= DailyXpCap) + json.XpDailyCapReachedAt ??= now; + + int oldLevel = progression.Level; + progression.XP = (int)Math.Min(progression.XP + applied, int.MaxValue); + progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + + await StorefrontShared.SendXpBalanceUpdateAsync(playerId, + LevelUpGiftService.XpIntoCurrentLevel(progression), applied, + StorefrontBalanceAddTypes.DirectBalanceWithMultiplier); + + int computedLevel = LevelUpGiftService.ComputeLevel(LevelUpGiftService.ParseMap(), progression.XP); + if (computedLevel > progression.Level) progression.Level = computedLevel; + + await LevelUpGiftService.DeliverLevelUpGiftsAsync(db, playerId, + recomputeFromXp: true, fromLevel: oldLevel, notify: true); + + await SendProgressionLevelUpdateAsync(db, playerId, progression); + + if (progression.Level > oldLevel) + { + await PushProgressionUpdateAsync(db, playerId); + } + + return applied; + } + + public static async Task SendProgressionLevelUpdateAsync(RecDBContext db, long playerId, + ProgressionData? progression = null) + { + if (playerId <= 0) return; + + try + { + progression ??= await db.Progression.AsNoTracking() + .FirstOrDefaultAsync(x => x.PlayerId == playerId); + if (progression == null) return; + + var notification = new Notification + { + Id = ProgressionLevelUpdateNotificationId, + Msg = new PlayerProgressionLevelUpdateNotification(progression) + }; + + await Notify.SendNotify(playerId, notification); + + await Notify.SendToSubscribers(playerId, notification); + } + catch (Exception) + { + } + } + + public static async Task PushProgressionUpdateAsync(RecDBContext db, long playerId) + { + await SendProgressionLevelUpdateAsync(db, playerId); + + try + { + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == playerId); + if (profile == null) return; + + var plrData = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == playerId); + + var progression = await db.Progression.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerId == playerId); + if (progression != null) + await StorefrontShared.SendXpBalanceUpdateAsync(playerId, + LevelUpGiftService.XpIntoCurrentLevel(progression), 0, + StorefrontBalanceAddTypes.LevelUp); + + await Notify.SendNotify(playerId, new Notification + { + Id = "AccountUpdate", + Msg = profile + }); + + await Notify.UpdateSubscriptions(playerId, Notifs.SubscriptionUpdateProfile, profile); + + if (plrData != null) + await Notify.SendNotify(playerId, new Notification + { + Id = "SelfAccountUpdate", + Msg = PlayerUtils.BuildSelfAccount(profile, plrData) + }); + } + catch (Exception) + { + } + } + } +} diff --git a/Server/Rooms/RoomRole.cs b/Server/Rooms/RoomRole.cs new file mode 100644 index 0000000..e84470f --- /dev/null +++ b/Server/Rooms/RoomRole.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Enums.Rooms; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomRole + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), JsonIgnore] + public long RoleId { get; set; } + [JsonIgnore] + public long RoomId { get; set; } + + public long AccountId { get; set; } + public RoleType Role { get; set; } + public RoleType InvitedRole { get; set; } + + public long? LastChangedByAccountId { get; set; } + } +} diff --git a/Server/Rooms/RoomRoleResponse.cs b/Server/Rooms/RoomRoleResponse.cs new file mode 100644 index 0000000..e510976 --- /dev/null +++ b/Server/Rooms/RoomRoleResponse.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomRoleResponse + { + public string context { get; set; } = null!; + public long? creatorPlayerCoOwnerRoomPermission { get; set; } + public long? creatorPlayerHostRoomPermission { get; set; } + public long? creatorPlayerModeratorRoomPermission { get; set; } + public long? creatorPlayerRole { get; set; } + public long roomId { get; set; } + } +} diff --git a/Server/Rooms/RoomSearchResult.cs b/Server/Rooms/RoomSearchResult.cs new file mode 100644 index 0000000..dc1f559 --- /dev/null +++ b/Server/Rooms/RoomSearchResult.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomSearchResult + { + public long TotalResults { get; set; } + public List Results { get; set; } = null!; + } +} diff --git a/Server/Rooms/RoomServerAPI.cs b/Server/Rooms/RoomServerAPI.cs new file mode 100644 index 0000000..abf92e0 --- /dev/null +++ b/Server/Rooms/RoomServerAPI.cs @@ -0,0 +1,1860 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SixLabors.ImageSharp; +using System; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.API.Studio; +using RadicadeServer.Radicade.Discord; +using RadicadeServer.Radicade.Discord.Templates; +using RadicadeServer.Utils; + +namespace RadicadeServer.RoomServer +{ + [Route("rooms/")] + public class RoomServerAPI : Controller + { + private const int RoomSearchHardLimit = 50; + + RecDBContext db; + public RoomServerAPI(RecDBContext context) + { + this.db = context; + } + + + + + + + + + + + + + [HttpGet("showcase/{playerId}")] + public async Task> GetPlayerShowcaseRooms(long playerId) + { + var roomIds = await db.RoomRoles + .Where(x => x.AccountId == playerId && x.Role >= RoleType.CoOwner) + .Select(x => x.RoomId) + .ToListAsync(); + + if (roomIds.Count == 0) + { + roomIds = await db.Rooms + .Where(x => x.CreatorAccountId == playerId && x.Accessibility >= Accessibility.Public) + .Select(x => x.RoomId) + .ToListAsync(); + } + + var showcaseRooms = await RoomUtils.VisibleRooms( + await db.Rooms.Where(x => roomIds.Contains(x.RoomId)).ToListAsync(), + User.GetUserId()); + + return Ok(showcaseRooms.Select(x => x.RoomId).Distinct().ToArray()); + } + + [HttpDelete("rooms/{roomId}"), Authorize] + public async Task DeleteRoom(long roomId) + { + var userId = User.GetUserId(); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return new Success(false, "Room does not exist.", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.Creator); + if (!canModify) return Success.Error("Insufficient permissions"); + + var roles = await db.RoomRoles.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var role in roles) + { + db.RoomRoles.Remove(role); + } + + var interactions = await db.RoomInteractions.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var i in interactions) + { + db.RoomInteractions.Remove(i); + } + + var keys = await db.RoomKeys.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var key in keys) + { + db.RoomKeys.Remove(key); + } + + var stats = await db.RoomStats.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (stats != null) + db.RoomStats.Remove(stats); + + var subrooms = await db.SubRooms.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var subroom in subrooms) + { + db.SubRooms.Remove(subroom); + } + + var prevSubrooms = await db.SubRoomHistory.Where(x => x.RoomId == roomId).ToListAsync(); + foreach (var subroom in prevSubrooms) + { + db.SubRoomHistory.Remove(subroom); + } + + db.Rooms.Remove(room); + + await db.SaveChangesAsync(); + + return new Success(true, null, null); + } + + [HttpDelete("rooms/{roomId}/subrooms/{subroomId}"), Authorize] + public async Task DeleteSubRoom(long roomId, long subroomId) + { + var userId = User.GetUserId(); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return new Success(false, "Room does not exist.", null); + + var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subroomId && x.RoomId == roomId); + if (subroom == null) return new Success(false, "Subroom does not exist.", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.Creator); + if (!canModify) return Success.Error("Insufficient permissions"); + + var history = await db.SubRoomHistory.Where(x => x.RoomId == roomId && x.SubRoomId == subroomId).ToListAsync(); + foreach (var historyItem in history) + { + db.SubRoomHistory.Remove(historyItem); + } + + db.SubRooms.Remove(subroom); + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, null); + } + + [HttpGet("rooms/{roomId}/playerdata/me")] + public ActionResult GetRoomPlayerData() + { + return Ok(new RoomPlayerData { data = "" }); + } + + [HttpGet("rooms/curated_playlists")] + public async Task GetCuratedPlaylists() + { + var playlistIds = await db.Playlists + .OrderBy(x => x.PlaylistId) + .Select(x => x.PlaylistId) + .ToListAsync(); + + if (playlistIds.Count == 0) + playlistIds.Add(24226488); + + return Ok(playlistIds.Distinct().ToArray()); + } + + [HttpGet("rooms/moderatedby/me"), Authorize] + public async Task?> GetRoomsModeratedByMe() + { + long? userId = User.GetUserId(); + if (userId == null) return null; + + var roles = await db.RoomRoles.Where(x => x.AccountId == userId && x.Role >= RoleType.Moderator).ToListAsync(); + var rooms = new List(); + foreach (var role in roles) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == role.RoomId); + if (room != null) rooms.Add(room); + } + + return await RoomUtils.VisibleRooms(rooms, userId); + } + + [HttpGet("rooms/visitedby/{playerId:long}"), Authorize] + public async Task GetVisitedRoomsForPlayer(long playerId, [FromQuery] int skip = 0, [FromQuery] int take = 10) + { + var viewerId = User.GetUserId(); + + skip = Math.Max(skip, 0); + take = Math.Clamp(take, 1, 100); + + var roomIds = await db.RoomInteractions + .Where(x => x.PlayerId == playerId && x.Visited) + .OrderByDescending(x => x.LastVisted) + .Select(x => x.RoomId) + .Distinct() + .Skip(skip) + .Take(take) + .ToListAsync(); + + var rooms = new List(); + foreach (var roomId in roomIds) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room != null) rooms.Add(room); + } + + rooms = await RoomUtils.VisibleRooms(rooms, viewerId); + var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); + return Ok(convert); + } + + [HttpGet("rooms/visitedby/me"), Authorize] + public async Task GetVisitedRooms([FromQuery] int skip = 0, [FromQuery] int take = 100) + { + long? userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + skip = Math.Max(skip, 0); + take = Math.Clamp(take, 1, 100); + + var roomIds = await db.RoomInteractions + .Where(x => x.PlayerId == userId && x.Visited) + .OrderByDescending(x => x.LastVisted) + .Select(x => x.RoomId) + .Distinct() + .Skip(skip) + .Take(take) + .ToListAsync(); + + var rooms = new List(); + foreach (var roomId in roomIds) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room != null) rooms.Add(room); + } + + rooms = await RoomUtils.VisibleRooms(rooms, userId); + var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); + return Ok(convert); + } + + [HttpGet("rooms/topcreators")] + public async Task?> GetRoomsFromTopCreators() + { + var topCreators = await db.RoomInteractions.Where(i => i.Favorited) + .Join(db.Rooms, + interaction => interaction.RoomId, + room => room.RoomId, + (interaction, room) => new + { + room.CreatorAccountId + }).GroupBy(x => x.CreatorAccountId) + .Select(g => new + { + CreatorId = g.Key, + FavoriteCount = g.Count() + }).OrderByDescending(x => x.FavoriteCount).Take(10).Select(x => x.CreatorId).ToListAsync(); + + if (!topCreators.Any()) + return []; + + var rooms = await db.Rooms.Where(r => topCreators.Contains(r.CreatorAccountId)).OrderByDescending(r => r.RoomId).Take(10 * 3).ToListAsync(); + rooms.RemoveAll(x => x.Accessibility != Accessibility.Public); + + return await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); + } + + [HttpGet("rooms/bulk")] + public async Task ?> GetBulkRooms([FromQuery] string? name, [FromQuery] List? id) + { + List rooms = new List(); + + if (name != null) + { + rooms = await db.Rooms.Where(x => x.Name.Contains(name)).ToListAsync(); + } else if (id != null) + { + rooms.Clear(); + foreach (var i in id) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == i); + if (room != null) + rooms.Add(room); + } + } + + return await RoomUtils.VisibleRooms(rooms, User.GetUserId()); + } + [HttpGet("featuredrooms/current")] + public async Task GetCurrentFeaturedRooms() + { + var candidates = await db.Rooms.AsNoTracking() + .Where(x => x.State == RoomState.Active && x.Tags.ToLower().Contains("featured")) + .OrderBy(x => x.RoomId) + .ToListAsync(); + + var featured = candidates + .Where(room => StringTools.ReadTags(room.Tags) + .Any(tag => string.Equals(tag.Tag, "featured", StringComparison.OrdinalIgnoreCase))) + .ToList(); + + featured = await RoomUtils.VisibleRooms(featured, User.GetUserId()); + + if (featured.Count == 0) + { + return NotFound(); + } + + return Ok(new FeaturedRoomGroup + { + FeaturedRoomGroupId = 1, + Name = "Featured Rooms", + Rooms = featured.Select(room => new FeaturedRoom + { + RoomId = room.RoomId, + RoomName = room.Name, + ImageName = string.IsNullOrWhiteSpace(room.ImageName) ? room.Name : room.ImageName + }).ToList() + }); + } + + + + [HttpGet("roomsandplaylists/search")] + public async Task GetBulkRoomsOld([FromQuery] string query) + { + if (query == null) return null; + + query = query.Replace("#", "").Trim(); + + var rooms = await db.Rooms.Where(x => (x.Name.ToLower().Contains(query) || x.Tags.ToLower().Contains(query)) && x.Accessibility == Accessibility.Public).ToListAsync(); + var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); + return new RoomSearchResult { TotalResults = rooms.Count, Results = convert }; + } + + + + + + + + + [HttpGet("rooms/search_rooms/{source}")] + public async Task SearchRoomsBySource(string source, [FromQuery] int skip = -1, [FromQuery] int take = 20) + { + var (src, pSkip, pTake) = ParseEmbeddedQuery(source, 20); + if (skip < 0) skip = pSkip; + if (take == 20 && pTake != 20) take = pTake; + skip = Math.Max(skip, 0); + take = Math.Clamp(take, 1, 100); + + if (string.Equals(src, "MyClubhouses", StringComparison.OrdinalIgnoreCase) || + string.Equals(src, "MyClubs", StringComparison.OrdinalIgnoreCase) || + string.Equals(src, "Clubhouse", StringComparison.OrdinalIgnoreCase)) + { + return Ok(new RoomSearchResult { TotalResults = 0, Results = new List() }); + } + + IQueryable q; + if (!string.IsNullOrWhiteSpace(src) && !src.Contains('&')) + { + var kw = Uri.UnescapeDataString(src).Replace('+', ' ').Trim().Split(' ', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(k => k.TrimStart('#')).Where(k => k.Length > 0).ToList(); + q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active + && kw.All(k => x.Name.ToLower().Contains(k) || x.Tags.ToLower().Contains(k))); + } + else + { + q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active); + } + + var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); + var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); + return Ok(new RoomSearchResult { TotalResults = convert.Count, Results = convert }); + } + + private static (string Source, int Skip, int Take) ParseEmbeddedQuery(string? source, int defTake) + { + var src = source ?? string.Empty; + int skip = 0, take = defTake; + int amp = src.IndexOf('&'); + if (amp >= 0) + { + foreach (var pair in src.Substring(amp + 1).Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var kv = pair.Split('=', 2); + if (kv.Length == 2 && int.TryParse(kv[1], out var n)) + { + if (kv[0] == "skip") skip = n; + else if (kv[0] == "take") take = n; + } + } + src = src.Substring(0, amp); + } + return (Uri.UnescapeDataString(src), Math.Max(skip, 0), Math.Clamp(take, 1, 100)); + } + + [HttpGet("rooms/hot_rooms/{*source}")] + [HttpGet("hot_rooms/{*source}")] + public async Task GetHotRoomsPaged(string? source) + { + var (query, skip, take) = ParseEmbeddedQuery(source, 24); + query = query.Trim(); + + IQueryable q = db.Rooms + .Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active); + + if (!string.IsNullOrWhiteSpace(query) && query != "all") + { + var tags = query.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(t => t.TrimStart('#')).Where(t => t.Length > 0).ToList(); + if (tags.Count > 0) + q = q.Where(x => tags.All(t => x.Tags.Contains(t))); + } + + var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); + if (rooms.Count == 0 && skip == 0) + { + rooms = await db.Rooms + .Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active) + .OrderBy(x => x.RoomId).Take(take).ToListAsync(); + } + return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms)); + } + + [HttpGet("rooms/carousel/{*source}")] + public async Task GetCarouselRooms(string? source, [FromQuery] int skip = -1, [FromQuery] int take = 20) + { + var (src, pSkip, pTake) = ParseEmbeddedQuery(source, 20); + if (skip < 0) skip = pSkip; + if (take == 20 && pTake != 20) take = pTake; + skip = Math.Max(skip, 0); + take = Math.Clamp(take, 1, 100); + + var q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active); + + var keyword = src.Trim().TrimStart('#').ToLowerInvariant(); + if (keyword.Length > 0) + q = q.Where(x => x.Name.ToLower().Contains(keyword) || x.Tags.ToLower().Contains(keyword)); + + var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); + + if (rooms.Count == 0 && skip == 0) + { + rooms = await db.Rooms + .Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active) + .OrderBy(x => x.RoomId).Take(take).ToListAsync(); + } + + var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); + return Ok(new RoomSearchResult { Results = convert, TotalResults = convert.Count }); + } + + + [HttpGet("rooms/my_visited_rooms/{*source}"), Authorize] + [HttpGet("my_visited_rooms/{*source}"), Authorize] + public async Task GetMyVisitedRoomsPaged(string? source) + { + var userId = User.GetUserId(); + if (!userId.HasValue) return Unauthorized(); + var (_, skip, take) = ParseEmbeddedQuery(source, 24); + + var rooms = await db.Rooms + .Where(x => db.RoomInteractions.Any(i => i.PlayerId == userId && i.RoomId == x.RoomId) + && x.Accessibility == Accessibility.Public) + .OrderByDescending(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); + return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms)); + } + + [HttpGet("rooms/roomsbycreators/{*source}")] + [HttpGet("roomsbycreators/{*source}")] + public async Task GetRoomsByCreatorsPaged(string? source) + { + var (ids, skip, take) = ParseEmbeddedQuery(source, 24); + var creatorIds = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(s => long.TryParse(s, out var n) ? n : -1) + .Where(n => n > 0).ToList(); + + var rooms = creatorIds.Count == 0 + ? new List() + : await db.Rooms.Where(x => creatorIds.Contains(x.CreatorAccountId) + && x.Accessibility == Accessibility.Public && x.State == RoomState.Active) + .OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); + return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms)); + } + + [HttpGet("rooms/rro_ids")] + public async Task GetRroRoomIds() + { + var ids = await db.Rooms.Where(x => x.IsRRO && x.Accessibility == Accessibility.Public) + .OrderBy(x => x.RoomId).Select(x => x.RoomId).ToListAsync(); + return Ok(ids); + } + + [HttpGet("rooms/search")] + public async Task SearchRooms([FromQuery] string query) + { + if (query == null) return null; + + query = query.ToLower(); + query = query.Replace("+", " ").Trim(); + query = query.Replace("#", "").Trim(); + + var keywords = query.Split(' ', StringSplitOptions.TrimEntries); + + var rooms = await db.Rooms.Where(x => + keywords.All(k => (x.Name.ToLower().Contains(k) || x.Tags.ToLower().Contains(k)) && x.Accessibility == Accessibility.Public) + ).ToListAsync(); + var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); + + return new RoomSearchResult { TotalResults = rooms.Count, Results = convert }; + } + + [HttpGet("rooms/createdby/{who}"), Authorize] + public async Task GetRoomsBy(string who) + { + long? userId; + + if (who == "me") + userId = User.GetUserId(); + else + userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId; + if (userId == null) + userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; + if (userId == null) return Unauthorized(); + + var roles = await db.RoomRoles.Where(x => x.AccountId == userId && x.Role >= RoleType.CoOwner).ToListAsync(); + var rooms = new List(); + foreach (var role in roles) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == role.RoomId); + if (room != null) + rooms.Add(room); + } + + return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail( + await RoomUtils.VisibleRooms(rooms, User.GetUserId()))); + } + + [HttpGet("rooms/ownedby/{who}"), Authorize] + public async Task GetRoomsByOld(string who) + { + long? userId; + + if (who == "me") + userId = User.GetUserId(); + else + userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId; + if (userId == null) + userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; + if (userId == null) return Unauthorized(); + + var roomIds = await db.RoomRoles + .Where(x => x.AccountId == userId && x.Role >= RoleType.CoOwner) + .Select(x => x.RoomId) + .Distinct() + .ToListAsync(); + + var rooms = new List(); + foreach (var roomId in roomIds) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room != null) rooms.Add(room); + } + + return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail( + await RoomUtils.VisibleRooms(rooms, User.GetUserId()))); + } + + [HttpGet("rooms/hot")] + public async Task GetHotRooms(string? tag) + { + + if (tag == null) + { + var list = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.Name != "DormRoom").ToListAsync(); + var bulkConvert = await RoomUtils.BulkConvertRoomDataToRoomDetail(list); + return new RoomSearchResult { Results = bulkConvert, TotalResults = bulkConvert.Count }; + } + else + { + switch (tag) + { + default: + var list1 = await db.Rooms.Where(x => x.Tags.Contains(tag) && x.Accessibility == Accessibility.Public && !x.IsDorm).ToListAsync(); + if (list1.Count == 0) + { + list1 = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && !x.IsDorm).ToListAsync(); + } + var bulkConvert1 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list1); + return new RoomSearchResult { Results = bulkConvert1, TotalResults = bulkConvert1.Count }; + case "new": + var list2 = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && !x.IsDorm).OrderByDescending(x => x.PublishedAt).ToListAsync(); + var bulkConvert2 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list2); + return new RoomSearchResult { Results = bulkConvert2, TotalResults = bulkConvert2.Count }; + case "rro": + var list3 = await db.Rooms.Where(x => x.IsRRO && !x.IsDorm).OrderByDescending(x => x.PublishedAt).ToListAsync(); + var bulkConvert3 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list3); + return new RoomSearchResult { Results = bulkConvert3, TotalResults = bulkConvert3.Count }; + } + } + + } + + [HttpGet("roomsandplaylists/hot")] + public async Task GetHotRoomsOld() + { + + var list = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.Name != "DormRoom").ToListAsync(); + var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(list); + + return new RoomSearchResult { Results = convert, TotalResults = convert.Count }; + } + + [HttpPut("rooms/{id}/interactionby/me/favorite"), Authorize] + public async Task FavouriteRoom(long id) + { + var userId = User.GetUserId(); + + PlayerInteractionDB? interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); + if (interaction == null) + { + var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = true }; + + await db.RoomInteractions.AddAsync(favoriteResult); + await db.SaveChangesAsync(); + + interaction = favoriteResult; + } else + { + interaction.Favorited = true; + await db.SaveChangesAsync(); + } + + return new PlayerInteraction(interaction); + } + [HttpDelete("rooms/{id}/interactionby/me/favorite"), Authorize] + public async Task UnfavouriteRoom(long id) + { + var userId = User.GetUserId(); + + PlayerInteractionDB? interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); + if (interaction == null) + { + var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = false }; + + await db.RoomInteractions.AddAsync(favoriteResult); + await db.SaveChangesAsync(); + + interaction = favoriteResult; + } + else + { + interaction.Favorited = false; + await db.SaveChangesAsync(); + } + + return new PlayerInteraction(interaction); + } + + [HttpPut("rooms/{id}/interactionby/me/cheer"), Authorize] + public async Task CheerRoom(long id) + { + var userId = User.GetUserId(); + + var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); + if (interaction == null) + { + var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = true, Favorited = false }; + + await db.RoomInteractions.AddAsync(favoriteResult); + await db.SaveChangesAsync(); + + interaction = favoriteResult; + } + else + { + interaction.Cheered = true; + await db.SaveChangesAsync(); + } + + return new PlayerInteraction(interaction); + } + [HttpDelete("rooms/{id}/interactionby/me/cheer"), Authorize] + public async Task UncheerRoom(long id) + { + var userId = User.GetUserId(); + + var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); + if (interaction == null) + { + var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = false }; + + await db.RoomInteractions.AddAsync(favoriteResult); + await db.SaveChangesAsync(); + + interaction = favoriteResult; + } + else + { + interaction.Cheered = false; + await db.SaveChangesAsync(); + } + + return new PlayerInteraction(interaction); + } + + [HttpGet("rooms/favoritedby/{who}"), Authorize] + public async Task GetFavouritedBy(string who) + { + long? userId; + if (who == "me") + userId = User.GetUserId(); + else + userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId; + if (userId == null) + userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; + if (userId == null) return Unauthorized(); + + var roomIds = await db.RoomInteractions + .Where(x => x.PlayerId == userId && x.Favorited) + .OrderByDescending(x => x.FavoritedAt) + .Select(x => x.RoomId) + .Distinct() + .ToListAsync(); + + var rooms = new List(); + foreach (var roomId in roomIds) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room != null) rooms.Add(room); + } + + return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail( + await RoomUtils.VisibleRooms(rooms, User.GetUserId()))); + } + + [HttpGet("rooms/{id}/interactionby/{who}"), Authorize] + public async Task GetInteractionBy(long id, string who) + { + long? userId; + if (who == "me") + userId = User.GetUserId(); + else + userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId; + if (userId == null) + userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; + if (userId == null) return null; + + var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); + if (interaction == null) return new PlayerInteraction { Cheered = false, Favorited = false }; + + return new PlayerInteraction(interaction); + } + + [HttpGet("rooms/base")] + public async Task?> GetBaseRooms() + { + var roomData = await db.Rooms.Where(x => x.CloningAllowed && x.Accessibility != Accessibility.Private && x.CreatorAccountId == 1).ToListAsync(); + var rooms = new List(); + + foreach (var room in roomData) + { + var fullRoom = await RoomUtils.GetRoom(room.RoomId); + if (fullRoom == null) continue; + rooms.Add(fullRoom); + } + + return rooms; + } + + [HttpPost("rooms/{id}/clone"), Authorize] + public async Task CloneRoom(long id, [FromForm] string name) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "You are not signed in.", null); + + if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null); + + var source = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == id); + if (source == null) return new Success(false, "Unknown error", null); + + if (!source.CloningAllowed) + { + return new Success(false, "This room cannot be cloned.", null); + } + + if (await RoomUtils.IsHiddenFrom(source, userId)) + { + return new Success(false, "This room cannot be cloned.", null); + } + + var doesRoomExist = (await db.Rooms.FirstOrDefaultAsync(x => x.Name == name)) != null; + if (doesRoomExist) return new Success(false, "Room already exists.", null); + + var roomData = await RoomUtils.CloneRoom(id, (long)userId!, name); + if (roomData == null) return new Success(false, "Unknown error", null); + + return new Success(true, null, roomData); + } + + private sealed record RoomPermissionRow(string Permission, long Role, bool Override, int Type, string Value); + + private static List DefaultPermissions() + { + static RoomPermissionRow Row(string permission, long role, bool overridden) => + new(permission, role, overridden, 0, "True"); + + return new List + { + Row("CAN_USE_ROOM_RESET_BUTTON", 0, true), + Row("CAN_USE_DELETE_ALL_BUTTON", 0, true), + Row("CAN_SAVE_INVENTIONS", 0, true), + Row("CAN_SPAWN_INVENTIONS", 0, true), + Row("CAN_USE_PLAY_GIZMOS_TOGGLE", 0, true), + + + + Row("CAN_INVITE", 0, true), + Row("CAN_USE_MAKER_PEN", 30, false), + Row("CAN_USE_ROOM_RESET_BUTTON", 30, true), + Row("CAN_USE_DELETE_ALL_BUTTON", 30, true), + Row("CAN_SAVE_INVENTIONS", 30, true), + Row("CAN_SPAWN_INVENTIONS", 30, true), + Row("CAN_USE_PLAY_GIZMOS_TOGGLE", 30, true), + }; + } + + [HttpGet("photon_access_token")] + public async Task GetPhotonAccessToken() + { + long? userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var presence = await db.Presences + .AsNoTracking() + .FirstOrDefaultAsync(x => x.playerId == userId.Value); + + var permissions = DefaultPermissions(); + + var profile = await db.Profiles + .AsNoTracking() + .FirstOrDefaultAsync(x => x.accountId == userId.Value); + if (profile != null && (profile.HasRole("developer") || profile.HasRole("moderator"))) + permissions.Insert(0, new RoomPermissionRow("CAN_USE_MAKER_PEN", 0, true, 0, "True")); + + long? subRoomId = null; + if (presence?.roomInstance != null) + { + subRoomId = await db.RoomInstances + .AsNoTracking() + .Where(x => x.roomInstanceId == presence.roomInstance) + .Select(x => (long?)x.subRoomId) + .FirstOrDefaultAsync(); + } + + if (subRoomId != null) + { + var overrides = await db.SubRoomPermissions + .AsNoTracking() + .Where(x => x.subRoomId == subRoomId) + .OrderBy(x => x.Id) + .ToListAsync(); + + foreach (var stored in overrides) + { + var entry = new RoomPermissionRow(stored.permission, stored.role, true, stored.type, stored.value); + + var index = permissions.FindIndex(x => + x.Permission == entry.Permission && x.Role == entry.Role); + if (index == -1) permissions.Add(entry); + else permissions[index] = entry; + } + } + + return Ok(new Dictionary + { + ["Permissions"] = permissions, + ["PhotonAccessToken"] = "", + ["RoomInstanceId"] = presence?.roomInstance + }); + } + + [HttpGet("rooms/{id}")] + public async Task> GetRoomById(long id) + { + var roomData = db.Rooms.FirstOrDefault(x => x.RoomId == id); + if (roomData == null) + { + var userId = User.GetUserId(); + if (userId != null) + { + var dorm = await RoomUtils.GetOrCreateDorm((long)userId!); + if (dorm != null) + return Ok(dorm); + } + + return NotFound(); + } + + if (!await RoomUtils.CanSeeRoom(roomData, User.GetUserId())) + return roomData.IsDorm ? NotFound() : Ok(RoomUtils.MaskHiddenRoom(roomData)); + + var fullRoom = await RoomUtils.GetRoom(roomData.RoomId); + if (fullRoom == null) return NotFound(); + + + + return Ok(fullRoom); + } + + [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/datahistory"), Authorize] + public async Task>> GetRoomDataHistory(long roomId, long subRoomId) + { + var userId = User.GetUserId(); + + var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (roomData == null) return NotFound(); + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == (long)subRoomId); + if (subRoom == null) return NotFound(); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Forbid(); + + var history = await db.SubRoomHistory.Where(x => x.RoomId == roomId && x.SubRoomId == subRoomId).ToListAsync(); + return Ok(history); + } + + [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves"), Authorize] + public async Task GetSubRoomSaves(long roomId, long subRoomId, int? skip = null, int? take = null) + { + var (refusal, saves) = await LoadSubRoomSavesAsync(roomId, subRoomId); + if (refusal != null) return refusal; + + var page = PageSaves(saves, skip, take); + + return Ok(new { Results = page, TotalResults = saves.Count, TotalCount = saves.Count }); + } + + [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves/no_unity_assets"), Authorize] + public async Task GetSubRoomSavesWithoutUnityAssets(long roomId, long subRoomId, int? skip = null, int? take = null) + { + var (refusal, saves) = await LoadSubRoomSavesAsync(roomId, subRoomId); + if (refusal != null) return refusal; + + var page = PageSaves(saves, skip, take) + .Select(x => new + { + x.SubRoomDataSaveId, + x.SubRoomId, + x.DataBlob, + x.ReferencedUnityAssetIds, + x.PersistenceVersion, + x.OMVersion, + x.UgcSubVersion, + x.SavedByAccountId, + x.SavedOnPlatform, + x.SavedOnDeviceClass, + x.Description, + x.ModerationState, + x.CreatedAt, + x.UnityAssetId + }) + .ToList(); + + return Ok(new { Results = page, TotalResults = saves.Count, TotalCount = saves.Count }); + } + + private async Task<(IActionResult? Refusal, List Saves)> LoadSubRoomSavesAsync(long roomId, long subRoomId) + { + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); + var subRoomExists = await db.SubRooms.AsNoTracking().AnyAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); + + if (room == null || !subRoomExists) return (Ok(EmptySavePage()), new List()); + + var userId = User.GetUserId(); + if (userId == null) return (Unauthorized(), new List()); + if (!await CanReadSavesAsync((long)userId!, room)) return (StatusCode(StatusCodes.Status403Forbidden), new List()); + + return (null, await RoomUtils.GetSaveHistory(db, roomId, subRoomId)); + } + + private async Task CanReadSavesAsync(long userId, RoomData room) + { + if (room.CreatorAccountId == userId) return true; + if (await MatchmakingUtils.IsAtLeastRole(userId, room.RoomId, RoleType.CoOwner)) return true; + + var presence = await db.Presences.AsNoTracking().FirstOrDefaultAsync(x => x.playerId == userId); + if (presence?.roomInstance == null) return false; + + var instance = await db.RoomInstances.AsNoTracking() + .FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance); + + return instance != null && instance.roomId == room.RoomId; + } + + private static object EmptySavePage() => new { Results = new List(), TotalResults = 0, TotalCount = 0 }; + + private static List PageSaves(List saves, int? skip, int? take) + { + var from = skip == null || skip < 0 ? 0 : skip.Value; + var remaining = saves.Skip(from); + + return (take == null || take < 0 ? remaining : remaining.Take(take.Value)).ToList(); + } + + + private async Task> ReadSaveValuesAsync() + { + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (Request.HasFormContentType) + { + foreach (var (key, value) in Request.Form) + values[key] = value.ToString(); + return values; + } + + Request.EnableBuffering(); + + string raw; + using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true)) + raw = (await reader.ReadToEndAsync()).Trim(); + + Request.Body.Position = 0; + + if (raw.Length == 0) return values; + + try + { + if (JsonNode.Parse(raw) is JsonObject json) FlattenSaveJson(json, string.Empty, values); + } + catch (JsonException) { } + + return values; + } + + private static void FlattenSaveJson(JsonObject json, string prefix, Dictionary values) + { + foreach (var (key, node) in json) + { + var path = prefix.Length == 0 ? key : prefix + "." + key; + + if (node is JsonObject nested) FlattenSaveJson(nested, path, values); + else if (node != null && node is not JsonArray) values[path] = node.ToString(); + } + } + + private static string? ReadSaveValue(Dictionary values, params string[] keys) + { + foreach (var key in keys) + if (values.TryGetValue(key, out var raw) && !string.IsNullOrWhiteSpace(raw)) + return raw; + return null; + } + + private static JsonObject BuildRoomSaveValue(FullRoom room, SubRoom subRoom, string? dataBlobHash) + { + var roomNode = JsonSerializer.SerializeToNode(room) as JsonObject ?? new JsonObject(); + var value = new JsonObject { ["room"] = roomNode.DeepClone() }; + + foreach (var property in roomNode) + value[property.Key] = property.Value?.DeepClone(); + + value["subRoomDataSave"] = JsonSerializer.SerializeToNode(RoomUtils.BuildSaveResponse(subRoom, dataBlobHash)); + + return value; + } + + private sealed record StudioSaveResult(bool success, string? error, object? value); + + [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/data"), Authorize] + public async Task UploadRoomData(long roomId, long subRoomId) + { + var userId = User.GetUserId(); + if (userId == null) return Success.Error("Invalid auth"); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (roomData == null) return new Success(false, "Invalid room.", null); + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId); + if (subRoom == null) return new Success(false, "Invalid subroom.", null); + + var values = await ReadSaveValuesAsync(); + + var blobName = ReadSaveValue(values, "subroomdata.filename", "filename", "datablob", "blob"); + if (string.IsNullOrWhiteSpace(blobName)) + { + return new Success(false, "No room data was uploaded.", null); + } + + var savedBy = (long)userId!; + var unityAssetId = ReadSaveValue(values, "unityassetid", "subroomdata.unityassetid"); + var unityAssetFilename = ReadSaveValue(values, "unityasset.filename", "unityasset"); + if (!string.IsNullOrWhiteSpace(unityAssetFilename)) + { + if (!StudioUnityAssetRegistry.IsValidAssetId(unityAssetFilename)) + return new StudioSaveResult(false, "Invalid Unity source-manifest filename.", null); + if (await StudioUnityAssetRegistry.ReadAsync(unityAssetFilename, HttpContext.RequestAborted) == null) + return new StudioSaveResult(false, "Unity source manifest has not been uploaded.", null); + } + + var blobHash = ReadSaveValue(values, "subroomdata.hash", "hash", "datablobhash"); + + await db.SubRoomHistory.AddAsync(new RestorableSubRoom(subRoom)); + + subRoom.DataBlob = blobName; + subRoom.SavedByAccountId = savedBy; + subRoom.CreatedAt = DateTime.UtcNow; + if (!string.IsNullOrWhiteSpace(unityAssetId)) subRoom.UnityAssetId = unityAssetId; + if (!string.IsNullOrWhiteSpace(unityAssetFilename)) subRoom.UnityAssetId = unityAssetFilename; + + await db.SaveChangesAsync(); + + var room = await RoomUtils.GetRoom(roomId); + if (room == null) return new Success(false, "Invalid room.", null); + + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = room }); + + + + + return new StudioSaveResult(true, null, BuildRoomSaveValue(room, subRoom, blobHash)); + } + + + [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/unityasset")] + public async Task GetStudioUnityAsset(long roomId, long subRoomId, [FromQuery] string? unityAssetId = null) + { + var subRoom = await db.SubRooms.AsNoTracking() + .FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); + if (subRoom == null) return NotFound(); + var assetId = string.IsNullOrWhiteSpace(unityAssetId) ? subRoom.UnityAssetId : unityAssetId; + if (!string.Equals(assetId, subRoom.UnityAssetId, StringComparison.Ordinal)) return NotFound(); + var userId = User.GetUserId(); + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null || !await RoomUtils.CanSeeRoom(room, userId)) return NotFound(); + var asset = await StudioUnityAssetRegistry.ReadAsync(assetId, HttpContext.RequestAborted); + return asset == null ? NotFound() : Ok(asset); + } + + [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/restoredata"), Authorize] + public async Task RestoreRoomData(long roomId, long subRoomId) + { + var userId = User.GetUserId(); + if (userId == null) return Success.Error("Invalid auth"); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (roomData == null) return new Success(false, "Invalid room.", null); + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId); + if (subRoom == null) return new Success(false, "Invalid subroom.", null); + + var values = await ReadSaveValuesAsync(); + var blobName = ReadSaveValue(values, "filename", "subroomdata.filename", "datablob", "blob"); + if (string.IsNullOrWhiteSpace(blobName)) return new Success(false, "No room data was given.", null); + + if (!System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, blobName))) + return new Success(false, "That save's data is no longer available.", null); + + if (!string.IsNullOrEmpty(subRoom.DataBlob) && subRoom.DataBlob != blobName) + { + await db.SubRoomHistory.AddAsync(new RestorableSubRoom(subRoom)); + } + + subRoom.DataBlob = blobName; + subRoom.SavedByAccountId = (long)userId!; + subRoom.CreatedAt = DateTime.UtcNow; + + await db.SaveChangesAsync(); + + var room = await RoomUtils.GetRoom(roomId); + if (room == null) return new Success(false, "Invalid room.", null); + + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = room }); + + return new Success(true, null, BuildRoomSaveValue(room, subRoom, null)); + } + + [HttpPost("rooms/{roomId}/subrooms"), Authorize] + public async Task CreateSubroom(long roomId, [FromForm] string name) + { + var userId = User.GetUserId(); + + + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (roomData == null) return new Success(false, "Invalid room.", null); + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.Name == name && x.RoomId == roomId); + if (subRoom != null) return new Success(false, "A subroom already exists with that name!", null); + + var mainSubRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (mainSubRoom == null) return new Success(false, "There is no main subroom?", null); + + var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId); + if (presence == null) return new Success(false, "Invalid auth", null); + + if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null); + + var newSubRoom = new SubRoom + { + DataBlob = "", + CreatedAt = DateTime.Now, + Accessibility = Accessibility.Unlised, + IsSandbox = true, + MaxPlayers = mainSubRoom.MaxPlayers, + Name = name, + RoomId = roomId, + SavedByAccountId = (long)userId!, + UnitySceneId = mainSubRoom.UnitySceneId + }; + + await db.SubRooms.AddAsync(newSubRoom); + await db.SaveChangesAsync(); + + var room = await RoomUtils.GetRoom((long)roomId); + if (room == null) return new Success(false, "WHAT THE FUCK???", null); + + + + + return new Success(true, null, room); + } + + [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/move"), Authorize] + public async Task MoveSubroom(long roomId, long subRoomId, [FromForm] long newRoomId) + { + var userId = User.GetUserId(); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, newRoomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + var newRoom = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == newRoomId); + var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId); + if (room == null) return Success.Error("Room not found"); + if (newRoom == null) return new Success(false, "Target room not found", null); + if (subroom == null) return new Success(false, "Subroom not found", null); + + subroom.RoomId = newRoomId; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, null); + } + + [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/modify")] + public async Task ModifySubroom(long roomId, long subRoomId, string? name, Accessibility? accessibility, int? maxPlayers) + { + var userId = User.GetUserId(); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId); + if (room == null) return Success.Error("Room not found"); + if (subroom == null) return new Success(false, "Subroom not found", null); + + if (name != null) + subroom.Name = name; + if (accessibility != null) + subroom.Accessibility = (Accessibility)accessibility; + if (maxPlayers != null) + subroom.MaxPlayers = (int)maxPlayers; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, null); + } + + [HttpPut("rooms/{roomId}/modify"), Authorize] + public async Task ModifyRoom(long roomId, string name, string? description, Accessibility accessibility, bool supportsScreens, bool supportsWalkVR, bool supportsTeleportVR, bool supportsJuniors, bool cloningAllowed, bool disableMicAutoMute, bool disableRoomComments, bool encryptVoiceChat) + { + var userId = User.GetUserId(); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return Success.Error("Room not found"); + + if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null); + if (!(description ?? string.Empty).IsPure()) return new Success(false, "Description contains offensive language.", null); + + room.Name = name; + room.Description = description; + room.SupportsScreens = supportsScreens; + room.SupportsWalkVR = supportsWalkVR; + room.SupportsTeleportVR = supportsTeleportVR; + room.SupportsJuniors = supportsJuniors; + room.Accessibility = accessibility; + room.CloningAllowed = cloningAllowed; + room.DisableMicAutoMute = disableMicAutoMute; + room.DisableRoomComments = disableRoomComments; + room.EncryptVoiceChat = encryptVoiceChat; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, null); + } + + [HttpPut("rooms/{roomId}/image"), Authorize] + public async Task ModifyImage(long roomId, string imageName) + { + var userId = User.GetUserId(); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return Success.Error("Room not found"); + + room.ImageName = imageName; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, null); + } + + private static readonly RoleType[] AssignableRoles = + { + RoleType.None, RoleType.Host, RoleType.Moderator, RoleType.CoOwner, RoleType.TemporaryCoOwner + }; + + private async Task<(RoleType? Role, long? ActorId)> ReadRoleRequestAsync() + { + var userId = User.GetUserId(); + if (userId == null) return (null, null); + + foreach (var key in new[] { "role", "Role", "roletype", "RoleType", "roleType", "invitedrole", "InvitedRole" }) + { + var raw = Request.HasFormContentType && Request.Form.TryGetValue(key, out var form) + ? form.ToString() + : Request.Query.TryGetValue(key, out var query) ? query.ToString() : null; + + if (raw == null && Request.RouteValues.TryGetValue(key, out var route)) raw = route?.ToString(); + if (!int.TryParse(raw, out var parsed)) continue; + if (!Enum.IsDefined(typeof(RoleType), parsed)) continue; + + return ((RoleType)parsed, userId); + } + + return (null, userId); + } + + private async Task PushRoomRoleUpdate(long roomId, long actorId, long targetId, FullRoom? room) + { + room ??= await RoomUtils.GetRoom(roomId); + if (room == null) return; + + foreach (var accountId in new[] { targetId, actorId }.Distinct()) + await Notify.SendNotify(accountId, new Notification { Id = "RoomUpdate", Msg = room }); + } + + [HttpPut("rooms/{roomId}/roles/{targetId}/invite"), Authorize] + public async Task InvitePlayerToRole(long roomId, long targetId) + { + var (role, userId) = await ReadRoleRequestAsync(); + if (userId == null) return Success.Error("Invalid auth"); + if (targetId == userId.Value) return Success.Error("You cannot invite yourself"); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return Success.Error("Room not found"); + + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == targetId); + if (player == null) return Success.Error("Player not found"); + + var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == targetId && x.RoomId == roomId); + if (role == null) return Success.Error("You must provide a valid role"); + if (!AssignableRoles.Contains(role.Value)) return Success.Error($"Unknown role {role.Value}"); + if (role.Value == RoleType.None) return Success.Error("You must provide a valid role"); + if (existingRole?.Role == role.Value && existingRole.InvitedRole == RoleType.None) + return Success.Error("Player already has this role"); + + if (existingRole != null && + existingRole.Role != RoleType.None && + existingRole.Role != RoleType.Banned) + return Success.Error("Player already has a room role"); + + if (existingRole == null) + { + var newRole = new RoomRole + { + AccountId = targetId, + InvitedRole = role.Value, + Role = RoleType.None, + RoomId = roomId, + LastChangedByAccountId = userId.Value + }; + + await db.RoomRoles.AddAsync(newRole); + } + else + { + existingRole.InvitedRole = role.Value; + existingRole.LastChangedByAccountId = userId.Value; + } + + await db.SaveChangesAsync(); + + if (role.Value == RoleType.CoOwner) + { + await Notify.SendMessage(targetId, new Message + { + FromPlayerId = userId.Value, + RoomId = roomId, + Type = MessageType.RoomCoOwnerInvited + }); + } + + var fullRoom = await RoomUtils.GetRoom(roomId); + await PushRoomRoleUpdate(roomId, userId.Value, targetId, fullRoom); + + return new Success(true, null, fullRoom); + } + + [HttpPut("rooms/{roomId}/roles/{targetId}"), Authorize] + public async Task SetPlayerRole(long roomId, long targetId) + { + var (role, userId) = await ReadRoleRequestAsync(); + if (userId == null) return Success.Error("Invalid auth"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return Success.Error("Room not found"); + + if (targetId == room.CreatorAccountId) + return Success.Error("The room's creator cannot be given a different role"); + + var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == targetId); + if (player == null) return Success.Error("Player not found"); + + var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == targetId && x.RoomId == roomId); + var wasCoOwner = existingRole?.Role == RoleType.CoOwner; + var acceptingInvite = targetId == userId.Value && + existingRole is { Role: RoleType.None, InvitedRole: not RoleType.None }; + if (acceptingInvite && role == null) role = existingRole!.InvitedRole; + if (role == null) return Success.Error("You must provide a valid role"); + if (!AssignableRoles.Contains(role.Value)) return Success.Error($"Unknown role {role.Value}"); + + if (acceptingInvite) + { + if (role.Value != existingRole!.InvitedRole) + return Success.Error("The requested role does not match the pending invitation"); + } + else + { + var canModify = await MatchmakingUtils.IsAtLeastRole(userId.Value, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + } + + if (role.Value == RoleType.None) + { + if (existingRole == null) return Success.Error("Player already has this role"); + if (acceptingInvite) return Success.Error("The pending invitation cannot be accepted as None"); + + db.RoomRoles.Remove(existingRole); + } + else if (existingRole == null) + { + await db.RoomRoles.AddAsync(new RoomRole + { + AccountId = targetId, + InvitedRole = RoleType.None, + Role = role.Value, + RoomId = roomId, + LastChangedByAccountId = userId.Value + }); + } + else + { + if (existingRole.Role == role.Value && existingRole.InvitedRole == RoleType.None) + return Success.Error("Player already has this role"); + + existingRole.Role = role.Value; + existingRole.InvitedRole = RoleType.None; + existingRole.LastChangedByAccountId = (long)userId!; + } + + await db.SaveChangesAsync(); + + if (wasCoOwner && role.Value < RoleType.CoOwner) + { + await Notify.SendMessage(targetId, new Message + { + FromPlayerId = userId.Value, + RoomId = roomId, + Type = MessageType.RoomCoOwnerRemoved + }); + } + + if (role.Value == RoleType.CoOwner && !wasCoOwner) + { + await Notify.SendMessage(targetId, new Message + { + FromPlayerId = userId.Value, + RoomId = roomId, + Type = MessageType.RoomCoOwnerAdded + }); + } + + var fullRoom = await RoomUtils.GetRoom(roomId); + await PushRoomRoleUpdate(roomId, userId.Value, targetId, fullRoom); + + return new Success(true, null, fullRoom); + } + + [HttpPut("rooms/{roomId}/tags"), Authorize] + public async Task ModifyTags(long roomId, string[]? tag, string[]? autoTag) + { + var userId = User.GetUserId(); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return Success.Error("Room not found"); + + foreach (var t in tag ?? Array.Empty()) + { + if (!t.TagAllowed()) + return new Success(false, "Tag not allowed: " + t, null); + } + + if (tag != null || autoTag != null) { + List tagsToAdd = new(); + + if (tag != null) + foreach (var t in tag) + tagsToAdd.Add(new ItemTag { Tag = t, Type = TagType.General }); + + if (autoTag != null) + foreach (var t in autoTag) + tagsToAdd.Add(new ItemTag { Tag = t, Type = TagType.Auto }); + + + + + + room.Tags = StringTools.CreateTagString(tagsToAdd); + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, null); + } else + { + room.Tags = ""; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, null); + } + } + + [HttpPut("rooms/{roomId}/accessibility"), Authorize] + public async Task ModifyAccessibility(long roomId, Accessibility accessibility) + { + var userId = User.GetUserId(); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return Success.Error("Room not found"); + + bool wasPublished = room.WasPublished; + + room.Accessibility = accessibility; + if (!room.WasPublished && accessibility == Accessibility.Public) { + room.WasPublished = true; + room.PublishedAt = DateTime.Now; + } + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + + + + + + + + + + return new Success(true, null, null); + } + + [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/permissions"), Authorize] + public async Task GetSubRoomPermissions(long roomId, long subRoomId) + { + if (User.GetUserId() == null) return Unauthorized(); + + var subRoom = await db.SubRooms + .AsNoTracking() + .FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); + if (subRoom == null) return NotFound(); + + var stored = await db.SubRoomPermissions + .AsNoTracking() + .Where(x => x.subRoomId == subRoomId) + .OrderBy(x => x.Id) + .ToListAsync(); + + return Ok(stored + .Select(x => new RoomPermissionRow(x.permission, x.role, true, x.type, x.value)) + .ToList()); + } + + [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/permissions"), Authorize] + public async Task SetSubRoomPermissions(long roomId, long subRoomId) + { + long? userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var subRoom = await db.SubRooms + .AsNoTracking() + .FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); + if (subRoom == null) return NotFound(); + + if (!await MatchmakingUtils.IsAtLeastRole(userId.Value, roomId, RoleType.CoOwner)) + return Forbid(); + + string? body; + using (var reader = new StreamReader(Request.Body, Encoding.UTF8)) + body = await reader.ReadToEndAsync(); + + var entries = ParseSubRoomPermissions(body); + + foreach (var entry in entries) + { + var existing = await db.SubRoomPermissions.FirstOrDefaultAsync(x => + x.subRoomId == subRoomId && x.permission == entry.Permission && x.role == entry.Role); + + if (!entry.Override) + { + if (existing != null) db.SubRoomPermissions.Remove(existing); + continue; + } + + if (existing == null) + { + await db.SubRoomPermissions.AddAsync(new SubRoomPermission + { + subRoomId = subRoomId, + permission = entry.Permission, + role = entry.Role, + type = entry.Type, + value = entry.Value, + }); + } + else + { + existing.type = entry.Type; + existing.value = entry.Value; + } + } + + await db.SaveChangesAsync(); + + var logged = body ?? ""; + if (logged.Length > 800) logged = logged[..800] + "..."; + + return Ok(new { Permissions = Array.Empty() }); + } + + private static List ParseSubRoomPermissions(string? body) + { + var entries = new List(); + if (string.IsNullOrWhiteSpace(body)) return entries; + + JsonDocument document; + try + { + document = JsonDocument.Parse(body); + } + catch (JsonException) + { + return entries; + } + + using (document) + { + if (document.RootElement.ValueKind != JsonValueKind.Array) return entries; + + foreach (var element in document.RootElement.EnumerateArray()) + { + if (element.ValueKind != JsonValueKind.Object) continue; + + var permission = ReadPermissionName(element); + if (permission.Length == 0) continue; + + var role = ReadPermissionRole(element); + if (role == null) continue; + + entries.Add(new RoomPermissionRow( + permission, + role.Value, + ReadPermissionBool(element, "Override"), + (int)(ReadPermissionLong(element, "Type") ?? 0), + ReadPermissionValue(element, "Value"))); + } + } + + return entries; + } + + private static bool TryGetPermissionField(JsonElement entry, string key, out JsonElement value) + { + foreach (var property in entry.EnumerateObject()) + { + if (string.Equals(property.Name, key, StringComparison.OrdinalIgnoreCase)) + { + value = property.Value; + return true; + } + } + + value = default; + return false; + } + + private static string? ReadPermissionString(JsonElement entry, string key) + { + if (!TryGetPermissionField(entry, key, out var value)) return null; + return value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString(); + } + + private static long? ReadPermissionLong(JsonElement entry, string key) + { + if (!TryGetPermissionField(entry, key, out var value)) return null; + + if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var number)) + return number; + + if (value.ValueKind == JsonValueKind.String && + long.TryParse(value.GetString(), out var parsed)) + return parsed; + + return null; + } + + private static bool ReadPermissionBool(JsonElement entry, string key) + { + if (!TryGetPermissionField(entry, key, out var value)) return false; + + if (value.ValueKind == JsonValueKind.True) return true; + if (value.ValueKind == JsonValueKind.False) return false; + + return string.Equals(value.ToString(), "true", StringComparison.OrdinalIgnoreCase); + } + + private static string ReadPermissionValue(JsonElement entry, string key) + { + if (!TryGetPermissionField(entry, key, out var value)) return ""; + + if (value.ValueKind == JsonValueKind.String) return value.GetString() ?? ""; + if (value.ValueKind == JsonValueKind.True) return "True"; + if (value.ValueKind == JsonValueKind.False) return "False"; + return value.ToString(); + } + + private static string ReadPermissionName(JsonElement entry) + { + var raw = (ReadPermissionString(entry, "Permission") + ?? ReadPermissionString(entry, "PermissionType") + ?? ReadPermissionString(entry, "PermissionTypeOld") + ?? "").Trim(); + if (raw.Length == 0) return ""; + + if (long.TryParse(raw, out var number) && + Enum.IsDefined(typeof(RolePermissions), (int)number)) + return ((RolePermissions)(int)number).ToString(); + + return raw; + } + + private static long? ReadPermissionRole(JsonElement entry) + { + var numeric = ReadPermissionLong(entry, "Role") ?? ReadPermissionLong(entry, "RoleId"); + if (numeric != null) return numeric; + + var name = ReadPermissionString(entry, "Role")?.Trim(); + if (!string.IsNullOrEmpty(name) && Enum.TryParse(name, true, out var parsed)) + return (long)parsed; + + return null; + } + + [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/accessibility"), Authorize] + public async Task ModifySubRoomAccessibility(long roomId, long subRoomId, Accessibility accessibility) + { + var userId = User.GetUserId(); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return Success.Error("Room not found"); + + var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); + if (subRoom == null) return new Success(false, "Subroom not found", null); + + subRoom.Accessibility = accessibility; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom(roomId); + await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, null); + } + } +} diff --git a/Server/Rooms/RoomStats.cs b/Server/Rooms/RoomStats.cs new file mode 100644 index 0000000..f6b9c36 --- /dev/null +++ b/Server/Rooms/RoomStats.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomStats + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public long RoomId { get; set; } + public int CheerCount { get; set; } = 0; + public int FavoriteCount { get; set; } = 0; + public int VisitorCount { get; set; } = 0; + public int VisitCount { get; set; } = 0; + } +} diff --git a/Server/Rooms/RoomUploadResponse.cs b/Server/Rooms/RoomUploadResponse.cs new file mode 100644 index 0000000..c4b4b4f --- /dev/null +++ b/Server/Rooms/RoomUploadResponse.cs @@ -0,0 +1,8 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class RoomUploadResponse + { + public Room Room { get; set; } = null!; + public SubRoom SubRoomDataSave { get; set; } = null!; + } +} diff --git a/Server/Rooms/RoomUtils.cs b/Server/Rooms/RoomUtils.cs new file mode 100644 index 0000000..a35a61f --- /dev/null +++ b/Server/Rooms/RoomUtils.cs @@ -0,0 +1,574 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Rooms; + +namespace RadicadeServer.Utils +{ + public static class RoomUtils + { + public static IDbContextFactory DbFactory = null!; + + public static async Task VerifyRole(long playerId, long roomId, RoleType roleType) + { + var role = await GetRoleForPlayer(playerId, roomId); + if (role == null) return false; + + return role.Role >= roleType; + } + + public static async Task CanSeeRoom(RoomData room, long? accountId) + { + if (room.Accessibility != Accessibility.Private) return true; + if (accountId == null) return false; + if (room.CreatorAccountId == accountId.Value) return true; + if (await VerifyRole(accountId.Value, room.RoomId, RoleType.CoOwner)) return true; + + if (DbFactory != null) + { + await using var db = await DbFactory.CreateDbContextAsync(); + if (await ( + from presence in db.Presences.AsNoTracking() + join instance in db.RoomInstances.AsNoTracking() + on presence.roomInstance equals instance.roomInstanceId + where presence.playerId == accountId.Value && presence.isOnline && + instance.roomId == room.RoomId + select presence.playerId).AnyAsync()) return true; + } + + return await HasLiveGameInvite(room.RoomId, accountId.Value); + } + + public static async Task HasLiveGameInvite(long roomId, long accountId) + { + if (DbFactory == null) return false; + + await using var db = await DbFactory.CreateDbContextAsync(); + + long? inviter = await db.Messages.AsNoTracking() + .Where(message => + message.ToPlayerId == accountId && + (message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2) && + message.RoomId == roomId) + .OrderByDescending(message => message.Id) + .Select(message => (long?)message.FromPlayerId) + .FirstOrDefaultAsync(); + if (inviter == null) return false; + + return await ( + from presence in db.Presences.AsNoTracking() + join instance in db.RoomInstances.AsNoTracking() + on presence.roomInstance equals instance.roomInstanceId + where presence.playerId == inviter.Value && + presence.isOnline && + instance.roomId == roomId + select presence.playerId).AnyAsync(); + } + + public static async Task> VisibleRooms(IEnumerable rooms, long? accountId) + { + var visible = new List(); + foreach (var room in rooms) + { + if (await CanSeeRoom(room, accountId)) visible.Add(room); + } + return visible; + } + + public const string PrivateRoomName = "PRIVATE ROOM"; + + public static async Task IsHiddenFrom(RoomData room, long? accountId) + => !room.IsDorm && !await CanSeeRoom(room, accountId); + + public static FullRoom MaskHiddenRoom(RoomData room) + { + return new FullRoom + { + RoomId = room.RoomId, + Name = PrivateRoomName, + Description = string.Empty, + ImageName = DBConfig.DefaultRoomImage, + IsDorm = false, + State = room.State, + Accessibility = Accessibility.Private, + CreatedAt = room.CreatedAt, + CreatorAccountId = room.CreatorAccountId, + MaxPlayers = room.MaxPlayers, + MaxPlayerCalculationMode = room.MaxPlayerCalculationMode, + CloningAllowed = false, + DisableRoomComments = true, + CustomWarning = string.Empty, + SubRooms = new List(), + Roles = new List(), + LoadScreens = new List(), + PromoImages = new List(), + PromoExternalContent = new List(), + DataBlob = string.Empty, + Tags = new List(), + Stats = new RoomStats { RoomId = room.RoomId }, + Version = 1 + }; + } + + public static async Task ForViewerAsync(RoomInstance? instance, long? viewerId) + { + if (instance == null) return null; + + await using var db = await DbFactory.CreateDbContextAsync(); + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == instance.roomId); + if (room == null || !await IsHiddenFrom(room, viewerId)) return instance; + + var masked = instance.Copy(); + masked.name = PrivateRoomName; + return masked; + } + public static async Task> GetRoles(long roomId) + { + await using var db = await DbFactory.CreateDbContextAsync(); + + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); + if (room == null) return []; + + var roomRole = await db.RoomRoles.AsNoTracking().Where(x => x.RoomId == roomId).ToListAsync(); + + var creatorRow = roomRole.FirstOrDefault(x => x.AccountId == room.CreatorAccountId); + if (creatorRow == null) + roomRole.Add(new RoomRole { AccountId = room.CreatorAccountId, InvitedRole = RoleType.None, Role = RoleType.Creator, RoomId = roomId }); + else if (creatorRow.Role < RoleType.Creator) + creatorRow.Role = RoleType.Creator; + + if (room.IsRRO) + { + var rroOwners = (await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "HighRanks"))?.Value; + + foreach (var dev in (rroOwners ?? string.Empty).Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + if (!long.TryParse(dev, out long userId)) continue; + + foreach (var role in new[] { RoleType.Creator, RoleType.Moderator }) + { + if (roomRole.Any(x => x.AccountId == userId && x.Role == role)) continue; + + roomRole.Add(new RoomRole { AccountId = userId, InvitedRole = RoleType.None, Role = role, RoomId = roomId }); + } + } + } + + return roomRole; + } + public static async Task GetRoleForPlayer(long playerId, long roomId) + { + var roomRoles = await GetRoles(roomId); + + return roomRoles + .Where(x => x.AccountId == playerId) + .OrderByDescending(x => x.Role) + .FirstOrDefault(); + } + + public static SubRoomDataSave BuildCurrentSave(SubRoom subRoom) + { + return new SubRoomDataSave + { + SubRoomDataSaveId = 1, + SubRoomId = subRoom.SubRoomId, + DataBlob = subRoom.DataBlob, + SavedByAccountId = subRoom.SavedByAccountId, + CreatedAt = subRoom.CreatedAt, + UnityAssetId = string.IsNullOrEmpty(subRoom.UnityAssetId) ? null : subRoom.UnityAssetId, + PersistenceVersion = 1, + OMVersion = 1, + UgcSubVersion = 1, + SavedOnPlatform = 0, + SavedOnDeviceClass = 0, + Description = string.Empty, + ModerationState = 0 + }; + } + + public static object BuildSaveResponse(SubRoom subRoom, string? dataBlobHash) + { + return new + { + subRoomDataSaveId = 1L, + subRoomId = subRoom.SubRoomId, + unityAssetId = string.IsNullOrEmpty(subRoom.UnityAssetId) ? null : subRoom.UnityAssetId, + unityAsset = (string?)null, + unityAssetHash = (string?)null, + dataBlob = subRoom.DataBlob ?? string.Empty, + dataBlobHash, + savedByAccountId = (long?)subRoom.SavedByAccountId, + savedOnPlatform = 0, + savedOnDeviceClass = 0, + description = string.Empty, + createdAt = subRoom.CreatedAt + }; + } + + public static async Task> GetSaveHistory(RecDBContext ctx, long roomId, long subRoomId) + { + var subRoom = await ctx.SubRooms.AsNoTracking() + .FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); + + if (subRoom == null) return new List(); + + var saves = new List(); + + if (!string.IsNullOrEmpty(subRoom.DataBlob)) saves.Add(BuildCurrentSave(subRoom)); + + var history = await ctx.SubRoomHistory.AsNoTracking() + .Where(x => x.RoomId == roomId && x.SubRoomId == subRoomId && x.DataBlob != "") + .OrderByDescending(x => x.CreatedAt) + .ThenByDescending(x => x.HistoryId) + .ToListAsync(); + + saves.AddRange(history.Select(BuildRestorableSave)); + + return saves; + } + + public static SubRoomDataSave BuildRestorableSave(RestorableSubRoom row) + { + return new SubRoomDataSave + { + SubRoomDataSaveId = row.HistoryId + 1, + SubRoomId = row.SubRoomId, + DataBlob = row.DataBlob, + SavedByAccountId = row.SavedByAccountId, + CreatedAt = row.CreatedAt, + PersistenceVersion = 1, + OMVersion = 1, + UgcSubVersion = 1, + SavedOnPlatform = 0, + SavedOnDeviceClass = 0, + Description = string.Empty, + ModerationState = 0 + }; + } + + public static FullRoom ConvertToFull(this RoomData room, List subRooms, RoomStats stats, List roles) + { + FullRoom fullRoom = new FullRoom + { + Name = room.Name, + RoomId = room.RoomId, + Description = room.Description ?? string.Empty, + ImageName = room.ImageName, + IsRRO = room.IsRRO, + IsDeveloperOwned = room.IsDeveloperOwned, + IsDorm = room.IsDorm, + State = room.State, + Accessibility = room.Accessibility, + CreatedAt = room.CreatedAt, + CreatorAccountId = room.CreatorAccountId, + SupportsJuniors = room.SupportsJuniors, + SupportsLevelVoting = room.SupportsLevelVoting, + SupportsMobile = room.SupportsMobile, + SupportsQuest2 = room.SupportsQuest2, + SupportsScreens = room.SupportsScreens, + SupportsTeleportVR = room.SupportsTeleportVR, + SupportsVRLow = room.SupportsVRLow, + SupportsWalkVR = room.SupportsWalkVR, + EncryptVoiceChat = room.EncryptVoiceChat, + MaxPlayerCalculationMode = room.MaxPlayerCalculationMode, + MaxPlayers = room.MaxPlayers, + SubRooms = subRooms, + Stats = stats, + CloningAllowed = room.CloningAllowed, + CustomWarning = room.CustomWarning ?? string.Empty, + Roles = roles, + DataBlob = null, + Tags = StringTools.ReadTags(room.Tags), + LoadScreens = room.loadScreen.FromJson>() ?? new(), + WarningMask = room.WarningMask, + Version = room.Version, + DisableRoomComments = room.DisableRoomComments, + DisableMicAutoMute = room.DisableMicAutoMute, + MinLevel = room.MinLevel + }; + + foreach (var subRoom in subRooms) + { + subRoom.CurrentSave = string.IsNullOrEmpty(subRoom.DataBlob) ? null : BuildCurrentSave(subRoom); + + subRoom.CreatorAccountId = subRoom.SavedByAccountId != 0 ? subRoom.SavedByAccountId : null; + } + + return fullRoom; + } + + public static async Task> BulkConvertRoomData(List data) + { + var fullRooms = new List(); + foreach (var d in data) + { + var found = await GetRoom(d.RoomId); + if (found != null) + fullRooms.Add(found); + } + return fullRooms; + } + public static async Task> BulkConvertRoomDataToRoomDetail(List data) + { + var rooms = new List(); + foreach (var d in data) + { + var found = await GetRoom(d.RoomId); + if (found != null) + rooms.Add(found); + } + return rooms; + } + + public static async Task GetRoom(string Name) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.Name == Name); + if (room == null && !string.IsNullOrWhiteSpace(Name) && Name.StartsWith("@", StringComparison.Ordinal) && Name.EndsWith("'s Dorm", StringComparison.OrdinalIgnoreCase)) + { + var username = Name.Substring(1, Name.Length - 8); + var owner = await ctx.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.username == username || x.displayName == username); + if (owner != null) + { + room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.IsDorm && x.CreatorAccountId == owner.accountId); + } + } + if (room == null) return null; + + return await GetRoom(room.RoomId); + } + public static async Task GetRoom(long Id) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.RoomId == Id); + if (room == null) return null; + + var cheers = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Cheered); + var favourites = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Favorited); + var visitorCount = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Visited); + var visitCount = await ctx.RoomInteractions.AsNoTracking() + .Where(x => x.RoomId == Id && x.Visited) + .SumAsync(x => (int?)x.VisitCount) ?? 0; + + var roomStats = new RoomStats { CheerCount = cheers, FavoriteCount = favourites, RoomId = Id, VisitCount = visitCount, VisitorCount = visitorCount }; + + var roomRoles = await GetRoles(Id); + + var subRooms = await ctx.SubRooms.AsNoTracking().Where((x) => x.RoomId == Id).ToListAsync(); + + var missingBlobs = subRooms + .Where(x => !string.IsNullOrEmpty(x.DataBlob) && !File.Exists(Path.Join(DBConfig.CDNStorage, x.DataBlob))) + .ToList(); + + if (missingBlobs.Count > 0) + { + var missingIds = missingBlobs.Select(x => x.SubRoomId).ToList(); + + var candidates = await ctx.SubRoomHistory.AsNoTracking() + .Where(x => missingIds.Contains(x.SubRoomId) && x.DataBlob != "") + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(); + + foreach (var subRoom in missingBlobs) + { + var recovered = candidates + .Where(x => x.SubRoomId == subRoom.SubRoomId) + .Select(x => x.DataBlob) + .FirstOrDefault(blob => !string.IsNullOrEmpty(blob) && File.Exists(Path.Join(DBConfig.CDNStorage, blob))); + + subRoom.DataBlob = recovered ?? ""; + ctx.SubRooms.Attach(subRoom); + ctx.Entry(subRoom).Property(x => x.DataBlob).IsModified = true; + } + + await ctx.SaveChangesAsync(); + } + + return room.ConvertToFull(subRooms, roomStats, roomRoles); + } + + public static async Task GetRoomStats(long roomId, long? playerId) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var cheers = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Cheered); + var favourites = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Favorited); + + var visitorCount = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Visited); + + var visited = 0; + if (playerId != null) + { + var interaction = await ctx.RoomInteractions.AsNoTracking() + .FirstOrDefaultAsync(x => x.RoomId == roomId && x.PlayerId == playerId); + + if (interaction != null) + visited = interaction.VisitCount; + } + + return new RoomStats() { CheerCount = cheers, FavoriteCount = favourites, RoomId = roomId, VisitCount = visited, VisitorCount = visitorCount }; + } + + + public static async Task CloneRoom(long originalRoomId, long newCreator, string newName) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + FullRoom? room = await GetRoom(originalRoomId); + if (room == null) + return null; + + + + var tags = room.Tags; + var rro = tags.Find(x => x.Tag.StartsWith("rro") && x.Type >= TagType.Auto); + if (rro != null) + tags.Remove(rro); + + RoomData roomData = new RoomData + { + State = RoomState.Active, + SupportsJuniors = room.SupportsJuniors, + SupportsLevelVoting = room.SupportsLevelVoting, + SupportsMobile = room.SupportsMobile, + SupportsQuest2 = room.SupportsQuest2, + SupportsScreens = room.SupportsScreens, + SupportsTeleportVR = room.SupportsTeleportVR, + SupportsVRLow = room.SupportsVRLow, + SupportsWalkVR = room.SupportsWalkVR, + Accessibility = Accessibility.Private, + CloningAllowed = false, + CreatedAt = DateTime.Now, + CreatorAccountId = newCreator, + CustomWarning = room.CustomWarning, + Description = room.Description, + DisableMicAutoMute = room.DisableMicAutoMute, + DisableRoomComments = room.DisableRoomComments, + EncryptVoiceChat = room.EncryptVoiceChat, + ImageName = room.ImageName, + IsDorm = room.IsDorm, + IsRRO = false, + + IsDeveloperOwned = !room.IsDorm && await ctx.IsDeveloper(newCreator), + MaxPlayerCalculationMode = room.MaxPlayerCalculationMode, + MaxPlayers = room.MaxPlayers, + MinLevel = room.MinLevel, + Name = newName, + Tags = StringTools.CreateTagString(tags), + WarningMask = room.WarningMask, + }; + + await ctx.AddAsync(roomData); + await ctx.SaveChangesAsync(); + + + if (room.SubRooms == null || room.SubRooms.Count == 0) + { + ctx.Remove(roomData); + await ctx.SaveChangesAsync(); + return null; + } + + foreach (SubRoom subRoom in room.SubRooms) + { + if (subRoom == null) continue; + await ctx.SubRooms.AddAsync(new SubRoom + { + Accessibility = subRoom.Accessibility, + DataBlob = subRoom.DataBlob, + UnityAssetId = subRoom.UnityAssetId, + IsSandbox = subRoom.IsSandbox, + UnitySceneId = subRoom.UnitySceneId, + MaxPlayers = subRoom.MaxPlayers, + Name = subRoom.Name, + RoomId = roomData.RoomId + }); + } + + await ctx.SaveChangesAsync(); + + + await ctx.RoomStats.AddAsync(new RoomStats + { + RoomId = roomData.RoomId, + FavoriteCount = 0, + CheerCount = 0, + VisitCount = 0, + VisitorCount = 0, + }); + + + await ctx.RoomRoles.AddAsync(new RoomRole + { + RoomId = roomData.RoomId, + Role = RoleType.Creator, + InvitedRole = RoleType.None, + AccountId = newCreator + }); + + await ctx.SaveChangesAsync(); + + return await GetRoom(roomData.RoomId); + } + + private static async Task GetDormTemplateRoom() + { + var templateRoom = await GetRoom(1); + if (templateRoom != null) return templateRoom; + + await using var ctx = await DbFactory.CreateDbContextAsync(); + var template = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync(room => room.IsDorm && room.CreatorAccountId == 1); + return template == null ? null : await GetRoom(template.RoomId); + } + + public static async Task GetOrCreateDorm(long playerId) + { + await using var ctx = await DbFactory.CreateDbContextAsync(); + + var roomData = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.IsDorm && x.CreatorAccountId == playerId); + if (roomData == null) + { + var templateRoom = await GetDormTemplateRoom(); + if (templateRoom == null) + { + return null; + } + + return await CloneRoom(templateRoom.RoomId, playerId, "DormRoom"); + } + + + var hasSubRoom = await ctx.SubRooms.AnyAsync(s => s.RoomId == roomData.RoomId); + if (!hasSubRoom) + { + var templateRoom = await GetDormTemplateRoom(); + var templateSub = templateRoom?.SubRooms?.FirstOrDefault(); + if (templateSub != null) + { + await ctx.SubRooms.AddAsync(new SubRoom + { + RoomId = roomData.RoomId, + DataBlob = templateSub.DataBlob, + UnityAssetId = templateSub.UnityAssetId, + IsSandbox = templateSub.IsSandbox, + UnitySceneId = templateSub.UnitySceneId, + MaxPlayers = templateSub.MaxPlayers, + Name = templateSub.Name, + Accessibility = templateSub.Accessibility + }); + await ctx.SaveChangesAsync(); + } + } + + return await GetRoom(roomData.RoomId); + } + } +} diff --git a/Server/Rooms/RoomsCDN.cs b/Server/Rooms/RoomsCDN.cs new file mode 100644 index 0000000..321a1f9 --- /dev/null +++ b/Server/Rooms/RoomsCDN.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using SixLabors.ImageSharp; + +namespace RadicadeServer.CDN +{ + [Route("cdn/room/")] + [Route("room/")] + [Route("unity/")] + [Route("unity_assets/")] + [Route("roomserver/unity_assets/")] + public class RoomsCDN : Controller + { + + + [HttpGet] + public IActionResult Index() => Ok(new { status = "ok" }); + + [HttpGet("{datablob}")] + public IActionResult GetDataBlob(string datablob) + { + if (!CdnStorageNames.IsServable(datablob)) return BadRequest(); + + var name = CdnStorageNames.Resolve(datablob); + if (name == null) return NotFound(); + + CdnCaching.Apply(this, CdnCaching.ForBlob(name)); + + return PhysicalFile(Path.Join(DBConfig.CDNStorage, name), "application/octet-stream", enableRangeProcessing: true); + } + + [HttpGet("{assetId}/{target}/{version}")] + [HttpGet("{assetId}/{target}")] + public IActionResult GetUnityAssetRaw(string assetId, int? target, int? version) + { + // The bytes half of the two-step download, for a client that asks by id rather than by + // the Filename the metadata route handed it: /unity/{id} is the id with no extension, + // while an imported bundle is stored as .assetbundle, so both spellings resolve. + if (!CdnStorageNames.IsServable(assetId)) return BadRequest(); + + var name = CdnStorageNames.Resolve(assetId); + if (name == null) return NotFound(); + + CdnCaching.Apply(this, CdnCaching.Immutable); + + return PhysicalFile(Path.Join(DBConfig.CDNStorage, name), "application/octet-stream", enableRangeProcessing: true); + } + } +} diff --git a/Server/Rooms/RoomsV1.cs b/Server/Rooms/RoomsV1.cs new file mode 100644 index 0000000..2c54e63 --- /dev/null +++ b/Server/Rooms/RoomsV1.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Rooms.V1 +{ + [Route("api/rooms/v1")] + public class RoomsV1 : Controller + { + RecDBContext db; + public RoomsV1(RecDBContext context) + { + this.db = context; + } + + [HttpGet("filters")] + public async Task> GetRoomFilters() + { + var dbFilters = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "Filters"); + if (dbFilters == null) + return NotFound(); + + var filters = dbFilters.Value.Split(',', StringSplitOptions.TrimEntries).ToList(); + + return new RoomFilters { PinnedFilters = filters , PopularFilters = filters, TrendingFilters = filters }; + } + + [HttpPost("verifyRole"), Authorize] + public async Task VerifyRole([FromForm] long roomId, [FromForm] RoleType role, [FromForm] string? context) + { + var userId = User.GetUserId(); + + var roomRole = await RoomUtils.VerifyRole((long)userId!, roomId, role); + + if (!roomRole) + return new Success(false, "Invalid roles", false); + else + return new Success(true, null, null); + } + } +} diff --git a/Server/Rooms/SceneLocations.cs b/Server/Rooms/SceneLocations.cs new file mode 100644 index 0000000..da25a34 --- /dev/null +++ b/Server/Rooms/SceneLocations.cs @@ -0,0 +1,39 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public static class SceneLocations + { + public const string MakerRoom = "a75f7547-79eb-47c6-8986-6767abcb4f92"; + + public const string Dorm = "76d98498-60a1-430c-ab76-b54a29b7a163"; + + private static readonly HashSet KnownBadValues = new(StringComparer.OrdinalIgnoreCase) + { + "unknown", + "none", + "null", + }; + + public static bool IsValid(string? unitySceneId) + { + if (string.IsNullOrWhiteSpace(unitySceneId)) + return false; + + var trimmed = unitySceneId.Trim(); + if (KnownBadValues.Contains(trimmed)) + return false; + + return Guid.TryParse(trimmed, out _); + } + + public static string ResolveFallback(IEnumerable candidateUnitySceneIds, bool isDorm) + { + foreach (var id in candidateUnitySceneIds) + { + if (IsValid(id)) + return id!.Trim(); + } + + return isDorm ? Dorm : MakerRoom; + } + } +} diff --git a/Server/Rooms/SubRoom.cs b/Server/Rooms/SubRoom.cs new file mode 100644 index 0000000..619fd9c --- /dev/null +++ b/Server/Rooms/SubRoom.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums; + +namespace RadicadeServer.RecNet.Rooms +{ + public class SubRoom + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SubRoomId { get; set; } + public long RoomId { get; set; } + public long SavedByAccountId { get; set; } + public string Name { get; set; } = "databasebroken"; + public string DataBlob { get; set; } = ""; + + public string UnityAssetId { get; set; } = ""; + + [NotMapped] + public SubRoomDataSave? CurrentSave { get; set; } + [NotMapped] + public long? CreatorAccountId { get; set; } + public bool IsSandbox { get; set; } + public int MaxPlayers { get; set; } + public Accessibility Accessibility { get; set; } + public string UnitySceneId { get; set; } = ""; + public DateTime CreatedAt { get; set; } + [NotMapped] + public DateTime DataSavedAt { get => CreatedAt; set => CreatedAt = value; } + } +} diff --git a/Server/Rooms/SubRoomDataSave.cs b/Server/Rooms/SubRoomDataSave.cs new file mode 100644 index 0000000..71068e9 --- /dev/null +++ b/Server/Rooms/SubRoomDataSave.cs @@ -0,0 +1,23 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class SubRoomDataSave + { + public List UnitySubAssets { get; set; } = new(); + public List ReferencedUnityAssets { get; set; } = new(); + public long SubRoomDataSaveId { get; set; } + public long SubRoomId { get; set; } + public string DataBlob { get; set; } = string.Empty; + public List ReferencedUnityAssetIds { get; set; } = new(); + public int PersistenceVersion { get; set; } + public int OMVersion { get; set; } + public int UgcSubVersion { get; set; } + public long? SavedByAccountId { get; set; } + public int SavedOnPlatform { get; set; } + public int SavedOnDeviceClass { get; set; } + public string Description { get; set; } = string.Empty; + public List Tags { get; set; } = new(); + public int ModerationState { get; set; } + public DateTime CreatedAt { get; set; } + public string? UnityAssetId { get; set; } + } +} \ No newline at end of file diff --git a/Server/Rooms/SubRoomPermission.cs b/Server/Rooms/SubRoomPermission.cs new file mode 100644 index 0000000..b6f4823 --- /dev/null +++ b/Server/Rooms/SubRoomPermission.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace RadicadeServer.RecNet.Rooms +{ + public class SubRoomPermission + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long subRoomId { get; set; } + + [MaxLength(128)] + public string permission { get; set; } = ""; + + public long role { get; set; } + + public int type { get; set; } + + public string value { get; set; } = ""; + } +} diff --git a/Server/Rooms/UserLoadScreen.cs b/Server/Rooms/UserLoadScreen.cs new file mode 100644 index 0000000..9809319 --- /dev/null +++ b/Server/Rooms/UserLoadScreen.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class UserLoadScreen + { + public string imageName { get; set; } = null!; + public string title { get; set; } = null!; + public string subtitle { get; set; } = null!; + } +} diff --git a/Server/Rooms/V2_RoomsAPI.cs b/Server/Rooms/V2_RoomsAPI.cs new file mode 100644 index 0000000..c3cd10f --- /dev/null +++ b/Server/Rooms/V2_RoomsAPI.cs @@ -0,0 +1,723 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Text; +using System.Text.Json.Nodes; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; + + +namespace RadicadeServer.RoomServer +{ + [Route("rooms/rooms/")] + public class V2_RoomsAPI(RecDBContext db) : Controller + { + [HttpGet("{id:long}/similar"), Authorize] + public async Task GetSimilarRooms(long id, [FromQuery] int skip = 0, [FromQuery] int take = 100) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Ok(EmptyPage()); + + var userId = User.GetUserId(); + skip = Math.Max(skip, 0); + take = Math.Clamp(take, 1, 100); + + var ownTags = StringTools.ReadTags(room.Tags) + .Select(t => t.Tag?.Trim().ToLowerInvariant()) + .Where(t => !string.IsNullOrEmpty(t)) + .ToHashSet(); + + var candidates = await db.Rooms + .Where(x => x.RoomId != id && x.Accessibility == Accessibility.Public) + .ToListAsync(); + + var scored = new List<(RoomData Room, int Score, int Visits)>(); + foreach (var candidate in candidates) + { + if (!await RoomUtils.CanSeeRoom(candidate, userId)) continue; + + int score; + if (ownTags.Count == 0) + { + score = candidate.CreatorAccountId == room.CreatorAccountId ? 1 : 0; + } + else + { + score = StringTools.ReadTags(candidate.Tags) + .Count(t => ownTags.Contains(t.Tag?.Trim().ToLowerInvariant())); + } + + if (score <= 0) continue; + + var stats = await db.RoomStats.FirstOrDefaultAsync(x => x.RoomId == candidate.RoomId); + scored.Add((candidate, score, stats?.VisitCount ?? 0)); + } + + var ordered = scored + .OrderByDescending(x => x.Score) + .ThenByDescending(x => x.Visits) + .Select(x => x.Room) + .ToList(); + + var picked = ordered.Skip(skip).Take(take).ToList(); + var details = await RoomUtils.BulkConvertRoomDataToRoomDetail(picked); + + return Ok(new RoomSearchResult { TotalResults = ordered.Count, Results = details }); + } + + private static RoomSearchResult EmptyPage() => new() { TotalResults = 0, Results = new List() }; + + [HttpGet("{id:long}/experience")] + public IActionResult GetRoomExperience(long id) + => Ok(new { Enabled = false, DailyLimit = 1000 }); + + [HttpGet("{id:long}/experience/player")] + public IActionResult GetRoomExperienceForPlayer(long id) + => Ok(Array.Empty()); + + [HttpPut("{id}/description"), Authorize] + public async Task SetDescription(long? id, string description) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + if (!description.IsPure()) return new Success(false, "Description contains offensive language.", null); + + room.Description = description; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + [HttpPut("{id}/name"), Authorize] + public async Task SetName(long? id, string name) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null); + + var sameRoomWithName = await db.Rooms.FirstOrDefaultAsync(x => x.Name == name); + if (sameRoomWithName != null) return new Success(false, "A room already exists with that name.", null); + + room.Name = name; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + [HttpPut("{id}/warning"), Authorize] + public async Task SetWarning(long? id, WarningMask? warningMask, string? customWarning) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + if (warningMask != null) + room.WarningMask = warningMask.Value; + if (customWarning != null) + room.CustomWarning = customWarning; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + [HttpPut("{id}/cloning"), Authorize] + public async Task SetAllowedCloning(long? id, bool cloningAllowed) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + room.CloningAllowed = cloningAllowed; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + [HttpPut("{id}/restrictions"), Authorize] + public async Task SetRestrictions(long? id, bool supportsScreens, bool supportsWalkVR, bool supportsTeleportVR, bool supportsJuniors) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + room.SupportsTeleportVR = supportsTeleportVR; + room.SupportsWalkVR = supportsWalkVR; + room.SupportsScreens = supportsScreens; + room.SupportsJuniors = supportsJuniors; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + [HttpPut("{id}/automute"), Authorize] + public async Task SetDisableAutoMute(long? id, bool disable) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + room.DisableMicAutoMute = disable; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + [HttpPut("{id}/comments"), Authorize] + public async Task SetDisableComments(long? id, bool disable) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + room.DisableRoomComments = disable; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + [HttpPut("{id}/voice_chat_encryption"), Authorize] + public async Task SetEncryptVoiceChat(long? id, bool encryptVoiceChat) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + room.EncryptVoiceChat = encryptVoiceChat; + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + [HttpPut("{id}/allow_new_users"), Authorize] + public async Task SetAllowNewUsers(long? id, bool allowNewUsers) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + + + + + + + [HttpGet("{id}/bans"), Authorize] + public async Task> GetRoomBans(long? id) + { + var userId = User.GetUserId(); + if (userId == null || id == null) return new List(); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return new List(); + + var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!); + if (!canModify) return new List(); + + return await db.RoomBans + .Where(x => x.roomId == id && (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow)) + .OrderByDescending(x => x.bannedAt) + .ToListAsync(); + } + + + + + + + [HttpGet("{id}/bans/{bannedPlayerId}"), Authorize] + public async Task GetRoomBan(long? id, long bannedPlayerId) + { + var userId = User.GetUserId(); + if (userId == null || id == null) return null; + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return null; + + + if (bannedPlayerId != (long)userId) + { + var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!); + if (!canModify) return null; + } + + return await db.RoomBans + .FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == bannedPlayerId && + (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow)); + } + + + + + + + + + + + + [HttpPost("{id}/bans/import"), Authorize] + public async Task ImportRoomBans(long? id) + { + var userId = User.GetUserId(); + if (userId == null || id == null) return new Success(false, "Invalid auth", null); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!); + if (!canModify) return Success.Error("Insufficient permissions"); + + var playerIds = new List(); + string rawBody = ""; + + try + { + if (Request.HasFormContentType) + { + var form = await Request.ReadFormAsync(); + foreach (var key in new[] { "playerIds", "PlayerIds", "ids", "Ids", "bannedPlayerIds", "BannedPlayerIds", "players", "Players" }) + if (form.TryGetValue(key, out var values)) + foreach (var value in values) + foreach (var part in (value ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + if (long.TryParse(part, out var parsedId) && parsedId > 0) + playerIds.Add(parsedId); + } + + if (Request.ContentLength is > 0) + { + using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true); + rawBody = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(rawBody)) + { + var parsedBody = JsonNode.Parse(rawBody); + if (parsedBody is JsonArray array) + { + foreach (var item in array) + { + if (item is JsonValue value && value.TryGetValue(out var bareId) && bareId > 0) + playerIds.Add(bareId); + else if (item is JsonObject obj) + playerIds.Add(ParsePlayerId(obj, new[] { "bannedPlayerId", "playerId", "accountId", "id" })); + } + } + else if (parsedBody is JsonObject body) + { + foreach (var key in new[] { "playerIds", "PlayerIds", "ids", "Ids", "bannedPlayerIds", "BannedPlayerIds", "players", "Players", "bans", "Bans" }) + { + if (!body.TryGetPropertyValue(key, out var node) || node == null) continue; + if (node is JsonArray arr) + { + foreach (var item in arr) + { + if (item is JsonValue value && value.TryGetValue(out var bareId) && bareId > 0) + playerIds.Add(bareId); + else if (item is JsonObject obj) + playerIds.Add(ParsePlayerId(obj, new[] { "bannedPlayerId", "playerId", "accountId", "id" })); + } + } + } + } + } + } + } + catch { } + + playerIds = playerIds.Distinct().ToList(); + + if (playerIds.Count == 0) + return Success.Error("No valid player ids found in the import request"); + + var actorProfile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == (long)userId); + var imported = new List(); + var now = DateTime.UtcNow; + + foreach (var targetPlayer in playerIds) + { + if (targetPlayer == (long)userId) continue; + if (await MatchmakingUtils.IsAtLeastRole(targetPlayer, (long)id!, RoleType.CoOwner)) continue; + if (await db.Profiles.AnyAsync(x => x.accountId == targetPlayer) == false) continue; + + var existing = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer && + (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow)); + if (existing != null) continue; + + var ban = new RoomBan + { + roomId = (long)id!, + actorId = (long)userId, + bannedPlayerId = targetPlayer, + bannedByName = actorProfile?.displayName ?? $"Player {(long)userId}", + reason = "", + bannedAt = now, + expiresAt = null + }; + + db.RoomBans.Add(ban); + imported.Add(ban); + } + + await db.SaveChangesAsync(); + + return new Success(true, null, new Dictionary + { + ["imported"] = imported.Count, + ["importedCount"] = imported.Count, + ["requested"] = playerIds.Count, + ["bans"] = imported, + ["playerIds"] = imported.Select(x => x.bannedPlayerId).ToArray() + }); + } + + [HttpPut("{id}/bans"), Authorize] + [HttpPost("{id}/bans"), Authorize] + public async Task AddRoomBan(long? id) + { + var userId = User.GetUserId(); + if (userId == null || id == null) return new Success(false, "Invalid auth", null); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!); + if (!canModify) + { + + + var deniedRole = await RoomUtils.GetRoleForPlayer((long)userId, (long)id!); + var highRanksVal = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "HighRanks"); + return Success.Error("Insufficient permissions"); + } + + long targetPlayer = 0; + string banReason = ""; + + + + + + string[] idKeys = + { + "bannedPlayerId", "bannedPlayerID", "bannedPlayer", "bannedplayerid", + "bannedId", "bannedid", "banned_id", + "bannerId", "bannerid", "banner_id", + "playerId", "playerid", "player_id", "player", "playerAccountId", + "bannedUserId", "bannedAccountId", + "accountId", "account_id", + "targetPlayerId", "targetPlayer", "targetId", "targetid", "target", + "victimId", "victimid", + "userId", "userid", + "id" + }; + + + if (Request.HasFormContentType) + { + foreach (var key in idKeys) + if (targetPlayer <= 0 && long.TryParse(Request.Form[key], out var formPlayer)) targetPlayer = formPlayer; + if (string.IsNullOrEmpty(banReason)) + foreach (var key in new[] { "reason", "banReason", "ban_reason" }) + if (Request.Form.TryGetValue(key, out var formReason)) { banReason = formReason.ToString(); break; } + } + + + foreach (var key in idKeys) + if (targetPlayer <= 0 && long.TryParse(Request.Query[key], out var queryPlayer)) targetPlayer = queryPlayer; + if (string.IsNullOrEmpty(banReason)) + foreach (var key in new[] { "reason", "banReason", "ban_reason" }) + if (Request.Query.TryGetValue(key, out var queryReason)) { banReason = queryReason.ToString(); break; } + + + + string rawBody = ""; + try + { + using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true); + rawBody = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(rawBody)) + { + var parsedBody = JsonNode.Parse(rawBody); + if (parsedBody is JsonObject body) + { + if (targetPlayer <= 0) targetPlayer = ParsePlayerId(body, idKeys); + if (string.IsNullOrEmpty(banReason)) banReason = ParseJsonString(body, "reason", "banReason", "ban_reason"); + } + else if (parsedBody is JsonValue bareValue && targetPlayer <= 0) + { + if (bareValue.TryGetValue(out var bareId)) targetPlayer = bareId; + else if (bareValue.TryGetValue(out var bareStr) && long.TryParse(bareStr, out var bareParsed)) targetPlayer = bareParsed; + } + } + } + catch { } + + if (targetPlayer <= 0) + { + return Success.Error("bannedPlayerId is required"); + } + + + + + + if (targetPlayer == (long)userId) + { + return Success.Error("You cannot ban yourself"); + } + + + + if (await MatchmakingUtils.IsAtLeastRole(targetPlayer, (long)id!, RoleType.CoOwner)) + return Success.Error("You cannot ban a Co-Owner or higher"); + + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == targetPlayer); + if (profile == null) return Success.Error("Player not found"); + + var existing = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer && + (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow)); + if (existing != null) return Success.Error("Player is already banned from this room"); + + var actorProfile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == (long)userId); + var ban = new RoomBan + { + roomId = (long)id!, + actorId = (long)userId, + bannedPlayerId = targetPlayer, + bannedByName = actorProfile?.displayName ?? $"Player {(long)userId}", + reason = banReason.Trim(), + bannedAt = DateTime.UtcNow, + expiresAt = null + }; + + await db.RoomBans.AddAsync(ban); + await db.SaveChangesAsync(); + + + await MatchmakingUtils.KickPlayerFromRoom(targetPlayer, (long)id!, + string.IsNullOrWhiteSpace(ban.reason) ? "You were banned from this room." : $"You were banned from this room: {ban.reason}"); + + return new Success(true, null, ban); + } + + + + + + + + + [HttpDelete("{id}/bans/{bannedPlayerId}"), HttpDelete("{id}/bans"), + HttpPost("{id}/bans/{bannedPlayerId}"), HttpPut("{id}/bans/{bannedPlayerId}"), Authorize] + public async Task RemoveRoomBan(long? id, long? bannedPlayerId) + { + var userId = User.GetUserId(); + if (userId == null || id == null) return new Success(false, "Invalid auth", null); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!); + if (!canModify) return Success.Error("Insufficient permissions"); + + + + + long targetPlayer = bannedPlayerId ?? 0; + string[] idKeys = + { + "bannedPlayerId", "bannedPlayerID", "bannedId", "bannedid", "banned_id", + "playerId", "playerid", "player_id", "player", + "bannerId", "bannerid", "banner_id", + "userId", "userid", "id" + }; + if (targetPlayer <= 0 && Request.HasFormContentType) + foreach (var key in idKeys) + if (long.TryParse(Request.Form[key], out var formPlayer)) { targetPlayer = formPlayer; break; } + if (targetPlayer <= 0) + foreach (var key in idKeys) + if (long.TryParse(Request.Query[key], out var queryPlayer)) { targetPlayer = queryPlayer; break; } + if (targetPlayer <= 0) + { + try + { + using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true); + var raw = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(raw) && JsonNode.Parse(raw) is JsonObject body) + targetPlayer = ParsePlayerId(body, idKeys); + } + catch { } + } + + if (targetPlayer <= 0) + { + return Success.Error("bannedPlayerId is required"); + } + + var ban = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer); + if (ban == null) return Success.Error("Player is not banned from this room"); + + db.RoomBans.Remove(ban); + await db.SaveChangesAsync(); + return new Success(true, null, ban); + } + + [HttpPut("{id}/loadscreen")] + public async Task SetLoadingScreen(long id, string imageName, string title, string subtitle) + { + var userId = User.GetUserId(); + if (userId == null) return new Success(false, "Invalid auth", null); + + var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner); + if (!canModify) return Success.Error("Insufficient permissions"); + + var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id); + if (room == null) return Success.Error("Room not found"); + + var entry = new RoomLoadScreen { ImageName = imageName, RoomId = room.RoomId, Subtitle = subtitle, Title = title }; + List loadScreen = room.loadScreen.FromJson>() ?? new(); + if (loadScreen.Count >= 1) + loadScreen[0] = entry; + else + loadScreen.Add(entry); + room.loadScreen = loadScreen.ToJson(); + + await db.SaveChangesAsync(); + + var fullRoom = await RoomUtils.GetRoom((long)id!); + await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom }); + + return new Success(true, null, fullRoom); + } + + + + + + private static long ParsePlayerId(JsonObject body, string[] idKeys) + { + foreach (var prop in body) + { + if (prop.Value is not JsonValue value) continue; + bool keyMatches = false; + foreach (var key in idKeys) + if (prop.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) { keyMatches = true; break; } + if (!keyMatches) continue; + + if (value.TryGetValue(out var longId)) return longId; + if (value.TryGetValue(out var strId) && long.TryParse(strId, out var parsed)) return parsed; + if (value.TryGetValue(out var doubleId) && doubleId > 0 && doubleId == Math.Floor(doubleId)) return (long)doubleId; + } + return 0; + } + + + + + + private static string ParseJsonString(JsonObject body, params string[] keys) + { + foreach (var prop in body) + { + if (prop.Value is not JsonValue value) continue; + bool keyMatches = false; + foreach (var key in keys) + if (prop.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) { keyMatches = true; break; } + if (keyMatches && value.TryGetValue(out var str)) return str ?? ""; + } + return ""; + } + } +} diff --git a/Server/Rooms/VerifyRoleRequest.cs b/Server/Rooms/VerifyRoleRequest.cs new file mode 100644 index 0000000..b2b2368 --- /dev/null +++ b/Server/Rooms/VerifyRoleRequest.cs @@ -0,0 +1,9 @@ +namespace RadicadeServer.RecNet.Rooms +{ + public class VerifyRoleRequest + { + public long roomId { get; set; } + public RoomRole role { get; set; } = null!; + public string? context { get; set; } + } +} diff --git a/Server/Royale/RoyaleDtos.cs b/Server/Royale/RoyaleDtos.cs new file mode 100644 index 0000000..57f0e5c --- /dev/null +++ b/Server/Royale/RoyaleDtos.cs @@ -0,0 +1,35 @@ +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Royale +{ + public class RoyaleProgressDTO + { + public long TotalXP { get; set; } + public int Level { get; set; } + public int RankIdx { get; set; } + public string RankName { get; set; } = string.Empty; + public long CurrentLevelXPThreshold { get; set; } + public long NextLevelXPThreshold { get; set; } + public int NextLevelAcornReward { get; set; } + } + + public class RoyaleMatchCompleteStats + { + public int Rank { get; set; } + public int NumEliminations { get; set; } + public int SecondsAlive { get; set; } + public bool WalkGame { get; set; } + public bool CustomGame { get; set; } + public int ChestsOpened { get; set; } + public int ShieldPotionsConsumed { get; set; } + public int HealthPotionsConsumed { get; set; } + public int SecondsInAir { get; set; } + } + + public class RoyaleMatchCompleteResponse + { + public List XPAwardStrings { get; set; } = new(); + public long TotalXPAwarded { get; set; } + public List NewProgress { get; set; } = new(); + } +} diff --git a/Server/Royale/RoyaleShared.cs b/Server/Royale/RoyaleShared.cs new file mode 100644 index 0000000..8c65ab8 --- /dev/null +++ b/Server/Royale/RoyaleShared.cs @@ -0,0 +1,97 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Leveling; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Royale; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Royale +{ + public static class RoyaleShared + { + public static readonly string[] RankNames = + { + "Recruit", "Cadet", "Private", "Corporal", "Sergeant", + "Lieutenant", "Captain", "Major", "Colonel", "General" + }; + + public static int RankIdxForLevel(int level) => + Math.Clamp((level - 1) / 5, 0, RankNames.Length - 1); + + public static (long current, long next) XpThresholds(List map, int level) + { + long cumulative = 0; + long current = 0; + long next = 0; + foreach (var entry in map.OrderBy(x => x.Level)) + { + if (entry.Level <= 0) continue; + if (entry.Level <= level) + { + cumulative += entry.RequiredXp; + current = cumulative; + } + else + { + if (next == 0) next = cumulative + entry.RequiredXp; + break; + } + } + if (next == 0) next = current; + return (current, next); + } + + public static RoyaleProgressDTO BuildProgress(ProgressionData data) + { + var map = LevelUpGiftService.ParseMap(); + int level = LevelUpGiftService.DisplayLevel(data.Level); + var (current, next) = XpThresholds(map, level); + int rankIdx = RankIdxForLevel(level); + long totalXp = level >= LevelUpGiftService.MaxLevel ? next : data.XP; + return new RoyaleProgressDTO + { + TotalXP = totalXp, + Level = level, + RankIdx = rankIdx, + RankName = RankNames[rankIdx], + CurrentLevelXPThreshold = current, + NextLevelXPThreshold = next, + NextLevelAcornReward = Math.Max(10, level * 5) + }; + } + + public static async Task AwardXpAsync( + RecDBContext db, long playerId, int xp, string description, List awardStrings) + { + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + if (progression == null) + { + progression = new ProgressionData { PlayerId = playerId }; + db.Progression.Add(progression); + } + + if (xp <= 0) + { + await db.SaveChangesAsync(); + return BuildProgress(progression); + } + + int granted = (int)Math.Min(xp, int.MaxValue); + int oldLevel = progression.Level; + progression.XP = (int)Math.Min(progression.XP + granted, int.MaxValue); + progression.RecordXpGrant(granted); + + await LevelUpGiftService.DeliverLevelUpGiftsAsync(db, playerId, + recomputeFromXp: true, fromLevel: oldLevel, notify: true); + + await db.SaveChangesAsync(); + if (granted > 0 && awardStrings.Count == 0) + awardStrings.Add($"{description}: +{granted} XP"); + + if (granted > 0) + await RoomProgressionService.SendProgressionLevelUpdateAsync(db, playerId, progression); + + return BuildProgress(progression); + } + } +} diff --git a/Server/Royale/RoyaleV1.cs b/Server/Royale/RoyaleV1.cs new file mode 100644 index 0000000..78cc263 --- /dev/null +++ b/Server/Royale/RoyaleV1.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Royale; + +namespace RadicadeServer.API.Royale.V1 +{ + [Route("api/royale/v1")] + public class RoyaleV1(RecDBContext db) : Controller + { + [HttpGet("current"), Authorize] + public async Task> GetCurrentProgress() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId.Value); + data ??= new ProgressionData { PlayerId = userId.Value }; + + return Ok(RoyaleShared.BuildProgress(data)); + } + } +} diff --git a/Server/Royale/RoyaleV2.cs b/Server/Royale/RoyaleV2.cs new file mode 100644 index 0000000..a189ccf --- /dev/null +++ b/Server/Royale/RoyaleV2.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Royale; + +namespace RadicadeServer.API.Royale.V2 +{ + [Route("api/royale/v2")] + public class RoyaleV2(RecDBContext db) : Controller + { + private static int PlacementXp(int rank) + { + if (rank <= 0) return 10; + if (rank == 1) return 50; + if (rank <= 3) return 30; + if (rank <= 8) return 15; + return 10; + } + + [HttpPost("matchcomplete"), Authorize] + public async Task> MatchComplete([FromBody] RoyaleMatchCompleteStats? stats) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + if (stats == null) return BadRequest(); + + var awardStrings = new List(); + int totalXp = 0; + + if (!stats.CustomGame) + { + int placement = PlacementXp(stats.Rank); + totalXp += placement; + awardStrings.Add($"Match finished: +{placement} XP"); + + if (stats.NumEliminations > 0) + { + int elimXp = Math.Min(stats.NumEliminations, 5) * 10; + totalXp += elimXp; + awardStrings.Add($"Eliminations: +{elimXp} XP"); + } + + if (stats.ChestsOpened > 0) + { + int chestXp = Math.Min(stats.ChestsOpened, 10) * 2; + totalXp += chestXp; + awardStrings.Add($"Chests opened: +{chestXp} XP"); + } + } + else + { + awardStrings.Add("Custom game: no XP awarded"); + } + + long xpBefore = await CurrentXp(userId.Value); + var progress = await RoyaleShared.AwardXpAsync(db, userId.Value, totalXp, "Rec Royale match", awardStrings); + + var response = new RoyaleMatchCompleteResponse + { + XPAwardStrings = awardStrings, + TotalXPAwarded = progress.TotalXP - xpBefore, + NewProgress = new List { progress } + }; + return Ok(response); + } + + private async Task CurrentXp(long playerId) + { + var data = await db.Progression.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerId == playerId); + return data?.XP ?? 0; + } + } +} diff --git a/Server/Server.csproj b/Server/Server.csproj new file mode 100644 index 0000000..673b1de --- /dev/null +++ b/Server/Server.csproj @@ -0,0 +1,77 @@ + + + + net9.0 + enable + enable + d0633c8b-2e66-42fc-9a45-4729a639a449 + RadicadeServer + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + StorefrontCaptures/%(Filename)%(Extension) + + + + + + + + LoadingScreens/%(Filename)%(Extension) + + + + + diff --git a/Server/Storage/StorageAPI.cs b/Server/Storage/StorageAPI.cs new file mode 100644 index 0000000..90003c6 --- /dev/null +++ b/Server/Storage/StorageAPI.cs @@ -0,0 +1,222 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; +using RadicadeServer.RecNet.CDN; + +namespace RadicadeServer.Storage +{ + [Route("storage")] + public class StorageAPI : Controller + { + private const long MaxUploadBytes = 256L * 1024 * 1024; + private const int MaxMultipartPreambleBytes = 1024 * 1024; + + private static async Task FindFirstBoundaryAsync(Stream body, string boundary, CancellationToken cancellationToken) + { + var marker = System.Text.Encoding.ASCII.GetBytes("--" + boundary); + if (marker.Length > MaxMultipartPreambleBytes) return null; + var failure = new int[marker.Length]; + for (var i = 1; i < marker.Length; i++) + { + var matched = failure[i - 1]; + while (matched > 0 && marker[i] != marker[matched]) matched = failure[matched - 1]; + if (marker[i] == marker[matched]) matched++; + failure[i] = matched; + } + + var buffer = new byte[8192]; + var matchedBytes = 0; + long scanned = 0; + while (scanned <= MaxMultipartPreambleBytes) + { + var read = await body.ReadAsync(buffer.AsMemory(), cancellationToken); + if (read == 0) break; + var endOfBuffer = scanned + read; + for (var i = 0; i < read; i++) + { + while (matchedBytes > 0 && buffer[i] != marker[matchedBytes]) + matchedBytes = failure[matchedBytes - 1]; + if (buffer[i] == marker[matchedBytes]) matchedBytes++; + if (matchedBytes != marker.Length) continue; + + var markerStart = scanned + i + 1 - marker.Length; + body.Position = markerStart + marker.Length; + var nextByte = body.ReadByte(); + var secondByte = body.ReadByte(); + body.Position = endOfBuffer; + if (markerStart <= MaxMultipartPreambleBytes && + (nextByte == 13 && secondByte == 10 || nextByte == 45 && secondByte == 45) && + (markerStart == 0 || ReadByteAt(body, markerStart - 1) == 10)) + { + body.Position = markerStart; + return markerStart; + } + matchedBytes = failure[matchedBytes - 1]; + } + scanned = endOfBuffer; + } + return null; + } + + private static int ReadByteAt(Stream body, long position) + { + var currentPosition = body.Position; + body.Position = position; + var value = body.ReadByte(); + body.Position = currentPosition; + return value; + } + + [HttpPost("upload/unityassets"), Authorize] + [RequestSizeLimit(MaxUploadBytes)] + public async Task UploadUnityAssets() + { + if (!Request.HasFormContentType) return BadRequest(new { error = "multipart files required" }); + + + + + var contentType = MediaTypeHeaderValue.Parse(Request.ContentType!); + var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; + if (string.IsNullOrWhiteSpace(boundary)) return BadRequest(new { error = "multipart boundary required" }); + if (boundary.Length > 200) return BadRequest(new { error = "multipart boundary too long" }); + + + + + var contentEncoding = Request.Headers["Content-Encoding"].ToString().Trim(); + var isGzip = string.Equals(contentEncoding, "gzip", StringComparison.OrdinalIgnoreCase); + if (!string.IsNullOrEmpty(contentEncoding) && !isGzip) + return StatusCode(StatusCodes.Status415UnsupportedMediaType, new { error = "unsupported content encoding" }); + + Stream multipartBody; + await using var decompressedBody = isGzip + ? new FileStream(Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".studio-upload"), FileMode.CreateNew, + FileAccess.ReadWrite, FileShare.None, 81920, FileOptions.Asynchronous | FileOptions.DeleteOnClose) + : null; + if (decompressedBody != null) + { + await using (var gzip = new System.IO.Compression.GZipStream(Request.Body, System.IO.Compression.CompressionMode.Decompress, leaveOpen: true)) + { + var buffer = new byte[81920]; + long total = 0; + int read; + while ((read = await gzip.ReadAsync(buffer.AsMemory(), HttpContext.RequestAborted)) > 0) + { + total += read; + if (total > MaxUploadBytes) return StatusCode(StatusCodes.Status413PayloadTooLarge); + await decompressedBody.WriteAsync(buffer.AsMemory(0, read), HttpContext.RequestAborted); + } + } + decompressedBody.Position = 0; + multipartBody = decompressedBody; + } + else + { + Request.EnableBuffering(); + multipartBody = Request.Body; + } + + var preambleLength = await FindFirstBoundaryAsync(multipartBody, boundary, HttpContext.RequestAborted); + if (preambleLength is null) + return BadRequest(new { error = "multipart boundary not found within the preamble limit" }); + if (preambleLength > 0) + Console.WriteLine($"[StudioUpload] skipped {preambleLength} multipart preamble bytes"); + + var reader = new MultipartReader(boundary, multipartBody) + { + + + + BodyLengthLimit = null, + HeadersLengthLimit = 64 * 1024 + }; + var responses = new List(); + MultipartSection? section; + while ((section = await reader.ReadNextSectionAsync(HttpContext.RequestAborted)) != null) + { + if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var disposition) || + !string.Equals(disposition.DispositionType.Value, "form-data", StringComparison.OrdinalIgnoreCase)) + continue; + + var fileName = HeaderUtilities.RemoveQuotes(disposition.FileNameStar.HasValue + ? disposition.FileNameStar + : disposition.FileName).Value; + if (string.IsNullOrWhiteSpace(fileName)) continue; + + responses.Add(await StudioAssetStorage.Save(section.Body, HttpContext.RequestAborted)); + } + + if (responses.Count == 0) return BadRequest(new { error = "no files supplied" }); + return Ok(responses); + } + + [HttpPost("upload"), Authorize] + [RequestSizeLimit(MaxUploadBytes)] + public async Task> Upload() + { + var boundaryHeader = Request.ContentType == null ? null : MediaTypeHeaderValue.Parse(Request.ContentType).Boundary; + var boundary = HeaderUtilities.RemoveQuotes(boundaryHeader).Value; + if (string.IsNullOrWhiteSpace(boundary)) return BadRequest(new { error = "multipart boundary required" }); + + var contentEncoding = Request.Headers["Content-Encoding"].ToString().Trim(); + var isGzip = string.Equals(contentEncoding, "gzip", StringComparison.OrdinalIgnoreCase); + if (!string.IsNullOrEmpty(contentEncoding) && !isGzip) + return StatusCode(StatusCodes.Status415UnsupportedMediaType, new { error = "unsupported content encoding" }); + + Stream multipartBody; + await using var decompressedBody = isGzip + ? new FileStream(Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".studio-upload"), FileMode.CreateNew, + FileAccess.ReadWrite, FileShare.None, 81920, FileOptions.Asynchronous | FileOptions.DeleteOnClose) + : null; + if (decompressedBody != null) + { + await using (var gzip = new System.IO.Compression.GZipStream(Request.Body, System.IO.Compression.CompressionMode.Decompress, leaveOpen: true)) + { + var buffer = new byte[81920]; + long total = 0; + int read; + while ((read = await gzip.ReadAsync(buffer.AsMemory(), HttpContext.RequestAborted)) > 0) + { + total += read; + if (total > MaxUploadBytes) return StatusCode(StatusCodes.Status413PayloadTooLarge); + await decompressedBody.WriteAsync(buffer.AsMemory(0, read), HttpContext.RequestAborted); + } + } + decompressedBody.Position = 0; + multipartBody = decompressedBody; + } + else + { + Request.EnableBuffering(); + multipartBody = Request.Body; + } + + var preambleLength = await FindFirstBoundaryAsync(multipartBody, boundary, HttpContext.RequestAborted); + if (preambleLength is null) return BadRequest(new { error = "multipart boundary not found within the preamble limit" }); + + var reader = new MultipartReader(boundary, multipartBody) + { + BodyLengthLimit = null, + HeadersLengthLimit = 64 * 1024 + }; + MultipartSection? section; + while ((section = await reader.ReadNextSectionAsync(HttpContext.RequestAborted)) != null) + { + if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var disposition) || + !string.Equals(disposition.DispositionType.Value, "form-data", StringComparison.OrdinalIgnoreCase)) continue; + + var fileName = HeaderUtilities.RemoveQuotes(disposition.FileNameStar.HasValue + ? disposition.FileNameStar + : disposition.FileName).Value; + if (string.IsNullOrWhiteSpace(fileName)) continue; + + var stored = await StudioAssetStorage.Save(section.Body, HttpContext.RequestAborted); + if (stored.Length <= 0) return BadRequest(new { error = "empty upload" }); + return new UploadResponse(stored.Filename); + } + return BadRequest(new { error = "no file supplied" }); + } + } +} diff --git a/Server/Storage/StudioAssetStorage.cs b/Server/Storage/StudioAssetStorage.cs new file mode 100644 index 0000000..00eb172 --- /dev/null +++ b/Server/Storage/StudioAssetStorage.cs @@ -0,0 +1,54 @@ +using System.Numerics; +using System.Security.Cryptography; + +namespace RadicadeServer.Storage; + +public sealed record StudioAssetUpload(string Filename, string Hash, long Length); + +public static class StudioAssetStorage +{ + + + public static string Filename(byte[] digest) + { + var value = new BigInteger(digest, isUnsigned: true, isBigEndian: false); + const string alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; + var result = ""; + while (value > 0) + { + value = BigInteger.DivRem(value, 36, out var remainder); + result = alphabet[(int)remainder] + result; + } + return result.PadLeft(50, '0')[..40]; + } + + public static async Task Save(IFormFile file, CancellationToken cancellation) + { + await using var input = file.OpenReadStream(); + return await Save(input, cancellation); + } + + public static async Task Save(Stream input, CancellationToken cancellation) + { + Directory.CreateDirectory(DBConfig.CDNStorage); + var temporary = Path.Combine(DBConfig.CDNStorage, Guid.NewGuid() + ".uploading"); + try + { + await using (var output = System.IO.File.Create(temporary)) + await input.CopyToAsync(output, cancellation); + var length = new FileInfo(temporary).Length; + byte[] hash; + await using (var storedFile = System.IO.File.OpenRead(temporary)) + hash = await SHA256.HashDataAsync(storedFile, cancellation); + var filename = Filename(hash); + var destination = Path.Combine(DBConfig.CDNStorage, filename); + try { System.IO.File.Move(temporary, destination); } + catch (IOException) when (System.IO.File.Exists(destination)) { } + return new StudioAssetUpload(filename, Convert.ToBase64String(hash), length); + } + finally + { + if (System.IO.File.Exists(temporary)) System.IO.File.Delete(temporary); + } + } +} diff --git a/Server/Storefront/BaseStorefrontDTO.cs b/Server/Storefront/BaseStorefrontDTO.cs new file mode 100644 index 0000000..25cf116 --- /dev/null +++ b/Server/Storefront/BaseStorefrontDTO.cs @@ -0,0 +1,60 @@ +namespace RadicadeServer.RecNet.Store +{ + public enum StorefrontTypes + { + None = 0, + LaserTag = 1, + RecCenter = 2, + Watch = 3, + MitMShop = 4, + Quest_LostSkulls = 100, + Quest_Dracula = 101, + Quest_GoldenTrophy = 102, + Quest_CrimsonCauldron = 103, + RecRoyale = 200, + Cafe = 300, + Paintball = 400, + Paintball_River = 401, + Paintball_Homestead = 402, + Paintball_Quarry = 403, + Paintball_ClearCut = 404, + Paintball_Spillway = 405, + Paintball_SunsetDriveIn = 406, + Bowling = 500, + StuntRunner = 600, + DormMirror = 700, + InventionStore = 800, + RoomKeys = 900, + Player_Profile = 1000, + Room_Save = 1100, + RoomCurrency = 1200, + Wishlist = 1300, + RoomConsumable = 1400, + RecRally = 1600, + PopUpShop_1 = 1700, + PopUpShop_2 = 1701, + PopUpShop_3 = 1702, + PopUpShop_4 = 1703, + PopUpShop_5 = 1704, + PopUpShop_6 = 1705, + PopUpShop_7 = 1706, + PopUpShop_8 = 1707, + PopUpShop_9 = 1708, + PopUpShop_10 = 1709, + CustomAvatarItems = 1800, + ProgressionEventPurchasableXpBoosts = 1900, + PopUpShop_ProgressionEvent_1 = 2000, + PopUpShop_ProgressionEvent_2 = 2001, + PopUpShop_ProgressionEvent_3 = 2002, + PopUpShop_ProgressionEvent_4 = 2003, + PopUpShop_ProgressionEvent_5 = 2004 + } + public class BaseStorefrontDTO + { + public StorefrontTypes StorefrontType { get; set; } + + public DateTime NextUpdate { get; set; } + + public DateTime? EndDate { get; set; } + } +} diff --git a/Server/Storefront/GameReward.cs b/Server/Storefront/GameReward.cs new file mode 100644 index 0000000..7e4fa3e --- /dev/null +++ b/Server/Storefront/GameReward.cs @@ -0,0 +1,23 @@ +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Storefront +{ + public class GameReward + { + public long RewardSelectionId { get; set; } + public string Message { get; set; } = null!; + public GiftContext GiftContext { get; set; } + public RewardType RewardType { get; set; } + public Gift GiftDrop1 { get; set; } = null!; + public Gift GiftDrop2 { get; set; } = null!; + public Gift GiftDrop3 { get; set; } = null!; + public DateTime CreatedAt { get; set; } + + public bool Consumed { get; set; } + + public long ConsumedGiftDropId { get; set; } + + public DateTime? ConsumedAt { get; set; } + } +} diff --git a/Server/Storefront/GiftDropDB.cs b/Server/Storefront/GiftDropDB.cs new file mode 100644 index 0000000..7dc8a89 --- /dev/null +++ b/Server/Storefront/GiftDropDB.cs @@ -0,0 +1,80 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Store; + +namespace RadicadeServer.RecNet.Store +{ + public class GiftDropDB + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long GiftDropId { get; set; } + + public string Name { get; set; } = ""; + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + + + + + + public long StorefrontId { get; set; } + + + public string Description { get; set; } = ""; + + public int CurrencyAmount { get; set; } + public int XPAmount { get; set; } + + + public string AvatarItems { get; set; } = ""; + + + public string EquipmentItems { get; set; } = ""; + + + public string ConsumableItems { get; set; } = ""; + + + + + + + + + public int PurchasableItemId { get; set; } + + public bool IsFeatured { get; set; } + + + public int Price { get; set; } + + public CurrencyType CurrencyType { get; set; } = CurrencyType.RecCenterTokens; + + public Rarity Rarity { get; set; } = Rarity.Common; + + public GiftContext Context { get; set; } = GiftContext.Game_Drop; + + public AvatarItemType AvatarItemType { get; set; } = AvatarItemType.Outfit; + + public string EquipmentModificationGuid { get; set; } = ""; + + public string Tooltip { get; set; } = ""; + + public bool IsQuery { get; set; } + public bool Unique { get; set; } + public bool SubscribersOnly { get; set; } + public long ItemSetId { get; set; } + public string ItemSetFriendlyName { get; set; } = ""; + } + + public class ItemSetDB + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long ItemSetId { get; set; } + + public string FriendlyName { get; set; } = string.Empty; + } +} diff --git a/Server/Storefront/GiftDropStorefront.cs b/Server/Storefront/GiftDropStorefront.cs new file mode 100644 index 0000000..3bc691b --- /dev/null +++ b/Server/Storefront/GiftDropStorefront.cs @@ -0,0 +1,13 @@ +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Store; + +namespace RadicadeServer.RecNet.Storefront +{ + public class GiftDropStorefront : BaseStorefrontDTO + { + public DateTime? NewUntil { get; set; } + + public List StoreItems { get; set; } = new(); + } +} diff --git a/Server/Storefront/GiftPackage.cs b/Server/Storefront/GiftPackage.cs new file mode 100644 index 0000000..66b4320 --- /dev/null +++ b/Server/Storefront/GiftPackage.cs @@ -0,0 +1,10 @@ +using RadicadeServer.RecNet.Avatar; + +namespace RadicadeServer.RecNet.Storefront +{ + public class GiftPackage + { + public long Id { get; set; } + public List Gifts { get; set; } = new(); + } +} diff --git a/Server/Storefront/PurchaseAPI.cs b/Server/Storefront/PurchaseAPI.cs new file mode 100644 index 0000000..40f629d --- /dev/null +++ b/Server/Storefront/PurchaseAPI.cs @@ -0,0 +1,170 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Storefront; + +namespace RadicadeServer.API.Storefront +{ + [Route("api/storefronts/")] + [Route("econ/api/storefronts/")] + public class PurchaseAPI(RecDBContext _db) : Controller + { + [HttpPost("v1/buyItem"), Authorize] + public async Task BuyItem([FromBody] RequestPurchaseItemDTO? request) + => await ProcessPurchase(request); + + [HttpPost("v1/buyTier"), Authorize] + public async Task BuyTier([FromBody] RequestPurchaseItemDTO? request) + => await ProcessPurchase(request); + + [HttpPost("v1/buyElite"), Authorize] + public async Task BuyElite([FromBody] RequestPurchaseItemDTO? request) + => await ProcessPurchase(request); + + [HttpGet("v2/buyInvention"), Authorize] + public async Task BuyInvention([FromQuery] long inventionId, [FromQuery] long requestedPrice) + => await ProcessInventionPurchase(inventionId, requestedPrice); + + private async Task ProcessPurchase(RequestPurchaseItemDTO? request) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + if (request == null) + { + return BadRequest(new { error = "invalid_request" }); + } + + var outcome = await StorefrontShared.ExecutePurchaseAsync( + _db, + userId.Value, + request.StorefrontType, + request.PurchasableItemId, + request.CurrencyType, + request.RequestedPrice, + request.Gift); + + if (outcome.Response != null) + return Ok(outcome.Response); + return outcome.Error switch + { + "cannot_gift_self" or "invalid_recipient" => BadRequest(new { error = outcome.Error, message = outcome.ErrorDetail }), + _ => StatusCode(StatusCodes.Status500InternalServerError, new { error = outcome.Error, message = outcome.ErrorDetail }) + }; + } + + private async Task ProcessInventionPurchase(long inventionId, long requestedPrice) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var invention = await _db.Inventions.FirstOrDefaultAsync(x => x.InventionId == inventionId); + if (invention == null) return NotFound(); + + var currency = await _db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId); + if (currency == null) return NotFound(); + + var unlocks = await _db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId); + if (unlocks == null) + { + unlocks = new PlayerUnlocks { AccountId = userId.Value }; + await _db.Unlocks.AddAsync(unlocks); + } + + var owned = ReadOwnedInventions(unlocks); + if (owned.Contains(inventionId)) return Ok(new Success(false, "Invention is already owned.", null)); + + var price = invention.Price ?? 0; + if (requestedPrice != price) + { + return Ok(new Success(false, "Invalid requested price.", null)); + } + + var (balance, delta, sufficient) = TryDeductCurrency(currency, CurrencyType.RecCenterTokens, price); + + if (!sufficient) + { + return Ok(new Success(false, "You do not have enough tokens for this invention.", null)); + } + + owned.Add(inventionId); + unlocks.Inventions = owned.ToJson(); + + await _db.SaveChangesAsync(); + + var response = new InventionPurchaseResponseDTO.BalanceResponse + { + Balance = balance, + BalanceAddType = StorefrontBalanceAddTypes.CommercePurchase, + CurrencyType = CurrencyType.RecCenterTokens, + Delta = delta, + Result = new List + { + new InventionPurchaseResponseDTO.BalanceResponse.InventionData{ Invention = invention.ToInvention(), Status = PurchaseStatus.OK } + } + }; + var purchased = new InventionPurchaseResponseDTO + { + InventionResponse = invention.ToInvention(), + BalanceUpdateResponse = response + }; + + await Notify.SendNotify((long)userId, new Notification { Id = NotificationType.StorefrontBalancePurchase, Msg = purchased }); + await Notify.SendNotify((long)userId, new Notification { Id = NotificationType.StorefrontBalanceUpdate, Msg = response }); + + var buyerProfile = await _db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == userId); + if (buyerProfile != null) + await Notify.SendNotify((long)userId, new Notification { Id = NotificationType.RefreshLogin, Msg = buyerProfile }); + + return Ok(new Success(true, null, purchased)); + } + + private static List ReadOwnedInventions(PlayerUnlocks unlocks) + { + if (string.IsNullOrWhiteSpace(unlocks.Inventions) || unlocks.Inventions == "null") + return new List(); + + try + { + return unlocks.Inventions.FromJson>() ?? new List(); + } + catch + { + return new List(); + } + } + + private static (long balance, long delta, bool sufficient) TryDeductCurrency( + RecNet.Accounts.Currency currency, CurrencyType type, long cost) + { long balance = type switch + { + CurrencyType.RecCenterTokens => currency.RecTokens, + CurrencyType.LaserTagTickets => currency.LaserTagTickets, + CurrencyType.LostSkullsGold => currency.Gold, + CurrencyType.DraculaSilver => currency.DraculaSilver, + _ => 0 + }; + + if (balance < cost) return (balance, 0, false); + + switch (type) + { + case CurrencyType.RecCenterTokens: currency.RecTokens -= cost; break; + case CurrencyType.LaserTagTickets: currency.LaserTagTickets -= cost; break; + case CurrencyType.LostSkullsGold: currency.Gold -= cost; break; + case CurrencyType.DraculaSilver: currency.DraculaSilver -= cost; break; + } + + return (balance - cost, cost, true); + } + } +} diff --git a/Server/Storefront/RoomGiftDropShopAPI.cs b/Server/Storefront/RoomGiftDropShopAPI.cs new file mode 100644 index 0000000..1a4828f --- /dev/null +++ b/Server/Storefront/RoomGiftDropShopAPI.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.API.Storefront; + +[ApiController] +[Route("econ/econ/roomGiftDropShops")] +public class RoomGiftDropShopAPI : ControllerBase +{ + [HttpGet("room/{roomId:long}")] + public IActionResult GetRoomGiftDropShops(long roomId) => Ok(Array.Empty()); +} diff --git a/Server/Storefront/SeasonStorefrontDTOs.cs b/Server/Storefront/SeasonStorefrontDTOs.cs new file mode 100644 index 0000000..a033c95 --- /dev/null +++ b/Server/Storefront/SeasonStorefrontDTOs.cs @@ -0,0 +1,40 @@ +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Store; + +namespace RadicadeServer.RecNet.Storefront +{ + public class SeasonRewardItemDTO + { + public bool RequiresEliteUpgrade { get; set; } + public Gift? GiftDrop { get; set; } + public float SeasonXpBoost { get; set; } + } + + public class SeasonTierDTO : PurchasableGiftDrop + { + public int Tier { get; set; } + public List Rewards { get; set; } = new(); + } + + public class SeasonPersonalDetailsDTO + { + public bool HasEliteUpgrade { get; set; } + public int HasEliteUpgradePlatformMask { get; set; } + public int CurrentSeasonTier { get; set; } + public DateTime ModifiedAt { get; set; } + } + + public class SeasonStorefrontDTO : BaseStorefrontDTO + { + public int Season { get; set; } + public string Name { get; set; } = string.Empty; + public DateTime StartAt { get; set; } + public DateTime EndAt { get; set; } + public CurrencyType CurrencyType { get; set; } + public PurchasableGiftDrop? EliteUpgrade { get; set; } + public List Tiers { get; set; } = new(); + public SeasonPersonalDetailsDTO PersonalDetails { get; set; } = new(); + public DateTime? NewUntil { get; set; } + } +} diff --git a/Server/Storefront/StorefrontAPI.cs b/Server/Storefront/StorefrontAPI.cs new file mode 100644 index 0000000..81c2ac1 --- /dev/null +++ b/Server/Storefront/StorefrontAPI.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Store; +using RadicadeServer.RecNet.Storefront; + +namespace RadicadeServer.API.Storefront +{ + [Route("api/storefronts/")] + public class StorefrontAPI : Controller + { + } +} diff --git a/Server/Storefront/StorefrontCatalog.cs b/Server/Storefront/StorefrontCatalog.cs new file mode 100644 index 0000000..6223c49 --- /dev/null +++ b/Server/Storefront/StorefrontCatalog.cs @@ -0,0 +1,355 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Store; + +namespace RadicadeServer.RecNet.Storefront +{ + public static class StorefrontCatalog + { + public const string ResourcePrefix = "StorefrontCaptures/"; + + public const string FileSearchPattern = "sf*.json"; + + private static readonly object Gate = new(); + private static readonly Dictionary Captures = new(); + private static readonly Dictionary ByPurchasableItemId = new(); + private static bool _loaded; + + public static Func> FileSource { get; set; } = DiscoverFiles; + + public static bool IsLoaded + { + get { lock (Gate) return _loaded; } + } + + public static IReadOnlyCollection StorefrontIds + { + get + { + Load(); + lock (Gate) return Captures.Keys.OrderBy(id => id).ToArray(); + } + } + + public static int Load() + { + lock (Gate) + { + if (_loaded) return Captures.Count; + + var captures = ReadCaptures(); + foreach (var capture in captures) + { + Captures[capture.StorefrontId] = capture.Capture; + for (var index = 0; index < capture.Capture.StoreItems.Count; index++) + { + var item = capture.Capture.StoreItems[index]; + if (item.PurchasableItemId <= 0) continue; + ByPurchasableItemId.TryAdd(item.PurchasableItemId, (capture.StorefrontId, index)); + } + } + + _loaded = true; + + if (captures.Count == 0) + { + return Captures.Count; + } + + var fromFiles = captures.Where(capture => capture.FromFile).Select(capture => capture.StorefrontId).OrderBy(id => id).ToList(); + return Captures.Count; + } + } + + public static List GetItems(long storefrontId) + { + Load(); + lock (Gate) + { + return Captures.TryGetValue(storefrontId, out var capture) + ? ToItems(capture) + : new List(); + } + } + + public static PurchasableGiftDrop? FindItem(long storefrontId, int purchasableItemId) + { + Load(); + lock (Gate) + { + if (!Captures.TryGetValue(storefrontId, out var capture)) return null; + var item = capture.StoreItems.FirstOrDefault(x => x.PurchasableItemId == purchasableItemId); + return item == null ? null : ToItem(item, capture); + } + } + + public static PurchasableGiftDrop? FindItem(int purchasableItemId) + { + Load(); + lock (Gate) + { + if (!ByPurchasableItemId.TryGetValue(purchasableItemId, out var location)) return null; + if (!Captures.TryGetValue(location.StorefrontId, out var capture)) return null; + if (location.Index >= capture.StoreItems.Count) return null; + return ToItem(capture.StoreItems[location.Index], capture); + } + } + + + public sealed record LoadedCapture(long StorefrontId, string Source, bool FromFile, StorefrontCapture Capture); + + public static List ReadCaptures() + { + var loaded = new List(); + + foreach (var (name, json) in EmbeddedCaptures()) + { + var capture = ParseCapture(json, name); + if (capture == null) continue; + var storefrontId = ResolveStorefrontId(capture, name); + if (storefrontId == null) + { + continue; + } + loaded.Add(new LoadedCapture(storefrontId.Value, name, FromFile: false, capture)); + } + + foreach (var path in FileSource()) + { + var name = Path.GetFileName(path); + var capture = ReadCapture(path); + if (capture == null) continue; + + var storefrontId = ResolveStorefrontId(capture, name); + if (storefrontId == null) + { + continue; + } + + var replaced = loaded.FindIndex(existing => existing.StorefrontId == storefrontId.Value); + if (replaced >= 0) + { + loaded[replaced] = new LoadedCapture(storefrontId.Value, path, FromFile: true, capture); + } + else + { + loaded.Add(new LoadedCapture(storefrontId.Value, path, FromFile: true, capture)); + } + } + + return loaded.OrderBy(capture => capture.StorefrontId).ToList(); + } + + public static IReadOnlyList<(string Name, string Json)> EmbeddedCaptures() + { + var assembly = typeof(StorefrontCatalog).Assembly; + var payloads = new List<(string Name, string Json)>(); + + foreach (var resource in assembly.GetManifestResourceNames() + .Where(name => name.StartsWith(ResourcePrefix, StringComparison.Ordinal)) + .OrderBy(name => name, StringComparer.Ordinal)) + { + using var stream = assembly.GetManifestResourceStream(resource); + if (stream == null) + { + continue; + } + + using var reader = new StreamReader(stream); + payloads.Add((resource[ResourcePrefix.Length..], reader.ReadToEnd())); + } + + return payloads; + } + + public static StorefrontCapture? ReadCapture(string path) + { + try + { + return ParseCapture(File.ReadAllText(path), Path.GetFileName(path)); + } + catch (Exception) + { + return null; + } + } + + public static StorefrontCapture? ParseCapture(string json, string sourceName) + { + try + { + var capture = JsonSerializer.Deserialize(json, JsonOptions); + if (capture == null) + { + return null; + } + return capture; + } + catch (Exception) + { + return null; + } + } + + public static long? ResolveStorefrontId(StorefrontCapture capture, string path) + { + if (capture.StorefrontType is > 0) return capture.StorefrontType.Value; + return StorefrontIdFromFileName(path); + } + + public static long? StorefrontIdFromFileName(string path) + { + var name = Path.GetFileNameWithoutExtension(path); + if (!name.StartsWith("sf", StringComparison.OrdinalIgnoreCase)) return null; + var digits = name[2..].Trim(); + return int.TryParse(digits, out var id) && id > 0 ? id : null; + } + + + public sealed class StorefrontCapture + { + [JsonPropertyName("StorefrontType")] public int? StorefrontType { get; set; } + [JsonPropertyName("NextUpdate")] public DateTime? NextUpdate { get; set; } + [JsonPropertyName("NewUntil")] public DateTime? NewUntil { get; set; } + [JsonPropertyName("SubscriberDiscountPercent")] public int SubscriberDiscountPercent { get; set; } + [JsonPropertyName("StoreItems")] public List StoreItems { get; set; } = new(); + } + + public sealed class StorefrontCaptureItem + { + [JsonPropertyName("PurchasableItemId")] public int PurchasableItemId { get; set; } + [JsonPropertyName("Type")] public int Type { get; set; } + [JsonPropertyName("IsFeatured")] public bool IsFeatured { get; set; } + [JsonPropertyName("NewUntil")] public DateTime? NewUntil { get; set; } + [JsonPropertyName("Prices")] public List Prices { get; set; } = new(); + [JsonPropertyName("SubscriberPrices")] public List SubscriberPrices { get; set; } = new(); + [JsonPropertyName("GiftDrop")] public StorefrontCaptureGift? GiftDrop { get; set; } + } + + public sealed class StorefrontCapturePrice + { + [JsonPropertyName("CurrencyType")] public int CurrencyType { get; set; } + [JsonPropertyName("Price")] public int Price { get; set; } + } + + public sealed class StorefrontCaptureGift + { + [JsonPropertyName("GiftDropId")] public long GiftDropId { get; set; } + [JsonPropertyName("FriendlyName")] public string? FriendlyName { get; set; } + [JsonPropertyName("Tooltip")] public string? Tooltip { get; set; } + [JsonPropertyName("ConsumableItemDesc")] public string? ConsumableItemDesc { get; set; } + [JsonPropertyName("AvatarItemDesc")] public string? AvatarItemDesc { get; set; } + [JsonPropertyName("AvatarItemType")] public int? AvatarItemType { get; set; } + [JsonPropertyName("EquipmentPrefabName")] public string? EquipmentPrefabName { get; set; } + [JsonPropertyName("EquipmentModificationGuid")] public string? EquipmentModificationGuid { get; set; } + [JsonPropertyName("IsQuery")] public bool IsQuery { get; set; } + [JsonPropertyName("Unique")] public bool Unique { get; set; } + [JsonPropertyName("SubscribersOnly")] public bool SubscribersOnly { get; set; } + [JsonPropertyName("Rarity")] public int? Rarity { get; set; } + [JsonPropertyName("Context")] public int? Context { get; set; } + [JsonPropertyName("Currency")] public int Currency { get; set; } + [JsonPropertyName("CurrencyType")] public int? CurrencyType { get; set; } + [JsonPropertyName("ItemSetId")] public long? ItemSetId { get; set; } + [JsonPropertyName("ItemSetFriendlyName")] public string? ItemSetFriendlyName { get; set; } + } + + + public static List ToItems(StorefrontCapture capture) + { + var items = new List(capture.StoreItems.Count); + foreach (var item in capture.StoreItems) + { + var mapped = ToItem(item, capture); + if (mapped != null) items.Add(mapped); + } + return items; + } + + public static PurchasableGiftDrop? ToItem(StorefrontCaptureItem item, StorefrontCapture capture) + { + var gift = item.GiftDrop; + if (gift == null || !HasContent(gift)) return null; + + return new PurchasableGiftDrop + { + PurchasableItemId = item.PurchasableItemId, + Type = Enum.IsDefined(typeof(PurchasableItem.PurchasableItemType), item.Type) + ? (PurchasableItem.PurchasableItemType)item.Type + : PurchasableItem.PurchasableItemType.GiftDrop, + IsFeatured = item.IsFeatured, + Prices = item.Prices.Select(ToPrice).ToList(), + SubscriberPrices = item.SubscriberPrices.Select(ToPrice).ToList(), + EndDate = null, + NewUntil = item.NewUntil, + GiftDrops = new List + { + new() + { + GiftDropId = gift.GiftDropId, + FriendlyName = gift.FriendlyName?.Trim() ?? string.Empty, + Tooltip = gift.Tooltip?.Trim() ?? string.Empty, + ConsumableItemDesc = gift.ConsumableItemDesc ?? string.Empty, + AvatarItemDesc = gift.AvatarItemDesc ?? string.Empty, + AvatarItemType = gift.AvatarItemType is null + ? AvatarItemType.Outfit + : (AvatarItemType)gift.AvatarItemType, + EquipmentPrefabName = gift.EquipmentPrefabName ?? string.Empty, + EquipmentModificationGuid = gift.EquipmentModificationGuid ?? string.Empty, + IsQuery = gift.IsQuery, + Unique = gift.Unique, + SubscribersOnly = gift.SubscribersOnly, + Rarity = (Rarity)(gift.Rarity ?? (int)Rarity.Common), + Context = (GiftContext)(gift.Context ?? (int)GiftContext.Commerce_Purchase), + Currency = gift.Currency, + CurrencyType = (CurrencyType)(gift.CurrencyType ?? (int)CurrencyType.RecCenterTokens), + ItemSetId = gift.ItemSetId ?? 0, + ItemSetFriendlyName = gift.ItemSetFriendlyName ?? string.Empty + } + } + }; + } + + private static PurchasablePrice ToPrice(StorefrontCapturePrice price) => new() + { + CurrencyType = (CurrencyType)price.CurrencyType, + Price = price.Price + }; + + private static bool HasContent(StorefrontCaptureGift gift) => + !string.IsNullOrWhiteSpace(gift.FriendlyName) + || !string.IsNullOrWhiteSpace(gift.AvatarItemDesc) + || !string.IsNullOrWhiteSpace(gift.ConsumableItemDesc) + || !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName); + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + private static IReadOnlyList DiscoverFiles() + { + var files = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var directory in DBConfig.StorefrontDirectories()) + { + if (!Directory.Exists(directory)) continue; + + foreach (var path in Directory.GetFiles(directory, FileSearchPattern)) + { + if (!seen.Add(Path.GetFileName(path))) continue; + files.Add(path); + } + } + + return files.OrderBy(path => StorefrontIdFromFileName(path) ?? long.MaxValue).ToList(); + } + } +} diff --git a/Server/Storefront/StorefrontRequests.cs b/Server/Storefront/StorefrontRequests.cs new file mode 100644 index 0000000..885bfb0 --- /dev/null +++ b/Server/Storefront/StorefrontRequests.cs @@ -0,0 +1,53 @@ +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Store; + +namespace RadicadeServer.RecNet.Storefront +{ + public class RequestPurchaseItemDTO + { + public StorefrontTypes StorefrontType { get; set; } + public int PurchasableItemId { get; set; } + public CurrencyType CurrencyType { get; set; } + + public long? RequestedPrice { get; set; } + + public long? CouponConsumablePlayerMappingId { get; set; } + + public GiftRequestItemDTO? Gift { get; set; } + } + + public class GiftRequestItemDTO + { + public int ToPlayerId { get; set; } + public string? Message { get; set; } + public bool Anonymous { get; set; } + public GiftContext GiftContext { get; set; } + } + + public class GrantBalanceRequest + { + public float Multiplier { get; set; } + public StorefrontBalanceAddTypes BalanceAddType { get; set; } + } + + public class GrantBalanceRewardDTO + { + public CurrencyType CurrencyType { get; set; } + public List BalanceAdds { get; set; } = new(); + } + + public class CompleteObjectiveDTO + { + public int ObjectiveType { get; set; } + public float CompletionPercentage { get; set; } + public long? RoomId { get; set; } + } + + public class StorefrontPurchaseRequestDTO + { + public StorefrontTypes Storefront { get; set; } + public int ItemId { get; set; } + public CurrencyType Currency { get; set; } + public long Price { get; set; } + } +} diff --git a/Server/Storefront/StorefrontResponses.cs b/Server/Storefront/StorefrontResponses.cs new file mode 100644 index 0000000..7275901 --- /dev/null +++ b/Server/Storefront/StorefrontResponses.cs @@ -0,0 +1,169 @@ +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Inventions; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Store; + +namespace RadicadeServer.RecNet.Storefront +{ + public enum BalanceType + { + Free = 0, + Paid = 1, + Promotional = 2 + } + + public class StorefrontBalanceDTO + { + public CurrencyType Currency { get; set; } + public BalanceType Type { get; set; } + public long Amount { get; set; } + } + + public class StorefrontItemDTO + { + public int Id { get; set; } + public string Name { get; set; } = null!; + public long Price { get; set; } + } + + public class StorefrontResponseDTO + { + public List Items { get; set; } = new(); + } + + public class AdCarouselItemDTO + { + public int AdCarouselItemId { get; set; } + public string ImageName { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List PurchasableItemIds { get; set; } = new(); + + public int Id { get; set; } + public string ImageUrl { get; set; } = string.Empty; + } + + public class StorefrontPurchaseResponseDTO + { + public StorefrontBalanceDTO BalanceUpdate { get; set; } = null!; + } + + + public class RewardBalanceModificationDTO + { + public StorefrontBalanceAddTypes BalanceAddType { get; set; } + public int BaseAward { get; set; } + public int BonusAward { get; set; } + public int RateLimit { get; set; } + public int CurrentCount { get; set; } + public int Total { get; set; } + public Platforms Platform { get; set; } = Platforms.All; + public bool BalanceInGiftBox { get; set; } + } + + + + + + + + + public enum StorefrontRateLimitTypes + { + Interval = 0, + PerDay = 1 + } + + public class StorefrontBalanceAddConfigDTO + { + public StorefrontBalanceAddTypes BalanceAddType { get; set; } + + public CurrencyType CurrencyType { get; set; } + + public int BaseAward { get; set; } + + public int BonusAwardMin { get; set; } + + public int BonusAwardMax { get; set; } + + public StorefrontRateLimitTypes RateLimitType { get; set; } = StorefrontRateLimitTypes.PerDay; + + public float MaxPartialMultiplier { get; set; } = 1f; + + public float Multiplier { get; set; } = 1f; + + public int RateLimit { get; set; } + + public int CurrentCount { get; set; } + + public int Total { get; set; } + + public bool BalanceInGiftBox { get; set; } + + public bool IgnorePartialMultiplier { get; set; } + } + + public class BalanceUpdateEntry + { + public UpdateResponseTypes UpdateResponse { get; set; } + public T? Data { get; set; } + } + + public class BalanceUpdateResponse : BalanceResponse + { + public List> BalanceUpdates { get; set; } = new(); + } + + public class PurchaseBalanceUpdateEntry + { + public UpdateResponseTypes UpdateResponse { get; set; } + public List Data { get; set; } = new(); + } + + public class PurchaseBalanceUpdateResponse : BalanceResponse + { + public List> BalanceUpdates { get; set; } = new(); + } + + public class BalanceResponseDTO + { + public StorefrontBalanceAddTypes BalanceAddType { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public long? Delta { get; set; } + + public long Balance { get; set; } + public CurrencyType CurrencyType { get; set; } + } + + public enum PurchaseStatus + { + OK, + TooManyRequests, + NotEnoughCredit, + AlreadyOwned, + NoItemAvailable, + CouponNotApplicable, + RequestedPriceDoesNotMatch + } + + public class InventionPurchaseResponseDTO + { + public class BalanceResponse : BalanceResponseDTO + { + public class InventionData + { + public PurchaseStatus Status { get; set; } + public Invention Invention { get; set; } = null!; + } + public List Result { get; set; } = null!; + } + + public Invention InventionResponse { get; set; } = null!; + public BalanceResponse BalanceUpdateResponse { get; set; } = null!; + } +} diff --git a/Server/Storefront/StorefrontSeason.cs b/Server/Storefront/StorefrontSeason.cs new file mode 100644 index 0000000..7d829a0 --- /dev/null +++ b/Server/Storefront/StorefrontSeason.cs @@ -0,0 +1,12 @@ +namespace RadicadeServer.RecNet.Storefront +{ + public class StorefrontSeasonDTO + { + public int SeasonNumber { get; set; } + public string Name { get; set; } = null!; + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public int MaxTier { get; set; } + public bool HasEliteTrack { get; set; } + } +} diff --git a/Server/Storefront/StorefrontShared.cs b/Server/Storefront/StorefrontShared.cs new file mode 100644 index 0000000..5e17d93 --- /dev/null +++ b/Server/Storefront/StorefrontShared.cs @@ -0,0 +1,926 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Store; +using RadicadeServer.RecNet.Storefront; +using static RadicadeServer.RecNet.Storefront.BalanceType; + +namespace RadicadeServer.API.Storefront +{ + public static class StorefrontShared + { + public static long NewGiftDropId() + { + int value = Random.Shared.Next(1, int.MaxValue); + return value; + } + + + public static List ParseGiftList(string? data) + { + if (string.IsNullOrWhiteSpace(data)) return new List(); + + try + { + using var doc = JsonDocument.Parse(data); + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + var gifts = doc.RootElement.Deserialize>(JsonOptions); + return gifts ?? new List(); + } + if (doc.RootElement.ValueKind == JsonValueKind.Object) + { + var gift = doc.RootElement.Deserialize(JsonOptions); + return gift == null ? new List() : new List { gift }; + } + } + catch (Exception) + { + } + return new List(); + } + + public static async Task> GetPendingGiftsAsync(RecDBContext db, long playerId) + { + var row = await db.Gifts.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == playerId); + if (row == null) return new List(); + return ParseGiftList(row.Data); + } + + + + + + public static Gift ToGift(this GiftDropDB drop) + { + return new Gift + { + GiftDropId = drop.GiftDropId, + ItemSetId = drop.ItemSetId, + ItemSetFriendlyName = drop.ItemSetFriendlyName, + FriendlyName = drop.Name ?? "Gift Drop", + Tooltip = !string.IsNullOrWhiteSpace(drop.Tooltip) ? drop.Tooltip : (drop.Description ?? ""), + IsQuery = drop.IsQuery, + Unique = drop.Unique, + SubscribersOnly = drop.SubscribersOnly, + ConsumableItemDesc = drop.ConsumableItems ?? "", + AvatarItemDesc = drop.AvatarItems ?? "", + AvatarItemType = drop.AvatarItemType, + EquipmentPrefabName = drop.EquipmentItems ?? "", + EquipmentModificationGuid = drop.EquipmentModificationGuid ?? "", + CurrencyType = drop.CurrencyType, + Currency = drop.CurrencyAmount, + Xp = drop.XPAmount, + Context = drop.Context, + Rarity = drop.Rarity + }; + } + + + + + + public static PurchasableGiftDrop ToPurchasableGiftDrop(this GiftDropDB drop) + { + var gift = drop.ToGift(); + return new PurchasableGiftDrop + { + PurchasableItemId = drop.PurchasableItemId > 0 ? drop.PurchasableItemId : (int)(drop.GiftDropId % int.MaxValue), + Type = PurchasableItem.PurchasableItemType.GiftDrop, + Prices = new List + { + new PurchasablePrice { CurrencyType = drop.CurrencyType, Price = drop.Price } + }, + SubscriberPrices = new List(), + IsFeatured = drop.IsFeatured, + GiftDrops = new List { gift } + }; + } + + + + + + + public static async Task> GetStoreItems(RecDBContext db, long storefrontId) + { + var shipped = StorefrontCatalog.GetItems(storefrontId); + if (shipped.Count > 0) + { + return shipped; + } + + var drops = await db.GiftItems + .Where(x => x.StorefrontId == storefrontId) + .OrderBy(x => x.PurchasableItemId) + .ToListAsync(); + + if (drops.Count > 0) + { + var result = drops.Select(x => x.ToPurchasableGiftDrop()).ToList(); + return result; + } + + if (storefrontId != 0) + { + var shared = await db.GiftItems + .Where(x => x.StorefrontId == 0) + .OrderBy(x => x.PurchasableItemId) + .ToListAsync(); + + if (shared.Count > 0) + { + var sharedItems = shared.Select(x => x.ToPurchasableGiftDrop()).ToList(); + return sharedItems; + } + } + return new List(); + } + + + + + + public static async Task<(GiftDropDB? dbDrop, PurchasableGiftDrop? item)> FindStoreItem(RecDBContext db, StorefrontTypes type, int itemId) + { + var shipped = StorefrontCatalog.FindItem((long)type, itemId); + if (shipped != null) return (null, shipped); + + var dbDrop = await db.GiftItems + .FirstOrDefaultAsync(x => x.StorefrontId == (long)type && x.PurchasableItemId == itemId); + if (dbDrop != null) + return (dbDrop, dbDrop.ToPurchasableGiftDrop()); + + return (null, null); + } + + + + + + + + public static async Task PersistGiftBoxAsync(RecDBContext db, long playerId, Gift giftBox, bool notify = true) + { + if (giftBox == null) + throw new InvalidOperationException("PersistGiftBoxAsync called with a null gift."); + + if (giftBox.GiftDropId == 0) + giftBox.GiftDropId = NewGiftDropId(); + + long overwritten = 0; + var row = await db.Gifts.FirstOrDefaultAsync(x => x.AccountId == playerId); + var gifts = row == null ? new List() : ParseGiftList(row.Data); + + var existing = gifts.FirstOrDefault(g => g.GiftDropId == giftBox.GiftDropId); + if (existing != null) + { + overwritten = existing.GiftDropId; + gifts[gifts.IndexOf(existing)] = giftBox; + } + else + { + gifts.Add(giftBox); + } + + if (row == null) + db.Gifts.Add(new GenericData { AccountId = playerId, Data = gifts.ToJson() }); + else + row.Data = gifts.ToJson(); + + await db.SaveChangesAsync(); + + if (notify) + await SendGiftPackageNotificationAsync(playerId, giftBox); + + return overwritten; + } + + public static async Task RemovePendingGiftAsync(RecDBContext db, long playerId, long giftDropId) + { + var row = await db.Gifts.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (row == null) return null; + + var gifts = ParseGiftList(row.Data); + var match = gifts.FirstOrDefault(g => g.GiftDropId == giftDropId); + if (match == null) return null; + + gifts.Remove(match); + if (gifts.Count == 0) + db.Gifts.Remove(row); + else + row.Data = gifts.ToJson(); + + await db.SaveChangesAsync(); + return match; + } + + public static async Task SendPendingGiftBoxNotificationAsync(RecDBContext db, long playerId) + { + var gifts = await GetPendingGiftsAsync(db, playerId); + if (gifts.Count == 0) return false; + + foreach (var gift in gifts) + { + await SendGiftPackageNotificationAsync(playerId, gift); + } + return true; + } + + + public sealed class GiftSendResult + { + public bool Success { get; init; } + public string? Error { get; init; } + public string? ErrorDetail { get; init; } + public Gift? Gift { get; init; } + public long SenderBalance { get; init; } + } + + public static async Task GeneratePlayerGiftAsync( + RecDBContext db, + long senderId, + long? recipientId, + int? purchasableItemId, + long? giftDropId, + StorefrontTypes storefront, + string? message, + CurrencyType? requestedCurrency, + long? requestedPrice) + { + if (recipientId is not > 0) + return new GiftSendResult { Success = false, Error = "invalid_recipient", ErrorDetail = "A valid recipientPlayerId is required." }; + if (recipientId.Value == senderId) + return new GiftSendResult { Success = false, Error = "cannot_gift_self", ErrorDetail = "You cannot gift yourself." }; + + var recipientExists = await db.Players.AnyAsync(x => x.AccountId == recipientId.Value); + if (!recipientExists) + return new GiftSendResult { Success = false, Error = "invalid_recipient", ErrorDetail = $"Recipient account {recipientId.Value} does not exist." }; + + int itemId = purchasableItemId ?? 0; + var shippedItem = itemId > 0 ? StorefrontCatalog.FindItem(itemId) : null; + + GiftDropDB? dbDrop = null; + if (shippedItem == null) + { + if (itemId > 0) + dbDrop = await db.GiftItems.FirstOrDefaultAsync(x => x.PurchasableItemId == itemId); + else if (giftDropId is > 0) + dbDrop = await db.GiftItems.FirstOrDefaultAsync(x => x.GiftDropId == giftDropId.Value); + } + + PurchasableGiftDrop? configItem = null; + if (shippedItem == null && dbDrop == null) + { + var configItems = (await db.Config.FirstOrDefaultAsync())?.GiftDropStoreItems?.FromJson>() ?? new List(); + if (itemId > 0) configItem = configItems.FirstOrDefault(x => x.PurchasableItemId == itemId); + else if (giftDropId is > 0) configItem = configItems.FirstOrDefault(x => x.GiftDrops.Any(g => g.GiftDropId == giftDropId.Value)); + } + + Gift gift; + PurchasablePrice? price; + if (shippedItem != null) + { + gift = shippedItem.GiftDrop?.Clone() ?? new Gift + { + FriendlyName = "Gift", + Tooltip = "A gift from a player!", + Context = GiftContext.Purchased_Gift_A, + Rarity = Rarity.Common + }; + price = shippedItem.Prices.FirstOrDefault(); + } + else if (dbDrop != null) + { + gift = dbDrop.ToGift(); + price = new PurchasablePrice { CurrencyType = dbDrop.CurrencyType, Price = dbDrop.Price }; + } + else if (configItem != null) + { + gift = configItem.GiftDrops?.FirstOrDefault()?.Clone() ?? new Gift + { + FriendlyName = "Gift", + Tooltip = "A gift from a player!", + Context = GiftContext.Purchased_Gift_A, + Rarity = Rarity.Common + }; + price = configItem.Prices?.FirstOrDefault(); + } + else + { + return new GiftSendResult + { + Success = false, + Error = "item_not_found", + ErrorDetail = $"No store item matches purchasableItemId={itemId} giftDropId={giftDropId}." + }; + } + + gift.GiftDropId = NewGiftDropId(); + gift.FromPlayerId = senderId; + if (!string.IsNullOrWhiteSpace(message)) + gift.Tooltip = message; + if (gift.Context == GiftContext.None || gift.Context == GiftContext.Default) + gift.Context = GiftContext.Purchased_Gift_A; + + var currencyType = requestedCurrency ?? price?.CurrencyType ?? CurrencyType.RecCenterTokens; + long priceAmount = price?.Price ?? 0; + if (requestedPrice is > 0 && requestedPrice.Value != priceAmount) + return new GiftSendResult + { + Success = false, + Error = "requested_price_mismatch", + ErrorDetail = $"Item costs {priceAmount}, request quoted {requestedPrice}." + }; + + var strategy = db.Database.CreateExecutionStrategy(); + return await strategy.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + try + { + var senderCurrency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == senderId); + if (senderCurrency == null) + { + senderCurrency = new Currency { AccountId = senderId }; + db.Currency.Add(senderCurrency); + } + + long balance = GetBalance(senderCurrency, currencyType); + if (priceAmount > 0 && balance < priceAmount) + return new GiftSendResult + { + Success = false, + Error = "not_enough_credit", + ErrorDetail = $"Balance {balance} is below price {priceAmount}.", + SenderBalance = balance + }; + + if (priceAmount > 0) + AddCurrency(senderCurrency, currencyType, -priceAmount); + await db.SaveChangesAsync(); + + await PersistGiftBoxAsync(db, recipientId.Value, gift, notify: false); + await tx.CommitAsync(); + + await SendGiftPackageNotificationAsync(recipientId.Value, gift); + return new GiftSendResult { Success = true, Gift = gift, SenderBalance = GetBalance(senderCurrency, currencyType) }; + } + catch (Exception ex) + { + await tx.RollbackAsync(); + return new GiftSendResult { Success = false, Error = "gift_creation_failed", ErrorDetail = ex.Message }; + } + }); + } + + public sealed class BalanceGrant + { + public CurrencyType CurrencyType { get; init; } + public long Granted { get; init; } + public long Balance { get; init; } + } + + public static async Task SendBalanceUpdateAsync(long playerId, CurrencyType currencyType, long balance, long granted, StorefrontBalanceAddTypes addType) + { + if (currencyType == CurrencyType.Invalid) return; + + try + { + await RadicadeServer.API.Notifications.Notify.SendNotify(playerId, new RadicadeServer.RecNet.Notifications.Notification + { + Id = RadicadeServer.RecNet.Enums.Messages.NotificationType.StorefrontBalanceUpdate, + Msg = new BalanceResponseDTO + { + BalanceAddType = addType, + Delta = granted, + Balance = balance, + CurrencyType = currencyType + } + }); + } + catch (Exception) + { + } + } + + public static Task SendXpBalanceUpdateAsync(long playerId, long xp, long granted, StorefrontBalanceAddTypes addType) + => SendBalanceUpdateAsync(playerId, CurrencyType.RecRoyale_Season1, xp, granted, addType); + + public static async Task SendGiftPackageNotificationAsync(long playerId, Gift giftBox) + { + await RadicadeServer.API.Notifications.Notify.SendNotify(playerId, new RadicadeServer.RecNet.Notifications.Notification + { + Id = RadicadeServer.RecNet.Enums.Messages.NotificationType.GiftPackageReceivedImmediate, + Msg = giftBox.ForClient() + }); + } + + internal static long GetBalance(RecNet.Accounts.Currency currency, CurrencyType type) => type switch + { + CurrencyType.RecCenterTokens => currency.RecTokens, + CurrencyType.LaserTagTickets => currency.LaserTagTickets, + CurrencyType.LostSkullsGold => currency.Gold, + CurrencyType.DraculaSilver => currency.DraculaSilver, + _ => 0 + }; + + internal static void AddCurrency(RecNet.Accounts.Currency currency, CurrencyType type, long amount) + { + switch (type) + { + case CurrencyType.RecCenterTokens: currency.RecTokens += amount; break; + case CurrencyType.LaserTagTickets: currency.LaserTagTickets += amount; break; + case CurrencyType.LostSkullsGold: currency.Gold += amount; break; + case CurrencyType.DraculaSilver: currency.DraculaSilver += amount; break; + } + } + + + + + + + + + + + internal static bool TryParseCurrencyType(string? raw, out CurrencyType type) + { + type = CurrencyType.Invalid; + if (string.IsNullOrWhiteSpace(raw)) return false; + + var trimmed = raw.Trim(); + + if (int.TryParse(trimmed, out var numeric) && + Enum.IsDefined(typeof(CurrencyType), numeric) && + (CurrencyType)numeric != CurrencyType.Invalid) + { + type = (CurrencyType)numeric; + return true; + } + + foreach (var name in Enum.GetNames()) + { + if (name == nameof(CurrencyType.Invalid)) continue; + + if (string.Equals(name, trimmed, StringComparison.OrdinalIgnoreCase) || + string.Equals(name.Replace("_", ""), trimmed.Replace("_", ""), StringComparison.OrdinalIgnoreCase)) + { + type = Enum.Parse(name); + return true; + } + } + + return false; + } + + + public sealed class PurchaseOutcome + { + public bool Success { get; init; } + public PurchaseBalanceUpdateResponse? Response { get; init; } + public string? Error { get; init; } + public string? ErrorDetail { get; init; } + } + + public static async Task ExecutePurchaseAsync( + RecDBContext db, + long userId, + StorefrontTypes storefrontType, + int purchasableItemId, + CurrencyType currencyType, + long? requestedPrice, + GiftRequestItemDTO? giftRequest) + { + if (purchasableItemId <= 0) + return new PurchaseOutcome + { + Success = false, + Error = "invalid_request", + ErrorDetail = $"purchasableItemId={purchasableItemId} is not valid." + }; + + var (dbDrop, item) = await FindStoreItem(db, storefrontType, purchasableItemId); + + if (item == null) + { + return new PurchaseOutcome + { + Success = true, + Response = BuildPurchaseResponse(currencyType, 0, UpdateResponseTypes.NoItemAvailable, null) + }; + } + + var price = item.Prices.FirstOrDefault(p => p.CurrencyType == currencyType) + ?? item.Prices.FirstOrDefault(); + long cost = price?.Price ?? 0; + + if (requestedPrice is > 0 && requestedPrice.Value != cost) + { + return new PurchaseOutcome + { + Success = true, + Response = BuildPurchaseResponse(currencyType, await BalanceFor(db, userId, currencyType), UpdateResponseTypes.RequestedPriceDoesNotMatch, null) + }; + } + + var gift = (dbDrop?.ToGift()) + ?? item.GiftDrop?.Clone() + ?? new Gift + { + FriendlyName = "Purchased Item", + Tooltip = "Purchased from the store.", + CurrencyType = currencyType, + Context = GiftContext.Commerce_Purchase, + Rarity = Rarity.Common + }; + gift.GiftDropId = NewGiftDropId(); + if (gift.Context == GiftContext.None || gift.Context == GiftContext.Default) + gift.Context = GiftContext.Commerce_Purchase; + + + + + + + if (item.GiftDrop?.IsQuery == true || gift.IsQuery) + { + var queryOwner = giftRequest is { ToPlayerId: > 0 } ? giftRequest.ToPlayerId : userId; + var pick = await PickQueryDropAsync(db, queryOwner, gift.Rarity); + if (pick != null) + { + gift.AvatarItemDesc = pick.AvatarItemDesc; + gift.AvatarItemType = AvatarItemType.Outfit; + gift.IsQuery = false; + if (!string.IsNullOrWhiteSpace(pick.FriendlyName)) gift.FriendlyName = pick.FriendlyName; + if (!string.IsNullOrWhiteSpace(pick.Tooltip)) gift.Tooltip = pick.Tooltip; + } + } + + if (giftRequest is { ToPlayerId: > 0 }) + { + var recipient = giftRequest.ToPlayerId; + if (recipient == userId) + return new PurchaseOutcome { Success = false, Error = "cannot_gift_self", ErrorDetail = "You cannot gift an item to yourself." }; + if (!await db.Players.AnyAsync(x => x.AccountId == recipient)) + return new PurchaseOutcome { Success = false, Error = "invalid_recipient", ErrorDetail = $"Recipient account {recipient} does not exist." }; + + if (!string.IsNullOrWhiteSpace(giftRequest.Message)) + gift.Tooltip = giftRequest.Message; + gift.Context = giftRequest.GiftContext != GiftContext.None + ? giftRequest.GiftContext + : GiftContext.Purchased_Gift_A; + gift.FromPlayerId = userId; + + var strategy = db.Database.CreateExecutionStrategy(); + return await strategy.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + try + { + var senderCurrency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId); + if (senderCurrency == null) + { + senderCurrency = new Currency { AccountId = userId }; + db.Currency.Add(senderCurrency); + } + + long balance = GetBalance(senderCurrency, currencyType); + if (balance < cost) + { + await tx.RollbackAsync(); + return new PurchaseOutcome + { + Success = true, + Response = BuildPurchaseResponse(currencyType, balance, UpdateResponseTypes.NotEnoughCredit, null) + }; + } + + if (cost > 0) + AddCurrency(senderCurrency, currencyType, -cost); + await db.SaveChangesAsync(); + + await PersistGiftBoxAsync(db, recipient, gift, notify: false); + await tx.CommitAsync(); + + await SendGiftPackageNotificationAsync(recipient, gift); + var newBalance = GetBalance(senderCurrency, currencyType); + return new PurchaseOutcome + { + Success = true, + Response = BuildPurchaseResponse(currencyType, newBalance, UpdateResponseTypes.OK, gift) + }; + } + catch (Exception ex) + { + await tx.RollbackAsync(); + return new PurchaseOutcome { Success = false, Error = "purchase_failed", ErrorDetail = ex.Message }; + } + }); + } + + var strategy2 = db.Database.CreateExecutionStrategy(); + return await strategy2.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + try + { + var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId); + if (currency == null) + { + currency = new Currency { AccountId = userId }; + db.Currency.Add(currency); + } + + long balance = GetBalance(currency, currencyType); + if (balance < cost) + { + await tx.RollbackAsync(); + return new PurchaseOutcome + { + Success = true, + Response = BuildPurchaseResponse(currencyType, balance, UpdateResponseTypes.NotEnoughCredit, null) + }; + } + + if (cost > 0) + AddCurrency(currency, currencyType, -cost); + await db.SaveChangesAsync(); + + await PersistGiftBoxAsync(db, userId, gift, notify: false); + await tx.CommitAsync(); + + var newBalance = GetBalance(currency, currencyType); + return new PurchaseOutcome + { + Success = true, + Response = BuildPurchaseResponse(currencyType, newBalance, UpdateResponseTypes.OK, gift) + }; + } + catch (Exception ex) + { + await tx.RollbackAsync(); + return new PurchaseOutcome { Success = false, Error = "purchase_failed", ErrorDetail = ex.Message }; + } + }); + } + + public static PurchaseBalanceUpdateResponse BuildPurchaseResponse( + CurrencyType currencyType, long balance, UpdateResponseTypes status, Gift? gift) + { + return new PurchaseBalanceUpdateResponse + { + Balance = balance, + CurrencyType = currencyType, + BalanceUpdates = new List> + { + new() + { + UpdateResponse = status, + Data = gift == null ? new List() : new List { gift.ForClient() } + } + } + }; + } + + private static async Task BalanceFor(RecDBContext db, long playerId, CurrencyType currencyType) + { + var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == playerId); + return currency == null ? 0 : GetBalance(currency, currencyType); + } + + + private static bool AlreadyOwnsEquipment(IEnumerable owned, Gift gift) + { + var prefab = (gift.EquipmentPrefabName ?? "").Trim(); + var guid = (gift.EquipmentModificationGuid ?? "").Trim(); + + return owned.Any(item => (item.PrefabName ?? "").Trim() == prefab + && (item.ModificationGuid ?? "").Trim() == guid); + } + + public static async Task GrantGiftAsync(RecDBContext db, long playerId, Gift gift) + { + BalanceGrant? balanceGrant = null; + + if (gift.Currency > 0) + { + if (gift.CurrencyType == CurrencyType.RecRoyale_Season1) + { + var progression = await GetOrCreateProgression(db, playerId); + progression.XP = (int)Math.Min(progression.XP + gift.Currency, int.MaxValue); + progression.RecordXpGrant((int)Math.Min(gift.Currency, int.MaxValue)); + } + else if (gift.CurrencyType != CurrencyType.Invalid) + { + var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == playerId); + if (currency == null) + { + currency = new Currency { AccountId = playerId }; + db.Currency.Add(currency); + } + AddCurrency(currency, gift.CurrencyType, gift.Currency); + balanceGrant = new BalanceGrant { CurrencyType = gift.CurrencyType, Granted = gift.Currency, Balance = GetBalance(currency, gift.CurrencyType) }; + } + } + + if (gift.Xp > 0) + { + var progression = await GetOrCreateProgression(db, playerId); + progression.XP = (int)Math.Min(progression.XP + gift.Xp, int.MaxValue); + progression.RecordXpGrant((int)Math.Min(gift.Xp, int.MaxValue)); + } + + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == playerId); + + bool hasItems = !string.IsNullOrWhiteSpace(gift.AvatarItemDesc) + || !string.IsNullOrWhiteSpace(gift.ConsumableItemDesc) + || !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName); + bool allowItems = true; + if (hasItems) + { + var ownedItems = unlocks?.GetAvatarItems() ?? new List(); + var ownedConsumables = unlocks?.GetConsumables() ?? new List(); + var ownedEquipment = unlocks?.GetEquipment() ?? new List(); + + int itemCount = 0; + var avatarDesc = NormalizeAvatarGiftDesc(gift.AvatarItemDesc); + if (!string.IsNullOrWhiteSpace(avatarDesc) && !ownedItems.Any(x => x.AvatarItemDesc == avatarDesc)) + itemCount++; + if (!string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)) + { + foreach (var item in gift.ConsumableItemDesc.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + if (!ownedConsumables.Any(x => x.ConsumableItemDesc == item)) itemCount++; + } + if (!string.IsNullOrWhiteSpace(gift.EquipmentPrefabName) + && !AlreadyOwnsEquipment(ownedEquipment, gift)) + itemCount++; + + } + + if (allowItems && !string.IsNullOrWhiteSpace(gift.AvatarItemDesc)) + { + if (unlocks == null) + { + unlocks = new PlayerUnlocks + { + AccountId = playerId, + AvatarItems = DBConfig.DefaultAvatarUnlocks + }; + db.Unlocks.Add(unlocks); + } + var existingItems = unlocks.GetAvatarItems(); + var clean = NormalizeAvatarGiftDesc(gift.AvatarItemDesc); + if (!string.IsNullOrWhiteSpace(clean) && !existingItems.Any(x => x.AvatarItemDesc == clean)) + { + unlocks.AddAvatarItem(UnlockFor(gift, clean)); + } + } + + if (allowItems && !string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)) + { + if (unlocks == null) + { + unlocks = new PlayerUnlocks + { + AccountId = playerId, + AvatarItems = DBConfig.DefaultAvatarUnlocks + }; + db.Unlocks.Add(unlocks); + } + var existingConsumables = unlocks.GetConsumables() ?? new List(); + foreach (var item in gift.ConsumableItemDesc.Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + var clean = item.Trim(); + if (!string.IsNullOrEmpty(clean) && !existingConsumables.Any(x => x.ConsumableItemDesc == clean)) + unlocks.AddConsumable(new Consumable + { + Id = StableId(clean), + ConsumableItemDesc = clean, + Count = 1, + InitialCount = 1, + CreatedAt = DateTime.UtcNow, + IsActive = true + }); + } + } + + if (allowItems && !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName)) + { + if (unlocks == null) + { + unlocks = new PlayerUnlocks + { + AccountId = playerId, + AvatarItems = DBConfig.DefaultAvatarUnlocks + }; + db.Unlocks.Add(unlocks); + } + var existingEquipment = unlocks.GetEquipment() ?? new List(); + var prefab = gift.EquipmentPrefabName.Trim(); + if (!string.IsNullOrEmpty(prefab) && !AlreadyOwnsEquipment(existingEquipment, gift)) + unlocks.AddEquipment(new Equipment + { + PrefabName = prefab, + ModificationGuid = gift.EquipmentModificationGuid ?? "", + FriendlyName = gift.FriendlyName ?? "", + Tooltip = gift.Tooltip ?? "", + Rarity = gift.Rarity + }); + } + + return balanceGrant; + } + + private static async Task PickQueryDropAsync(RecDBContext db, long playerId, Rarity rarity) + { + + + var catalogue = RadicadeServer.Utils.LevelUpGiftService.ClothingCatalogue(DBConfig.DefaultAvatarCatalog); + var pool = new RadicadeServer.Utils.LevelUpRewardPool.Pool + { + AvatarItems = catalogue + .Where(item => !string.IsNullOrWhiteSpace(item.AvatarItemDesc)) + .Select(item => item.AvatarItemDesc) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList() + }; + + var owned = await RadicadeServer.Utils.LevelUpGiftService.CollectOwnedKeysAsync(db, playerId); + return RadicadeServer.Utils.LevelUpRewardPool.PickAvatarItem( + catalogue, rarity, owned, Random.Shared, pool); + } + + private static AvatarItem UnlockFor(Gift gift, string avatarItemDesc) + { + var catalogueItem = AvatarUnlockCatalog.FindItem(avatarItemDesc) + ?? RadicadeServer.Utils.LevelUpGiftService.ClothingCatalogue() + .FirstOrDefault(x => string.Equals(x.AvatarItemDesc, avatarItemDesc, StringComparison.OrdinalIgnoreCase)); + + if (catalogueItem != null) + { + if (string.IsNullOrWhiteSpace(catalogueItem.FriendlyName) && !string.IsNullOrWhiteSpace(gift.FriendlyName)) + catalogueItem.FriendlyName = gift.FriendlyName; + if (catalogueItem.Rarity == 0 && gift.Rarity != 0) + catalogueItem.Rarity = (int)gift.Rarity; + if (catalogueItem.CreatedAt == default) + catalogueItem.CreatedAt = DateTime.UtcNow; + catalogueItem.AvatarItemDesc = avatarItemDesc; + return catalogueItem; + } + return new AvatarItem + { + AvatarItemDesc = avatarItemDesc, + FriendlyName = gift.FriendlyName ?? string.Empty, + Tooltip = gift.Tooltip ?? string.Empty, + Rarity = (int)gift.Rarity, + AvatarItemType = (int)gift.AvatarItemType, + CreatedAt = DateTime.UtcNow + }; + } + + private static async Task GetOrCreateProgression(RecDBContext db, long playerId) + { + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); + if (progression != null) return progression; + + progression = new ProgressionData { PlayerId = playerId }; + db.Progression.Add(progression); + return progression; + } + + private static long StableId(string value) + { + uint hash = 2166136261; + foreach (var c in value) + { + hash ^= c; + hash *= 16777619; + } + return (hash & 0x7FFFFFFF) + 1; + } + + private static string NormalizeAvatarGiftDesc(string? value) + { + return AvatarItemDescription.Normalize(value); + } + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + } +} \ No newline at end of file diff --git a/Server/Storefront/StorefrontsV1.cs b/Server/Storefront/StorefrontsV1.cs new file mode 100644 index 0000000..fbecde2 --- /dev/null +++ b/Server/Storefront/StorefrontsV1.cs @@ -0,0 +1,166 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Store; +using RadicadeServer.RecNet.Storefront; +using RadicadeServer.API.Storefront; + +namespace RadicadeServer.API.Storefront.V1; + +[Route("api/storefronts/v1")] +[Route("econ/api/storefronts/v1")] +public class StorefrontsV1(RecDBContext db) : Controller +{ + [HttpGet("season/{type}")] + public IActionResult GetSeason(StorefrontTypes type) + { + if (type != StorefrontTypes.RecRoyale) + return NotFound(); + + const int maxTiers = 20; + const int elitePurchasableItemId = 200100; + const int tierPriceTokens = 500; + + var tiers = new List(); + for (int tier = 1; tier <= maxTiers; tier++) + { + var gift = new Gift + { + GiftDropId = StorefrontShared.NewGiftDropId(), + FriendlyName = $"Season 1 Tier {tier} Reward", + Tooltip = "Rec Royale Season 1 reward.", + CurrencyType = CurrencyType.RecRoyale_Season1, + Currency = 100 + tier * 50, + Context = GiftContext.RecRoyale, + Rarity = tier >= 15 ? Rarity.Epic : tier >= 8 ? Rarity.Rare : Rarity.Common + }; + tiers.Add(new SeasonTierDTO + { + PurchasableItemId = 200000 + tier, + Type = PurchasableItem.PurchasableItemType.SeasonTier, + IsFeatured = tier == 1, + Prices = new List + { + new() { CurrencyType = CurrencyType.RecCenterTokens, Price = tierPriceTokens + (tier - 1) * 250 } + }, + SubscriberPrices = new List(), + EndDate = null, + NewUntil = DateTime.MaxValue, + GiftDrops = new List { gift }, + Tier = tier, + Rewards = new List + { + new() + { + RequiresEliteUpgrade = false, + GiftDrop = gift, + SeasonXpBoost = 0f + } + } + }); + } + + var eliteGift = new Gift + { + GiftDropId = StorefrontShared.NewGiftDropId(), + FriendlyName = "Rec Royale Season 1 Elite Upgrade", + Tooltip = "Unlocks the elite track for every tier.", + CurrencyType = CurrencyType.RecRoyale_Season1, + Currency = 500, + Context = GiftContext.RecRoyale, + Rarity = Rarity.Epic + }; + var eliteUpgrade = new PurchasableGiftDrop + { + PurchasableItemId = elitePurchasableItemId, + Type = PurchasableItem.PurchasableItemType.SeasonEliteUpgrade, + IsFeatured = true, + Prices = new List + { + new() { CurrencyType = CurrencyType.RecCenterTokens, Price = 2000 } + }, + SubscriberPrices = new List(), + EndDate = null, + GiftDrops = new List { eliteGift } + }; + + var now = DateTime.UtcNow; + var season = new SeasonStorefrontDTO + { + StorefrontType = StorefrontTypes.RecRoyale, + NextUpdate = DateTime.MaxValue, + NewUntil = DateTime.MaxValue, + EndDate = DateTime.MaxValue, + Season = 1, + Name = "Rec Royale Season 1", + StartAt = now.AddDays(-30), + EndAt = now.AddDays(30), + CurrencyType = CurrencyType.RecRoyale_Season1, + EliteUpgrade = eliteUpgrade, + Tiers = tiers, + PersonalDetails = new SeasonPersonalDetailsDTO + { + HasEliteUpgrade = false, + HasEliteUpgradePlatformMask = 0, + CurrentSeasonTier = 0, + ModifiedAt = now + } + }; + return Ok(season); + } + + [HttpGet("balanceAddType/{type}/{addType}"), Authorize] + public IActionResult GetBalanceAddConfig(CurrencyType type, StorefrontBalanceAddTypes addType) + { + var userId = User.GetUserId(); + + return Ok(new StorefrontBalanceAddConfigDTO + { + BalanceAddType = addType, + CurrencyType = type, + BaseAward = 10, + RateLimit = 0, + Multiplier = 1f + }); + } + + [HttpGet("{type}")] + public async Task GetStorefront(StorefrontTypes type) + { + var items = await StorefrontShared.GetStoreItems(db, (long)type); + if (items.Count == 0 && !Enum.IsDefined(typeof(StorefrontTypes), type)) + return NotFound(); + return Ok(new GiftDropStorefront + { + NextUpdate = DateTime.MaxValue, + NewUntil = null, + StorefrontType = type, + StoreItems = items + }); + } + + [HttpGet("toptoday")] + public async Task GetTopItems() + { + var items = await StorefrontShared.GetStoreItems(db, (long)StorefrontTypes.Watch); + + return Ok(items.Where(x => x.IsFeatured).Select(x => x.PurchasableItemId).ToList()); + } + + [HttpPost("objectives"), Authorize] + public IActionResult CompleteObjectives([FromBody] List objectives) + { + var userId = User.GetUserId(); + + return Ok(); + } + + [HttpGet("adcarouselitems")] + public IActionResult GetAdCarouselItems() + { + return Ok(Array.Empty()); + } +} diff --git a/Server/Storefront/StorefrontsV2.cs b/Server/Storefront/StorefrontsV2.cs new file mode 100644 index 0000000..215aa75 --- /dev/null +++ b/Server/Storefront/StorefrontsV2.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Storefront; + +namespace RadicadeServer.API.Storefront.V2; + +[Route("api/storefronts/v2")] +[Route("econ/api/storefronts/v2")] +public class StorefrontsV2(RecDBContext db) : Controller +{ + [HttpGet("balance"), Authorize] + public async Task GetBalances() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + var data = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId.Value); + if (data == null) + return Ok(new List()); + + return Ok(new List + { + new() { CurrencyType = CurrencyType.RecCenterTokens, Balance = data.RecTokens }, + new() { CurrencyType = CurrencyType.LaserTagTickets, Balance = data.LaserTagTickets }, + new() { CurrencyType = CurrencyType.LostSkullsGold, Balance = data.Gold }, + new() { CurrencyType = CurrencyType.DraculaSilver, Balance = data.DraculaSilver } + }); + } + + [HttpPost("balance"), Authorize] + public async Task ModifyBalance([FromBody] GrantBalanceRewardDTO? request) + { + var userId = User.GetUserId(); + if (userId == null || request == null || request.BalanceAdds is not { Count: > 0 }) + return BadRequest(); + + var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId.Value); + if (currency == null) return NotFound(); + + + + + + + const int baseAward = 10; + long awarded = 0; + var updates = new List>(); + + foreach (var grant in request.BalanceAdds) + { + if (grant == null) continue; + + long total = (long)(baseAward * (double)grant.Multiplier); + if (total <= 0) total = baseAward; + + StorefrontShared.AddCurrency(currency, request.CurrencyType, total); + awarded += total; + + updates.Add(new() + { + UpdateResponse = UpdateResponseTypes.OK, + Data = new RewardBalanceModificationDTO + { + BalanceAddType = grant.BalanceAddType, + BaseAward = baseAward, + BonusAward = 0, + Total = (int)total, + CurrentCount = 1, + RateLimit = 0 + } + }); + } + + await db.SaveChangesAsync(); + + long newBalance = StorefrontShared.GetBalance(currency, request.CurrencyType); + + return Ok(new BalanceUpdateResponse + { + CurrencyType = request.CurrencyType, + Balance = newBalance, + BalanceUpdates = updates + }); + } + + + + [HttpPost("buyItem"), Authorize] + public async Task BuyItem([FromBody] RequestPurchaseItemDTO? request) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + + if (request == null) + { + return BadRequest(new { error = "invalid_request" }); + } + + var outcome = await StorefrontShared.ExecutePurchaseAsync( + db, + userId.Value, + request.StorefrontType, + request.PurchasableItemId, + request.CurrencyType, + request.RequestedPrice, + request.Gift); + + if (outcome.Response != null) + return Ok(outcome.Response); + return outcome.Error switch + { + "cannot_gift_self" or "invalid_recipient" => BadRequest(new { error = outcome.Error, message = outcome.ErrorDetail }), + _ => StatusCode(StatusCodes.Status500InternalServerError, new { error = outcome.Error, message = outcome.ErrorDetail }) + }; + } +} \ No newline at end of file diff --git a/Server/Storefront/StorefrontsV3.cs b/Server/Storefront/StorefrontsV3.cs new file mode 100644 index 0000000..859375d --- /dev/null +++ b/Server/Storefront/StorefrontsV3.cs @@ -0,0 +1,98 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Store; +using RadicadeServer.RecNet.Storefront; + +namespace RadicadeServer.API.Storefront.V3; + +[Route("api/storefronts/v3")] +[Route("econ/api/storefronts/v3")] +public class StorefrontsV3(RecDBContext db) : Controller +{ + [HttpGet("giftdropstore/{type}")] + public async Task GetGiftDropStore(StorefrontTypes type) + { + var items = await StorefrontShared.GetStoreItems(db, (long)type); + items = ProjectStoreItemsForClient( + items, + User.FindFirst("rn.plat")?.Value, + AvatarUnlockCatalog.IsClientResolvableDescription); + Console.WriteLine($"[Storefront v3] GET giftdropstore {type} ({(long)type}): returning {items.Count} item(s)."); + + return Ok(new GiftDropStorefront + { + StorefrontType = type, + NextUpdate = DateTime.MaxValue, + NewUntil = null, + StoreItems = items + }); + } + + public static List ProjectStoreItemsForClient( + IEnumerable items, + string? platformClaim, + Func canResolveAvatarItem) + { + ArgumentNullException.ThrowIfNull(items); + ArgumentNullException.ThrowIfNull(canResolveAvatarItem); + + if (!string.Equals(platformClaim, ((int)Platforms.Standalone).ToString(), StringComparison.Ordinal)) + return items.ToList(); + + var projected = new List(); + foreach (var item in items) + { + var projectedGifts = new List(); + var changed = false; + + foreach (var gift in item.GiftDrops) + { + if (string.IsNullOrWhiteSpace(gift.AvatarItemDesc) || canResolveAvatarItem(gift.AvatarItemDesc)) + { + projectedGifts.Add(gift); + continue; + } + + changed = true; + if (HasNonAvatarReward(gift)) + { + var giftCopy = gift.Clone(); + giftCopy.AvatarItemDesc = string.Empty; + projectedGifts.Add(giftCopy); + } + } + + if (!changed) + { + projected.Add(item); + continue; + } + + + + if (projectedGifts.Count == 0) continue; + + projected.Add(new PurchasableGiftDrop + { + PurchasableItemId = item.PurchasableItemId, + Type = item.Type, + Prices = item.Prices.ToList(), + SubscriberPrices = item.SubscriberPrices.ToList(), + IsFeatured = item.IsFeatured, + EndDate = item.EndDate, + NewUntil = item.NewUntil, + GiftDrops = projectedGifts + }); + } + + return projected; + } + + private static bool HasNonAvatarReward(Gift gift) => + !string.IsNullOrWhiteSpace(gift.ConsumableItemDesc) + || !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName) + || gift.Currency > 0 + || gift.Xp > 0; +} diff --git a/Server/Storefront/StorefrontsV4.cs b/Server/Storefront/StorefrontsV4.cs new file mode 100644 index 0000000..ac3d170 --- /dev/null +++ b/Server/Storefront/StorefrontsV4.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Storefront.V4; + +[Route("api/storefronts/v4")] +[Route("econ/api/storefronts/v4")] +public class StorefrontsV4(RecDBContext db) : Controller +{ + [HttpGet("balance/{type}"), Authorize] + public async Task?> GetBalance(CurrencyType type) + { + var userId = User.GetUserId(); var data = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId); + + long balance; + if (type == CurrencyType.RecRoyale_Season1) + { + + + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId); + balance = progression == null ? 0 : LevelUpGiftService.XpIntoCurrentLevel(progression); + } + else + { + if (data == null) return []; + balance = type switch + { + CurrencyType.RecCenterTokens => data.RecTokens, + CurrencyType.LaserTagTickets => data.LaserTagTickets, + CurrencyType.LostSkullsGold => data.Gold, + CurrencyType.DraculaSilver => data.DraculaSilver, + _ => 0 + }; + } + + return new List { new() { CurrencyType = type, Balance = balance } }; + } +} \ No newline at end of file diff --git a/Server/Storefront/WishlistAPI.cs b/Server/Storefront/WishlistAPI.cs new file mode 100644 index 0000000..4857c2b --- /dev/null +++ b/Server/Storefront/WishlistAPI.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.Extensions; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RadicadeServer.DB; + +namespace RadicadeServer.API.Wishlists; + +public class WishlistItemDB +{ + [Key] public string WishlistItemId { get; set; } = Guid.NewGuid().ToString(); + public long AccountId { get; set; } + public long PurchasableItemId { get; set; } + public DateTime CreatedAt { get; set; } +} + +[Route("api/itemWishlists/v1")] +[Route("econ/api/itemWishlists/v1")] +public class WishlistAPI : Controller +{ + private readonly RecDBContext db; + public WishlistAPI(RecDBContext db) => this.db = db; + + [HttpGet("wishlist/me"), Authorize] + public async Task GetMyWishlist() + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + return Ok(await db.Set().AsNoTracking() + .Where(x => x.AccountId == userId.Value).OrderBy(x => x.CreatedAt).ToListAsync()); + } + + [HttpGet("wishlist/{accountId}"), Authorize] + public async Task GetWishlistByAccountId(long accountId) + { + return Ok(await db.Set().AsNoTracking() + .Where(x => x.AccountId == accountId).OrderBy(x => x.CreatedAt).ToListAsync()); + } + + [HttpPut("wishlist/me/{purchasableItemId}"), Authorize] + [HttpPost("wishlist/me/{purchasableItemId}"), Authorize] + public async Task AddToWishlist(long purchasableItemId) + { + var userId = User.GetUserId(); + if (userId == null || purchasableItemId <= 0) return BadRequest(new { error = "invalid_request" }); + var set = db.Set(); + var item = await set.FirstOrDefaultAsync(x => x.AccountId == userId.Value && x.PurchasableItemId == purchasableItemId); + if (item == null) + { + item = new WishlistItemDB { WishlistItemId = Guid.NewGuid().ToString(), AccountId = userId.Value, PurchasableItemId = purchasableItemId, CreatedAt = DateTime.UtcNow }; + await set.AddAsync(item); + await db.SaveChangesAsync(); + } + return Ok(new { error = "", success = true, value = item }); + } + + [HttpDelete("wishlist/me/{purchasableItemId}"), Authorize] + public async Task RemoveFromWishlist(long purchasableItemId) + { + var userId = User.GetUserId(); + if (userId == null) return Unauthorized(); + var item = await db.Set().FirstOrDefaultAsync(x => x.AccountId == userId.Value && x.PurchasableItemId == purchasableItemId); + if (item != null) { db.Remove(item); await db.SaveChangesAsync(); } + return Ok(new { Success = true }); + } +} \ No newline at end of file diff --git a/Server/Studio/StudioAPI.cs b/Server/Studio/StudioAPI.cs new file mode 100644 index 0000000..883245e --- /dev/null +++ b/Server/Studio/StudioAPI.cs @@ -0,0 +1,170 @@ +using System.Collections.Concurrent; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Studio +{ + [Route("studio")] + public class StudioAPI : Controller + { + private sealed record StoredCloudBuild(long RoomId, long SubRoomId, StudioCloudBuild Build); + private static readonly ConcurrentDictionary CloudBuilds = new(StringComparer.Ordinal); + + private readonly ILogger logger; + private readonly StudioBuildWorker worker; + private readonly IDbContextFactory dbFactory; + + public StudioAPI(ILogger logger, StudioBuildWorker worker, IDbContextFactory dbFactory) + { + this.logger = logger; + this.worker = worker; + this.dbFactory = dbFactory; + } + + [HttpGet("cloud-builds/for-room"), Authorize] + public async Task CloudBuildsForRoom( + [FromQuery] long roomId, + [FromQuery] long subRoomId, + [FromQuery] int skip = 0, + [FromQuery] int take = 30) + { + if (roomId <= 0 || subRoomId <= 0) return BadRequest(); + var accountId = CurrentAccountId(); + if (accountId == null) return Unauthorized(); + if (!await CanBuildInRoomAsync(roomId, accountId.Value, HttpContext.RequestAborted)) return Forbid(); + + logger.LogInformation("Studio cloud builds requested: room={RoomId}, subRoom={SubRoomId}, skip={Skip}, take={Take}.", + roomId, subRoomId, skip, take); + + return Ok(BuildPage(roomId, subRoomId, skip, take)); + } + + private static object BuildPage(long roomId, long subRoomId, int skip, int take) + { + var builds = CloudBuilds.Values + .Where(x => (roomId == 0 || x.RoomId == roomId) && (subRoomId == 0 || x.SubRoomId == subRoomId)) + .OrderByDescending(x => x.Build.StartedAt) + .ToList(); + var page = builds.Skip(Math.Max(0, skip)).Take(Math.Clamp(take, 0, 100)).Select(x => x.Build).ToList(); + return new { Results = page, TotalResults = builds.Count, TotalCount = builds.Count }; + } + + [HttpPost("cloud-builds/create"), Authorize] + public async Task CreateCloudBuild( + [FromQuery] long? roomId = null, + [FromQuery] long? subRoomId = null, + [FromQuery] long? subRoomDataSaveId = null, + [FromQuery] bool? autoPublish = null, + [FromQuery] string? buildBranch = null) + { + + + var form = Request.HasFormContentType + ? await Request.ReadFormAsync(HttpContext.RequestAborted) + : null; + long? FormLong(string key) => long.TryParse(form?[key].ToString(), System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out var value) ? value : null; + bool? FormBool(string key) => bool.TryParse(form?[key].ToString(), out var value) ? value : null; + roomId ??= FormLong("roomId"); + subRoomId ??= FormLong("subRoomId"); + subRoomDataSaveId ??= FormLong("subRoomDataSaveId"); + autoPublish ??= FormBool("autoPublish"); + buildBranch ??= form?["buildBranch"].ToString(); + + if (roomId is not > 0 || subRoomId is not > 0 || subRoomDataSaveId is not > 0) + return BadRequest(new { Success = false, Error = "roomId, subRoomId and subRoomDataSaveId are required", Value = (object?)null }); + + var resolvedRoomId = roomId.Value; + var resolvedSubRoomId = subRoomId.Value; + var resolvedSaveId = subRoomDataSaveId.Value; + var resolvedAutoPublish = autoPublish ?? false; + var resolvedBuildBranch = buildBranch; + + var accountId = CurrentAccountId(); + if (accountId == null || accountId > int.MaxValue) + return Unauthorized(new { Success = false, Error = "The Studio account could not be resolved.", Value = (object?)null }); + + await using (var db = await dbFactory.CreateDbContextAsync(HttpContext.RequestAborted)) + { + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == resolvedRoomId, HttpContext.RequestAborted); + var canBuild = room != null && (room.CreatorAccountId == accountId.Value || + await db.RoomRoles.AsNoTracking().AnyAsync(x => x.RoomId == resolvedRoomId && x.AccountId == accountId.Value && x.Role >= RoleType.CoOwner, + HttpContext.RequestAborted)); + if (!canBuild) + return Forbid(); + } + + var now = DateTime.UtcNow; + var build = new StudioCloudBuild( + Guid.NewGuid().ToString("N"), now, null, + null, string.Empty, (int)accountId); + CloudBuilds[build.CloudBuildId] = new StoredCloudBuild(resolvedRoomId, resolvedSubRoomId, build); + var job = new StudioBuildWorker.BuildJob(build.CloudBuildId, resolvedRoomId, resolvedSubRoomId, + resolvedSaveId, accountId.Value, dbFactory, build, updated => + CloudBuilds.AddOrUpdate(build.CloudBuildId, + _ => new StoredCloudBuild(resolvedRoomId, resolvedSubRoomId, updated), + (_, existing) => existing with { Build = updated })); + worker.Enqueue(job); + logger.LogInformation("Queued local Studio Windows build: room={RoomId}, subRoom={SubRoomId}, save={SaveId}, autoPublish={AutoPublish}, branch={BuildBranch}, cloudBuild={CloudBuildId}.", + resolvedRoomId, resolvedSubRoomId, resolvedSaveId, resolvedAutoPublish, resolvedBuildBranch, build.CloudBuildId); + + return Ok(new { Success = true, Error = (string?)null, Value = build }); + } + + [HttpGet("cloud-builds"), Authorize] + public async Task CloudBuild([FromQuery] string cloudBuildId) + { + if (!CloudBuilds.TryGetValue(cloudBuildId, out var stored)) return NotFound(); + var accountId = CurrentAccountId(); + if (accountId == null) return Unauthorized(); + if (!await CanBuildInRoomAsync(stored.RoomId, accountId.Value, HttpContext.RequestAborted)) return Forbid(); + return Ok(stored.Build); + } + + private long? CurrentAccountId() + { + if (long.TryParse(User.FindFirstValue("userId"), out var userId) && userId > 0) return userId; + if (long.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var identifier) && identifier > 0) return identifier; + return null; + } + + private async Task CanBuildInRoomAsync(long roomId, long accountId, CancellationToken cancellationToken) + { + await using var db = await dbFactory.CreateDbContextAsync(cancellationToken); + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId, cancellationToken); + return room != null && (room.CreatorAccountId == accountId || + await db.RoomRoles.AsNoTracking().AnyAsync(x => x.RoomId == roomId && x.AccountId == accountId && x.Role >= RoleType.CoOwner, + cancellationToken)); + } + + [HttpGet("collaboration/owners-in-room")] + public IActionResult OwnersInRoom([FromQuery] long roomId) + { + logger.LogInformation("Studio owners-in-room requested: room={RoomId}; answering an empty owner list.", roomId); + return Ok(new { Success = true, Error = (string?)null, Value = Array.Empty() }); + } + + [HttpGet("analytics/rudderstack-api-key")] + public IActionResult RudderstackApiKey() + { + + + + return Ok(new { key = string.Empty, value = string.Empty }); + } + } +} + +public sealed record StudioCloudBuild( + string CloudBuildId, + DateTime StartedAt, + DateTime? CompletedAt, + string? Error, + string UnityAssetId, + int CreatedByAccountId); diff --git a/Server/Studio/StudioAssetsCDN.cs b/Server/Studio/StudioAssetsCDN.cs new file mode 100644 index 0000000..bd837be --- /dev/null +++ b/Server/Studio/StudioAssetsCDN.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.CDN; + +[Route("studiocdn")] +public class StudioAssetsCDN : Controller +{ + [HttpGet("unityasset/{filename}"), HttpHead("unityasset/{filename}")] + [HttpGet("roomunityasset/{filename}"), HttpHead("roomunityasset/{filename}")] + public IActionResult Download(string filename) + { + if (string.IsNullOrEmpty(filename) || filename.Length > 80 || + filename.Any(c => !char.IsAsciiLetterOrDigit(c) && c != '-')) return BadRequest(); + var path = Path.GetFullPath(Path.Combine(DBConfig.CDNStorage, filename)); + if (!System.IO.File.Exists(path)) return NotFound(); + return PhysicalFile(path, "application/octet-stream", enableRangeProcessing: true); + } +} diff --git a/Server/Studio/StudioBuildWorker.cs b/Server/Studio/StudioBuildWorker.cs new file mode 100644 index 0000000..3ec4731 --- /dev/null +++ b/Server/Studio/StudioBuildWorker.cs @@ -0,0 +1,646 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Storage; +using RadicadeServer.Utils; + +namespace RadicadeServer.API.Studio; + +public sealed class StudioBuildWorker : BackgroundService +{ + private const byte WindowsDesktopTarget = 0; + private const int UnityBundleVersion = 1; + private readonly ConcurrentQueue jobs = new(); + private readonly SemaphoreSlim signal = new(0); + private readonly ILogger logger; + private readonly IConfiguration configuration; + + public StudioBuildWorker(ILogger logger, IConfiguration configuration) + { + this.logger = logger; + this.configuration = configuration; + } + + public void Enqueue(BuildJob job) + { + jobs.Enqueue(job); + signal.Release(); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await signal.WaitAsync(stoppingToken); + if (!jobs.TryDequeue(out var job)) continue; + try + { + await BuildWindowsAsync(job, stoppingToken); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogError(ex, "Local Studio Windows build failed for {CloudBuildId}.", job.Build.CloudBuildId); + job.Update(job.Build with + { + CompletedAt = DateTime.UtcNow, + Error = CleanError(ex.GetBaseException().Message), + UnityAssetId = string.Empty + }); + } + } + } + + private async Task BuildWindowsAsync(BuildJob job, CancellationToken cancellationToken) + { + var unityPath = configuration["Radicade:Studio:UnityPath"]; + var projectPath = configuration["Radicade:Studio:ProjectPath"]; + if (string.IsNullOrWhiteSpace(unityPath) || !File.Exists(unityPath)) + throw new InvalidOperationException("Configure Radicade:Studio:UnityPath to the installed Unity 2020.3.33f1 Editor.exe."); + if (string.IsNullOrWhiteSpace(projectPath) || !Directory.Exists(projectPath)) + throw new InvalidOperationException("Configure Radicade:Studio:ProjectPath to the complete local Rec Room Studio ProjectTemplate."); + + var source = await GetBuildSourceAsync(job, cancellationToken); + var manifestAssetId = source.ManifestAssetId; + if (!StudioUnityAssetRegistry.IsValidAssetId(manifestAssetId)) + throw new InvalidOperationException("The saved room has no valid Studio source-manifest ID; upload the room from Studio again."); + + var manifest = await StudioUnityAssetRegistry.ReadAsync(manifestAssetId, cancellationToken); + if (manifest == null || string.IsNullOrWhiteSpace(manifest.Filename)) + throw new InvalidOperationException("Studio source manifest was not found in local CDN storage."); + + var manifestFile = Path.Combine(DBConfig.CDNStorage, manifest.Filename); + using var document = JsonDocument.Parse(await File.ReadAllBytesAsync(manifestFile, cancellationToken)); + if (!document.RootElement.TryGetProperty("Version", out var versionElement) || versionElement.ValueKind != JsonValueKind.Number || + versionElement.GetInt32() is < 1 or > 2) + throw new InvalidDataException("This worker requires source manifest version 1 or 2."); + if (!document.RootElement.TryGetProperty("MainSceneProjectRelativePath", out var scenePathElement) || + scenePathElement.ValueKind != JsonValueKind.String || + !document.RootElement.TryGetProperty("Files", out var filesElement) || filesElement.ValueKind != JsonValueKind.Array) + throw new InvalidDataException("Source manifest does not contain a scene path and file list."); + var scenePath = scenePathElement.GetString(); + if (string.IsNullOrWhiteSpace(scenePath) || !IsSafeAssetPath(scenePath)) + throw new InvalidDataException("Source manifest has an invalid main-scene path."); + + var sourceFiles = filesElement.EnumerateArray().ToArray(); + if (sourceFiles.Length == 0) + throw new InvalidDataException("Source manifest file list is empty."); + var sourcePaths = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var file in sourceFiles) + { + if (!file.TryGetProperty("p", out var pathElement) || pathElement.ValueKind != JsonValueKind.String || + !file.TryGetProperty("n", out var storageElement) || storageElement.ValueKind != JsonValueKind.String) + throw new InvalidDataException("Source manifest contains a file without a path or storage filename."); + var sourcePath = pathElement.GetString(); + var storageName = storageElement.GetString(); + if (!IsSafeAssetPath(sourcePath) || !StudioUnityAssetRegistry.IsValidAssetId(storageName)) + throw new InvalidDataException("Source manifest contains an invalid path or storage filename."); + if (!sourcePaths.Add(sourcePath!)) throw new InvalidDataException("Source manifest contains a duplicate project path."); + if (file.TryGetProperty("g", out var guidElement) && guidElement.ValueKind == JsonValueKind.String) + { + var guid = guidElement.GetString(); + if (!string.IsNullOrWhiteSpace(guid) && (guid.Length != 32 || !guid.All(Uri.IsHexDigit))) + throw new InvalidDataException("Source manifest contains an invalid Unity GUID."); + } + } + if (!sourcePaths.Contains(scenePath!)) throw new InvalidDataException("Manifest main scene path is not present in the file list."); + var scenePathValue = scenePath!; + var sceneName = Path.GetFileNameWithoutExtension(scenePathValue); + if (string.IsNullOrWhiteSpace(sceneName)) + throw new InvalidDataException("Source manifest has an invalid main-scene name."); + + projectPath = Path.GetFullPath(projectPath); + var assetsPath = Path.GetFullPath(Path.Combine(projectPath, "Assets")); + var editorDir = Path.Combine(assetsPath, "Editor"); + var editorDirectoryExisted = Directory.Exists(editorDir); + var editorMetaExisted = File.Exists(editorDir + ".meta"); + var runnerPath = Path.Combine(editorDir, "RadicadeLocalStudioBuild.cs"); + var runnerMetaPath = runnerPath + ".meta"; + if (editorDirectoryExisted && (File.Exists(runnerPath) || File.Exists(runnerMetaPath))) + throw new InvalidOperationException("Assets/Editor/RadicadeLocalStudioBuild.cs already exists; refusing to overwrite project content."); + + var buildRoot = Path.Combine(DBConfig.CDNStorage, ".studio-builds", job.Build.CloudBuildId); + Directory.CreateDirectory(buildRoot); + if (Directory.EnumerateFileSystemEntries(buildRoot).Any()) + throw new InvalidOperationException("This build ID already has worker files; refusing to reuse its output directory."); + var runnerContents = CreateRunnerSource(); + var argsFile = Path.Combine(buildRoot, "build-args.json"); + var sourceGuid = Guid.NewGuid().ToString("N"); + var ownsEditorDirectory = false; + var ownsRunner = false; + var ownsRunnerMeta = false; + var args = new BuildArguments(manifestAssetId, manifest.Filename, scenePathValue, sceneName, job.RoomId, job.SubRoomId, job.SaveId, job.AccountId, + job.CloudBuildId, configuration["Radicade:Studio:LocalHttpBaseUrl"]?.TrimEnd('/') ?? "http://127.0.0.1:3031", + source.RoomJson, source.SubRoomJson); + try + { + if (!Directory.Exists(editorDir)) + { + Directory.CreateDirectory(editorDir); + ownsEditorDirectory = !editorDirectoryExisted; + } + await File.WriteAllTextAsync(argsFile, JsonSerializer.Serialize(args), cancellationToken); + await using (var runnerFile = new FileStream(runnerPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, useAsync: true)) + { + ownsRunner = true; + await runnerFile.WriteAsync(Encoding.UTF8.GetBytes(runnerContents), cancellationToken); + } + await using (var runnerMetaFile = new FileStream(runnerMetaPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, useAsync: true)) + { + ownsRunnerMeta = true; + var metadata = $"fileFormatVersion: 2\nguid: {sourceGuid}\nMonoImporter:\n externalObjects: {{}}\n serializedVersion: 2\n defaultReferences: []\n executionOrder: 0\n icon: {{instanceID: 0}}\n userData: \n assetBundleName: \n assetBundleVariant: \n"; + await runnerMetaFile.WriteAsync(Encoding.UTF8.GetBytes(metadata), cancellationToken); + } + + await RunUnityAsync(unityPath, projectPath, argsFile, buildRoot, cancellationToken); + var resultPath = Path.Combine(buildRoot, "build-result.json"); + if (!File.Exists(resultPath)) throw new InvalidOperationException("Unity exited without producing a build result."); + var result = JsonSerializer.Deserialize(await File.ReadAllTextAsync(resultPath, cancellationToken), + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) + ?? throw new InvalidDataException("Unity returned an invalid build result."); + if (!string.IsNullOrWhiteSpace(result.Error)) throw new InvalidOperationException(result.Error); + if (result.BundlePaths == null || result.BundlePaths.Length is < 1 or > 2 || result.BundlePaths.Any(path => !File.Exists(path))) + throw new InvalidDataException("Unity did not produce the Windows room asset bundles."); + + var uploadedBundles = new List(); + foreach (var bundlePath in result.BundlePaths) + { + await using var bundle = File.OpenRead(bundlePath); + uploadedBundles.Add(await StudioAssetStorage.Save(bundle, cancellationToken)); + } + await StudioUnityAssetRegistry.AddBakedAssetAsync(manifestAssetId, + new StudioBakedUnityAsset(manifestAssetId, WindowsDesktopTarget, UnityBundleVersion, uploadedBundles[0].Filename, + uploadedBundles.Count > 1 ? uploadedBundles[1].Filename : null), cancellationToken); + + job.Update(job.Build with + { + CompletedAt = DateTime.UtcNow, + Error = null, + UnityAssetId = manifestAssetId + }); + logger.LogInformation("Local Studio Windows build finished for {CloudBuildId}; uploaded {BundleCount} bundles.", + job.Build.CloudBuildId, uploadedBundles.Count); + } + finally + { + if (ownsRunner) + { + try { File.Delete(runnerPath); } catch (IOException ex) { logger.LogWarning(ex, "Could not remove temporary Studio build runner {RunnerPath}.", runnerPath); } + } + if (ownsRunnerMeta) + { + try { File.Delete(runnerMetaPath); } catch (IOException ex) { logger.LogWarning(ex, "Could not remove temporary Studio build runner metadata {RunnerPath}.", runnerMetaPath); } + } + if (ownsEditorDirectory && Directory.Exists(editorDir) && !Directory.EnumerateFileSystemEntries(editorDir).Any()) + { + try + { + Directory.Delete(editorDir); + if (!editorMetaExisted) File.Delete(editorDir + ".meta"); + } + catch (IOException ex) { logger.LogWarning(ex, "Could not remove temporary Studio Editor directory {EditorDirectory}.", editorDir); } + } + } + } + + private async Task GetBuildSourceAsync(BuildJob job, CancellationToken cancellationToken) + { + await using var db = await job.DbFactory.CreateDbContextAsync(cancellationToken); + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == job.RoomId, cancellationToken) + ?? throw new InvalidOperationException("The requested room no longer exists."); + var subRoom = await db.SubRooms.AsNoTracking() + .FirstOrDefaultAsync(x => x.RoomId == job.RoomId && x.SubRoomId == job.SubRoomId, cancellationToken) + ?? throw new InvalidOperationException("The requested room subroom no longer exists."); + if (job.SaveId != 1) + throw new InvalidOperationException("Only the current saved Studio room version can be built on this server."); + + var save = RoomUtils.BuildCurrentSave(subRoom); + var roomDto = new + { + RoomId = room.RoomId, + Name = room.Name, + Description = room.Description, + ImageName = room.ImageName, + Accessibility = (int)room.Accessibility, + SupportsScreens = room.SupportsScreens, + SupportsWalkVR = room.SupportsWalkVR, + SupportsTeleportVR = room.SupportsTeleportVR, + SupportsJuniors = room.SupportsJuniors, + WarningMask = (int)room.WarningMask, + CustomWarning = room.CustomWarning, + MaxPlayerCalculationMode = (int)room.MaxPlayerCalculationMode, + CreatedAt = room.CreatedAt, + SubRooms = Array.Empty(), + Tags = Array.Empty(), + Roles = Array.Empty() + }; + var subRoomDto = new + { + SubRoomId = subRoom.SubRoomId, + Name = subRoom.Name, + UnitySceneId = subRoom.UnitySceneId, + MaxPlayers = subRoom.MaxPlayers, + CurrentSave = new + { + SubRoomDataSaveId = save.SubRoomDataSaveId, + SubRoomId = save.SubRoomId, + UnityAssetId = save.UnityAssetId, + DataBlob = save.DataBlob, + DataBlobHash = (string?)null, + PersistenceVersion = save.PersistenceVersion, + OMVersion = (byte)save.OMVersion, + SavedByAccountId = save.SavedByAccountId == null ? null : (int?)save.SavedByAccountId, + Description = save.Description, + CreatedAt = save.CreatedAt + }, + Accessibility = (int)subRoom.Accessibility + }; + var serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = null }; + return new BuildSource(subRoom.UnityAssetId, + JsonSerializer.Serialize(roomDto, serializerOptions), + JsonSerializer.Serialize(subRoomDto, serializerOptions)); + } + + private async Task RunUnityAsync(string unityPath, string projectPath, string argsFile, string buildRoot, CancellationToken cancellationToken) + { + var editorLog = Path.Combine(buildRoot, "unity-editor.log"); + var startInfo = new ProcessStartInfo + { + FileName = unityPath, + WorkingDirectory = projectPath, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + foreach (var argument in new[] { "-batchmode", "-nographics", "-logFile", editorLog, + "-projectPath", projectPath, "-executeMethod", "RadicadeLocalStudioBuild.Build", + "-radicadeStudioBuildArgs", argsFile + }) startInfo.ArgumentList.Add(argument); + + using var process = new Process { StartInfo = startInfo }; + if (!process.Start()) throw new InvalidOperationException("Could not start Unity batch build."); + var stdout = process.StandardOutput.ReadToEndAsync(cancellationToken); + var stderr = process.StandardError.ReadToEndAsync(cancellationToken); + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromMinutes(30)); + try + { + await process.WaitForExitAsync(timeout.Token); + } + catch (OperationCanceledException) + { + try { process.Kill(entireProcessTree: true); } catch (InvalidOperationException) { } + if (!cancellationToken.IsCancellationRequested) throw new TimeoutException("Unity build exceeded the 30 minute worker timeout."); + throw; + } + + var output = (await stdout) + Environment.NewLine + (await stderr); + if (process.ExitCode == 0) return; + var logText = File.Exists(editorLog) ? await File.ReadAllTextAsync(editorLog, cancellationToken) : string.Empty; + var lastMessage = (output + Environment.NewLine + logText).Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()).LastOrDefault(x => x.Contains("Error", StringComparison.OrdinalIgnoreCase) || x.Contains("Exception", StringComparison.OrdinalIgnoreCase)); + throw new InvalidOperationException(string.IsNullOrWhiteSpace(lastMessage) + ? $"Unity batch build failed with exit code {process.ExitCode}. See worker log {editorLog}." + : $"Unity batch build failed: {lastMessage}"); + } + + private static bool IsSafeAssetPath(string? path) => + !string.IsNullOrWhiteSpace(path) && path.StartsWith("Assets/", StringComparison.Ordinal) && + !Path.IsPathRooted(path) && !path.Contains('\\') && !path.Contains(':') && + path.Split('/').All(segment => segment.Length > 0 && segment is not ("." or "..")) && + path.IndexOfAny(Path.GetInvalidPathChars()) < 0; + + private static string CleanError(string message) => message.Length <= 1200 ? message : message[..1200]; + + private static string CreateRunnerSource() => """ + using System; + using System.IO; + using System.Linq; + using UnityEditor; + using UnityEditor.SceneManagement; + using UnityEngine; + using UnityEngine.SceneManagement; + using System.Text.Json; + using UnityEditor.Build.Pipeline; + using UnityEditor.Build.Pipeline.Utilities; + using UnityEditor.Build.Pipeline.Interfaces; + using System.Threading.Tasks; + using RecRoom.Core.Studio; + + public static class RadicadeLocalStudioBuild + { + [Serializable] public sealed class BuildArguments + { public string ManifestAssetId { get; set; } + + public string ManifestFilename { get; set; } + public string ScenePath { get; set; } + public string SceneName { get; set; } + public long RoomId { get; set; } + public long SubRoomId { get; set; } + public long SaveId { get; set; } + public long AccountId { get; set; } + public string CloudBuildId { get; set; } + public string LocalHttpBaseUrl { get; set; } + public string RoomJson { get; set; } + public string SubRoomJson { get; set; } + } + [Serializable] public sealed class SourceManifest + { + public string MainSceneProjectRelativePath { get; set; } + public SourceFile[] Files { get; set; } + } + [Serializable] public sealed class SourceFile + { + [System.Text.Json.Serialization.JsonPropertyName("n")] public string n { get; set; } + [System.Text.Json.Serialization.JsonPropertyName("p")] public string p { get; set; } + [System.Text.Json.Serialization.JsonPropertyName("g")] public string g { get; set; } + } + public sealed class BuildResult + { + public string[] BundlePaths { get; set; } = Array.Empty(); + public string Error { get; set; } + } + + public static async void Build() + { + var argsPath = ReadArg("-radicadeStudioBuildArgs"); + var args = JsonSerializer.Deserialize(File.ReadAllText(argsPath)) + ?? throw new InvalidDataException("Build arguments file was invalid."); + var resultPath = Path.Combine(Path.GetDirectoryName(argsPath), "build-result.json"); + var result = new BuildResult(); + var projectRoot = Directory.GetParent(Application.dataPath).FullName; + var stagingName = "RadicadeBuildTemp-" + args.CloudBuildId; + var roomSource = Path.Combine(Application.dataPath, stagingName); + var stagingAssetPath = "Assets/" + stagingName; + var ownsStagingFolder = false; + try + { + var projectRootFull = Path.GetFullPath(projectRoot) + Path.DirectorySeparatorChar; + if (Directory.Exists(roomSource)) throw new InvalidOperationException("Worker staging folder already exists; refusing to overwrite it."); + Directory.CreateDirectory(roomSource); + ownsStagingFolder = true; + using var client = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromSeconds(90) }; + var manifestUrl = args.LocalHttpBaseUrl + "/studiocdn/roomunityasset/" + Uri.EscapeDataString(args.ManifestFilename); + var manifest = JsonSerializer.Deserialize(client.GetStringAsync(manifestUrl).GetAwaiter().GetResult()) + ?? throw new InvalidDataException("Source manifest response was invalid."); + if (manifest == null || manifest.Files == null) throw new InvalidDataException("Source manifest response was invalid."); + if (!String.Equals(manifest.MainSceneProjectRelativePath, args.ScenePath, StringComparison.Ordinal)) + throw new InvalidDataException("Queued scene path does not match the source manifest."); + if (!manifest.Files.Any(file => String.Equals(file.p, args.ScenePath, StringComparison.OrdinalIgnoreCase))) + throw new InvalidDataException("The source manifest does not include its main scene."); + var sourcePaths = new System.Collections.Generic.HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var file in manifest.Files) + { + var storageName = file.n; + var relativePath = file.p; + if (String.IsNullOrWhiteSpace(storageName) || storageName.Length != 40 || !storageName.All(character => character <= 0x7f && Char.IsLetterOrDigit(character)) || + String.IsNullOrWhiteSpace(relativePath) || !relativePath.StartsWith("Assets/", StringComparison.Ordinal) || + Path.IsPathRooted(relativePath) || relativePath.Contains('\\') || relativePath.Contains(':') || + relativePath.Split('/').Any(segment => segment.Length == 0 || segment == ".." || segment == ".") || + relativePath.IndexOfAny(Path.GetInvalidPathChars()) >= 0 || !sourcePaths.Add(relativePath)) + throw new InvalidDataException("The source manifest contains an unsafe or duplicate path."); + if (!String.IsNullOrEmpty(file.g) && (file.g.Length != 32 || !file.g.All(Uri.IsHexDigit))) + throw new InvalidDataException("The source manifest contains an invalid Unity GUID."); + var stagingRelativePath = stagingAssetPath + relativePath.Substring("Assets".Length); + var destination = Path.GetFullPath(Path.Combine(projectRoot, stagingRelativePath.Replace('/', Path.DirectorySeparatorChar))); + if (!destination.StartsWith(projectRootFull, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Manifest path escaped the project."); + if (relativePath.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) + { + var metaAssetPath = "Assets/" + stagingName + relativePath.Substring("Assets".Length); + var targetAssetPath = metaAssetPath.Substring(0, metaAssetPath.Length - ".meta".Length); + if (!manifest.Files.Any(x => String.Equals(x.p, targetAssetPath, StringComparison.OrdinalIgnoreCase))) + throw new InvalidDataException("Manifest contains a .meta file without its source asset."); + } + Directory.CreateDirectory(Path.GetDirectoryName(destination)); + var bytes = client.GetByteArrayAsync(args.LocalHttpBaseUrl + "/studiocdn/unityasset/" + Uri.EscapeDataString(storageName)).GetAwaiter().GetResult(); + File.WriteAllBytes(destination, bytes); + if (relativePath.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) + { + var guidLine = File.ReadLines(destination).Select(line => line.Trim()) + .FirstOrDefault(line => line.StartsWith("guid:", StringComparison.Ordinal)); + var expectedGuid = manifest.Files.First(x => String.Equals(x.p, relativePath.Substring(0, relativePath.Length - ".meta".Length), StringComparison.OrdinalIgnoreCase)).g; + if (guidLine == null || String.IsNullOrWhiteSpace(expectedGuid) || + !String.Equals(guidLine.Substring("guid:".Length).Trim(), expectedGuid, StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("A source .meta file does not match its manifest GUID."); + } + if (!relativePath.EndsWith(".meta", StringComparison.OrdinalIgnoreCase) && !String.IsNullOrWhiteSpace(file.g)) + { + var metaPath = destination + ".meta"; + if (File.Exists(metaPath)) + { + var existingGuid = File.ReadLines(metaPath) + .Select(line => line.Trim()) + .FirstOrDefault(line => line.StartsWith("guid:", StringComparison.Ordinal)); + if (existingGuid != null && !String.Equals(existingGuid.Substring("guid:".Length).Trim(), file.g, StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("A source .meta file does not match its manifest GUID."); + if (existingGuid == null) throw new InvalidDataException("A source .meta file has no Unity GUID."); + } + else File.WriteAllText(metaPath, "fileFormatVersion: 2\nguid: " + file.g + "\n"); + } + } + if (!manifest.Files.Any(x => String.Equals(x.p, args.ScenePath + ".meta", StringComparison.OrdinalIgnoreCase)) && + String.IsNullOrWhiteSpace(manifest.Files.First(x => String.Equals(x.p, args.ScenePath, StringComparison.OrdinalIgnoreCase)).g)) + throw new InvalidDataException("Main scene has neither a .meta file nor a Unity GUID."); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + var scenePath = stagingAssetPath + args.ScenePath.Substring("Assets".Length); + var scene = AssetDatabase.LoadAssetAtPath(scenePath); + if (scene == null) throw new InvalidDataException("Studio main scene was not imported: " + scenePath); + var openedScene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); + var roomModel = CreateRoomModel(args); + InvokeRoomPreparation(openedScene, roomModel); + var output = Path.Combine(Path.GetDirectoryName(argsPath), "output"); + Directory.CreateDirectory(output); + var rejectedDependencyPaths = GetRejectedDependencyPaths(scene); + if (EditorUserBuildSettings.activeBuildTarget != (BuildTarget)5 && + !EditorUserBuildSettings.SwitchActiveBuildTarget((BuildTargetGroup)1, (BuildTarget)5)) + throw new InvalidOperationException("Unity could not switch to the Windows standalone build target."); + await Task.Delay(TimeSpan.FromSeconds(5)); + const int version = 1; + var sceneBundleName = "content.0." + version + ".assetbundle"; + var rejectedBundleName = "rejectedDeps.0." + version + ".assetbundle"; + var builds = rejectedDependencyPaths.Count == 0 + ? new[] { new AssetBundleBuild { assetBundleName = sceneBundleName, assetBundleVariant = null, assetNames = new[] { scenePath }, addressableNames = null } } + : new[] + { + new AssetBundleBuild { assetBundleName = sceneBundleName, assetBundleVariant = null, assetNames = new[] { scenePath }, addressableNames = null }, + new AssetBundleBuild { assetBundleName = rejectedBundleName, assetBundleVariant = null, assetNames = rejectedDependencyPaths.ToArray(), addressableNames = null } + }; + var parameters = new BundleBuildParameters((BuildTarget)5, (BuildTargetGroup)1, output) + { + BundleCompression = BuildCompression.LZ4 + }; + var content = new BundleBuildContent((System.Collections.Generic.IEnumerable)builds); + IBundleBuildResults buildResults = new BundleBuildResults(); + var code = ContentPipeline.BuildAssetBundles((IBundleBuildParameters)parameters, (IBundleBuildContent)content, ref buildResults); + if ((int)code != 0 && (int)code != 1) throw new InvalidOperationException("Asset bundle pipeline returned " + code); + var sceneBundlePath = Path.Combine(output, sceneBundleName); + var rejectedBundlePath = Path.Combine(output, rejectedBundleName); + if (!File.Exists(sceneBundlePath)) + throw new InvalidOperationException("Unity did not create the Windows scene bundle."); + if (rejectedDependencyPaths.Count == 0) + { + if (File.Exists(rejectedBundlePath)) File.Delete(rejectedBundlePath); + result.BundlePaths = new[] { sceneBundlePath }; + } + else + { + if (!File.Exists(rejectedBundlePath)) + throw new InvalidOperationException("Unity did not create the Windows rejected-dependency bundle."); + result.BundlePaths = new[] { sceneBundlePath, rejectedBundlePath }; + } + AssetDatabase.DeleteAsset(stagingAssetPath); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + } + catch (Exception ex) + { + result.Error = ex.GetBaseException().Message; + Debug.LogException(ex); + } + finally + { + try + { + if (ownsStagingFolder) + { + AssetDatabase.DeleteAsset(stagingAssetPath); + if (Directory.Exists(roomSource)) Directory.Delete(roomSource, true); + var stagingMeta = roomSource + ".meta"; + if (File.Exists(stagingMeta)) File.Delete(stagingMeta); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + } + } + catch (IOException) { } + } + File.WriteAllText(resultPath, JsonSerializer.Serialize(result)); + EditorApplication.Exit(String.IsNullOrEmpty(result.Error) ? 0 : 1); + } + + private static object CreateRoomModel(BuildArguments args) + { + var roomType = FindType("RoomDetailsDTO"); + var subRoomType = FindType("SubRoomDTO"); + var saveType = FindType("SubRoomDataSaveDTO"); + var room = Activator.CreateInstance(roomType, true); + var subRoom = Activator.CreateInstance(subRoomType, true); + var save = Activator.CreateInstance(saveType, true); + PopulateProperties(room, args.RoomJson); + PopulateProperties(subRoom, args.SubRoomJson); + PopulateProperties(save, ExtractCurrentSaveJson(args.SubRoomJson)); + subRoomType.GetProperty("CurrentSave").SetValue(subRoom, save); + var subRoomsProperty = roomType.GetProperty("SubRooms"); + var subRooms = (System.Collections.IList)Activator.CreateInstance(typeof(System.Collections.Generic.List<>).MakeGenericType(subRoomsProperty.PropertyType.GetGenericArguments()[0])); + subRooms.Add(subRoom); + subRoomsProperty.SetValue(room, subRooms); + var modelType = FindType("BOFMOKEGMFC"); + var constructor = modelType.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) + .Single(x => x.GetParameters().Length == 2); + return constructor.Invoke(new[] { room, subRoom }); + } + + private static string ExtractCurrentSaveJson(string json) + { + using var document = JsonDocument.Parse(json); + return document.RootElement.GetProperty("CurrentSave").GetRawText(); + } + + private static void PopulateProperties(object target, string json) + { + using var document = JsonDocument.Parse(json); + foreach (var property in target.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)) + { + if (!property.CanWrite || !document.RootElement.TryGetProperty(property.Name, out var value)) continue; + object converted; + if (property.PropertyType.IsEnum) + converted = Enum.ToObject(property.PropertyType, value.GetInt32()); + else if (Nullable.GetUnderlyingType(property.PropertyType)?.IsEnum == true) + converted = Enum.ToObject(Nullable.GetUnderlyingType(property.PropertyType), value.GetInt32()); + else + converted = System.Text.Json.JsonSerializer.Deserialize(value.GetRawText(), property.PropertyType); + property.SetValue(target, converted); + } + } + + private static void InvokeRoomPreparation(Scene scene, object roomModel) + { + var type = FindType("CKMKDCCLONK"); + var instance = type.GetProperty("ONCPNMDGGKG", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic).GetValue(null); + var method = type.GetMethod("MDEOKEFNEIO", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); + method.Invoke(instance, new[] { (object)scene, Activator.CreateInstance(typeof(Scene)), roomModel }); + } + + private static System.Collections.Generic.List GetRejectedDependencyPaths(SceneAsset scene) + { + var type = FindType("PCCMIBMBNDP"); + var instance = type.GetProperty("ONCPNMDGGKG", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic).GetValue(null); + var method = type.GetMethod("OCDABPILJAK", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); + var arguments = new object[] { scene, null }; + method.Invoke(instance, arguments); + return (System.Collections.Generic.List)arguments[1]; + } + + private static Type FindType(string name) + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + var type = assemblies.Select(assembly => assembly.GetType("RecRoom.Core.Studio.RecNet." + name, false)) + .FirstOrDefault(value => value != null) + ?? assemblies.Select(assembly => assembly.GetType(name, false)).FirstOrDefault(value => value != null); + if (type != null) return type; + foreach (var assembly in assemblies) + { + try + { + type = assembly.GetTypes().FirstOrDefault(candidate => candidate.Name == name); + if (type != null) return type; + } + catch (System.Reflection.ReflectionTypeLoadException ex) + { + type = ex.Types.FirstOrDefault(candidate => candidate?.Name == name); + if (type != null) return type; + } + } + throw new InvalidOperationException("Required local Rec Room Studio type was not loaded: " + name); + } + + private static string ReadArg(string name) + { + var args = Environment.GetCommandLineArgs(); + for (var i = 0; i + 1 < args.Length; i++) if (args[i] == name) return args[i + 1]; + throw new InvalidOperationException("Missing command-line argument " + name); + } + } + """; + + private sealed record BuildSource(string ManifestAssetId, string RoomJson, string SubRoomJson); + private sealed record BuildArguments(string ManifestAssetId, string ManifestFilename, string ScenePath, string SceneName, + long RoomId, long SubRoomId, long SaveId, long AccountId, string CloudBuildId, string LocalHttpBaseUrl, + string RoomJson, string SubRoomJson); + private sealed record BuildResult(string[] BundlePaths, string? Error); + + public sealed class BuildJob + { + public BuildJob(string cloudBuildId, long roomId, long subRoomId, long saveId, long accountId, + IDbContextFactory dbFactory, StudioCloudBuild build, + Action update) + { + CloudBuildId = cloudBuildId; + RoomId = roomId; + SubRoomId = subRoomId; + SaveId = saveId; + AccountId = accountId; + DbFactory = dbFactory; + Build = build; + Update = update; + } + public string CloudBuildId { get; } + public long RoomId { get; } + public long SubRoomId { get; } + public long SaveId { get; } + public long AccountId { get; } + public IDbContextFactory DbFactory { get; } + public StudioCloudBuild Build { get; } + public Action Update { get; } + } +} diff --git a/Server/Studio/StudioUnityAssetRegistry.cs b/Server/Studio/StudioUnityAssetRegistry.cs new file mode 100644 index 0000000..c6c4004 --- /dev/null +++ b/Server/Studio/StudioUnityAssetRegistry.cs @@ -0,0 +1,109 @@ +using System.Security.Cryptography; +using System.Text.Json; +using RadicadeServer.Storage; + +namespace RadicadeServer.API.Studio; + +public sealed record StudioBakedUnityAsset(string UnityAssetId, byte Target, int Version, string Filename, string? RejectedDependenciesFilename = null); + +public sealed record StudioUnityAsset( + string UnityAssetId, + int? CreatedByAccountId, + string Filename, + string Hash, + IReadOnlyList BakedUnityAssets); + +public static class StudioUnityAssetRegistry +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + private static readonly SemaphoreSlim Gate = new(1, 1); + + public static bool IsValidAssetId(string? value) => + value is { Length: 40 } && value.All(char.IsAsciiLetterOrDigit); + + public static async Task ReadAsync(string assetId, CancellationToken cancellationToken = default) + { + if (!IsValidAssetId(assetId)) return null; + var manifestPath = Path.Combine(DBConfig.CDNStorage, assetId); + if (!File.Exists(manifestPath)) return null; + + byte[] hash; + await using (var manifest = File.OpenRead(manifestPath)) + hash = await SHA256.HashDataAsync(manifest, cancellationToken); + + var path = MetadataPath(assetId); + IReadOnlyList baked = Array.Empty(); + if (File.Exists(path)) + { + try + { + var metadata = JsonSerializer.Deserialize(await File.ReadAllTextAsync(path, cancellationToken), JsonOptions); + if (metadata?.BakedUnityAssets != null) baked = metadata.BakedUnityAssets; + } + catch (JsonException) + { + + } + } + + return new StudioUnityAsset(assetId, null, assetId, Convert.ToBase64String(hash), baked); + } + + public static async Task ResolveBundleAsync(string assetId, int target, int version, CancellationToken cancellationToken = default) + { + var asset = await ReadAsync(assetId, cancellationToken); + return asset?.BakedUnityAssets.FirstOrDefault(x => x.Target == target && x.Version == version); + } + + public static async Task AddBakedAssetAsync(string assetId, StudioBakedUnityAsset bakedAsset, CancellationToken cancellationToken = default) + { + if (!IsValidAssetId(assetId) || !IsValidAssetId(bakedAsset.UnityAssetId) || !IsValidAssetId(bakedAsset.Filename) || + bakedAsset.Version < 0 || !File.Exists(Path.Combine(DBConfig.CDNStorage, bakedAsset.Filename)) || + (bakedAsset.RejectedDependenciesFilename != null && + (!IsValidAssetId(bakedAsset.RejectedDependenciesFilename) || + !File.Exists(Path.Combine(DBConfig.CDNStorage, bakedAsset.RejectedDependenciesFilename))))) + throw new InvalidDataException("Invalid Studio Unity asset metadata."); + await Gate.WaitAsync(cancellationToken); + try + { + Directory.CreateDirectory(DBConfig.CDNStorage); + var path = MetadataPath(assetId); + Metadata current = new(); + if (File.Exists(path)) + { + try + { + current = JsonSerializer.Deserialize(await File.ReadAllTextAsync(path, cancellationToken), JsonOptions) ?? new Metadata(); + } + catch (JsonException) { } + } + + var assets = current.BakedUnityAssets + .Where(x => x.Target != bakedAsset.Target || x.Version != bakedAsset.Version) + .Append(bakedAsset) + .ToArray(); + var temporary = path + ".tmp-" + Guid.NewGuid().ToString("N"); + try + { + await File.WriteAllTextAsync(temporary, JsonSerializer.Serialize(new Metadata(assets), JsonOptions), cancellationToken); + File.Move(temporary, path, overwrite: true); + } + finally + { + if (File.Exists(temporary)) File.Delete(temporary); + } + } + finally + { + Gate.Release(); + } + } + + private static string MetadataPath(string assetId) => + Path.Combine(DBConfig.CDNStorage, $".studio-unityasset-{assetId}.json"); + + private sealed record Metadata(IReadOnlyList BakedUnityAssets) + { + public Metadata() : this(Array.Empty()) { } + } +} diff --git a/Server/Tooling/Custom/CCAPI.cs b/Server/Tooling/Custom/CCAPI.cs new file mode 100644 index 0000000..94cb19f --- /dev/null +++ b/Server/Tooling/Custom/CCAPI.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Cryptography; +using RadicadeServer.DB; + +using IOFile = System.IO.File; + +namespace RadicadeServer.Custom.API +{ + [Route("custom/")] + public class CCAPI(RecDBContext _db) : Controller + { + private static string GetHashAndCache(string hashName, string fileName) + { + if (!IOFile.Exists(hashName)) + { + string hash = CalculateMD5(fileName); + IOFile.WriteAllText(hashName, hash); + return hash; + } + return IOFile.ReadAllText(hashName); + } + private static string CalculateMD5(string filename) + { + using (var md5 = MD5.Create()) + { + using (var stream = IOFile.OpenRead(filename)) + { + var hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + + [HttpGet("all")] + public async Task> GetAllContent() + { + return await _db.CustomContent.ToListAsync(); + } + [HttpGet("download/{modId}/{fileId}")] + public async Task DownloadContent(long? modId, int? fileId) + { + if (modId == null || fileId == null) return BadRequest(); + + var mod = await _db.CustomContent.FirstOrDefaultAsync(x => x.Id == modId); + if (mod == null) return NotFound(); + + var files = mod.Files.Split(";", StringSplitOptions.TrimEntries); + if (fileId >= files.Length || fileId < 0) return BadRequest(); + + string path = Path.Join(DBConfig.CDNCustom, mod.Name, files[(int)fileId]); + if (!System.IO.File.Exists(path)) return NotFound(); + + return PhysicalFile(path, "application/octet-stream", enableRangeProcessing: true); + } + [HttpGet("hash/{modId}/{file}")] + public async Task GetHashForMod(string modId, string file) + { + if (modId == null || file == null) return BadRequest(); + + file = file.ToLower().Trim(); + + var mod = await _db.CustomContent.FirstOrDefaultAsync(x => x.Name == modId); + if (mod == null) return NotFound(); + + var files = mod.Files.Split(";", StringSplitOptions.TrimEntries); + var expectedFile = files.FirstOrDefault(x => x.ToLower().Trim().Contains(file)); + + string path = Path.Join(DBConfig.CDNCustom, mod.Name, file); + string hashPath = Path.Join(DBConfig.CDNCustom, mod.Name, file + "-hash"); + if (!IOFile.Exists(path)) return NotFound(); + + string hash = GetHashAndCache(hashPath, path); + + return Ok(hash); + } + } +} diff --git a/Server/Tooling/Custom/CustomAddition.cs b/Server/Tooling/Custom/CustomAddition.cs new file mode 100644 index 0000000..2cf4f9f --- /dev/null +++ b/Server/Tooling/Custom/CustomAddition.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace RadicadeServer.Custom +{ + public class CustomAddition + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public string Files { get; set; } = null!; + } +} diff --git a/Server/Tooling/DebugAPI.cs b/Server/Tooling/DebugAPI.cs new file mode 100644 index 0000000..c3649c5 --- /dev/null +++ b/Server/Tooling/DebugAPI.cs @@ -0,0 +1,195 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RecNet.Maintenance; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Board; +using RadicadeServer.Utils; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; + +namespace RadicadeServer.Debug +{ + [Route("debug/")] + public class DebugAPI : Controller + { + RecDBContext db; + public DebugAPI(RecDBContext context) + { + db = context; + } + +#if DEBUG + [HttpGet("{key}/data/player/{who}/room/{name}/")] + public async Task DebugRoom(string key, string who, string name) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var room = await RoomUtils.GetRoom(name); + var profile = await db.Profiles.FirstOrDefaultAsync(x => x.username == who); + var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.Name == name && (profile != null) ? x.CreatorAccountId == profile.accountId : false); + var subRooms = await db.SubRooms.Where(x => (roomData != null) ? x.RoomId == roomData.RoomId : false).ToListAsync(); + var profileAccountId = profile!.accountId; + + return Ok(new + { + roomFound = room != null, + roomDataFound = roomData != null, + subRoomsCount = subRooms.Count, + presenceExists = await db.Presences.AnyAsync(x => x.playerId == profileAccountId), + roomData = roomData, + subRooms = subRooms, + profile = profile + }); + } + + [HttpGet("{key}/global/refresh/board/announcement")] + public async Task RefreshBoardAnnouncement(string key) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var board = (await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "CommunityBoard"))!.Value.FromJson()!; + var players = await db.Profiles.ToListAsync(); + long playerCount = 0; + foreach (var player in players) + { + await Notify.SendNotify(player.accountId, new Notification { Id = Notifs.CommunityBoardAnnouncementUpdate, Msg = board.CurrentAnnouncement }); + playerCount++; + } + return Ok("Players affected: " + playerCount.ToString()); + } + [HttpGet("{key}/global/refresh/board")] + public async Task RefreshBoard(string key) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var board = (await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "CommunityBoard"))!.Value.FromJson()!; + var players = await db.Profiles.ToListAsync(); + long playerCount = 0; + foreach (var player in players) + { + await Notify.SendNotify(player.accountId, new Notification { Id = Notifs.CommunityBoardUpdate, Msg = board }); + playerCount++; + } + return Ok("Players affected: " + playerCount.ToString()); + } + [HttpGet("{key}/global/announce")] + public async Task AnnounceToEveryone(string key, [FromQuery] string msg, [FromQuery] long from = 1) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var players = await db.Profiles.ToListAsync(); + long playerCount = 0; + foreach (var player in players) + { + await Notify.SendMessage(player.accountId, new Message { Type = MessageType.CoachMessage, Data = msg, FromPlayerId = from }); + playerCount++; + } + return Ok("Players affected: " + playerCount.ToString()); + } + [HttpGet("{key}/global/maintenance")] + public async Task SetMaintenance(string key, [FromQuery] int min) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var players = await db.Profiles.ToListAsync(); + long playerCount = 0; + foreach (var player in players) + { + await Notify.SendNotify(player.accountId, new Notification { Id = Notifs.ServerMaintenance, Msg = new ServerMaintenance { StartsInMinutes = min } }); + playerCount++; + } + return Ok("Players affected: " + playerCount.ToString()); + } + [HttpGet("{key}/coach/friend/{id}")] + public async Task CoachFriendRequest(string key, long id) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (player == null) return NotFound(); + + await Notify.SendMessage(id, new Message { Type = MessageType.FriendInvite, FromPlayerId = 1 }); + + return Ok("Players affected: 1"); + } + [HttpGet("{key}/coach/friended/{id}")] + public async Task CoachFriended(string key, long id) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (player == null) return NotFound(); + + await Notify.SendMessage(id, new Message { Type = MessageType.FriendRequestAccepted, FromPlayerId = 1 }); + + return Ok("Players affected: 1"); + } + [HttpGet("{key}/coach/invite/{id}/{roomId}")] + public async Task CoachInvite(string key, long id, long roomId) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (player == null) return NotFound(); + + await Notify.SendMessage(id, new Message { Type = MessageType.GameInvite, FromPlayerId = 1, RoomId = roomId }); + + return Ok("Players affected: 1"); + } + [HttpGet("{key}/{playerId}/invite/{id}/{roomId}")] + public async Task PlayerInvite(string key, long playerId, long id, long roomId) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (player == null) return NotFound(); + + await Notify.SendMessage(id, new Message { Type = MessageType.GameInvite, FromPlayerId = playerId, RoomId = roomId }); + + return Ok("Players affected: 1"); + } + [HttpGet("{key}/coach/join/{id}/{roomId}")] + public async Task CoachJoin(string key, long id, long roomId) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (player == null) return NotFound(); + + await Notify.SendMessage(id, new Message { Type = MessageType.RequestGameInvite, FromPlayerId = 1, RoomId = roomId }); + + return Ok("Players affected: 1"); + } + [HttpGet("{key}/mod/quit/{id}")] + public async Task ModQuit(string key, long id) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (player == null) return NotFound(); + + await Notify.SendNotify(id, new Notification { Id = NotificationType.ModerationQuitGame, Msg = null }); + + return Ok("Players affected: 1"); + } + [HttpGet("{key}/mod/kick/{id}")] + public async Task ModKick(string key, long id) + { + if (key != DBConfig.DebugKey) return Forbid(); + + var player = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id); + if (player == null) return NotFound(); + + await Notify.SendNotify(id, new Notification { Id = NotificationType.ModerationKick, Msg = PlayerUtils.BuildBlockDetails(true, ReportCategory.Misc, "Kicked via debug command", DateTime.UtcNow.AddMinutes(15)) }); + + return Ok("Players affected: 1"); + } +#endif + } +} diff --git a/Server/Tooling/TestCaseManagementAPI.cs b/Server/Tooling/TestCaseManagementAPI.cs new file mode 100644 index 0000000..6d52f77 --- /dev/null +++ b/Server/Tooling/TestCaseManagementAPI.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace RadicadeServer.API.TestCaseManagement +{ + + + + + + + + + + [Route("api/testcasemanagement/")] + public class TestCaseManagementAPI : Controller + { + [HttpGet("v1/testpasssummary"), Authorize] + public IActionResult TestPassSummary() + { + return Ok(Array.Empty()); + } + } +} diff --git a/Server/Utils/BuildInfo.cs b/Server/Utils/BuildInfo.cs new file mode 100644 index 0000000..1a5a9a9 --- /dev/null +++ b/Server/Utils/BuildInfo.cs @@ -0,0 +1,37 @@ +using System.Reflection; + +namespace RadicadeServer.Utils +{ + public static class BuildInfo + { + public const string Tag = "notify-ids-numeric-string"; + + public const string NotifyIdFormat = "numeric-string"; + + public static readonly DateTime StartedUtc = DateTime.UtcNow; + + public static DateTime? AssemblyBuiltUtc + { + get + { + try + { + var location = Assembly.GetEntryAssembly()?.Location; + return string.IsNullOrEmpty(location) || !File.Exists(location) + ? null + : File.GetLastWriteTimeUtc(location); + } + catch + { + return null; + } + } + } + + public static string Describe() + => $"{Tag}: notify ids are sent as the client enum's NUMBER in a JSON string (MessageReceived=\"2\", ModerationKick=\"22\", RefreshLogin=\"5\") - " + + "never a bare number (the client cannot parse it) and never a name (it parses and matches no handler). " + + $"Started {StartedUtc:u} from an assembly published {AssemblyBuiltUtc?.ToString("u") ?? "unknown"}. " + + "If this line is missing, or the publish time is older than your deploy, the running server is not the build you just made."; + } +} diff --git a/Server/Utils/StringTools.cs b/Server/Utils/StringTools.cs new file mode 100644 index 0000000..f2df4a7 --- /dev/null +++ b/Server/Utils/StringTools.cs @@ -0,0 +1,167 @@ +using System.Text; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet; +using System.Buffers.Text; +using System.Security.Cryptography; +using System.Text.RegularExpressions; + +namespace RadicadeServer.Utils +{ + public static class StringTools + { + public static string AltGuid(this Guid guid) + { + return guid.ToString().Replace("-", ""); + } + + public static string FromAES(this string encrypted, string? b64Key, string? b64IV) + { + if (string.IsNullOrEmpty(b64Key)) return ""; + if (string.IsNullOrEmpty(b64IV)) return ""; + + try + { + byte[] cipherBytes = Convert.FromBase64String(encrypted); + byte[] keyBytes = Convert.FromBase64String(b64Key); + byte[] ivBytes = Convert.FromBase64String(b64IV); + + using (Aes aes = Aes.Create()) + { + aes.KeySize = 128; + aes.BlockSize = 128; + aes.Key = keyBytes; + aes.IV = ivBytes; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + using (MemoryStream ms = new MemoryStream(cipherBytes)) + using (CryptoStream cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read)) + using (StreamReader sr = new StreamReader(cs, Encoding.UTF8)) + { + return sr.ReadToEnd(); + } + } + } catch + { + return ""; + } + } + + public static List ParseCardBoxWords(this string req) + { + var split = req.Split(';', StringSplitOptions.TrimEntries); + var words = new List(); + + foreach (var word in split) + { + words.Add(new CardBoxWord { Difficulty = CardDifficulty.CharadesEasy, EN_US = word }); + } + + return words; + } + + public static string CreateTagString(List? Tags) + { + if (Tags == null) + return ""; + + StringBuilder stringBuilder = new StringBuilder(); + foreach (ItemTag itemTag in Tags) + { + stringBuilder.Append(itemTag.Tag.ToString() + "," + ((int)itemTag.Type).ToString() + "|"); + } + return stringBuilder.ToString().TrimEnd('|'); + } + + public static string CreateTagString(string[]? Tags) + { + if (Tags == null) + return ""; + + StringBuilder stringBuilder = new StringBuilder(); + foreach (string itemTag in Tags) + { + stringBuilder.Append(itemTag.ToString() + "," + ((int)TagType.General).ToString() + "|"); + } + return stringBuilder.ToString().TrimEnd('|'); + } + + public static List ReadTags(string Tags) + { + List list = new List(); + if (string.IsNullOrWhiteSpace(Tags)) return list; + string[] tags = Tags.Split("|", StringSplitOptions.None); + foreach (string text in tags) + { + string[] array3 = text.Split(",", StringSplitOptions.None); + if (array3.Length != 2) continue; + if (!int.TryParse(array3[1], out var typeValue) || !Enum.IsDefined(typeof(TagType), typeValue)) + continue; + list.Add(new ItemTag + { + Tag = array3[0], + Type = (TagType)typeValue + }); + } + return list; + } + + public static List TagsToStrings(List Tags) + { + List list = new List(); + foreach (var tag in Tags) + { + list.Add(tag.Tag); + } + return list; + } + + public static string ToBase64(this string str) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(str); + var b64 = Convert.ToBase64String(bytes); + return b64; + } + + + private static string[] BlacklistedWords = { + "nigger", + "nigga", + "faggot", + "fag", + "nigg", + }; + private static string[] BlacklistedTags = { + "rro", + "featured" + }; + + public static string Sanitize(this string str) + { + foreach (var tag in BlacklistedWords) + { + str = Regex.Replace(str, tag, "****", RegexOptions.IgnoreCase); + } + return str; + } + public static bool IsPure(this string str) + { + if (string.IsNullOrEmpty(str)) return true; + + foreach (var tag in BlacklistedWords) + { + if (Regex.IsMatch(str, tag, RegexOptions.IgnoreCase)) + return false; + } + return true; + } + public static bool TagAllowed(this string str) + { + foreach (var tag in BlacklistedTags) { + if (Regex.IsMatch(tag, str)) + return false; + } + return str.IsPure(); + } + } +} diff --git a/Server/Utils/VoiceChatRestrictions.cs b/Server/Utils/VoiceChatRestrictions.cs new file mode 100644 index 0000000..2483dc5 --- /dev/null +++ b/Server/Utils/VoiceChatRestrictions.cs @@ -0,0 +1,155 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; + +namespace RadicadeServer.Utils +{ + public static class VoiceChatRestrictions + { + public const string NotificationId = "VoiceChatRestriction"; + + public const string Method = "Voice Restriction"; + + public const long CoachAccountId = 1; + + public const string CoachMessage = + "You have had your voice privileges temporarily restricted due to speech which violates the Code of Conduct."; + + public static readonly TimeSpan DefaultDuration = TimeSpan.FromHours(24); + + public static readonly TimeSpan MinimumDuration = TimeSpan.FromHours(1); + + public static readonly TimeSpan MaximumDuration = TimeSpan.FromDays(30); + + public static TimeSpan ClampDuration(TimeSpan duration) + { + if (duration < MinimumDuration) return MinimumDuration; + if (duration > MaximumDuration) return MaximumDuration; + return duration; + } + + public static Task ActiveAsync(RecDBContext db, long playerId) => + db.VoiceChatRestrictions.AsNoTracking() + .Where(x => x.PlayerId == playerId && x.ClearedAt == null && x.ExpiresAt > DateTime.UtcNow) + .OrderByDescending(x => x.ExpiresAt) + .FirstOrDefaultAsync(); + + public static async Task IsRestrictedAsync(RecDBContext db, long playerId) => + await ActiveAsync(db, playerId) != null; + + public static async Task ApplyAsync( + RecDBContext db, + long playerId, + string reason, + ReportCategory category, + TimeSpan duration, + string issuedBy, + string? issuedByUsername = null, + string? moderatorNote = null) + { + var now = DateTime.UtcNow; + var clamped = ClampDuration(duration); + + var restriction = await db.VoiceChatRestrictions + .Where(x => x.PlayerId == playerId && x.ClearedAt == null && x.ExpiresAt > now) + .OrderByDescending(x => x.ExpiresAt) + .FirstOrDefaultAsync(); + + if (restriction == null) + { + restriction = new VoiceChatRestriction + { + PlayerId = playerId, + StartedAt = now, + ExpiresAt = now + clamped + }; + await db.VoiceChatRestrictions.AddAsync(restriction); + } + else + { + var extended = now + clamped; + if (extended > restriction.ExpiresAt) restriction.ExpiresAt = extended; + restriction.StartedAt = now; + } + + restriction.Reason = reason; + restriction.Category = category; + restriction.IssuedBy = issuedBy; + restriction.IssuedByUsername = issuedByUsername; + restriction.ModeratorNote = moderatorNote; + + await db.SaveChangesAsync(); + await TellPlayerAsync(playerId); + return restriction; + } + + private static async Task TellPlayerAsync(long playerId) + { + try + { + await Notify.SendMessage(playerId, new Message + { + Type = MessageType.CoachMessage, + Data = CoachMessage, + FromPlayerId = CoachAccountId + }); + } + catch + { + } + } + + public static async Task ClearAsync(RecDBContext db, long playerId, string? clearedBy = null) + { + var now = DateTime.UtcNow; + var active = await db.VoiceChatRestrictions + .Where(x => x.PlayerId == playerId && x.ClearedAt == null && x.ExpiresAt > now) + .ToListAsync(); + + if (active.Count == 0) return false; + + foreach (var restriction in active) + { + restriction.ClearedAt = now; + restriction.ClearedBy = clearedBy; + } + + await db.SaveChangesAsync(); + return true; + } + + public static object Payload(VoiceChatRestriction? restriction) => new + { + restricted = restriction != null, + playerId = restriction?.PlayerId ?? 0, + reason = restriction?.Reason ?? "", + category = restriction?.Category.ToString() ?? "", + startedAtUtc = restriction?.StartedAt, + expiresAtUtc = restriction?.ExpiresAt, + secondsRemaining = restriction == null + ? 0 + : (int)Math.Max(0, (restriction.ExpiresAt - DateTime.UtcNow).TotalSeconds) + }; + + public static async Task PushAsync(long playerId, VoiceChatRestriction? restriction) + { + try + { + await Notify.SendNotify(playerId, new Notification + { + Id = NotificationId, + Msg = Payload(restriction) + }); + } + catch + { + + } + } + } +} diff --git a/Server/Web/WebAPI.cs b/Server/Web/WebAPI.cs new file mode 100644 index 0000000..b2e1f50 --- /dev/null +++ b/Server/Web/WebAPI.cs @@ -0,0 +1,278 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.CDN; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Relationships; +using RadicadeServer.Radicade.Mail; + +namespace RadicadeServer.Web +{ + + + + + + [Route("web")] + public class WebAPI : Controller + { + private readonly RecDBContext db; + + public WebAPI(RecDBContext context) { + this.db = context; + } + + private static string ImageUrl(string? name) + { + if (string.IsNullOrWhiteSpace(name)) return $"{DBConfig.NameServer.Images}/default"; + if (name.Equals("default", StringComparison.OrdinalIgnoreCase) || + name.Equals("default.png", StringComparison.OrdinalIgnoreCase) || + name.Equals("DefaultRoomImage", StringComparison.OrdinalIgnoreCase) || + name.Equals("DefaultProfileImage", StringComparison.OrdinalIgnoreCase)) + return $"{DBConfig.NameServer.Images}/default"; + return $"{DBConfig.NameServer.Images}/img/{name}"; + } + + private async Task SubscriberCountAsync(long accountId) + { + + + return await db.Relationships.AsNoTracking() + .CountAsync(x => x.PlayerId == accountId && x.RelationshipType == RelationshipType.Friend); + } + + + [HttpGet("me"), Authorize] + public async Task Me() + { + var userId = User.GetUserId(); + if (!userId.HasValue) return Unauthorized(); + + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == userId.Value); + if (profile == null) return NotFound(new { success = false, message = "Account not found." }); + + var plrData = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == userId.Value); + + return Ok(new + { + success = true, + accountId = profile.accountId, + username = profile.username, + displayName = profile.displayName, + bio = profile.bio ?? "", + createdAt = profile.createdAt, + profileImage = ImageUrl(profile.profileImage), + email = plrData?.Email ?? "", + emailVerified = plrData?.EmailVerified ?? false, + hasPassword = !string.IsNullOrWhiteSpace(plrData?.Hashword), + subscriberCount = await SubscriberCountAsync(userId.Value) + }); + } + + + + + + [HttpGet("profile/{accountId:long}")] + public async Task Profile(long accountId) + { + var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId); + if (profile == null) + return NotFound(new { success = false, message = "Player not found." }); + + + var photos = new List(); + var imageRows = await db.Images.AsNoTracking() + .Where(x => x.OwnerId == accountId && x.Accessibility == SavedImageAccessibility.Public) + .OrderByDescending(x => x.CreatedAt) + .Take(24) + .ToListAsync(); + foreach (var img in imageRows) + { + string? roomName = null; + if (img.RoomId.HasValue) + { + var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == img.RoomId.Value); + if (room != null) roomName = room.Name; + } + var cheers = await db.ImageInteractions.AsNoTracking().CountAsync(x => x.SavedImageId == img.Id); + photos.Add(new + { + id = img.Id, + image = ImageUrl(img.Name), + roomName = roomName ?? "Unknown room", + cheers, + createdAt = img.CreatedAt + }); + } + + + var rooms = await db.Rooms.AsNoTracking() + .Where(x => x.CreatorAccountId == accountId) + .OrderByDescending(x => x.CreatedAt) + .Take(24) + .Select(x => new + { + roomId = x.RoomId, + name = x.Name, + description = x.Description ?? "", + image = ImageUrl(x.ImageName), + maxPlayers = x.MaxPlayers, + createdAt = x.CreatedAt + }) + .ToListAsync(); + + + var events = await db.PlayerEvents.AsNoTracking() + .Where(x => x.CreatorPlayerId == accountId) + .OrderBy(x => x.StartTime) + .Take(24) + .Select(x => new + { + eventId = x.PlayerEventId, + name = x.Name, + description = x.Description, + image = ImageUrl(x.ImageName), + startTime = x.StartTime, + endTime = x.EndTime, + attendeeCount = x.AttendeeCount + }) + .ToListAsync(); + + return Ok(new + { + success = true, + profile = new + { + accountId = profile.accountId, + username = profile.username, + displayName = profile.displayName, + bio = profile.bio ?? "", + createdAt = profile.createdAt, + profileImage = ImageUrl(profile.profileImage), + subscriberCount = await SubscriberCountAsync(accountId) + }, + photos, + rooms, + events + }); + } + + + public class ForgotPasswordRequest + { + public string? Email { get; set; } + } + + + + + + + [HttpPost("password/forgot")] + public async Task ForgotPassword([FromBody] ForgotPasswordRequest? req) + { + var email = (req?.Email ?? "").Trim(); + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) + return Ok(new { success = false, message = "Enter a valid email address." }); + + var plrData = await db.Players.FirstOrDefaultAsync(x => x.Email.ToLower() == email.ToLower()); + if (plrData != null) + { + plrData.PasswordResetToken = Guid.NewGuid().ToString("N"); + plrData.PasswordResetExpires = DateTime.UtcNow.AddHours(1); + await db.SaveChangesAsync(); + + SendResetEmail(plrData); + } + + + return Ok(new { success = true, message = "If an account exists for that email, a reset link has been sent." }); + } + + + public class ResetPasswordRequest + { + public string? Code { get; set; } + public string? NewPassword { get; set; } + } + + + [HttpGet("password/reset/result")] + public async Task ResetResult([FromQuery] string code) + { + var (ok, message) = await ValidateResetCode(code); + return Ok(new { success = ok, message }); + } + + + [HttpPost("password/reset")] + public async Task ResetPassword([FromBody] ResetPasswordRequest? req) + { + var code = req?.Code?.Trim() ?? ""; + var newPassword = req?.NewPassword ?? ""; + + if (string.IsNullOrWhiteSpace(code)) return Ok(new { success = false, message = "This reset link is missing its code." }); + if (newPassword.Length < Auth.AuthAPI.MinPasswordLength) + return Ok(new { success = false, message = $"Password must be at least {Auth.AuthAPI.MinPasswordLength} characters." }); + + var plrData = await db.Players.FirstOrDefaultAsync(x => x.PasswordResetToken == code); + if (plrData == null) + return Ok(new { success = false, message = "This reset link is invalid or has already been used." }); + if (!plrData.PasswordResetExpires.HasValue || plrData.PasswordResetExpires.Value < DateTime.UtcNow) + return Ok(new { success = false, message = "This reset link has expired. Request a new one." }); + + var hasher = new Microsoft.AspNetCore.Identity.PasswordHasher(); + plrData.Hashword = hasher.HashPassword(plrData, newPassword); + plrData.PasswordResetToken = null; + plrData.PasswordResetExpires = null; + + + + + var staleLogins = await db.CachedLogins.Where(x => x.accountId == plrData.AccountId).ToListAsync(); + if (staleLogins.Count > 0) + db.CachedLogins.RemoveRange(staleLogins); + plrData.TokenVersion++; + Utils.TokenRevocation.Bump(plrData.AccountId, plrData.TokenVersion); + + await db.SaveChangesAsync(); + + Utils.AuthSecurityMonitor.NoteCredentialChange(plrData.AccountId, "password reset completed"); + return Ok(new { success = true, message = "Password updated. You can now sign in with your new password." }); + } + + private async Task<(bool Ok, string Message)> ValidateResetCode(string code) + { + if (string.IsNullOrWhiteSpace(code)) + return (false, "This reset link is missing its code."); + var plrData = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.PasswordResetToken == code); + if (plrData == null) + return (false, "This reset link is invalid or has already been used."); + if (!plrData.PasswordResetExpires.HasValue || plrData.PasswordResetExpires.Value < DateTime.UtcNow) + return (false, "This reset link has expired. Request a new one."); + return (true, "This reset link is valid."); + } + + private void SendResetEmail(PlayerData plrData) + { + if (string.IsNullOrWhiteSpace(plrData.PasswordResetToken)) return; + + var link = $"https://radicade.app/reset?code={plrData.PasswordResetToken}"; + var html = + "
" + + "\"Radicade\"" + + "

Reset your Radicade password

" + + "

Hi there,

" + + "

We received a request to reset the password on your Radicade account. If that was you, click the button below to choose a new password:

" + + "

Reset my password

" + + "

This link expires in one hour. If you didn't request this, you can safely ignore this email.

"; + + _ = RadicadeMail.SendAsync(plrData.Email, "Reset your password - Radicade", html); + } + } +} diff --git a/Server/appsettings.Development.json b/Server/appsettings.Development.json new file mode 100644 index 0000000..3797739 --- /dev/null +++ b/Server/appsettings.Development.json @@ -0,0 +1,44 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=.\\SQLEXPRESS;Database=RadicadeServer;Trusted_Connection=True;TrustServerCertificate=True;Encrypt=False;MultipleActiveResultSets=true" + }, + "AllowedHosts": "*", + + "Radicade": { + "ServerUrl": "https://localhost:3030", + "ListenUrls": "https://localhost:3030;http://127.0.0.1:3031", + "JwtKey": "radicade-local-development-jwt-key-change-before-production", + "Photon": { + "RealtimeAppId": "USEYOURS", + "VoiceAppId": "USEYOURS", + "ChatAppId": "USEYOURS", + "Region": "us" + }, + "AdminApiKey": "radicade-local-development-admin-key", + "AdminPanelUrl": "http://localhost:8788", + "Admins": [ + { + "Username": "admin", + "Password": "change-me" + } + ], + "AntiCheat": { + "DefaultPerIpLimit": 600, + "DefaultPerIpWindowSeconds": 60, + "AuthPerIpLimit": 120, + "AuthPerIpWindowSeconds": 60, + "AuthPerAccountLimit": 30, + "AuthPerAccountWindowSeconds": 60, + "CreateAccountPerIpLimit": 15, + "CreateAccountPerIpWindowSeconds": 3600 + } + }, + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Server/appsettings.Production.json b/Server/appsettings.Production.json new file mode 100644 index 0000000..5772388 --- /dev/null +++ b/Server/appsettings.Production.json @@ -0,0 +1,26 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=.\\SQLEXPRESS;Database=RadicadeServer;Trusted_Connection=True;TrustServerCertificate=True;Encrypt=False;MultipleActiveResultSets=true" + }, + "AllowedHosts": "*", + "Radicade": { + "ServerUrl": "https://ns.radicade.app", + "ListenUrls": "http://0.0.0.0:3030", + "JwtKey": "USEYOURS", + "SteamApiKey": "USEYOURS", + "AdminApiKey": "USEYOURS", + "Photon": { + "RealtimeAppId": "USEYOURS", + "VoiceAppId": "USEYOURS", + "ChatAppId": "USEYOURS", + "Region": "us" + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Server/images.txt b/Server/images.txt new file mode 100644 index 0000000..e9e1730 --- /dev/null +++ b/Server/images.txt @@ -0,0 +1,44 @@ +3DCharades=https://cdn.radicade.app/3DCharades.png +ARRoom=https://cdn.radicade.app/ARRoom.png +Bowling=https://cdn.radicade.app/Bowling.png +BowlingAlley=https://cdn.radicade.app/BowlingAlley.png +Clearcut=https://cdn.radicade.app/Clearcut.png +Crescendo=https://cdn.radicade.app/Crescendo.png +CrimsonCauldron=https://cdn.radicade.app/CrimsonCauldron.png +CyberJunkCity=https://cdn.radicade.app/CyberJunkCity.png +DiscGolfLake=https://cdn.radicade.app/DiscGolfLake.png +DiscGolfPropulsion=https://cdn.radicade.app/DiscGolfPropulsion.png +Dodgeball=https://cdn.radicade.app/Dodgeball.png +DodgeballVR=https://cdn.radicade.app/DodgeballVR.png +Dorm=https://cdn.radicade.app/Dorm.png +DriveIn=https://cdn.radicade.app/DriveIn.png +GoldenTrophy=https://cdn.radicade.app/GoldenTrophy.png +Gym=https://cdn.radicade.app/Gym.png +Hangar=https://cdn.radicade.app/Hangar.png +Homestead=https://cdn.radicade.app/Homestead.png +IsleOfLostSkulls=https://cdn.radicade.app/IsleOfLostSkulls.png +Lake=https://cdn.radicade.app/Lake.png +LaserTag=https://cdn.radicade.app/LaserTag.png +Legacy3DCharades=https://cdn.radicade.app/Legacy3DCharades.png +Lounge=https://cdn.radicade.app/Lounge.png +MakerRoom=https://cdn.radicade.app/MakerRoom.png +Orientation=https://cdn.radicade.app/Orientation.png +Paddleball=https://cdn.radicade.app/Paddleball.png +Paintball=https://cdn.radicade.app/Paintball.png +Park=https://cdn.radicade.app/Park.png +PropulsionTestRange=https://cdn.radicade.app/PropulsionTestRange.png +Quarry=https://cdn.radicade.app/Quarry.png +RecCenter=https://cdn.radicade.app/RecCenter.png +RecRoyaleSquads=https://cdn.radicade.app/RecRoyaleSquads.png +RecRoyaleSolos=https://cdn.radicade.app/RecRoyaleSolos.png +Registration=https://cdn.radicade.app/Registration.png +River=https://cdn.radicade.app/River.png +Soccer=https://cdn.radicade.app/Soccer.png +Spillway=https://cdn.radicade.app/Spillway.png +Stadium=https://cdn.radicade.app/Stadium.png +StuntRunner=https://cdn.radicade.app/StuntRunner.png +StuntRunnerBaseRoom=https://cdn.radicade.app/StuntRunnerBaseRoom.png +TheInkSpace=https://cdn.radicade.app/TheInkSpace.png +TheRiseofJumbotron=https://cdn.radicade.app/TheRiseofJumbotron.png +RecRally=https://cdn.radicade.app/RecRally.png +Showdown=https://cdn.radicade.app/Showdown.png diff --git a/Server/server-2023-debug.txt b/Server/server-2023-debug.txt new file mode 100644 index 0000000..46b134b --- /dev/null +++ b/Server/server-2023-debug.txt @@ -0,0 +1 @@ +ÿþ \ No newline at end of file diff --git a/ServerTests/AccountLockTests.cs b/ServerTests/AccountLockTests.cs new file mode 100644 index 0000000..730ed72 --- /dev/null +++ b/ServerTests/AccountLockTests.cs @@ -0,0 +1,642 @@ +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.Auth; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Auth.Connection; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +public class AccountLockTests +{ + private const string Secret = "test-bot-secret"; + private const string JwtKey = "account-lock-test-signing-key-with-sufficient-length"; + + private const string ExactMessage = + "@GreatEagle: This account has been locked for security reasons. Contact us at https://support.radicade.app."; + + private const string ExactReason = + "Moderator (Account lock. Please submit a ticket with us to discuss. https://support.radicade.app/new/account-support/)"; + + private static RecDBContext NewDb(DbContextOptions? options = null) => + new(options ?? TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static async Task SeedAccountAsync(RecDBContext db, long id, string username, string password, string deviceId = "device-1") + { + db.Profiles.Add(new Profile { accountId = id, username = username, displayName = username, profileImage = "default" }); + var player = new PlayerData { AccountId = id, DeviceId = deviceId, DeviceClass = DeviceClass.Screen, AppVersion = "20230831", Email = "" }; + player.Hashword = new PasswordHasher().HashPassword(player, password); + db.Players.Add(player); + db.Presences.Add(new RealPresence { playerId = id, deviceClass = DeviceClass.Screen }); + await db.SaveChangesAsync(); + } + + private static AuthAPI CreateApi(RecDBContext db) + { + var context = new DefaultHttpContext { User = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity()) }; + context.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback; + return new AuthAPI(db) { ControllerContext = new ControllerContext { HttpContext = context } }; + } + + private static TokenConnectData PasswordRequest(string username, string password) => new() + { + grant_type = "password", + platform = Platforms.Standalone, + device_id = "device-1", + client_id = "recroom", + ver = "20230831", + device_class = DeviceClass.Screen, + username = username, + password = password + }; + + private static TokenConnectData LinkedRefreshRequest() => new() + { + grant_type = "refresh_token", + platform = Platforms.Steam, + platform_id = "76561198000000000", + device_id = "device-1", + client_id = "recroom", + ver = "20230831", + device_class = DeviceClass.Screen, + refresh_token = GuidFromAccount(17) + }; + + private static string GuidFromAccount(long accountId) + { + var bytes = new byte[16]; + Array.Copy(BitConverter.GetBytes(accountId), 0, bytes, 0, 8); + return new Guid(bytes).ToString(); + } + + private static async Task PanelAction(RecDBContext db, long playerId, string kind) + { + var context = new DefaultHttpContext(); + var api = new AdminPanelAPI(db) { ControllerContext = new ControllerContext { HttpContext = context } }; + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try { return await api.PlayerPanelAction(playerId, kind, Secret); } + finally { DBConfig.AdminApiKey = original; } + } + + + [Fact] + public void TheLoginErrorIsExactlyTheSpecifiedMessageIncludingTheAtSign() + { + Assert.Equal(ExactMessage, AccountLock.LoginErrorMessage("GreatEagle")); + Assert.Equal(ExactMessage, AccountLock.LoginErrorMessage("@GreatEagle")); + Assert.Equal(AccountLock.ErrorCode, "ACCOUNT_PERMALOCKED"); + Assert.Equal("https://support.radicade.app", AccountLock.SupportUrl); + } + + [Fact] + public void TheLoginErrorHasNoTitleOrPrefixAndNeverShowsTheInternalBanReason() + { + var message = AccountLock.LoginErrorMessage("GreatEagle"); + + Assert.DoesNotContain("Login", message); + Assert.DoesNotContain("Error:", message); + Assert.DoesNotContain("Login:", message); + Assert.DoesNotContain("Account lock.", message); + Assert.DoesNotContain("Please submit a ticket", message); + Assert.StartsWith("@GreatEagle: This account has been locked", message); + Assert.EndsWith("Contact us at https://support.radicade.app.", message); + } + + [Fact] + public void TheLoginErrorFallsBackSecurelyWhenTheUsernameCannotBeResolved() + { + foreach (var blank in new[] { null, "", " ", "@" }) + { + var message = AccountLock.LoginErrorMessage(blank); + Assert.Equal( + "@Account: This account has been locked for security reasons. Contact us at https://support.radicade.app.", + message); + } + } + + [Fact] + public void TheBanReasonIsTheExactModeratorWrappedStringAndIsNotTheLoginError() + { + Assert.StartsWith("Moderator (", AccountLock.Reason); + Assert.EndsWith("https://support.radicade.app/new/account-support/)", AccountLock.Reason); + Assert.Equal(ExactReason, AccountLock.Reason); + + var login = AccountLock.LoginErrorMessage("GreatEagle"); + Assert.DoesNotContain(AccountLock.Reason, login); + Assert.DoesNotContain("Moderator", login); + Assert.False(login.Contains(AccountLock.Reason, StringComparison.Ordinal)); + } + + + [Fact] + public async Task ApplyingALockPersistsThePermanentModeratorBan() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + + var result = await AccountLock.ApplyAsync(db, 17, "StaffOne", "RAD-42"); + + Assert.True(result.Success); + Assert.True(result.Changed); + + var ban = await db.Moderation.SingleAsync(x => x.PlayerId == 17); + Assert.True(ban.IsBan); + Assert.Equal(ReportCategory.Moderator, ban.Category); + Assert.Equal(ExactReason, ban.Message); + Assert.Equal(AccountLock.BanMethod, ban.BanMethod); + Assert.Equal(DateTime.MaxValue, ban.ExpireDuration); + Assert.Equal("RAD-42", ban.ModeratorNote); + Assert.True(await AccountLock.IsLockedAsync(db, 17)); + } + + [Fact] + public async Task ApplyingALockTwiceIsIdempotent() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + var second = await AccountLock.ApplyAsync(db, 17, "StaffTwo", null); + + Assert.True(second.Success); + Assert.False(second.Changed); + Assert.Single(await db.Moderation.Where(x => x.PlayerId == 17).ToListAsync()); + Assert.True(await AccountLock.IsLockedAsync(db, 17)); + } + + [Fact] + public async Task UnlockingRemovesOnlyTheAccountLock() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + var result = await AccountLock.RemoveAsync(db, 17, "StaffOne", "RAD-42"); + + Assert.True(result.Success); + Assert.Empty(await db.Moderation.Where(x => x.PlayerId == 17).ToListAsync()); + Assert.False(await AccountLock.IsLockedAsync(db, 17)); + } + + [Fact] + public async Task UnlockingRefusesToRemoveAnUnrelatedBan() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + db.Moderation.Add(new ModerationDB + { + PlayerId = 17, + IsBan = true, + Category = ReportCategory.CoC_Trolling, + Message = "trolling", + BanMethod = "Admin Panel", + CreatedAt = DateTime.UtcNow, + ExpireDuration = DateTime.UtcNow.AddDays(3) + }); + await db.SaveChangesAsync(); + + var result = await AccountLock.RemoveAsync(db, 17, "StaffOne", null); + + Assert.False(result.Success); + var ban = await db.Moderation.SingleAsync(x => x.PlayerId == 17); + Assert.Equal("Admin Panel", ban.BanMethod); + Assert.Equal("trolling", ban.Message); + } + + [Fact] + public async Task LockStateFailsClosedWhenItCannotBeRead() + { + var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + await db.DisposeAsync(); + + Assert.True(await AccountLock.IsLockedAsync(db, 17)); + } + + + [Fact] + public async Task ANormalAccountCanStillLogIn() + { + var oldKey = DBConfig.JwtKey; + DBConfig.JwtKey = JwtKey; + try + { + AuthSecurityMonitor.Reset(); + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + + var response = Assert.IsType(Assert.IsType( + (await CreateApi(db).ConnectWithToken(PasswordRequest("GreatEagle", "pw123456"))).Result).Value); + + Assert.True(string.IsNullOrEmpty(response.error)); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + } + finally { DBConfig.JwtKey = oldKey; } + } + + [Fact] + public async Task ALockedAccountCannotLogInWithCorrectCredentialsAndNoSessionIsIssued() + { + var oldKey = DBConfig.JwtKey; + DBConfig.JwtKey = JwtKey; + try + { + AuthSecurityMonitor.Reset(); + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + var response = Assert.IsType(Assert.IsType( + (await CreateApi(db).ConnectWithToken(PasswordRequest("GreatEagle", "pw123456"))).Result).Value); + + Assert.Equal("ACCOUNT_PERMALOCKED", response.error); + Assert.Equal(ExactMessage, response.error_description); + Assert.True(string.IsNullOrEmpty(response.access_token)); + Assert.DoesNotContain(await db.CachedLogins.ToListAsync(), x => x.accountId == 17); + } + finally { DBConfig.JwtKey = oldKey; } + } + + [Fact] + public async Task ALockedAccountCanStillSignInThroughAnIdentityThatIsAlreadyLinked() + { + var oldKey = DBConfig.JwtKey; + DBConfig.JwtKey = JwtKey; + try + { + AuthSecurityMonitor.Reset(); + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + db.CachedLogins.Add(new CachedLogin + { + accountId = 17, + platform = Platforms.Steam, + platformId = "76561198000000000", + lastLoginTime = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + var response = Assert.IsType(Assert.IsType( + (await CreateApi(db).ConnectWithToken(LinkedRefreshRequest())).Result).Value); + + Assert.True(string.IsNullOrEmpty(response.error), response.error); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + } + finally { DBConfig.JwtKey = oldKey; } + } + + [Fact] + public async Task ALockedAccountIsRefusedAgainOnceTheLinkedIdentityIsDeleted() + { + var oldKey = DBConfig.JwtKey; + DBConfig.JwtKey = JwtKey; + try + { + AuthSecurityMonitor.Reset(); + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + db.CachedLogins.Add(new CachedLogin + { + accountId = 17, + platform = Platforms.Steam, + platformId = "76561198000000000", + lastLoginTime = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + db.CachedLogins.RemoveRange(db.CachedLogins.Where(x => x.accountId == 17)); + await db.SaveChangesAsync(); + + for (var attempt = 0; attempt < 2; attempt++) + { + var response = Assert.IsType(Assert.IsType( + (await CreateApi(db).ConnectWithToken(LinkedRefreshRequest())).Result).Value); + + Assert.Equal("ACCOUNT_PERMALOCKED", response.error); + Assert.Equal(ExactMessage, response.error_description); + Assert.True(string.IsNullOrEmpty(response.access_token)); + } + } + finally { DBConfig.JwtKey = oldKey; } + } + + [Fact] + public async Task ALockedAccountCanStillUseItsPasswordOnALinkedIdentityButNotAFreshOne() + { + var oldKey = DBConfig.JwtKey; + DBConfig.JwtKey = JwtKey; + try + { + AuthSecurityMonitor.Reset(); + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + db.CachedLogins.Add(new CachedLogin + { + accountId = 17, + platform = Platforms.Standalone, + platformId = "radicade-quest:device-1", + lastLoginTime = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + var linked = Assert.IsType(Assert.IsType( + (await CreateApi(db).ConnectWithToken(PasswordRequest("GreatEagle", "pw123456"))).Result).Value); + Assert.True(string.IsNullOrEmpty(linked.error), linked.error); + Assert.False(string.IsNullOrWhiteSpace(linked.access_token)); + + var fresh = PasswordRequest("GreatEagle", "pw123456"); + fresh.device_id = "device-2"; + var refused = Assert.IsType(Assert.IsType( + (await CreateApi(db).ConnectWithToken(fresh)).Result).Value); + Assert.Equal("ACCOUNT_PERMALOCKED", refused.error); + Assert.Equal(ExactMessage, refused.error_description); + Assert.True(string.IsNullOrEmpty(refused.access_token)); + Assert.DoesNotContain(await db.CachedLogins.ToListAsync(), x => x.platformId == "radicade-quest:device-2"); + } + finally { DBConfig.JwtKey = oldKey; } + } + + [Fact] + public async Task TheLockSurvivesAServerRestart() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + await using (var first = NewDb(options)) + { + await SeedAccountAsync(first, 17, "GreatEagle", "pw123456"); + await AccountLock.ApplyAsync(first, 17, "StaffOne", null); + } + + await using var restarted = NewDb(options); + Assert.True(await AccountLock.IsLockedAsync(restarted, 17)); + + var lockBan = await AccountLock.FindActiveLockAsync(restarted, 17); + Assert.NotNull(lockBan); + Assert.Equal(ReportCategory.Moderator, lockBan!.Category); + Assert.Equal(ExactReason, lockBan.Message); + Assert.Equal(DateTime.MaxValue, lockBan.ExpireDuration); + } + + [Fact] + public async Task GenerateTokenNeverMintsATokenForALockedAccount() + { + var oldKey = DBConfig.JwtKey; + DBConfig.JwtKey = JwtKey; + try + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + var generate = typeof(AuthAPI).GetMethod("GenerateToken", + BindingFlags.NonPublic | BindingFlags.Static)!; + var result = await (Task)generate.Invoke(null, new object[] + { + db, 17L, "20230831", DeviceClass.Screen, Platforms.Steam, "studio:17" + })!; + + Assert.False(result.success); + Assert.Equal("ACCOUNT_PERMALOCKED", result.error); + Assert.Null(result.value); + } + finally { DBConfig.JwtKey = oldKey; } + } + + + [Fact] + public async Task TheLockIsARealBanForTheInGameGatesAndCarriesTheExactReason() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + var lockBan = await db.Moderation.SingleAsync(x => x.PlayerId == 17); + Assert.True(lockBan.IsBan); + Assert.Equal(ReportCategory.Moderator, lockBan.Category); + Assert.Equal(ExactReason, lockBan.Message); + Assert.Equal(DateTime.MaxValue, lockBan.ExpireDuration); + Assert.NotNull(await AccountLock.FindActiveLockAsync(db, 17)); + + Assert.True(await db.IsBanned(17)); + var ban = await db.ResolveBanAsync(17); + Assert.NotNull(ban); + Assert.Equal(ReportCategory.Moderator, ban!.Category); + Assert.Equal(ExactReason, ban.Reason); + Assert.True(ban.IsPermanent); + + var details = PlayerUtils.BuildBlockDetails(true, ban.Category, ban.Reason, ban.ExpiryUtc, startedAtUtc: ban.StartedAtUtc); + Assert.Equal(ReportCategory.Moderator, details.ReportCategory); + Assert.Equal(ExactReason, details.Message); + Assert.Equal("Moderation", details.BanType); + Assert.DoesNotContain("locked for security reasons", details.Message); + } + + [Fact] + public async Task LockingAnOnlinePlayerLandsAsABanEvenIfTheDormMoveCannotRun() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + + var presence = await db.Presences.SingleAsync(x => x.playerId == 17); + presence.isOnline = true; + presence.roomInstance = 999999; + await db.SaveChangesAsync(); + + var result = await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + Assert.True(result.Success); + Assert.True(await AccountLock.IsLockedAsync(db, 17)); + Assert.Single(await db.Moderation.Where(x => x.PlayerId == 17).ToListAsync()); + Assert.True(await db.IsBanned(17)); + + var ban = await db.ResolveBanAsync(17); + Assert.NotNull(ban); + Assert.Equal(ReportCategory.Moderator, ban!.Category); + Assert.Equal(ExactReason, ban.Reason); + Assert.True(ban.IsPermanent); + } + + + [Fact] + public async Task ALockCannotBeAppealedThroughTheAppealEndpoints() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + await AccountLock.ApplyAsync(db, 17, "StaffOne", null); + + var context = new DefaultHttpContext + { + User = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity( + new[] { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "17") }, + "test")) + }; + var appeal = new RadicadeServer.API.Moderation.BanAppealAPI(db) + { + ControllerContext = new ControllerContext { HttpContext = context } + }; + var submitApi = new RadicadeServer.API.Moderation.AppealAPI(db) + { + ControllerContext = new ControllerContext { HttpContext = context } + }; + + var generated = await appeal.GenerateCode(); + var payload = Assert.IsAssignableFrom(generated.Result).Value!; + var payloadType = payload.GetType(); + Assert.False((bool)payloadType.GetProperty("success")!.GetValue(payload)!); + Assert.Equal(AccountLock.NotAppealableMessage, payloadType.GetProperty("error")!.GetValue(payload) as string); + Assert.Null((await db.Moderation.SingleAsync(x => x.PlayerId == 17)).AppealCode); + + var lockBan = await db.Moderation.SingleAsync(x => x.PlayerId == 17); + lockBan.AppealCode = "LOCKCODE"; + await db.SaveChangesAsync(); + + var submitted = await submitApi.Submit(new RadicadeServer.API.Moderation.SubmitAppealRequest + { + Code = "LOCKCODE", + Text = "please let me back in, this is a genuine appeal" + }); + + var refused = Assert.IsType(submitted.Result); + Assert.Equal(AccountLock.NotAppealableMessage, Assert.IsType(refused.Value).Message); + Assert.Null((await db.Moderation.SingleAsync(x => x.PlayerId == 17)).AppealText); + } + + [Fact] + public void TheAppealCooldownTextIsTheAgreedWording() + { + const string expectedPrefix = "You cannot file another ban appeal for: "; + + Assert.Equal(expectedPrefix, PlayerUtils.AppealCooldownPrefix); + Assert.Equal(expectedPrefix + "29 days, 3 hours", + PlayerUtils.AppealCooldownPrefix + PlayerUtils.FormatAppealCooldown(TimeSpan.FromDays(29) + TimeSpan.FromHours(3))); + } + + + [Fact] + public void RepeatedFailuresAlertAndSlowButNeverImposeMoreThanTheCap() + { + AuthSecurityMonitor.Reset(); + var count = 0; + for (var i = 0; i < AuthSecurityMonitor.AccountFailureAlertThreshold; i++) + count = AuthSecurityMonitor.RecordFailedLogin(7, "203.0.113.9"); + + Assert.Equal(AuthSecurityMonitor.AccountFailureAlertThreshold, count); + Assert.Equal(0, AuthSecurityMonitor.ProgressiveDelayMs(3)); + Assert.True(AuthSecurityMonitor.ProgressiveDelayMs(count) > 0); + Assert.True(AuthSecurityMonitor.ProgressiveDelayMs(10_000) <= AuthSecurityMonitor.MaxProgressiveDelayMs); + } + + [Fact] + public void SprayingAcrossManyAccountsFromOneSourceIsDetected() + { + AuthSecurityMonitor.Reset(); + for (long account = 100; account < 100 + AuthSecurityMonitor.SourceSprayAccountThreshold; account++) + AuthSecurityMonitor.RecordFailedLogin(account, "198.51.100.4"); + + Assert.True(AuthSecurityMonitor.IsSourceThrottled("198.51.100.4")); + } + + [Fact] + public void RateLimitingCannotBeAbusedToLockOutAnotherPlayer() + { + AuthSecurityMonitor.Reset(); + + for (var i = 0; i < 60; i++) AuthSecurityMonitor.RecordFailedLogin(42, "198.51.100.7"); + Assert.False(AuthSecurityMonitor.IsSourceThrottled("198.51.100.7")); + } + + [Fact] + public async Task SecuritySignalsAloneNeverCreateALock() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 17, "GreatEagle", "pw123456"); + + AuthSecurityMonitor.Reset(); + for (var i = 0; i < 50; i++) AuthSecurityMonitor.RecordFailedLogin(17, "198.51.100.7"); + AuthSecurityMonitor.RecordSuccessfulLogin(17, "203.0.113.55", newDevice: true, newIp: true); + AuthSecurityMonitor.NoteCredentialChange(17, "password changed"); + + Assert.Empty(await db.Moderation.ToListAsync()); + Assert.False(await AccountLock.IsLockedAsync(db, 17)); + } + + + [Fact] + public async Task StaffCanLockAndUnlockThroughThePanel() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 2, "GreatEagle", "pw123456"); + + Assert.IsType(await PanelAction(db, 2, "lock")); + + var ban = await db.Moderation.SingleAsync(x => x.PlayerId == 2); + Assert.Equal(ReportCategory.Moderator, ban.Category); + Assert.Equal(AccountLock.BanMethod, ban.BanMethod); + Assert.Equal(ExactReason, ban.Message); + Assert.Equal(DateTime.MaxValue, ban.ExpireDuration); + + Assert.IsType(await PanelAction(db, 2, "unlock")); + Assert.Empty(await db.Moderation.Where(x => x.PlayerId == 2).ToListAsync()); + } + + [Fact] + public async Task UnlockingThroughThePanelRefusesAndPreservesAnUnrelatedBan() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 2, "GreatEagle", "pw123456"); + db.Moderation.Add(new ModerationDB + { + PlayerId = 2, + IsBan = true, + Category = ReportCategory.CoC_Trolling, + Message = "trolling", + BanMethod = "Admin Panel", + CreatedAt = DateTime.UtcNow, + ExpireDuration = DateTime.UtcNow.AddDays(3) + }); + await db.SaveChangesAsync(); + + Assert.IsType(await PanelAction(db, 2, "unlock")); + Assert.Equal("Admin Panel", (await db.Moderation.SingleAsync(x => x.PlayerId == 2)).BanMethod); + } + + [Fact] + public async Task UnauthorizedStaffCannotLock() + { + await using var db = NewDb(); + await SeedAccountAsync(db, 2, "GreatEagle", "pw123456"); + + var context = new DefaultHttpContext(); + var api = new AdminPanelAPI(db) { ControllerContext = new ControllerContext { HttpContext = context } }; + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + var result = await api.PlayerPanelAction(2, "lock", "not-the-key"); + Assert.IsNotType(result); + Assert.Empty(await db.Moderation.ToListAsync()); + } + finally { DBConfig.AdminApiKey = original; } + } +} diff --git a/ServerTests/AccountOnboardingSettingsTests.cs b/ServerTests/AccountOnboardingSettingsTests.cs new file mode 100644 index 0000000..b959104 --- /dev/null +++ b/ServerTests/AccountOnboardingSettingsTests.cs @@ -0,0 +1,234 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer; +using RadicadeServer.API.Config; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using Xunit; + +namespace RadicadeServer.Tests; + +public class AccountOnboardingSettingsTests +{ + // Every key the 2023 title screen reads to decide which page of its own new-user flow to open. + // They all have to be written, not just the ones this server checks itself: a key the client looks + // for and cannot find makes it resume at a page it believes it already showed, and a true one skips + // that page outright. This is the whole reason a brand-new account opens on "Welcome to Rec Room!". + private static readonly string[] AccountCreationKeys = + [ + "Recroom.AccountCreation.HasStarted", + "Recroom.AccountCreation.HasChosenUsername", + "Recroom.AccountCreation.HasCreatedPassword", + "Recroom.AccountCreation.HasFinished", + "Recroom.AccountCreation.FinishedAvatarCustomization", + "Recroom.AccountCreation.AcceptedCodeOfConduct", + "Recroom.AccountCreation.HasImportedFriends" + ]; + + [Fact] + public void NewAccountSettings_LeaveOnboardingIncompleteAndRequireOrientation() + { + var settings = JsonSerializer.Deserialize>(DBConfig.NewAccountSettings2023)!; + string Value(string key) => settings.Single(setting => setting.Key == key).Value; + + foreach (var key in AccountCreationKeys) + Assert.Equal("False", Value(key)); + + Assert.Equal("0", Value("Recroom.OOBE")); + Assert.Equal("1", Value("Radicade.NeedsOrientation")); + Assert.DoesNotContain(settings, setting => setting.Key == "HAS_COMPLETED_ORIENTATION"); + } + + [Fact] + public void AccountCreationInProgressSettings_ResumeAtThePageAfterWhatThePlayerSubmitted() + { + var settings = JsonSerializer.Deserialize>( + DBConfig.AccountCreationInProgressSettings2023(usernameChosen: true, passwordCreated: true))!; + string Value(string key) => settings.Single(setting => setting.Key == key).Value; + + // An account the flow itself created: it started, the name and password are in, and the pages + // the player has not reached yet (avatar, Code of Conduct, finished) are still open. + Assert.Equal("True", Value("Recroom.AccountCreation.HasStarted")); + Assert.Equal("True", Value("Recroom.AccountCreation.HasChosenUsername")); + Assert.Equal("True", Value("Recroom.AccountCreation.HasCreatedPassword")); + Assert.Equal("False", Value("Recroom.AccountCreation.HasFinished")); + Assert.Equal("False", Value("Recroom.AccountCreation.FinishedAvatarCustomization")); + Assert.Equal("False", Value("Recroom.AccountCreation.AcceptedCodeOfConduct")); + Assert.Equal("1", Value("Radicade.NeedsOrientation")); + + // A create that carried neither: the client still has to ask for both. + var bare = JsonSerializer.Deserialize>( + DBConfig.AccountCreationInProgressSettings2023(usernameChosen: false, passwordCreated: false))!; + Assert.Equal("False", bare.Single(setting => setting.Key == "Recroom.AccountCreation.HasChosenUsername").Value); + Assert.Equal("False", bare.Single(setting => setting.Key == "Recroom.AccountCreation.HasCreatedPassword").Value); + } + + [Fact] + public void Regular2023DefaultSettings_DoNotForceNewAccountOnboarding() + { + var settings = JsonSerializer.Deserialize>(DBConfig.DefaultSettings2023)!; + + Assert.DoesNotContain(settings, setting => setting.Key.StartsWith("Recroom.AccountCreation.", StringComparison.Ordinal)); + Assert.DoesNotContain(settings, setting => setting.Key == "Radicade.NeedsOrientation"); + Assert.Contains(settings, setting => setting.Key == "HAS_COMPLETED_ORIENTATION" && setting.Value == "True"); + } + + [Fact] + public async Task CannotMarkOnboardingFinishedUntilPasswordBirthdayAndCocAreSaved() + { + await using var db = CreateDb(); + db.Profiles.Add(new Profile { accountId = 2, username = "NewUser", displayName = "NewUser", profileImage = "default" }); + db.Players.Add(new PlayerData { AccountId = 2, Hashword = "hashed", Birthday = new DateTime(2000, 1, 1) }); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = 2, + Settings = "[{\"Key\":\"Recroom.AccountCreation.HasChosenUsername\",\"Value\":\"True\"}]" + }); + await db.SaveChangesAsync(); + var api = CreateApi(db); + + await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" }); + + var saved = (await db.PlayerSettings.SingleAsync()).DeserializeSettings(); + Assert.DoesNotContain(saved, setting => setting.Key == "Recroom.AccountCreation.HasFinished"); + Assert.DoesNotContain(saved, setting => setting.Key == "Radicade.NeedsOrientation"); + } + + [Fact] + public async Task CompletingOnboardingSetsOrientationMarkerOnlyAfterRequiredFieldsArePresent() + { + await using var db = CreateDb(); + db.Profiles.Add(new Profile { accountId = 2, username = "NewUser", displayName = "NewUser", profileImage = "default" }); + db.Players.Add(new PlayerData + { + AccountId = 2, + Hashword = "hashed", + Birthday = new DateTime(2000, 1, 1), + CodeOfConductAccepted = true + }); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = 2, + Settings = "[{\"Key\":\"Recroom.AccountCreation.HasChosenUsername\",\"Value\":\"True\"}]" + }); + await db.SaveChangesAsync(); + var api = CreateApi(db); + + await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" }); + + var saved = (await db.PlayerSettings.SingleAsync()).DeserializeSettings(); + Assert.Contains(saved, setting => setting.Key == "Recroom.AccountCreation.HasFinished" && setting.Value == "True"); + Assert.Contains(saved, setting => setting.Key == "Radicade.NeedsOrientation" && setting.Value == "1"); + } + + /// + /// The flow records the acceptance as a setting on this build's own Code of Conduct page; the + /// account's account/me/acceptcoc route is never called. Before this, an account whose + /// player had walked that page still answered hasAcceptedCodeOfConduct = false - so the + /// client offered the page again on the next boot, and the finish marker was refused over a flag + /// the client's own pages never write. + /// + [Fact] + public async Task TheConductPageRecordsTheAcceptanceOnTheAccountAndUnblocksTheFinishMarker() + { + await using var db = CreateDb(); + db.Profiles.Add(new Profile { accountId = 2, username = "NewUser", displayName = "NewUser", profileImage = "default" }); + db.Players.Add(new PlayerData { AccountId = 2, Hashword = "hashed", Birthday = new DateTime(2000, 1, 1) }); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = 2, + Settings = "[{\"Key\":\"Recroom.AccountCreation.HasChosenUsername\",\"Value\":\"True\"}]" + }); + await db.SaveChangesAsync(); + var api = CreateApi(db); + + await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.AcceptedCodeOfConduct", Value = "True" }); + + Assert.True((await db.Players.AsNoTracking().SingleAsync()).CodeOfConductAccepted); + + // And that is now enough for the flow to be able to finish. + await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" }); + + var saved = (await db.PlayerSettings.SingleAsync()).DeserializeSettings(); + Assert.Contains(saved, setting => setting.Key == "Recroom.AccountCreation.HasFinished" && setting.Value == "True"); + Assert.Contains(saved, setting => setting.Key == "Radicade.NeedsOrientation" && setting.Value == "1"); + } + + /// + /// A birthday that never arrived must not hold the finish marker hostage. Every account the flow + /// has created on this server still reads 0001-01-01 - its age page has never handed this server + /// one - so requiring it meant no player could ever leave the flow, and the title screen kept + /// opening on "Welcome back! - Continue creating account" for an account whose every other page + /// was done. + /// + [Fact] + public async Task TheFinishMarkerIsRecordedForAnAccountWhoseAgePageNeverArrived() + { + await using var db = CreateDb(); + db.Profiles.Add(new Profile { accountId = 2, username = "NewUser", displayName = "NewUser", profileImage = "default" }); + db.Players.Add(new PlayerData { AccountId = 2, Hashword = "hashed", Birthday = null }); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = 2, + Settings = "[{\"Key\":\"Recroom.AccountCreation.HasChosenUsername\",\"Value\":\"True\"}]" + }); + await db.SaveChangesAsync(); + var api = CreateApi(db); + + await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.AcceptedCodeOfConduct", Value = "True" }); + await api.SetSetting(new Setting { Key = "Recroom.AccountCreation.HasFinished", Value = "True" }); + + var saved = (await db.PlayerSettings.SingleAsync()).DeserializeSettings(); + Assert.Contains(saved, setting => setting.Key == "Recroom.AccountCreation.HasFinished" && setting.Value == "True"); + + // Still owed, so the account keeps asking for it until the age page hands one over. + Assert.Null((await db.Players.AsNoTracking().SingleAsync()).Birthday); + } + + [Theory] + [InlineData("")] + [InlineData("hashed-password")] + public async Task SettingsDoNotSkipAnUnfinishedPasswordPage(string passwordHash) + { + await using var db = CreateDb(); + db.Players.Add(new PlayerData { AccountId = 2, Hashword = passwordHash }); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = 2, + Settings = "[{\"Key\":\"Recroom.AccountCreation.HasCreatedPassword\",\"Value\":\"True\"}," + + "{\"Key\":\"Recroom.AccountCreation.HasFinished\",\"Value\":\"True\"}," + + "{\"Key\":\"Recroom.OOBE\",\"Value\":\"100\"}]" + }); + await db.SaveChangesAsync(); + var settings = CreateApi(db).GetSettings(); + var expected = passwordHash.Length == 0 ? "False" : "True"; + Assert.Equal(expected, settings.Single(x => x.Key == "Recroom.AccountCreation.HasCreatedPassword").Value); + Assert.Equal(expected, settings.Single(x => x.Key == "Recroom.AccountCreation.HasFinished").Value); + Assert.Equal(passwordHash.Length == 0 ? "0" : "100", settings.Single(x => x.Key == "Recroom.OOBE").Value); + } + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new RecDBContext(options); + } + + private static PlayerSettingsAPI CreateApi(RecDBContext db) + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, "2")], "test")) + }; + return new PlayerSettingsAPI(db) + { + ControllerContext = new ControllerContext { HttpContext = context } + }; + } +} diff --git a/ServerTests/AccountRegistrationTests.cs b/ServerTests/AccountRegistrationTests.cs new file mode 100644 index 0000000..f41ba3d --- /dev/null +++ b/ServerTests/AccountRegistrationTests.cs @@ -0,0 +1,1051 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Storage; +using RadicadeServer.API.Accounts; +using RadicadeServer.Auth; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Auth.Connection; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The registration contract: what a new account is made of, what gets refused, and what a refusal +/// is allowed to leave behind. The onboarding-flow tests next door cover how the client drives the +/// flow; these cover the account the server hands it. +/// +[Collection(DbFactoryCollection.Name)] +public class AccountRegistrationTests +{ + private const string PlatformId = "76561198000000042"; + private const string DeviceId = "registration-device"; + private const string ChosenName = "BrassLantern7412"; + private const string ChosenPassword = "correct-horse-battery"; + + /// The seeded server's own row (see ) is never a player account. + private const long SystemAccountId = 1; + + public AccountRegistrationTests() => AllowEveryRegistration(); + + // --------------------------------------------------------------------------------------- + // What a successful registration produces + // --------------------------------------------------------------------------------------- + + [Fact] + public async Task ANewAccountGetsEveryRecordItNeedsToBePlayable() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + await SeedSystemAccountAsync(db); + var created = await PlayerUtils.CreateAccount(Registration(), clientIp: "203.0.113.7"); + var profile = Assert.IsType(created.value); + Assert.True(created.success, created.error); + + // Profile: the identity every other route reads. + var savedProfile = await db.Profiles.SingleAsync(x => x.accountId == profile.accountId); + Assert.Equal(profile.accountId, savedProfile.accountId); + Assert.Equal(ChosenName, savedProfile.username); + Assert.Equal(ChosenName, savedProfile.displayName); + Assert.Equal("DefaultProfileImage", savedProfile.profileImage); + Assert.Equal(PlayerUtils.PlatformMask(Platforms.Steam), savedProfile.platforms); + Assert.False(savedProfile.isJunior); + Assert.NotEqual(default, savedProfile.createdAt); + + // Player: credentials plus the identity later boots are matched on. The device id lands + // trimmed, because every lookup compares it against a trimmed value. + var player = await db.Players.SingleAsync(); + Assert.Equal(savedProfile.accountId, player.AccountId); + Assert.Equal(DeviceId, player.DeviceId); + Assert.Equal("203.0.113.7", player.LastIpAddress); + Assert.Equal(DeviceClass.Screen, player.DeviceClass); + Assert.Equal(new DateTime(1998, 4, 30), player.Birthday); + Assert.False(player.IsJunior); + Assert.Equal(JuniorState.BasedOnBirthday, player.JuniorState); + Assert.False(string.IsNullOrWhiteSpace(player.Hashword)); + var hasher = new PasswordHasher(); + Assert.NotEqual(PasswordVerificationResult.Failed, + hasher.VerifyHashedPassword(player, player.Hashword, ChosenPassword)); + Assert.Equal(DBConfig.GameVersion, player.AppVersion); + Assert.Equal(99, player.NameChanges); + + // Everything else the game reads on the first frame. + var avatar = await db.Avatars.SingleAsync(); + Assert.Equal(savedProfile.accountId, avatar.AccountId); + Assert.False(string.IsNullOrWhiteSpace(avatar.Avatar)); + Assert.Equal("[]", avatar.Outfits); + + var unlocks = await db.Unlocks.SingleAsync(); + Assert.Equal(savedProfile.accountId, unlocks.AccountId); + Assert.False(string.IsNullOrWhiteSpace(unlocks.AvatarItems)); + + var progression = await db.Progression.SingleAsync(); + Assert.Equal(savedProfile.accountId, progression.PlayerId); + Assert.Equal(0, progression.XP); + Assert.Equal(1, progression.Level); + + var presence = await db.Presences.SingleAsync(); + Assert.Equal(savedProfile.accountId, presence.playerId); + Assert.Equal(DeviceClass.Screen, presence.deviceClass); + + Assert.Equal(savedProfile.accountId, (await db.Currency.SingleAsync()).AccountId); + Assert.Equal(savedProfile.accountId, (await db.Reputations.SingleAsync()).AccountId); + Assert.NotNull(await db.PlayerSettings.SingleAsync()); + + // The link that makes the next boot resolve to this account instead of to the sign-up flow. + var login = await db.CachedLogins.SingleAsync(); + Assert.Equal(savedProfile.accountId, login.accountId); + Assert.Equal(Platforms.Steam, login.platform); + Assert.Equal(PlatformId, login.platformId); + + // And the account is immediately usable: this is the call the title screen makes. + var auth = CreateApi(db, PlatformId); + var loginRequest = Token(Registration(), "cached_login"); + loginRequest.account_id = savedProfile.accountId; + var response = Assert.IsType( + Assert.IsType((await auth.ConnectWithToken(loginRequest)).Result).Value); + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + } + + [Fact] + public async Task ARegistrationWithoutANameStillGetsAUsableGeneratedOne() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + var registration = Registration(); + registration.username = null; + + var created = await PlayerUtils.CreateAccount(registration); + var profile = Assert.IsType(created.value); + + Assert.True(created.success, created.error); + Assert.Null(UsernameRules.Rejection(profile.username)); + Assert.False(string.IsNullOrEmpty(profile.username)); + } + + [Fact] + public async Task AnEmailIsKeptOnceAndNeverAttachedToASecondAccount() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + + var first = Registration(); + first.username = "FirstComer"; + first.email = "someone@example.com"; + var firstAccount = Assert.IsType((await PlayerUtils.CreateAccount(first)).value); + + var second = Registration(); + second.username = "SecondComer"; + second.email = "someone@example.com"; + var secondAccount = Assert.IsType((await PlayerUtils.CreateAccount(second)).value); + + Assert.Equal("someone@example.com", (await db.Players.SingleAsync(x => x.AccountId == firstAccount.accountId)).Email); + // Still registered, but the address is not attached twice. + Assert.Equal("", (await db.Players.SingleAsync(x => x.AccountId == secondAccount.accountId)).Email); + } + + [Fact] + public async Task AYoungBirthdayMakesAJuniorAccount() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + var registration = Registration(); + registration.username = "YoungerPlayer"; + registration.birthday = DateTime.UtcNow.AddYears(-9); + + var created = await PlayerUtils.CreateAccount(registration); + var profile = Assert.IsType(created.value); + + Assert.True(created.success, created.error); + Assert.True(profile.isJunior); + Assert.True((await db.Players.SingleAsync()).IsJunior); + Assert.True(PlayerUtils.BuildSelfAccount(profile, await db.Players.SingleAsync()).isJunior); + } + + [Fact] + public async Task ARegistrationWithoutABirthdayStoresNoneRatherThanAPlaceholderDate() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + var registration = Registration(); + registration.birthday = null; + registration.birthdayUTC = null; + + var created = await PlayerUtils.CreateAccount(registration); + var profile = Assert.IsType(created.value); + + Assert.True(created.success, created.error); + var player = await db.Players.SingleAsync(); + Assert.Null(player.Birthday); + Assert.False(player.IsJunior); + Assert.False(PlayerUtils.BuildSelfAccount(profile, player).hasBirthday); + } + + // --------------------------------------------------------------------------------------- + // What registration refuses + // --------------------------------------------------------------------------------------- + + [Theory] + [InlineData("ab", "at least")] // too short + [InlineData("AdventurousAlligator99999", "24")] // longer than the client itself can generate + [InlineData("Has Space", "letters and numbers")] + [InlineData("Not-Alphanumeric", "letters and numbers")] + [InlineData("Emoji🙂", "letters and numbers")] + [InlineData("Ünicode", "letters and numbers")] // non-ASCII letters are not letters here + [InlineData("123456", "at least one letter")] + [InlineData("system", "reserved")] + [InlineData("Official", "reserved")] + public async Task AnUnusableNameIsRefusedAndThePlayerIsNotSilentlyRenamed(string name, string expectedReason) + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + var registration = Registration(); + registration.username = name; + + var created = await PlayerUtils.CreateAccount(registration); + + Assert.False(created.success); + Assert.Contains(expectedReason, created.error, StringComparison.OrdinalIgnoreCase); + // A refusal costs no rows: not even a profile with nothing behind it. + Assert.Empty(await db.Profiles.ToListAsync()); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + } + + [Fact] + public async Task ADuplicateNameIsRefusedWhateverItsCase() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + db.Profiles.Add(new Profile + { + accountId = 4, + username = "PandaParty", + displayName = "PandaParty", + profileImage = "default", + createdAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + + var registration = Registration(); + registration.username = "pandaparty"; + + var created = await PlayerUtils.CreateAccount(registration); + + Assert.False(created.success); + Assert.Contains("already taken", created.error, StringComparison.OrdinalIgnoreCase); + Assert.Single(await db.Profiles.ToListAsync()); + Assert.Empty(await db.Players.ToListAsync()); + } + + [Theory] + [InlineData("", DeviceId, ChosenPassword, null)] // no platform id + [InlineData(PlatformId, "", ChosenPassword, null)] // no device id + [InlineData("76561198000000042; DROP TABLE Players--", DeviceId, ChosenPassword, null)] // crafted platform id + [InlineData(PlatformId, "device with spaces", ChosenPassword, null)] // crafted device id + [InlineData(PlatformId, DeviceId, "short", null)] // password under the floor + [InlineData(PlatformId, DeviceId, ChosenPassword, "2999-01-01")] // birthday in the future + public async Task AMalformedRegistrationIsRefusedBeforeTheDatabaseIsTouched( + string platformId, string deviceId, string? password, string? birthday) + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + var registration = Registration(); + registration.username = "WouldBePlayer"; + registration.platformId = platformId; + registration.deviceId = deviceId; + registration.password = password; + if (birthday != null) + registration.birthday = DateTime.Parse(birthday, null, System.Globalization.DateTimeStyles.AdjustToUniversal); + + var created = await PlayerUtils.CreateAccount(registration); + + Assert.False(created.success); + Assert.False(string.IsNullOrWhiteSpace(created.error)); + Assert.Empty(await db.Profiles.ToListAsync()); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + } + + [Fact] + public async Task AnOversizedDeviceIdIsRefusedRatherThanStored() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + var registration = Registration(); + registration.username = "TooLongDevice"; + registration.deviceId = new string('a', 257); + + var created = await PlayerUtils.CreateAccount(registration); + + Assert.False(created.success); + Assert.Empty(await db.Players.ToListAsync()); + } + + // --------------------------------------------------------------------------------------- + // One identity, one account + // --------------------------------------------------------------------------------------- + + [Fact] + public async Task ARepeatBootGrantResumesTheAccountInsteadOfAddingASecond() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + await SeedSystemAccountAsync(db); + var boot = Token(Registration()); + + var first = Assert.IsType( + Assert.IsType((await CreateApi(db, PlatformId).ConnectWithToken(boot)).Result).Value); + Assert.True(string.IsNullOrEmpty(first.error), first.error_description); + + var second = Assert.IsType( + Assert.IsType((await CreateApi(db, PlatformId).ConnectWithToken(Token(Registration()))).Result).Value); + Assert.True(string.IsNullOrEmpty(second.error), second.error_description); + + Assert.Single(await db.Profiles.Where(x => x.accountId != SystemAccountId).ToListAsync()); + Assert.Single(await db.Players.ToListAsync()); + Assert.Single(await db.CachedLogins.ToListAsync()); + } + + [Fact] + public async Task SimultaneousBootGrantsForOneIdentityCreateExactlyOneAccount() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using (var seed = factory.CreateDbContext()) + await SeedSystemAccountAsync(seed); + + // Each call gets its own context and its own controller, exactly as concurrent requests do. + var apis = Enumerable.Range(0, 6).Select(_ => factory.CreateDbContext()).ToList(); + try + { + var responses = await Task.WhenAll(apis.Select( + db => CreateApi(db, PlatformId).ConnectWithToken(Token(Registration())))); + + foreach (var result in responses) + { + var response = Assert.IsType(Assert.IsType(result.Result).Value); + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + } + + await using var verify = factory.CreateDbContext(); + Assert.Single(await verify.Profiles.Where(x => x.accountId != SystemAccountId).ToListAsync()); + Assert.Single(await verify.Players.ToListAsync()); + Assert.Single(await verify.CachedLogins.ToListAsync()); + } + finally + { + foreach (var db in apis) await db.DisposeAsync(); + } + } + + [Fact] + public async Task TwoDifferentIdentitiesOnOneDeviceEachGetTheirOwnAccount() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + + var first = Registration(); + first.username = "FirstIdentity"; + first.platformId = "76561198000000001"; + var firstAccount = Assert.IsType((await PlayerUtils.CreateAccount(first)).value); + + // A different account on the same machine: allowed, and it does not steal the first one's rows. + var second = Registration(); + second.username = "SecondIdentity"; + second.platformId = "76561198000000002"; + var secondAccount = Assert.IsType((await PlayerUtils.CreateAccount(second)).value); + + Assert.NotEqual(firstAccount.accountId, secondAccount.accountId); + Assert.Equal(2, await db.Profiles.CountAsync()); + Assert.Equal(2, await db.Players.CountAsync()); + Assert.Equal(2, await db.CachedLogins.CountAsync()); + Assert.Single(await db.Avatars.Where(x => x.AccountId == secondAccount.accountId).ToListAsync()); + } + + /// + /// A name taken by one identity is not available to another, whichever of them asks second, and + /// the refusal leaves nothing behind. Note this is the *sequential* form of the rule: on the + /// in-memory provider these two calls do not actually interleave (every await completes + /// synchronously, so the first finishes before the second reads), which is why the gate has its + /// own test below instead of being inferred from this one. + /// + [Fact] + public async Task ASecondIdentityCannotTakeTheNameTheFirstOneAlreadyHas() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + // Two brand-new identities, one name between them. + var first = Registration(); + first.platformId = "76561198000000011"; + var second = Registration(); + second.platformId = "76561198000000012"; + + var results = await Task.WhenAll( + PlayerUtils.CreateAccount(first), + PlayerUtils.CreateAccount(second)); + + var winner = Assert.Single(results, x => x.success); + var loser = Assert.Single(results, x => !x.success); + Assert.Equal(ChosenName, Assert.IsType(winner.value).username); + Assert.Contains("already taken", loser.error, StringComparison.OrdinalIgnoreCase); + + await using var db = factory.CreateDbContext(); + Assert.Single(await db.Profiles.Where(x => x.username == ChosenName).ToListAsync()); + Assert.Single(await db.CachedLogins.ToListAsync()); + } + + [Fact] + public async Task ANewRegistrationLeavesAnExistingAccountAlone() + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + db.Profiles.Add(new Profile + { + accountId = 4, + username = "Established", + displayName = "Established", + profileImage = "default", + createdAt = DateTime.UtcNow.AddYears(-1) + }); + db.Players.Add(new PlayerData + { + AccountId = 4, + Hashword = "existing-hash", + Birthday = new DateTime(1990, 2, 2), + DeviceId = "someone-elses-device" + }); + await db.SaveChangesAsync(); + + var created = await PlayerUtils.CreateAccount(Registration()); + Assert.True(created.success, created.error); + + var untouched = await db.Players.SingleAsync(x => x.AccountId == 4); + Assert.Equal("existing-hash", untouched.Hashword); + Assert.Equal(new DateTime(1990, 2, 2), untouched.Birthday); + Assert.Equal("someone-elses-device", untouched.DeviceId); + Assert.Equal("Established", (await db.Profiles.SingleAsync(x => x.accountId == 4)).username); + Assert.Equal(2, await db.Profiles.CountAsync()); + Assert.Equal(2, await db.Players.CountAsync()); + } + + // --------------------------------------------------------------------------------------- + // Abuse control + // --------------------------------------------------------------------------------------- + + [Fact] + public async Task CreatingAccountsFasterThanADeviceShouldIsRefused() + { + RegistrationGuard.Reset(); + RegistrationGuard.Configure(maxCreationsPerIp: 1000, maxCreationsPerDevice: 1000, + minimumDeviceInterval: TimeSpan.FromMinutes(5)); + try + { + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + await SeedSystemAccountAsync(db); + var first = Assert.IsType( + Assert.IsType((await CreateApi(db, PlatformId).ConnectWithToken(Token(Registration()))).Result).Value); + Assert.True(string.IsNullOrEmpty(first.error), first.error_description); + + // The first account has finished onboarding, so it is not up for adoption by the next + // identity that boots on this machine. That leaves provisioning as the only way the + // second boot could get an account - which is exactly what the device gap has to stop. + var settingsRow = await db.PlayerSettings.FirstAsync(x => x.AccountId != SystemAccountId); + var values = settingsRow.DeserializeSettings(); + PlayerUtils.SetSettingFlag(values, "Recroom.AccountCreation.HasFinished", true); + settingsRow.Settings = values.ToJson(); + await db.SaveChangesAsync(); + + // Same device, brand-new identity: refused, and nothing is written for it. + var second = Registration(); + second.username = "ThrottledSecond"; + second.platformId = "76561198000000099"; + var secondResult = await CreateApi(db, second.platformId).ConnectWithToken(Token(second)); + var refusal = Assert.IsType(secondResult.Result); + Assert.Equal(400, refusal.StatusCode); + Assert.Contains("just created", Assert.IsType(refusal.Value).error_description, + StringComparison.OrdinalIgnoreCase); + + Assert.Single(await db.Profiles.Where(x => x.accountId != SystemAccountId).ToListAsync()); + Assert.Single(await db.CachedLogins.ToListAsync()); + + // Resuming is never throttled: the identity that already has an account can still get in. + var resume = Token(Registration(), "cached_login"); + resume.account_id = (await db.Profiles.SingleAsync(x => x.accountId != SystemAccountId)).accountId; + var resumed = Assert.IsType( + Assert.IsType((await CreateApi(db, PlatformId).ConnectWithToken(resume)).Result).Value); + Assert.True(string.IsNullOrEmpty(resumed.error), resumed.error_description); + Assert.False(string.IsNullOrWhiteSpace(resumed.access_token)); + } + finally + { + AllowEveryRegistration(); + } + } + + // --------------------------------------------------------------------------------------- + // The deployed configuration + // --------------------------------------------------------------------------------------- + + [Fact] + public async Task CreatingAnAccountSucceedsUnderARetryingExecutionStrategy() + { + // The production server runs SQL Server with EnableRetryOnFailure(3). A retrying execution + // strategy does not permit a hand-opened transaction - "does not support user-initiated + // transactions" - so account creation failed for every player at the boot grant, with + // "Nothing was saved", while these tests stayed green because they never enabled retrying. + // Same provider shape, real primary keys: this is the test that would have caught it. + using var harness = new SqliteHarness(failOnSaveCall: 0, retrying: true); + + var created = await PlayerUtils.CreateAccount(Registration()); + + Assert.True(created.success, created.error); + + await using var verify = harness.Factory.CreateDbContext(); + Assert.Single(await verify.Profiles.ToListAsync()); + Assert.Single(await verify.Players.ToListAsync()); + Assert.Single(await verify.CachedLogins.ToListAsync()); + } + + // --------------------------------------------------------------------------------------- + // Failure handling + // --------------------------------------------------------------------------------------- + + [Fact] + public async Task AWriteFailurePartWayThroughLeavesNoAccountBehind() + { + // A real relational provider, because this test is about the transaction: the profile row and + // every row that hangs off it have to commit together or not at all. The interceptor makes the + // second save fail, which is after the profile row has already been written. + using var harness = new SqliteHarness(failOnSaveCall: 2); + + var created = await PlayerUtils.CreateAccount(Registration()); + + Assert.False(created.success); + Assert.False(string.IsNullOrWhiteSpace(created.error)); + + await using var verify = harness.Factory.CreateDbContext(); + Assert.Empty(await verify.Profiles.ToListAsync()); + Assert.Empty(await verify.Players.ToListAsync()); + Assert.Empty(await verify.CachedLogins.ToListAsync()); + Assert.Empty(await verify.PlayerSettings.ToListAsync()); + Assert.Empty(await verify.Avatars.ToListAsync()); + Assert.Empty(await verify.Unlocks.ToListAsync()); + Assert.Empty(await verify.Progression.ToListAsync()); + } + + [Fact] + public async Task TheSameRegistrationSucceedsOnARelationalDatabase() + { + using var harness = new SqliteHarness(failOnSaveCall: 0); + + var created = await PlayerUtils.CreateAccount(Registration()); + var profile = Assert.IsType(created.value); + Assert.True(created.success, created.error); + + await using var verify = harness.Factory.CreateDbContext(); + Assert.Single(await verify.Profiles.ToListAsync()); + Assert.Single(await verify.Players.ToListAsync()); + Assert.Single(await verify.CachedLogins.ToListAsync()); + Assert.Equal(profile.accountId, (await verify.Players.SingleAsync()).AccountId); + } + + // --------------------------------------------------------------------------------------- + // Username rules on their own + // --------------------------------------------------------------------------------------- + + [Theory] + [InlineData("BrilliantZebra5750")] + [InlineData("abc")] + [InlineData("AdventurousAlligator9999")] + [InlineData("@ANameWithAt")] + [InlineData(" PaddedName ")] + public void UsableNamesAreUsable(string name) + { + var parsed = UsernameRules.Parse(name, out var rejection); + Assert.Null(rejection); + Assert.NotNull(parsed); + Assert.InRange(parsed!.Length, UsernameRules.MinLength, UsernameRules.MaxLength); + } + + [Theory] + [InlineData("ab")] + [InlineData("AdventurousAlligator99999")] + [InlineData("two words")] + [InlineData("under_score")] + [InlineData("emoji😀")] + [InlineData("1234")] + [InlineData("admin")] + public void UnusableNamesAreReportedNotTrimmed(string name) + { + var parsed = UsernameRules.Parse(name, out var rejection); + Assert.Null(parsed); + Assert.False(string.IsNullOrWhiteSpace(rejection)); + } + + [Fact] + public void AnAbsentNameIsNotAnInvalidName() + { + Assert.Null(UsernameRules.Parse(null, out var missing)); + Assert.Null(missing); + Assert.Null(UsernameRules.Parse(" ", out missing)); + Assert.Null(missing); + } + + /// + /// The name gate has to do one thing: two callers holding the same name never overlap. Unlike + /// the account-level test above this cannot pass by accident, because the holders are forced to + /// be inside the gate at the same time - if they ever are, the flag says so. The three names + /// differ only in case, which is also what the duplicate-name check is case-insensitive about. + /// + [Fact] + public async Task TwoHoldersOfTheSameNameAreNeverInsideTheGateAtOnce() + { + var inside = 0; + var overlapped = 0; + + async Task Hold(string name) + { + using var held = await RegistrationGuard.LockNameAsync(name); + if (Interlocked.Increment(ref inside) > 1) Interlocked.Exchange(ref overlapped, 1); + await Task.Delay(25); + Interlocked.Decrement(ref inside); + } + + await Task.WhenAll(Hold("ContestedName"), Hold("contestedname"), Hold("CONTESTEDNAME")); + + Assert.Equal(0, overlapped); + } + + // --------------------------------------------------------------------------------------- + // The username page + // --------------------------------------------------------------------------------------- + + [Fact] + public async Task ChoosingAUsernameAlsoNamesTheAccountSoItIsNotLeftAsTheGeneratedOne() + { + // This is the route the 2023 client's account-creation username page calls. The account is + // created with a generated name in both its fields, so a route that sets only the username + // leaves the player finished with onboarding and still appearing under the name the server + // invented for them. + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + var created = Assert.IsType((await PlayerUtils.CreateAccount(Registration())).value); + + var api = new AccountsAPI(db) { ControllerContext = ContextFor(created.accountId) }; + var result = await api.SetUsername("WildRabbit"); + + Assert.True(result.success, result.error); + var profile = await db.Profiles.SingleAsync(x => x.accountId == created.accountId); + Assert.Equal("WildRabbit", profile.username); + Assert.Equal("WildRabbit", profile.displayName); + } + + [Fact] + public async Task ChoosingAUsernameLeavesACustomDisplayNameAlone() + { + // A display name the player deliberately set to something other than their username is not + // overwritten by a later username change. Setting one is a settings-screen action, so the + // account here is a finished one: a mid-flow account always has the two names in step, + // because the creation flow has no display-name page of its own to diverge them with. + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + var created = Assert.IsType((await PlayerUtils.CreateAccount(Registration())).value); + + // Both rows are written through this context: the profile CreateAccount hands back belongs to + // its own, so mutating it here would not reach the database the route reads. + var settings = await db.PlayerSettings.SingleAsync(x => x.AccountId == created.accountId); + var values = settings.DeserializeSettings(); + PlayerUtils.SetSettingFlag(values, "Recroom.AccountCreation.HasFinished", true); + settings.Settings = values.ToJson(); + (await db.Profiles.SingleAsync(x => x.accountId == created.accountId)).displayName = "Captain Lantern"; + await db.SaveChangesAsync(); + + var api = new AccountsAPI(db) { ControllerContext = ContextFor(created.accountId) }; + var result = await api.SetUsername("WildRabbit"); + + Assert.True(result.success, result.error); + var profile = await db.Profiles.SingleAsync(x => x.accountId == created.accountId); + Assert.Equal("WildRabbit", profile.username); + Assert.Equal("Captain Lantern", profile.displayName); + } + + // --------------------------------------------------------------------------------------- + // The onboarding hand-off + // --------------------------------------------------------------------------------------- + + [Fact] + public async Task AnAccountCreatedAtBootStartsTheAccountCreationFlow() + { + // The 2023 client creates its account on the boot grant and only then walks its creation + // pages: birthday, username, password, code of conduct, avatar. That happens only if the row + // says the flow has started. With HasStarted=false the client reads IsInAccountCreationState + // as false, takes the cached-account route and launches, so the welcome screen and every one + // of those steps is skipped - the player never gets to choose a name at all. + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + await SeedSystemAccountAsync(db); + + var boot = Token(Registration()); + boot.username = null; + boot.password = null; + boot.birthday = null; + boot.email = null; + boot.avatar = null; + boot.avatarJson = null; + + var response = Assert.IsType( + Assert.IsType((await CreateApi(db, PlatformId).ConnectWithToken(boot)).Result).Value); + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + + var settings = (await db.PlayerSettings.SingleAsync()).DeserializeSettings(); + Assert.True(PlayerUtils.HasSettingFlag(settings, "Recroom.AccountCreation.HasStarted")); + Assert.False(PlayerUtils.HasSettingFlag(settings, "Recroom.AccountCreation.HasChosenUsername")); + Assert.False(PlayerUtils.HasSettingFlag(settings, "Recroom.AccountCreation.HasCreatedPassword")); + Assert.False(PlayerUtils.HasSettingFlag(settings, "Recroom.AccountCreation.HasFinished")); + } + + [Fact] + public async Task ResumingAnAccountThatNeverStartedTheFlowPutsItBackIntoIt() + { + // Accounts written before the started marker was set correctly still carry HasStarted=false, + // and that state is permanent unless a resume repairs it: the client would skip onboarding + // for those accounts on every launch from here on. + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + await SeedStrandedAccountAsync(db, accountId: 7, finished: false); + + var boot = BlankBoot(); + var response = Assert.IsType( + Assert.IsType((await CreateApi(db, PlatformId).ConnectWithToken(boot)).Result).Value); + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + + var settings = (await db.PlayerSettings.SingleAsync(x => x.AccountId == 7)).DeserializeSettings(); + Assert.True(PlayerUtils.HasSettingFlag(settings, "Recroom.AccountCreation.HasStarted")); + Assert.False(PlayerUtils.HasSettingFlag(settings, "Recroom.AccountCreation.HasFinished")); + } + + [Fact] + public async Task ResumingAFinishedAccountLeavesItsSettingsUntouched() + { + // The repair is for accounts stuck mid-flow. A player who finished onboarding keeps exactly + // the settings the client wrote for them. + var factory = new TestDbContextFactory(); + using var harness = new Harness(factory); + + await using var db = factory.CreateDbContext(); + await SeedStrandedAccountAsync(db, accountId: 8, finished: true); + var before = (await db.PlayerSettings.SingleAsync(x => x.AccountId == 8)).Settings; + + var response = Assert.IsType( + Assert.IsType((await CreateApi(db, PlatformId).ConnectWithToken(BlankBoot())).Result).Value); + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + + var after = (await db.PlayerSettings.SingleAsync(x => x.AccountId == 8)).Settings; + Assert.Equal(before, after); + } + + // --------------------------------------------------------------------------------------- + // Harness + // --------------------------------------------------------------------------------------- + + private static AccountCreationData Registration() => new() + { + platform = Platforms.Steam, + platformId = PlatformId, + deviceId = " " + DeviceId + " ", + deviceClass = DeviceClass.Screen, + username = ChosenName, + password = ChosenPassword, + birthday = new DateTime(1998, 4, 30) + }; + + private static TokenConnectData Token(AccountCreationData source, string grant = "create_account") => new() + { + grant_type = grant, + account_id = 0, + ver = "20230831", + device_class = source.deviceClass, + device_id = source.deviceId, + platform = source.platform, + platform_id = source.platformId, + client_id = "recroom", + platform_auth = "{\"Ticket\":\"140test-ticket\",\"AppId\":\"471710\"}", + username = source.username, + password = source.password, + birthday = source.birthday, + birthdayUTC = source.birthdayUTC, + email = source.email, + avatar = source.avatar, + avatarJson = source.avatarJson + }; + + /// + /// A seeded server's first row is the system account, so real ids start at 2 and account 1 is + /// never issued a client token. The in-memory provider numbers from 1, so the fixture has to put + /// that row there or the first account this test makes would be the one the login refuses. + /// + private static async Task SeedSystemAccountAsync(RecDBContext db) + { + if (await db.Profiles.AnyAsync(x => x.accountId == 1)) return; + db.Profiles.Add(new Profile + { + accountId = 1, + profileImage = "default", + username = "SystemAccount", + displayName = "SystemAccount", + createdAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + } + + /// A boot grant carrying nothing the player has chosen yet. + private static TokenConnectData BlankBoot() + { + var boot = Token(Registration()); + boot.username = null; + boot.password = null; + boot.birthday = null; + boot.email = null; + boot.avatar = null; + boot.avatarJson = null; + return boot; + } + + /// + /// An account whose identity is linked but which is stranded: it has a row and a link, and its + /// settings say the flow never started. + /// + private static async Task SeedStrandedAccountAsync(RecDBContext db, long accountId, bool finished) + { + db.Profiles.Add(new Profile + { + accountId = accountId, + username = "Stranded", + displayName = "Stranded", + profileImage = "default", + createdAt = DateTime.UtcNow + }); + db.Players.Add(new PlayerData { AccountId = accountId, DeviceId = DeviceId, Hashword = "" }); + db.CachedLogins.Add(new CachedLogin + { + accountId = accountId, + platform = Platforms.Steam, + platformId = PlatformId, + lastLoginTime = DateTime.Now + }); + // Sign-in itself needs a presence row, so the account has to look like a real one here or + // the test would fail on MissingPresence rather than on the marker it is about. + db.Presences.Add(new RealPresence + { + playerId = accountId, + deviceClass = DeviceClass.Screen, + statusVisibility = StatusVisibility.Public, + vRMovementMode = VRMovementModes.TELEPORT + }); + + var values = DBConfig.AccountCreationInProgressSettings2023(false, false).FromJson>()!; + PlayerUtils.SetSettingFlag(values, "Recroom.AccountCreation.HasStarted", false); + PlayerUtils.SetSettingFlag(values, "Recroom.AccountCreation.HasFinished", finished); + db.PlayerSettings.Add(new PlayerSettings { AccountId = accountId, Settings = values.ToJson() }); + await db.SaveChangesAsync(); + } + + private static void AllowEveryRegistration() => + RegistrationGuard.Configure(maxCreationsPerIp: int.MaxValue, maxCreationsPerDevice: int.MaxValue, + minimumDeviceInterval: TimeSpan.Zero); + + private static TestAuthAPI CreateApi(RecDBContext db, string? authenticatedSteamId) => + new(db, authenticatedSteamId) { ControllerContext = CreateControllerContext() }; + + private static ControllerContext CreateControllerContext() + { + var httpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity()) + }; + httpContext.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback; + return new ControllerContext { HttpContext = httpContext }; + } + + /// A request signed in as this account, the way an authorized route sees one. + private static ControllerContext ContextFor(long accountId) + { + var httpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, accountId.ToString())])) + }; + httpContext.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback; + return new ControllerContext { HttpContext = httpContext }; + } + + /// Points the process-wide factories at this test's database for the test's lifetime. + private sealed class Harness : IDisposable + { + private readonly IDbContextFactory previousPlayerFactory = PlayerUtils.DbFactory; + private readonly IDbContextFactory previousRoomFactory = RoomUtils.DbFactory; + private readonly string previousJwtKey = DBConfig.JwtKey; + private readonly string previousSteamKey = DBConfig.SteamApiKey; + + public Harness(IDbContextFactory factory) + { + DBConfig.JwtKey = "registration-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + } + + public void Dispose() + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + /// + /// A real database, in memory, over one open connection so it outlives any single context. The + /// interceptor can be told to fail a given save, which is how a mid-write database error is + /// simulated without a live SQL Server. + /// + private sealed class SqliteHarness : IDisposable + { + private readonly SqliteConnection connection = new("Data Source=:memory:"); + private readonly Harness harness; + + public SqliteDbContextFactory Factory { get; } + + /// + /// Turns on the provider's retrying execution strategy, which is what the production server + /// configures (EnableRetryOnFailure(3) in Program). It changes what the provider allows + /// you to do with a transaction, so a test that never turns it on cannot see violations of it. + /// + public SqliteHarness(int failOnSaveCall, bool retrying = false) + { + connection.Open(); + var builder = TestDbOptions.Create().UseSqlite(connection); + // SQL Server's strategy is the one production configures; SQLite has no equivalent to + // turn on, so the test substitutes a strategy that reports retrying the same way. That + // flag is the single thing EF checks before refusing a user-initiated transaction. + if (retrying) builder.ReplaceService(); + var options = builder + .AddInterceptors(new FailOnSaveNumber(failOnSaveCall)) + .Options; + Factory = new SqliteDbContextFactory(options); + using (var seed = Factory.CreateDbContext()) + seed.Database.EnsureCreated(); + harness = new Harness(Factory); + } + + public void Dispose() + { + harness.Dispose(); + connection.Dispose(); + } + } + + private sealed class SqliteDbContextFactory(DbContextOptions options) + : IDbContextFactory + { + public RecDBContext CreateDbContext() => new(options); + } + + /// + /// Stands in for the server's SqlServerRetryingExecutionStrategy. It reports the same + /// , which is the flag the framework's own + /// OnFirstExecution check consults before it refuses a user-initiated transaction - so the + /// exception this produces is EF's, not this shim's. + /// + private sealed class RetryingStrategyFactory(ExecutionStrategyDependencies dependencies) : IExecutionStrategyFactory + { + public IExecutionStrategy Create() => new RetryingStrategy(dependencies); + } + + private sealed class RetryingStrategy(ExecutionStrategyDependencies dependencies) + : ExecutionStrategy(dependencies, maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(30)) + { + public override bool RetriesOnFailure => true; + + protected override bool ShouldRetryOn(Exception exception) => true; + } + + private sealed class FailOnSaveNumber(int failingCall) : SaveChangesInterceptor + { + private int calls; + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + if (failingCall > 0 && Interlocked.Increment(ref calls) == failingCall) + throw new InvalidOperationException("simulated database failure"); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + } + + private sealed class TestAuthAPI(RecDBContext db, string? authenticatedSteamId) : AuthAPI(db) + { + protected override Task AuthenticateSteamTicketAsync(string ticket, string appId, CancellationToken cancellationToken) => + Task.FromResult(authenticatedSteamId); + } +} diff --git a/ServerTests/AdminAnnouncementTests.cs b/ServerTests/AdminAnnouncementTests.cs new file mode 100644 index 0000000..f6c3d91 --- /dev/null +++ b/ServerTests/AdminAnnouncementTests.cs @@ -0,0 +1,149 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The announcement feed lives as one JSON array in the ServerConfig "Announcement" row, and the +/// panel's save re-derives the next id from the ids already in it. A row saved by an older panel can +/// carry its id as a JSON string ("5"); reading that with GetValue<long>() threw, the route's +/// catch answered HTTP 200 with an error body and no write, and the panel therefore looked like it +/// had saved while nothing reached the client feed. These tests pin the tolerant read. +/// +public class AdminAnnouncementTests +{ + private const string Secret = "test-bot-secret"; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static JsonElement Payload(string json) => JsonDocument.Parse(json).RootElement; + + private static string StoredValue(RecDBContext db) => + db.ServerConfig.First(x => x.Key == "Announcement").Value; + + private static async Task AsAdmin(Func> work) + { + var originalKey = DBConfig.AdminApiKey; + var originalLogs = DBConfig.PersistentLogs; + DBConfig.AdminApiKey = Secret; + DBConfig.PersistentLogs = Path.Join(Path.GetTempPath(), "radicade-admin-announcement-tests-" + Guid.NewGuid().ToString("N")); + try + { + return await work(); + } + finally + { + DBConfig.AdminApiKey = originalKey; + DBConfig.PersistentLogs = originalLogs; + } + } + + private static async Task SaveAsync(AdminPanelAPI api, string rowJson) + => await AsAdmin(async () => + { + var ok = Assert.IsType(await api.Insert("announcements", Secret, Payload(rowJson))); + var created = Assert.IsType(ok.Value); + Assert.False(created.ContainsKey("error"), created["error"]?.ToString()); + return created; + }); + + [Fact] + public async Task ASaveOnAnEmptyConfigCreatesTheAnnouncementTheClientFeedServes() + { + var db = NewDb(); + + var created = await SaveAsync( + new AdminPanelAPI(db), + "{\"type\":\"Regular Announcement\",\"message\":\"Double XP all weekend.\",\"title\":\"Weekend event\"}"); + + Assert.Equal(1L, created["id"]!.GetValue()); + + var feed = new AnnouncementAPI(db).GetAnnouncement(); + var served = Assert.Single(feed!); + Assert.Equal(1L, served.AnnouncementId); + Assert.Equal("Weekend event", served.Title); + Assert.Equal("Double XP all weekend.", served.Body); + Assert.True(served.IsMostRecent); + } + + [Fact] + public async Task ASaveStillLandsWhenAnExistingRowCarriesItsIdAsAString() + { + var db = NewDb(); + db.ServerConfig.Add(new ServerConfigVal("Announcement", + "[{\"id\":\"5\",\"type\":\"Regular Announcement\",\"message\":\"legacy row\",\"title\":\"Legacy\"}]")); + await db.SaveChangesAsync(); + + var created = await SaveAsync( + new AdminPanelAPI(db), + "{\"type\":\"Regular Announcement\",\"message\":\"second\",\"title\":\"Second\"}"); + + // The save numbers the new row after the legacy one, and normalises that id on the way + // through - GetValue() on the stored row is what used to throw. + Assert.Equal(6L, created["id"]!.GetValue()); + + var stored = JsonNode.Parse(StoredValue(db))!.AsArray(); + Assert.Equal(2, stored.Count); + Assert.Equal(5L, stored[0]!["id"]!.GetValue()); + Assert.Equal(6L, stored[1]!["id"]!.GetValue()); + + // Both rows are now servable: the legacy entry keeps the fields it was saved with. + var feed = new AnnouncementAPI(db).GetAnnouncement(); + Assert.Equal(2, feed!.Count); + Assert.Equal("Second", feed[0].Title); + } + + [Fact] + public async Task ADeleteStillLandsWhenTheRowCarriesItsIdAsAString() + { + var db = NewDb(); + db.ServerConfig.Add(new ServerConfigVal("Announcement", + "[{\"id\":\"9\",\"type\":\"Regular Announcement\",\"message\":\"legacy row\"}]")); + await db.SaveChangesAsync(); + + var api = new AdminPanelAPI(db); + var removed = await AsAdmin(async () => + { + var ok = Assert.IsType( + await api.Delete("announcements", Secret, null, "9", null, null, null, null, null)); + return Assert.IsType>(ok.Value); + }); + + Assert.Single(removed); + Assert.Equal("[]", StoredValue(db)); + } + + // The panel posts back every field of its row, so the boxes the admin left empty arrive as an + // explicit JSON null rather than as missing keys. JsonElement.TryGetInt32 throws on a null token + // (it only returns false for numbers it cannot represent), which made "minutes": null and + // "link_type": null fail the whole save with "The requested operation requires an element of + // type 'Number', but the target element has type 'Null'." + [Fact] + public async Task ASaveLandsWhenThePanelSendsItsEmptyFieldsAsNull() + { + var db = NewDb(); + + var created = await SaveAsync( + new AdminPanelAPI(db), + "{\"id\":null,\"type\":\"Regular Announcement\",\"message\":\"hi\",\"image\":null,\"minutes\":null," + + "\"expires_at\":null,\"created_at\":null,\"title\":\"Hi\",\"link_name\":null,\"link_type\":null," + + "\"link_uri\":null,\"AnnouncementId\":null,\"Body\":\"hi\",\"IsMostRecent\":null,\"Platform\":null}"); + + Assert.Equal(1L, created["id"]!.GetValue()); + Assert.Null(created["minutes"]); + Assert.Equal("Learn More", created["link_name"]!.GetValue()); + + var feed = new AnnouncementAPI(db).GetAnnouncement(); + Assert.Equal("Hi", Assert.Single(feed!).Title); + } +} diff --git a/ServerTests/AdminBundleBindingTests.cs b/ServerTests/AdminBundleBindingTests.cs new file mode 100644 index 0000000..34f0cf9 --- /dev/null +++ b/ServerTests/AdminBundleBindingTests.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class AdminBundleBindingTests +{ + [Fact] + public async Task BindingRequiresAdminExistingBundleAndMatchingRoomAndPreservesRoomSave() + { + var root = Directory.CreateTempSubdirectory("radicade-bundle-binding-").FullName; + var oldStorage = DBConfig.CDNStorage; + var oldKey = DBConfig.AdminApiKey; + DBConfig.CDNStorage = root; + DBConfig.AdminApiKey = "binding-test-key"; + try + { + await using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + var sub = new SubRoom { RoomId = 228, SubRoomId = 280, DataBlob = "original.room" }; + db.SubRooms.Add(sub); + await db.SaveChangesAsync(); + var api = new AdminRoomAPI(db); + var id = new string('a', 40); + Assert.IsType(await api.BindAssetBundle("wrong", 228, 280, id)); + Assert.IsType(await api.BindAssetBundle("binding-test-key", 228, 280, "../bad")); + Assert.IsType(await api.BindAssetBundle("binding-test-key", 228, 280, id)); + await File.WriteAllTextAsync(Path.Join(root, id + ".assetbundle"), "test bundle"); + Assert.IsType(await api.BindAssetBundle("binding-test-key", 229, 280, id)); + Assert.Equal("", sub.UnityAssetId); + Assert.IsType(await api.BindAssetBundle("binding-test-key", 228, 280, id)); + db.ChangeTracker.Clear(); + var stored = await db.SubRooms.SingleAsync(); + Assert.Equal(id, stored.UnityAssetId); + Assert.Equal("original.room", stored.DataBlob); + Assert.IsType(await api.BindAssetBundle("binding-test-key", 228, 280, id)); + await File.WriteAllTextAsync(Path.Join(root, id), "shadowing manifest"); + Assert.IsType(await api.BindAssetBundle("binding-test-key", 228, 280, id)); + } + finally + { + DBConfig.CDNStorage = oldStorage; + DBConfig.AdminApiKey = oldKey; + Directory.Delete(root, recursive: true); + } + } +} diff --git a/ServerTests/AdminBundleUploadTests.cs b/ServerTests/AdminBundleUploadTests.cs new file mode 100644 index 0000000..49660dc --- /dev/null +++ b/ServerTests/AdminBundleUploadTests.cs @@ -0,0 +1,72 @@ +using System.Net; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class AdminBundleUploadTests +{ + [Theory] + [InlineData("/admin/panel/upload/datablob")] + [InlineData("/admin/rooms/upload/datablob")] + public async Task LargeBundleUploadsPreserveBytesAndStillRequireTheKey(string route) + { + var root = Directory.CreateTempSubdirectory("radicade-upload-test-").FullName; + var oldStorage = DBConfig.CDNStorage; + var oldKey = DBConfig.AdminApiKey; + DBConfig.CDNStorage = root; + DBConfig.AdminApiKey = "bundle-upload-test-key"; + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.Logging.ClearProviders(); + builder.Services.AddControllers().AddApplicationPart(typeof(AdminPanelAPI).Assembly); + builder.Services.AddCors(options => options.AddPolicy("AdminPanel", policy => policy.AllowAnyOrigin())); + builder.Services.Configure(options => options.MultipartBodyLengthLimit = 256L * 1024 * 1024); + builder.Services.AddDbContext(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); + await using var app = builder.Build(); + app.UseCors(); + app.MapControllers(); + try + { + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) }; + // Exercise real Kestrel and MVC model binding, beyond the default 30 MB ceiling. + var bytes = new byte[39_000_000]; + RandomNumberGenerator.Fill(bytes); + using var form = new MultipartFormDataContent(); + form.Add(new ByteArrayContent(bytes), "file", "bundle.assetbundle"); + using var response = await client.PostAsync(route + "?key=bundle-upload-test-key&name=bundle.assetbundle", form); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(await File.ReadAllBytesAsync(Path.Join(root, "bundle.assetbundle")))); + + using var unauthorizedForm = new MultipartFormDataContent(); + unauthorizedForm.Add(new ByteArrayContent(new byte[] { 1, 2, 3 }), "file", "unauthorized.assetbundle"); + using var unauthorized = await client.PostAsync(route + "?key=wrong&name=unauthorized.assetbundle", unauthorizedForm); + Assert.Equal(HttpStatusCode.Unauthorized, unauthorized.StatusCode); + Assert.False(File.Exists(Path.Join(root, "unauthorized.assetbundle"))); + + using var malformed = new ByteArrayContent(new byte[] { 1, 2, 3 }); + malformed.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("multipart/form-data"); + using var badRequest = await client.PostAsync(route + "?key=bundle-upload-test-key&name=broken.assetbundle", malformed); + Assert.Equal(HttpStatusCode.BadRequest, badRequest.StatusCode); + Assert.DoesNotContain("Invalid secret", await badRequest.Content.ReadAsStringAsync()); + Assert.False(File.Exists(Path.Join(root, "broken.assetbundle"))); + } + finally + { + await app.StopAsync(); + DBConfig.CDNStorage = oldStorage; + DBConfig.AdminApiKey = oldKey; + Directory.Delete(root, recursive: true); + } + } +} diff --git a/ServerTests/AdminClubImageTests.cs b/ServerTests/AdminClubImageTests.cs new file mode 100644 index 0000000..273fd25 --- /dev/null +++ b/ServerTests/AdminClubImageTests.cs @@ -0,0 +1,247 @@ +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.Clubs; +using RadicadeServer.DB; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Setting a club's image from the admin panel: POST admin/clubs/upload/image and +/// GET admin/clubs/set/image. +/// +/// Clubs are JSON in the "Clubs" server config rather than a table, so the panel's generic table +/// routes cannot reach one and these two endpoints are the whole of the write path. The name they +/// store is the one hands the client as MainImageName, so the +/// image the panel picks is the image the game shows. +/// +[Collection(DbFactoryCollection.Name)] +public class AdminClubImageTests +{ + private const string Secret = "test-club-image-secret"; + private const long OwnerId = 77; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static async Task CreateClubAsync(RecDBContext db) + { + var club = await ClubStore.CreateAsync(db, OwnerId, "Image Test Club", "a club"); + return club.clubId; + } + + private static IFormFile FormFile(string name, byte[] bytes) => + new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", name); + + // A real PNG header, so the file has the bytes its extension claims. + private static readonly byte[] PngBytes = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + + /// Runs an action with the admin secret and CDN image folder pointed at a temp dir. + private static async Task WithAdminAsync(string imagesDirectory, Func> action) + { + var originalKey = DBConfig.AdminApiKey; + var originalImages = DBConfig.CDNImages; + DBConfig.AdminApiKey = Secret; + DBConfig.CDNImages = imagesDirectory; + try + { + return await action(); + } + finally + { + DBConfig.AdminApiKey = originalKey; + DBConfig.CDNImages = originalImages; + } + } + + private static string TempImages() => + Path.Combine(Path.GetTempPath(), "radicade-club-image-" + Guid.NewGuid().ToString("N")); + + [Fact] + public void ThePanelCallsTheseRoutes() + { + var setImage = typeof(AdminClubAPI).GetMethod(nameof(AdminClubAPI.SetImage))!; + Assert.Contains("set/image", setImage + .GetCustomAttributes(typeof(HttpMethodAttribute), false) + .Cast() + .Select(attribute => attribute.Template)); + + var upload = typeof(AdminClubAPI).GetMethod(nameof(AdminClubAPI.UploadImage))!; + Assert.Contains("upload/image", upload + .GetCustomAttributes(typeof(HttpMethodAttribute), false) + .Cast() + .Select(attribute => attribute.Template)); + + // The controller's own prefix is what the panel's fetch URLs are built from. + var prefix = typeof(AdminClubAPI).GetCustomAttributes(typeof(RouteAttribute), false) + .Cast() + .Single(); + Assert.Equal("admin/clubs", prefix.Template); + } + + [Fact] + public async Task SetImageNamesTheImageTheGameIsServed() + { + var db = NewDb(); + var clubId = await CreateClubAsync(db); + var api = new AdminClubAPI(db); + + var result = await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId, "custom-club.png")); + + Assert.IsType(result); + var club = await ClubStore.GetAsync(db, clubId); + Assert.Equal("custom-club.png", club!.imageName); + + // What the client is actually told is the name that matters. + var served = (Dictionary)ClubStore.BuildClub(club, OwnerId); + Assert.Equal("custom-club.png", served["MainImageName"]); + } + + [Fact] + public async Task AnEmptyNamePutsTheClubBackOnTheDefaultImage() + { + var db = NewDb(); + var clubId = await CreateClubAsync(db); + var api = new AdminClubAPI(db); + + await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId, "something.png")); + // Clearing is the same call with an empty name, which is what the panel sends. + var result = await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId, "")); + + Assert.IsType(result); + var club = await ClubStore.GetAsync(db, clubId); + Assert.Equal(ClubStore.DefaultImageName, club!.imageName); + } + + [Fact] + public async Task SetImageKeepsOnlyTheFileName() + { + var db = NewDb(); + var clubId = await CreateClubAsync(db); + var api = new AdminClubAPI(db); + + // An image name is a key into the CDN's image folder, so a path must not survive. + await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId, "../../elsewhere/evil.png")); + + var club = await ClubStore.GetAsync(db, clubId); + Assert.Equal("evil.png", club!.imageName); + } + + [Fact] + public async Task SetImageRefusesAnInvalidKeyAndAnUnknownClub() + { + var db = NewDb(); + var clubId = await CreateClubAsync(db); + var api = new AdminClubAPI(db); + + var wrongKey = await api.SetImage("not-the-secret", clubId, "x.png"); + Assert.IsType(wrongKey); + + var missing = await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId + 9999, "x.png")); + Assert.IsType(missing); + + // Neither attempt touched the club. + var club = await ClubStore.GetAsync(db, clubId); + Assert.Equal(ClubStore.DefaultImageName, club!.imageName); + } + + [Fact] + public async Task UploadStoresTheFileAndNamesItOnTheClub() + { + var directory = TempImages(); + var db = NewDb(); + var clubId = await CreateClubAsync(db); + var api = new AdminClubAPI(db); + + try + { + var result = await WithAdminAsync(directory, () => api.UploadImage(Secret, clubId, FormFile("logo.png", PngBytes))); + + var ok = Assert.IsType(result); + var club = await ClubStore.GetAsync(db, clubId); + Assert.EndsWith(".png", club!.imageName); + Assert.True(File.Exists(Path.Join(directory, club.imageName))); + + // The response names the URL the panel shows, so it has to point at the stored file. + Assert.Contains($"/images/img/{club.imageName}", ok.Value!.ToString()); + } + finally + { + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } + + [Fact] + public async Task UploadRefusesAFileThatIsNotAnImageAndLeavesNothingBehind() + { + var directory = TempImages(); + var db = NewDb(); + var clubId = await CreateClubAsync(db); + var api = new AdminClubAPI(db); + + try + { + var result = await WithAdminAsync(directory, () => api.UploadImage(Secret, clubId, FormFile("save.room", PngBytes))); + + Assert.IsType(result); + Assert.False(Directory.Exists(directory)); + var club = await ClubStore.GetAsync(db, clubId); + Assert.Equal(ClubStore.DefaultImageName, club!.imageName); + } + finally + { + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } + + [Fact] + public async Task UploadRefusesAnEmptyFile() + { + var directory = TempImages(); + var db = NewDb(); + var clubId = await CreateClubAsync(db); + var api = new AdminClubAPI(db); + + try + { + var result = await WithAdminAsync(directory, () => api.UploadImage(Secret, clubId, FormFile("logo.png", []))); + + Assert.IsType(result); + var club = await ClubStore.GetAsync(db, clubId); + Assert.Equal(ClubStore.DefaultImageName, club!.imageName); + } + finally + { + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } + + [Fact] + public async Task UploadOnAnUnknownClubWritesNoFile() + { + var directory = TempImages(); + var db = NewDb(); + var clubId = await CreateClubAsync(db); + var api = new AdminClubAPI(db); + + try + { + // The club is looked up before the upload is stored, so a mistyped club id cannot + // leave an orphaned file in the CDN. + var result = await WithAdminAsync(directory, () => api.UploadImage(Secret, clubId + 9999, FormFile("logo.png", PngBytes))); + + Assert.IsType(result); + Assert.False(Directory.Exists(directory)); + } + finally + { + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } +} diff --git a/ServerTests/AdminGiftFromTests.cs b/ServerTests/AdminGiftFromTests.cs new file mode 100644 index 0000000..399f3a0 --- /dev/null +++ b/ServerTests/AdminGiftFromTests.cs @@ -0,0 +1,188 @@ +using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The Gift Player page in the admin panel posts one gift_log row per account to +/// POST admin/panel/gift_log, and AddGiftLogRows turns each row into a real gift box on +/// that account. The box carries its sender as RecNet's Gift.FromPlayerId, and that is what the +/// player reads off the box - so the panel's "Gift From" field has to land there, not only on the +/// panel's own log row. +/// +/// The panel sends ids (both player_id and from_player_id) as digits in a JSON string, because a +/// platform id is seventeen digits and a JSON number cannot hold that many exactly. +/// +/// These tests drive the real route and then read the box back the way a client does - +/// GET api/avatar/v2/gifts - rather than inspecting the row the route answered with, so a sender +/// stored in the log but not on the box fails here. +public class AdminGiftFromTests +{ + private const string Secret = "test-bot-secret"; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + /// Posts one gift_log row through the route, and returns the row the route answered with. + private static async Task GiveAsync(RecDBContext db, string rowJson) + { + var api = new AdminPanelAPI(db); + var originalKey = DBConfig.AdminApiKey; + var originalLogs = DBConfig.PersistentLogs; + DBConfig.AdminApiKey = Secret; + DBConfig.PersistentLogs = Path.Join(Path.GetTempPath(), "radicade-admin-tests-" + Guid.NewGuid().ToString("N")); + try + { + var payload = JsonDocument.Parse(rowJson).RootElement; + var result = await api.Insert("gift_log", Secret, payload); + var ok = Assert.IsType(result); + return Assert.IsType(ok.Value); + } + finally + { + DBConfig.AdminApiKey = originalKey; + DBConfig.PersistentLogs = originalLogs; + } + } + + /// The box as the recipient's client reads it: GET api/avatar/v2/gifts. + private static async Task> ClientGiftsAsync(RecDBContext db, long playerId) + { + var api = new AvatarV2(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, playerId.ToString())], "test")) + } + } + }; + + var result = await api.GetGifts(); + var ok = Assert.IsType(result); + return Assert.IsAssignableFrom>(ok.Value).ToList(); + } + + /// The JSON a client actually receives for one box, with the server's own naming. + private static JsonElement SerializeAsClient(Gift gift) + => JsonDocument.Parse(JsonSerializer.Serialize(gift)).RootElement; + + private const long Recipient = 114903603133502227; + private const long Sender = 733901112234567890; + + [Fact] + public async Task TheGiftFromBecomesTheBoxSenderTheClientReads() + { + var db = NewDb(); + + var row = await GiveAsync(db, $$""" + { + "player_id": "{{Recipient}}", + "from_player_id": "{{Sender}}", + "message": "Thanks for the help!", + "context": "Game_Drop", + "rarity": "Rare", + "avatar": "8aa79563-ace1-4ba7-ad0c-f3210a78142f," + } + """); + + // The route's answer is the panel's record of what it did. + Assert.Equal(true, row["sent"]?.GetValue()); + Assert.Equal(Sender, row["from_player_id"]!.GetValue()); + + // The box itself, stored against the recipient. + var stored = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, Recipient)); + Assert.Equal(Sender, stored.FromPlayerId); + + // And what the client reads off that stored box. + var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); + Assert.Equal(Sender, read.FromPlayerId); + + var wire = SerializeAsClient(read); + Assert.Equal(Sender, wire.GetProperty("FromPlayerId").GetInt64()); + Assert.Equal(Rarity.Rare, read.Rarity); + Assert.Equal(GiftContext.Game_Drop, read.Context); + } + + [Fact] + public async Task ABlankGiftFromSendsItFromTheRadicadeTeam() + { + var db = NewDb(); + + // The panel leaves from_player_id out entirely when the field is blank; the box then + // comes from the Radicade account, the same sender the server's own admin gifts use. + var row = await GiveAsync(db, $$""" + { + "player_id": "{{Recipient}}", + "context": "Game_Drop", + "rarity": "Common", + "cons": "00000000-0000-0000-0000-000000000000," + } + """); + + Assert.Equal(true, row["sent"]?.GetValue()); + Assert.Equal(1L, row["from_player_id"]!.GetValue()); + + var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); + Assert.Equal(1L, read.FromPlayerId); + } + + [Fact] + public async Task ASeventeenDigitSenderIsNotRoundedOrOverflowed() + { + var db = NewDb(); + + // Both ids are past int.MaxValue, which is why the panel posts them as text. + Assert.True(Sender > int.MaxValue); + Assert.True(Recipient > int.MaxValue); + + await GiveAsync(db, $$""" + { + "player_id": "{{Recipient}}", + "from_player_id": "{{Sender}}", + "context": "Game_Drop", + "rarity": "Common", + "equip": "Sword" + } + """); + + var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); + Assert.Equal(Sender, read.FromPlayerId); + } + + [Fact] + public async Task ASenderThePanelDidNotSendDoesNotClearTheTeamDefault() + { + var db = NewDb(); + + // Nonsense in the field is refused by the panel before it posts; if it ever reaches the + // route anyway the box still has a sender rather than a bare zero. + await GiveAsync(db, $$""" + { + "player_id": "{{Recipient}}", + "from_player_id": "not-a-player", + "context": "Game_Drop", + "rarity": "Common", + "cons": "00000000-0000-0000-0000-000000000000," + } + """); + + var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); + Assert.Equal(1L, read.FromPlayerId); + } +} diff --git a/ServerTests/AdminGiftSkinTests.cs b/ServerTests/AdminGiftSkinTests.cs new file mode 100644 index 0000000..ba22d1e --- /dev/null +++ b/ServerTests/AdminGiftSkinTests.cs @@ -0,0 +1,300 @@ +using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using RadicadeServer.Admin; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A gift of equipment is a gift of a *skin*, and a skin is two halves: the tool prefab and the +/// modification guid that says which skin of it. The client resolves one from the pair +/// (EquipmentSkinCatalog's own key, and what EquipmentCatalogTests pins as "a resolvable skin"), +/// so a box carrying only the prefab is a box carrying the bare tool. +/// +/// The shipped catalog has nine rows for [Basketball] alone, all named "[Basketball]", one per +/// skin. Two things followed from that, and this file is both of them: +/// +/// - the page the gift is set up on listed those nine rows as one choice, because every row +/// arrived with the same prefab name and nothing to tell them apart. The panel keys that box +/// by the value it gets here, so the catalogue has to name each row's guid. +/// - the box the gift became held the prefab with an empty guid, whatever skin was picked, so +/// the recipient was handed a plain basketball. ProcessGiftRowAsync hardcoded that guid to "". +/// +/// The tests drive the real route and then claim the box back through the client's own claim +/// route, reading the equipment list the client reads to resolve a skin - so a skin that the +/// catalogue can name but the box cannot carry fails here. +public class AdminGiftSkinTests +{ + private const string Secret = "test-bot-secret"; + private const long Recipient = 114903603133502227; + + private static string Fixture(string name) => Path.Combine(AppContext.BaseDirectory, "Fixtures", name); + + private static string ShippedCatalog() => File.ReadAllText(Fixture("equipment.json")); + + private static List Shipped() => + JsonSerializer.Deserialize>(ShippedCatalog())!; + + /// A skin of [Basketball] that the shipped catalogue names, taken from it rather + /// than written down here, so the test names a skin the client could actually resolve. + private static Equipment ShippedBasketballSkin() => + Shipped().First(row => row.PrefabName == "[Basketball]" && row.FriendlyName.Contains("Purple")); + + /* The in-memory store is not transactional, and the claim route wraps its work in a + * transaction - which the provider reports as a warning raised as an error, so the route + * throws before granting anything. The claim is the end of the chain these tests are here to + * prove, so the warning is turned off rather than the route skipped: nothing about the store's + * lack of transactions changes what a claimed box grants. */ + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ConfigureWarnings(warnings => warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options); + + private static void SetUser(Controller controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + /// One catalogue row as the page receives it, whatever shape the route built it in. + private static List RowsOf(IActionResult result) + { + var ok = Assert.IsType(result); + return JsonNode.Parse(JsonSerializer.Serialize(ok.Value))!.AsArray() + .Select(node => node!.AsObject()) + .ToList(); + } + + /// A catalogue, read through the route that serves it to the page. + private static async Task> CatalogAsync(RecDBContext db, string table) + { + var originalKey = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + return RowsOf(await new AdminPanelAPI(db).List(table, Secret, null, null, null)); + } + finally + { + DBConfig.AdminApiKey = originalKey; + } + } + + /// Posts one gift_log row through the route, and returns the row it answered with. + private static async Task GiveAsync(RecDBContext db, string rowJson) + { + var api = new AdminPanelAPI(db); + var originalKey = DBConfig.AdminApiKey; + var originalLogs = DBConfig.PersistentLogs; + DBConfig.AdminApiKey = Secret; + DBConfig.PersistentLogs = Path.Join(Path.GetTempPath(), "radicade-admin-skin-tests-" + Guid.NewGuid().ToString("N")); + try + { + var payload = JsonDocument.Parse(rowJson).RootElement; + var ok = Assert.IsType(await api.Insert("gift_log", Secret, payload)); + return Assert.IsType(ok.Value); + } + finally + { + DBConfig.AdminApiKey = originalKey; + DBConfig.PersistentLogs = originalLogs; + } + } + + /// The box as the recipient's client reads it: GET api/avatar/v2/gifts. + private static async Task> ClientGiftsAsync(RecDBContext db, long playerId) + { + var api = new AvatarV2(db); + SetUser(api, playerId); + var ok = Assert.IsType(await api.GetGifts()); + return Assert.IsAssignableFrom>(ok.Value).ToList(); + } + + /// The recipient claims every box it holds, through POST api/avatar/v2/gifts/claim. + private static async Task ClaimAsync(RecDBContext db, long playerId) + { + var api = new AvatarV2(db); + SetUser(api, playerId); + return await api.ClaimGifts(); + } + + /// The equipment the client is served: GET api/equipment/v2/getUnlocked. + private static List ServedEquipment(RecDBContext db, long playerId) + { + var api = new EquipmentV2(db); + SetUser(api, playerId); + return api.GetUnlockedEquipment(); + } + + [Fact] + public async Task TheEquipmentListNamesTheSkinOfEveryRow() + { + var previous = DBConfig.DefaultEquipmentItems; + try + { + DBConfig.DefaultEquipmentItems = ShippedCatalog(); + + using var db = NewDb(); + var rows = await CatalogAsync(db, "equipment_items"); + + // The rows are real, and the nine basketball ones are the one prefab: the name alone + // cannot tell any of them from another, which is why one row used to answer for nine. + Assert.NotEmpty(rows); + + var names = rows.Select(row => row["equipment_prefab_name"]!.GetValue()).ToList(); + Assert.Equal(9, names.Count(name => name == "[Basketball]")); + + // So every row is named by its own guid, and no two rows share one - that is the value + // the gift page keys its equipment box by, and one row there is one choice. + var keys = rows.Select(row => row["modification_guid"]!.GetValue()).ToList(); + Assert.All(keys, guid => Assert.False(string.IsNullOrWhiteSpace(guid))); + Assert.Equal(keys.Count, keys.Distinct(StringComparer.OrdinalIgnoreCase).Count()); + } + finally + { + DBConfig.DefaultEquipmentItems = previous; + } + } + + [Fact] + public async Task ThePickedSkinIsWhatTheBoxCarries() + { + var db = NewDb(); + var skin = ShippedBasketballSkin(); + + // What the page posts for that row, both halves included. + var row = await GiveAsync(db, $$""" + { + "player_id": "{{Recipient}}", + "equip": "{{skin.PrefabName}}", + "equipment_modification_guid": "{{skin.ModificationGuid}}", + "message": "Here is your ball back.", + "context": "Game_Drop", + "rarity": "Uncommon" + } + """); + + Assert.Equal(true, row["sent"]?.GetValue()); + + // The panel's own record of what it did names the skin too. + Assert.Equal(skin.ModificationGuid, row["equipment_modification_guid"]!.GetValue()); + + // The box itself, stored against the recipient: both halves of the skin, not the bare tool. + var stored = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, Recipient)); + Assert.Equal("[Basketball]", stored.EquipmentPrefabName); + Assert.Equal(skin.ModificationGuid, stored.EquipmentModificationGuid); + + // And what the client reads off that stored box. + var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); + Assert.Equal("[Basketball]", read.EquipmentPrefabName); + Assert.Equal(skin.ModificationGuid, read.EquipmentModificationGuid); + } + + /// One box of one skin, posted the way the page posts it. + private static Task GiveSkinAsync(RecDBContext db, Equipment skin) => GiveAsync(db, $$""" + { + "player_id": "{{Recipient}}", + "equip": "{{skin.PrefabName}}", + "equipment_modification_guid": "{{skin.ModificationGuid}}", + "context": "Game_Drop", + "rarity": "Uncommon" + } + """); + + [Fact] + public async Task ClaimingTheBoxHandsThePlayerThatSkin() + { + var db = NewDb(); + var skin = ShippedBasketballSkin(); + + await GiveSkinAsync(db, skin); + var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, Recipient)); + + Assert.IsType(await ClaimAsync(db, Recipient)); + + // The box is gone... + Assert.DoesNotContain(await StorefrontShared.GetPendingGiftsAsync(db, Recipient), + gift => gift.GiftDropId == box.GiftDropId); + + // ...and the skin is the player's own. That row is the part no catalogue can account for: + // a shipped equipment list is merged into what the client is served either way, so the + // gift arriving is only visible here, on the player. + var unlocks = await db.Unlocks.FirstAsync(x => x.AccountId == Recipient); + var owned = Assert.Single(unlocks.GetEquipment()!, item => item.PrefabName == skin.PrefabName); + Assert.Equal(skin.ModificationGuid, owned.ModificationGuid); + + // And the pair is what the client is served from the list it resolves a skin from. + Assert.Single(ServedEquipment(db, Recipient), + item => item.PrefabName == skin.PrefabName && item.ModificationGuid == skin.ModificationGuid); + } + + [Fact] + public async Task ASkinOfAToolThePlayerAlreadyHasIsStillGiven() + { + var db = NewDb(); + var basketball = Shipped().Where(row => row.PrefabName == "[Basketball]").Take(2).ToList(); + var held = basketball[0]; + var coming = basketball[1]; + Assert.NotEqual(held.ModificationGuid, coming.ModificationGuid); + + // The player already has one skin of the tool, which is the ordinary case: the gift is + // asked for by whoever set it up, and what they are adding is the skin, not the tool. + var unlocks = new PlayerUnlocks { AccountId = Recipient }; + unlocks.AddEquipment(new Equipment + { + PrefabName = held.PrefabName, + ModificationGuid = held.ModificationGuid, + FriendlyName = held.FriendlyName + }); + db.Unlocks.Add(unlocks); + await db.SaveChangesAsync(); + + await GiveSkinAsync(db, coming); + Assert.IsType(await ClaimAsync(db, Recipient)); + + // Both skins, not the first one twice: asking "do they have this?" by prefab alone + // answered yes and threw the second skin away, with the box consumed and nothing said. + var equipment = (await db.Unlocks.FirstAsync(x => x.AccountId == Recipient)).GetEquipment()!; + var ownedSkins = equipment.Where(item => item.PrefabName == "[Basketball]").ToList(); + + Assert.Equal(2, ownedSkins.Count); + Assert.Contains(ownedSkins, item => item.ModificationGuid == held.ModificationGuid); + Assert.Contains(ownedSkins, item => item.ModificationGuid == coming.ModificationGuid); + } + + [Fact] + public async Task ABareToolIsStillJustTheTool() + { + var db = NewDb(); + + // A tool with no skin named is a tool with no skin. The route does not reach for a guid to + // make the box look complete, because the first guid it found would be some other skin. + await GiveAsync(db, $$""" + { + "player_id": "{{Recipient}}", + "equip": "[Basketball]", + "context": "Game_Drop", + "rarity": "Common" + } + """); + + var stored = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, Recipient)); + Assert.Equal("[Basketball]", stored.EquipmentPrefabName); + Assert.Equal("", stored.EquipmentModificationGuid); + } +} diff --git a/ServerTests/AdminPanelFractionalReputationTests.cs b/ServerTests/AdminPanelFractionalReputationTests.cs new file mode 100644 index 0000000..3ed0403 --- /dev/null +++ b/ServerTests/AdminPanelFractionalReputationTests.cs @@ -0,0 +1,110 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Moderation; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// PATCH admin/panel/players/{id} with the values the panel echoes back from its own GET. +/// +/// Noteriety and ReputationScore are doubles on Reputation (NoterietyPerCheer is 0.5), so an account +/// that has been cheered carries a fractional value. The endpoint used to read them with GetInt32, +/// which throws System.FormatException on a fractional JSON number - the whole edit answered 500 and +/// saved nothing. Because only accounts with a fractional score hit it, it looked account-specific. +/// +public class AdminPanelFractionalReputationTests +{ + private const string Secret = "test-bot-secret"; + private const long Player = 3; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static async Task WithPlayer() + { + var db = NewDb(); + db.Profiles.Add(new Profile + { + accountId = Player, + username = "fractional", + displayName = "Fractional Player", + profileImage = "" + }); + db.Reputations.Add(new Reputation + { + AccountId = Player, + Noteriety = 0.5, + ReputationScore = 7.5 + }); + await db.SaveChangesAsync(); + return db; + } + + private static async Task Patch(RecDBContext db, string json) + { + var payload = JsonDocument.Parse(json).RootElement.Clone(); + var api = new AdminPanelAPI(db); + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + return await api.Update("players", Player, Secret, payload); + } + finally + { + DBConfig.AdminApiKey = original; + } + } + + [Fact] + public async Task AFractionalNoterietyIsAcceptedAndStoredAsADouble() + { + using var db = await WithPlayer(); + + // Exactly what the panel sends for an account whose score is not a whole number. + var result = await Patch(db, """ + { + "display_name": "Fractional Player", + "reputation": 9.5, + "noteriety": 1.5, + "cheer_general": 3 + } + """); + + Assert.IsType(result); + + var rep = await db.Reputations.SingleAsync(x => x.AccountId == Player); + Assert.Equal(1.5, rep.Noteriety); + Assert.Equal(9.5, rep.ReputationScore); + Assert.Equal(3, rep.CheerGeneral); + } + + [Fact] + public async Task AnOutOfRangeOrNumericStringIntegerDoesNotThrow() + { + using var db = await WithPlayer(); + + // A value Int32 cannot hold, and an integer sent as a string, must both land (clamped / + // parsed) rather than 500 the edit. + var result = await Patch(db, """ + { + "noteriety": 0.5, + "cheer_helpful": "4", + "cheer_creative": 9999999999999999999 + } + """); + + Assert.IsType(result); + + var rep = await db.Reputations.SingleAsync(x => x.AccountId == Player); + Assert.Equal(4, rep.CheerHelpful); + Assert.Equal(int.MaxValue, rep.CheerCreative); + } +} diff --git a/ServerTests/AdminPanelPlayerActionTests.cs b/ServerTests/AdminPanelPlayerActionTests.cs new file mode 100644 index 0000000..ac39972 --- /dev/null +++ b/ServerTests/AdminPanelPlayerActionTests.cs @@ -0,0 +1,216 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The panel drives every player action through POST admin/panel/players/{id}/{kind}, and some of +/// those actions are named with a slash in them: the roles box's Add ("roles/add") and Clear +/// ("roles/clear"), plus settings/birthday, settings/delete, settings/reset and +/// settings/profilepicture. A slash-named action is two path segments, so routing hands the +/// controller kind="roles" and sub="add" - while the action the switch matches on is the rejoined +/// "roles/add". These tests hand the action its arguments exactly the way routing splits them, so +/// splitting the name again fails here instead of only in the deployed panel, where a miss shows +/// up as the toast "Gone" (the panel's name for 404). +/// +public class AdminPanelPlayerActionTests +{ + private const string Secret = "test-bot-secret"; + private const long Player = 2; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + /// A database holding just the profile a player action needs. + private static async Task WithPlayer(string? roles) + { + var db = NewDb(); + db.Profiles.Add(new Profile + { + accountId = Player, + username = "testplayer", + displayName = "Test Player", + profileImage = "", + role = roles + }); + await db.SaveChangesAsync(); + return db; + } + + /// + /// Runs one slash-named action the way the route delivers it: the request is + /// POST admin/panel/players/{id}/{kind}/{sub}, so the controller receives the name in halves. + /// + private static async Task SendAction(RecDBContext db, string kind, string sub, string query) + { + var context = new DefaultHttpContext(); + context.Request.QueryString = new QueryString("?" + query); + var api = new AdminPanelAPI(db) + { + ControllerContext = new ControllerContext { HttpContext = context } + }; + + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + return await api.PlayerPanelSubAction(Player, kind, sub, Secret); + } + finally + { + DBConfig.AdminApiKey = original; + } + } + + private static async Task StoredProfile(RecDBContext db) + => await db.Profiles.SingleAsync(x => x.accountId == Player); + + /// + /// The link every test here rests on: the URL the panel builds for a slash-named action - + /// POST admin/panel/players/{id}/roles/add - is the route whose last two segments are + /// {kind}/{sub}, and the values it hands the action really are kind="roles", sub="add". + /// If the route is ever reshaped so the panel's own URL no longer lands there, the actions + /// below would start passing for the wrong reason; this fails first. + /// + [Fact] + public void ThePanelsUrlForASlashNamedActionBindsKindAndSub() + { + var matcher = new TemplateMatcher( + TemplateParser.Parse("admin/panel/players/{playerId:long}/{kind}/{sub}"), + new RouteValueDictionary()); + var values = new RouteValueDictionary(); + + Assert.True(matcher.TryMatch(new PathString("/admin/panel/players/2/roles/add"), values)); + Assert.Equal("2", values["playerId"]); + Assert.Equal("roles", values["kind"]); + Assert.Equal("add", values["sub"]); + } + + [Fact] + public async Task AddingRolesStoresThemOnThePlayer() + { + var db = await WithPlayer(null); + + var result = await SendAction(db, "roles", "add", "roles=developer,moderator"); + + Assert.IsType(result); + + var profile = await StoredProfile(db); + Assert.Equal("developer,moderator", profile.role); + + // The stored role is what the game reads, so the flags have to follow it. + Assert.True(profile.HasRole("moderator")); + Assert.False(profile.HasRole("influencer")); + } + + [Fact] + public async Task AddingARoleKeepsTheRolesThePlayerAlreadyHad() + { + var db = await WithPlayer("creatorforum"); + + // The panel's boxes are ticked independently, so it can send a role the player already + // holds alongside new ones; that must not duplicate it or drop what was already there. + var result = await SendAction(db, "roles", "add", "roles=Developer,creatorforum,keepsake"); + + Assert.IsType(result); + Assert.Equal("creatorforum,Developer,keepsake", (await StoredProfile(db)).role); + } + + [Fact] + public async Task AddingWithNothingPickedIsRefusedRatherThanClearingTheRoles() + { + var db = await WithPlayer("developer"); + + // The panel keeps Add disabled until a box is ticked, but a lost pick must still be a + // refusal - "picked nothing" must never read as "clear what this player has". + var result = await SendAction(db, "roles", "add", ""); + + Assert.IsType(result); + Assert.Equal("developer", (await StoredProfile(db)).role); + } + + [Fact] + public async Task ClearingRolesEmptiesThem() + { + var db = await WithPlayer("developer,moderator,keepsake"); + + var result = await SendAction(db, "roles", "clear", ""); + + Assert.IsType(result); + + var profile = await StoredProfile(db); + Assert.Equal("", profile.role); + Assert.False(profile.HasRole("developer")); + Assert.False(profile.HasRole("moderator")); + } + + [Fact] + public async Task ClearingRolesOnAPlayerWithNoneLeftIsStillAccepted() + { + var db = await WithPlayer(null); + + // Nothing to clear is not an error: the panel's confirm dialog already ran. + var result = await SendAction(db, "roles", "clear", ""); + + Assert.IsType(result); + Assert.Equal("", (await StoredProfile(db)).role); + } + + [Fact] + public async Task TheOtherSlashNamedActionsReachTheirCasesToo() + { + var db = await WithPlayer("developer"); + + // Fixing the split must lift every slash-named action, not just the roles box - the + // settings panels build their URLs the same way. + var result = await SendAction(db, "settings", "birthday", "birthday=2004-02-29"); + + Assert.IsType(result); + + var settings = await db.PlayerSettings.SingleAsync(x => x.AccountId == Player); + Assert.Equal("2004-02-29", settings.DeserializeSettings().Single(x => x.Key == "birthday").Value); + } + + [Fact] + public async Task AnUnknownSlashNamedActionIsStillNotFound() + { + var db = await WithPlayer("developer"); + + // Rejoining the halves must not turn the route into a catch-all. + var result = await SendAction(db, "roles", "bogus", ""); + + var notFound = Assert.IsType(result); + Assert.Equal("Unknown player action 'roles/bogus'.", notFound.Value); + } + + [Fact] + public async Task ASingleSegmentActionStillReachesItsCase() + { + var db = await WithPlayer("developer"); + + // The one-segment route and the two-segment one share the same switch; a plain action + // (no slash in its name) still arrives whole. + var api = new AdminPanelAPI(db); + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + var result = await api.PlayerPanelAction(Player, "unban", Secret); + Assert.IsType(result); + } + finally + { + DBConfig.AdminApiKey = original; + } + } +} diff --git a/ServerTests/AdminPanelPlayerTimesTests.cs b/ServerTests/AdminPanelPlayerTimesTests.cs new file mode 100644 index 0000000..c760f25 --- /dev/null +++ b/ServerTests/AdminPanelPlayerTimesTests.cs @@ -0,0 +1,126 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Enums.Players; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The players list sends joined_at and last_online_at so the panel can render them in the +/// admin's own time zone. SQL Server hands a datetime2 back with Kind=Unspecified, and +/// System.Text.Json writes such a value with no trailing "Z"; a browser parses a zone-less ISO +/// string as already-local time, so an offline player's "Last Online" sat one admin time-zone +/// offset away from the truth - it looked hours into the future. Only the online branch carried +/// the marker, because there the value is DateTime.UtcNow. These tests pin the instants down +/// exactly: the fix is the marker, never a shift of the clock itself. +/// +public class AdminPanelPlayerTimesTests +{ + private const string Secret = "test-bot-secret"; + private const long Player = 2; + + /// Unspecified is what SQL Server gives back for a datetime2, so that is what the fixtures store. + private static readonly DateTime SignedUp = new(2026, 7, 29, 0, 56, 0, DateTimeKind.Unspecified); + private static readonly DateTime LastLogin = new(2026, 9, 30, 21, 38, 0, DateTimeKind.Unspecified); + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + /// A database holding one player, with or without the cached login row that records their last sign-in. + private static async Task WithPlayer(bool withLogin) + { + var db = NewDb(); + db.Profiles.Add(new Profile + { + accountId = Player, + username = "coach", + displayName = "Coach", + profileImage = "", + createdAt = SignedUp + }); + + if (withLogin) + { + db.CachedLogins.Add(new CachedLogin + { + accountId = Player, + platform = Platforms.Steam, + platformId = "76561198000000000", + lastLoginTime = LastLogin + }); + } + + await db.SaveChangesAsync(); + return db; + } + + /// The two timestamps exactly as the panel's JSON.parse receives them. + private static async Task<(string Joined, string LastOnline)> PlayerTimes(RecDBContext db) + { + var context = new DefaultHttpContext(); + context.Request.QueryString = new QueryString("?key=" + Secret); + var api = new AdminPanelAPI(db) + { + ControllerContext = new ControllerContext { HttpContext = context } + }; + + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + var ok = Assert.IsType(await api.List("players", Secret, null, null, null, true)); + + // Same writer the panel is served by, so this is the wire text the browser sees + // rather than a stand-in for it. + using var document = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value)); + var row = document.RootElement[0]; + return (row.GetProperty("joined_at").GetString()!, row.GetProperty("last_online_at").GetString()!); + } + finally + { + DBConfig.AdminApiKey = original; + } + } + + [Fact] + public async Task AnOfflinePlayersLastOnlineIsMarkedAsUtc() + { + var db = await WithPlayer(withLogin: true); + + var (_, lastOnline) = await PlayerTimes(db); + + // 21:38 UTC is the moment they signed out, and the panel turns that into whatever o'clock + // the admin's own zone reads it as. Without the "Z" the browser read it as 21:38 local. + Assert.Equal("2026-09-30T21:38:00Z", lastOnline); + } + + [Fact] + public async Task TheAccountAgeIsMarkedAsUtcToo() + { + var db = await WithPlayer(withLogin: true); + + var (joined, _) = await PlayerTimes(db); + + Assert.Equal("2026-07-29T00:56:00Z", joined); + } + + [Fact] + public async Task APlayerWhoNeverLoggedInFallsBackToTheirAccountAge() + { + var db = await WithPlayer(withLogin: false); + + var (joined, lastOnline) = await PlayerTimes(db); + + // No cached login row means the profile's own createdAt is the newest thing known about them. + Assert.Equal("2026-07-29T00:56:00Z", joined); + Assert.Equal("2026-07-29T00:56:00Z", lastOnline); + } +} diff --git a/ServerTests/AdminRoomCreateTests.cs b/ServerTests/AdminRoomCreateTests.cs new file mode 100644 index 0000000..251ddf8 --- /dev/null +++ b/ServerTests/AdminRoomCreateTests.cs @@ -0,0 +1,181 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Room creation through POST admin/rooms/create/room, which is how every import puts a +/// room on this server (the Taco archives, tools/import_room_dump.mjs). +/// +/// The route reads its body with a helper that only knew scalars. Enums and arrays are neither, +/// so Accessibility, WarningMask, MaxPlayerCalculationMode and Tags +/// silently took their defaults while the strings and bools around them bound: an imported room +/// arrived Private (invisible to every list, unsearchable) and untagged, with nothing in the +/// response to say a field had been dropped. These pin the whole payload, and the defaults the +/// route still falls back to when a field is absent. +/// +public class AdminRoomCreateTests +{ + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private const string Secret = "test-bot-secret"; + + private static async Task CreateAsync(string json) + { + var db = NewDb(); + var api = new AdminRoomAPI(db); + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + var result = await api.CreateRoom(Secret, 2, JsonNode.Parse(json)!); + Assert.IsType(result); + } + finally + { + DBConfig.AdminApiKey = original; + } + + return db; + } + + private const string RoomPayload = """ + { + "Name": "MakeItToMidnight", + "Description": "A night of horror ensues.", + "Accessibility": 1, + "State": 0, + "IsRRO": true, + "IsDorm": false, + "ImageName": "default.png", + "MaxPlayers": 5, + "MaxPlayerCalculationMode": 1, + "WarningMask": 1, + "CloningAllowed": false, + "SupportsScreens": 1, + "SupportsWalkVR": true, + "SupportsTeleportVR": "true", + "SupportsJuniors": "1", + "Tags": [ { "Tag": "import", "Type": 1 } ], + "CreatedAt": "2023-07-26T23:23:35Z", + "SubRooms": [ + { + "Name": "Home", + "Accessibility": 1, + "IsSandbox": true, + "MaxPlayers": 5, + "UnitySceneId": "a75f7547-79eb-47c6-8986-6767abcb4f92", + "DataBlob": "aq4a6u116zm6uathf9i9b7n0y.room" + } + ] + } + """; + + [Fact] + public async Task AnImportedRoomKeepsItsAccessibilityWarningMaskAndPlayerCountMode() + { + var db = await CreateAsync(RoomPayload); + var room = db.Rooms.Single(); + + Assert.Equal(Accessibility.Public, room.Accessibility); + Assert.Equal(WarningMask.Scary, room.WarningMask); + Assert.Equal(MaxPlayerMode.OnlyEntrySubrooms, room.MaxPlayerCalculationMode); + Assert.Equal(5, room.MaxPlayers); + Assert.True(room.IsRRO); + Assert.False(room.IsDorm); + Assert.Equal(2, room.CreatorAccountId); + Assert.Equal(new DateTime(2023, 7, 26, 23, 23, 35), room.CreatedAt); + } + + [Fact] + public async Task TheTagListIsKeptWithItsTagTypes() + { + var db = await CreateAsync(RoomPayload); + + // One tag, of type Auto - the same "name,type|name,type" string the room's own tag + // screen writes, which is what the tag doors and the list queries read back. + Assert.Equal("import,1", db.Rooms.Single().Tags); + } + + [Fact] + public async Task EnumsArriveAsNumbersOrNamesAndBoolsAsTrueOnesOrStrings() + { + var db = await CreateAsync(""" + { + "Name": "NamedEnums", + "Accessibility": "Public", + "WarningMask": "Scary", + "MaxPlayerCalculationMode": "OnlyEntrySubrooms", + "SupportsScreens": 1, + "SupportsWalkVR": true, + "SupportsQuest2": "true", + "SupportsTeleportVR": "false", + "IsRRO": 1 + } + """); + var room = db.Rooms.Single(); + + Assert.Equal(Accessibility.Public, room.Accessibility); + Assert.Equal(WarningMask.Scary, room.WarningMask); + Assert.Equal(MaxPlayerMode.OnlyEntrySubrooms, room.MaxPlayerCalculationMode); + Assert.True(room.SupportsScreens); + Assert.True(room.SupportsWalkVR); + Assert.True(room.SupportsQuest2); + Assert.False(room.SupportsTeleportVR); + Assert.True(room.IsRRO); + } + + [Fact] + public async Task AnAbsentFieldStillTakesTheRoutesDefault() + { + var db = await CreateAsync("""{ "Name": "BareRoom" }"""); + var room = db.Rooms.Single(); + + Assert.Equal(Accessibility.Private, room.Accessibility); + Assert.Equal(WarningMask.None, room.WarningMask); + Assert.Equal(MaxPlayerMode.AllSubrooms, room.MaxPlayerCalculationMode); Assert.Equal("", room.Tags); + Assert.False(room.IsRRO); + + // LoadScreens is the one field whose absence used to fail the insert outright: the + // column is NOT NULL and the payload's missing array was written over the default. + Assert.Equal("[]", room.loadScreen); + } + + [Fact] + public async Task TheSubroomKeepsTheSceneAndTheSaveItWasImportedWith() + { + var db = await CreateAsync(RoomPayload); + var subRoom = db.SubRooms.Single(); + + Assert.Equal("Home", subRoom.Name); + Assert.Equal("a75f7547-79eb-47c6-8986-6767abcb4f92", subRoom.UnitySceneId); + Assert.Equal("aq4a6u116zm6uathf9i9b7n0y.room", subRoom.DataBlob); + Assert.Equal(2, subRoom.SavedByAccountId); + Assert.True(subRoom.IsSandbox); + Assert.Equal(Accessibility.Public, subRoom.Accessibility); + } + + [Fact] + public async Task ASceneTheClientDoesNotKnowBecomesTheMakerRoom() + { + var db = await CreateAsync(""" + { + "Name": "OddScene", + "SubRooms": [ { "Name": "Home", "UnitySceneId": "not-a-scene", "DataBlob": "empty.room" } ] + } + """); + + Assert.Equal(SceneLocations.MakerRoom, db.SubRooms.Single().UnitySceneId); + } +} diff --git a/ServerTests/AdminRoomImportTests.cs b/ServerTests/AdminRoomImportTests.cs new file mode 100644 index 0000000..fdc3f4d --- /dev/null +++ b/ServerTests/AdminRoomImportTests.cs @@ -0,0 +1,308 @@ +using System.Net; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Importing a subroom is three files, not one. A subroom loads a .room save, that save +/// references its data/audio blobs by name (GET /data/<name>.htr), and its Unity scene +/// is fetched through /rooms/unity_assets/<name> by the name the bundle is stored as. +/// +/// The admin upload route used to rename whatever it was handed to a fresh GUID and only accept +/// .room/.htr, so an import kept the room and lost the rest: every .htr and the +/// bundle answered 404 under the name the save pointed at. These pin the three names travelling +/// intact, the bundle binding, and the refusal of anything that is not one of the three. +/// +[Collection(DbFactoryCollection.Name)] +public class AdminRoomImportTests +{ + private const string Secret = "room-import-test-key"; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + /// A CDN directory of its own, since every one of these routes writes into the storage root. + private sealed class Storage : IDisposable + { + private readonly string _previous = DBConfig.CDNStorage; + + public Storage() + { + Root = Directory.CreateTempSubdirectory("radicade-room-import-").FullName; + DBConfig.CDNStorage = Root; + } + + public string Root { get; } + + public string Write(string name, string contents = "blob") + { + Directory.CreateDirectory(Root); + File.WriteAllText(Path.Join(Root, name), contents); + return name; + } + + public void Dispose() + { + DBConfig.CDNStorage = _previous; + try { Directory.Delete(Root, recursive: true); } catch (IOException) { } + } + } + + private sealed class AdminKey : IDisposable + { + private readonly string _previous = DBConfig.AdminApiKey; + + public AdminKey() => DBConfig.AdminApiKey = Secret; + + public void Dispose() => DBConfig.AdminApiKey = _previous; + } + + // ------------------------------------------------------------------ names + + [Theory] + [InlineData("Showdown_0_1.assetbundle")] + [InlineData("8aas7b5ivtaggn9swhe3wkgdj.assetbundle")] + [InlineData(" Showdown_0_1.assetbundle ")] + public void AnUploadedBundleNameIsAccepted(string requested) + { + using var storage = new Storage(); + storage.Write("Showdown_0_1.assetbundle"); + storage.Write("8aas7b5ivtaggn9swhe3wkgdj.assetbundle"); + + Assert.True(AdminUtils.TryResolveBundleName(requested, out var name, out var error)); + Assert.Null(error); + Assert.Equal(requested.Trim(), name); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("Showdown_0_1.room")] + [InlineData("Showdown_0_1")] + [InlineData("Showdown_0_1.assetbundle.bak")] + [InlineData("../Showdown_0_1.assetbundle")] + [InlineData("sub/Showdown_0_1.assetbundle")] + [InlineData("Home bundle.assetbundle")] + [InlineData("missing.assetbundle")] + public void AnythingThatIsNotAnUploadedBundleIsRefused(string? requested) + { + using var storage = new Storage(); + storage.Write("Showdown_0_1.assetbundle"); + + Assert.False(AdminUtils.TryResolveBundleName(requested, out var name, out var error)); + Assert.Null(name); + Assert.NotNull(error); + } + + [Fact] + public void BundleNamesAndRoomSaveNamesAreNotInterchangeable() + { + using var storage = new Storage(); + storage.Write("Showdown.room"); + storage.Write("Showdown_0_1.assetbundle"); + + // A .room is a save blob, never a subroom's Unity bundle. + Assert.False(AdminUtils.TryResolveBundleName("Showdown.room", out _, out _)); + Assert.True(AdminUtils.TryResolveUploadName("Showdown.room", out var saveName, out _)); + Assert.Equal("Showdown.room", saveName); + } + + // ----------------------------------------------------------------- binding + + [Fact] + public async Task BindingPointsTheSubroomAtTheBundleAndClearingTakesItBackOff() + { + using var storage = new Storage(); + using var key = new AdminKey(); + var bundle = storage.Write("Showdown_0_1.assetbundle"); + storage.Write("Showdown.room"); + + await using var db = NewDb(); + db.SubRooms.Add(new SubRoom { RoomId = 58, SubRoomId = 61, Name = "Home", DataBlob = "Showdown.room" }); + await db.SaveChangesAsync(); + var api = new AdminRoomAPI(db); + + Assert.IsType(await api.SetBundle("wrong", 58, "Home", bundle)); + Assert.IsType(await api.SetBundle(Secret, 59, "Home", bundle)); + Assert.IsType(await api.SetBundle(Secret, 58, "Basement", bundle)); + + // A blob that is not a bundle, and a bundle that was never uploaded, are both refused + // rather than bound to a name the client would then fail to fetch. + Assert.IsType(await api.SetBundle(Secret, 58, "Home", "Showdown.room")); + Assert.IsType(await api.SetBundle(Secret, 58, "Home", "absent.assetbundle")); + + Assert.IsType(await api.SetBundle(Secret, 58, "Home", bundle)); + db.ChangeTracker.Clear(); + var stored = await db.SubRooms.SingleAsync(); + Assert.Equal(bundle, stored.UnityAssetId); + Assert.Equal("Showdown.room", stored.DataBlob); + + Assert.IsType(await api.SetBundle(Secret, 58, "Home", "")); + db.ChangeTracker.Clear(); + Assert.Equal("", (await db.SubRooms.SingleAsync()).UnityAssetId); + } + + [Fact] + public async Task CreatingASubroomTakesTheBundleThatWasImportedWithIt() + { + using var storage = new Storage(); + using var key = new AdminKey(); + storage.Write("Showdown_0_1.assetbundle"); + + await using var db = NewDb(); + db.Rooms.Add(new RoomData { RoomId = 58, Name = "Showdown", CreatorAccountId = 1, MaxPlayers = 6 }); + await db.SaveChangesAsync(); + var api = new AdminRoomAPI(db); + + var created = await api.CreateSubRoom(Secret, 58, JsonNode.Parse(""" + { + "Name": "Home", + "Accessibility": 1, + "MaxPlayers": 6, + "UnitySceneId": "a75f7547-79eb-47c6-8986-6767abcb4f92", + "DataBlob": "Showdown.room", + "UnityAssetId": "Showdown_0_1.assetbundle" + } + """)!); + Assert.IsType(created); + + db.ChangeTracker.Clear(); + var subRoom = await db.SubRooms.SingleAsync(); + Assert.Equal("Showdown.room", subRoom.DataBlob); + Assert.Equal("Showdown_0_1.assetbundle", subRoom.UnityAssetId); + + // A bundle that is not on disk fails the create instead of leaving a subroom pointing at + // nothing. + var failed = await api.CreateSubRoom(Secret, 58, JsonNode.Parse(""" + { "Name": "Basement", "DataBlob": "basement.room", "UnityAssetId": "absent.assetbundle" } + """)!); + Assert.IsType(failed); + db.ChangeTracker.Clear(); + Assert.Single(await db.SubRooms.ToListAsync()); + } + + [Fact] + public async Task CreatingARoomTakesTheBundleForItsEntrySubroom() + { + using var storage = new Storage(); + using var key = new AdminKey(); + storage.Write("Showdown_0_1.assetbundle"); + + await using var db = NewDb(); + var api = new AdminRoomAPI(db); + + var created = await api.CreateRoom(Secret, 2, JsonNode.Parse(""" + { + "Name": "Showdown", + "Accessibility": 1, + "MaxPlayers": 6, + "SubRooms": [ + { + "Name": "Home", + "DataBlob": "Showdown.room", + "UnityAssetId": "Showdown_0_1.assetbundle" + } + ] + } + """)!); + Assert.IsType(created); + + var subRoom = await db.SubRooms.SingleAsync(); + Assert.Equal("Showdown.room", subRoom.DataBlob); + Assert.Equal("Showdown_0_1.assetbundle", subRoom.UnityAssetId); + } + + // ----------------------------------------------------------------- uploads + + /// The route itself, over real Kestrel and MVC model binding: whatever the admin sends is what + /// the CDN ends up holding, one file at a time, under the name it was sent with. + [Fact] + public async Task EachRoomFileLandsInTheCdnUnderTheNameItWasUploadedAs() + { + using var storage = new Storage(); + using var key = new AdminKey(); + + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.Logging.ClearProviders(); + builder.Services.AddControllers().AddApplicationPart(typeof(AdminPanelAPI).Assembly); + builder.Services.AddCors(options => options.AddPolicy("AdminPanel", policy => policy.AllowAnyOrigin())); + builder.Services.Configure(options => options.MultipartBodyLengthLimit = 256L * 1024 * 1024); + builder.Services.AddDbContext(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); + await using var app = builder.Build(); + app.UseCors(); + app.MapControllers(); + + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) }; + + async Task<(HttpStatusCode Status, JsonElement Body)> PostAsync(string fileName, byte[] bytes, string? name = null) + { + using var form = new MultipartFormDataContent(); + form.Add(new ByteArrayContent(bytes), "file", fileName); + var query = $"/admin/panel/upload/room?key={Secret}" + (name == null ? "" : $"&name={Uri.EscapeDataString(name)}"); + using var response = await client.PostAsync(query, form); + var body = await response.Content.ReadAsStringAsync(); + var parsed = body.StartsWith('{') ? JsonDocument.Parse(body).RootElement : default; + return (response.StatusCode, parsed); + } + + var (roomStatus, roomBody) = await PostAsync("Showdown.room", new byte[] { 1, 2, 3 }); + Assert.Equal(HttpStatusCode.OK, roomStatus); + Assert.Equal("room", roomBody.GetProperty("kind").GetString()); + Assert.Equal("Showdown.room", roomBody.GetProperty("name").GetString()); + Assert.Equal(new byte[] { 1, 2, 3 }, await File.ReadAllBytesAsync(Path.Join(storage.Root, "Showdown.room"))); + + var htr = "315cb6bf6af14c0b83bfe2fdc8ef0c79.htr"; + var (htrStatus, htrBody) = await PostAsync(htr, new byte[] { 4, 5 }); + Assert.Equal(HttpStatusCode.OK, htrStatus); + Assert.Equal("htr", htrBody.GetProperty("kind").GetString()); + Assert.True(File.Exists(Path.Join(storage.Root, htr))); + + var (bundleStatus, bundleBody) = await PostAsync("Showdown_0_1.assetbundle", new byte[] { 6 }); + Assert.Equal(HttpStatusCode.OK, bundleStatus); + Assert.Equal("assetbundle", bundleBody.GetProperty("kind").GetString()); + Assert.True(File.Exists(Path.Join(storage.Root, "Showdown_0_1.assetbundle"))); + + // An explicit ?name= still wins, which is how the Data Import page stores a renamed file. + var (renamedStatus, _) = await PostAsync("whatever.bin", new byte[] { 7 }, name: "renamed.room"); + Assert.Equal(HttpStatusCode.OK, renamedStatus); + Assert.True(File.Exists(Path.Join(storage.Root, "renamed.room"))); + + var (refusedStatus, _) = await PostAsync("archive.zip", new byte[] { 8 }); + Assert.Equal(HttpStatusCode.BadRequest, refusedStatus); + Assert.False(File.Exists(Path.Join(storage.Root, "archive.zip"))); + + // A name the CDN could never be asked for is refused rather than quietly renamed. + var (oddNameStatus, _) = await PostAsync("spot the odd name.room", new byte[] { 9 }); + Assert.Equal(HttpStatusCode.BadRequest, oddNameStatus); + + Assert.Equal( + new[] { "renamed.room", "Showdown.room" }.OrderBy(x => x), + Directory.GetFiles(storage.Root, "*.room").Select(Path.GetFileName).OrderBy(x => x)); + + // The uploaded bundle is now something the panel can list and bind. + using var uploads = await client.GetAsync($"/admin/panel/uploads?key={Secret}"); + var uploadsBody = await uploads.Content.ReadAsStringAsync(); + Assert.Contains("Showdown_0_1.assetbundle", uploadsBody); + Assert.Contains("assetbundles", uploadsBody); + + await app.StopAsync(); + } +} diff --git a/ServerTests/AdminRoomPartUploadTests.cs b/ServerTests/AdminRoomPartUploadTests.cs new file mode 100644 index 0000000..2ce16dd --- /dev/null +++ b/ServerTests/AdminRoomPartUploadTests.cs @@ -0,0 +1,280 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Importing a large room file, over real Kestrel and MVC model binding. +/// +/// The room upload route never raised Kestrel's default 30 MB request ceiling, so every +/// .room, .htr and .assetbundle above it - the user's own Showdown_0_1.assetbundle is 73 MB - +/// had its connection killed part-way through the body. The panel saw a broken connection +/// instead of a refusal, which is what made a big import look like an authentication failure, +/// and every big room import failed. These pin the ceiling the route is actually held to, +/// the part-by-part path the panel now uses for large files, and the rule that a file being +/// assembled from parts is invisible until it is whole. +/// +[Collection(DbFactoryCollection.Name)] +public class AdminRoomPartUploadTests +{ + private const string Secret = "room-part-test-key"; + private const int PartBytes = 4 * 1024 * 1024; + + private sealed class Storage : IDisposable + { + private readonly string _previousStorage = DBConfig.CDNStorage; + private readonly string _previousKey = DBConfig.AdminApiKey; + + public Storage() + { + Root = Directory.CreateTempSubdirectory("radicade-room-part-").FullName; + DBConfig.CDNStorage = Root; + DBConfig.AdminApiKey = Secret; + } + + public string Root { get; } + + public void Dispose() + { + DBConfig.CDNStorage = _previousStorage; + DBConfig.AdminApiKey = _previousKey; + try { Directory.Delete(Root, recursive: true); } catch (IOException) { } + } + } + + private static WebApplication BuildApp() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.Logging.ClearProviders(); + builder.Services.AddControllers().AddApplicationPart(typeof(AdminPanelAPI).Assembly); + builder.Services.AddCors(options => options.AddPolicy("AdminPanel", policy => policy.AllowAnyOrigin())); + builder.Services.Configure(options => options.MultipartBodyLengthLimit = AdminUtils.MaxUploadBytes); + builder.Services.AddDbContext(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); + var app = builder.Build(); + app.UseCors(); + app.MapControllers(); + return app; + } + + private static byte[] Bytes(long length, int seed = 7) + { + var bytes = new byte[length]; + new Random(seed).NextBytes(bytes); + return bytes; + } + + private static async Task<(HttpStatusCode Status, JsonElement Body)> PostPartAsync( + HttpClient client, string name, long offset, long total, byte[] chunk) + { + using var content = new ByteArrayContent(chunk); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + using var response = await client.PostAsync( + $"/admin/panel/upload/part?key={Secret}&name={Uri.EscapeDataString(name)}&offset={offset}&total={total}", content); + var text = await response.Content.ReadAsStringAsync(); + return (response.StatusCode, text.StartsWith('{') ? JsonDocument.Parse(text).RootElement : default); + } + + /// A body bigger than the old 30 MB ceiling now arrives intact, byte for byte, under the + /// name it was sent as - the failure that started this, over real Kestrel. + [Fact] + public async Task ARoomFileBiggerThanKestrelsDefaultCeilingStillImports() + { + using var storage = new Storage(); + await using var app = BuildApp(); + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()), Timeout = TimeSpan.FromMinutes(5) }; + + var bytes = Bytes(40L * 1024 * 1024, seed: 11); + using var form = new MultipartFormDataContent(); + form.Add(new ByteArrayContent(bytes), "file", "Showdown_0_1.assetbundle"); + using var response = await client.PostAsync( + $"/admin/panel/upload/room?key={Secret}&name=Showdown_0_1.assetbundle", form); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("assetbundle", JsonDocument.Parse(body).RootElement.GetProperty("kind").GetString()); + Assert.Equal( + SHA256.HashData(bytes), + SHA256.HashData(await File.ReadAllBytesAsync(Path.Join(storage.Root, "Showdown_0_1.assetbundle")))); + + await app.StopAsync(); + } + + /// A large file sent in parts lands as one file, byte for byte, under its own name. + [Fact] + public async Task AFileSentInPartsIsAssembledUnderTheNameItWasSentWith() + { + using var storage = new Storage(); + await using var app = BuildApp(); + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) }; + + var bytes = Bytes(10L * 1024 * 1024 + 1234, seed: 3); + JsonElement last = default; + for (long offset = 0; offset < bytes.LongLength; offset += PartBytes) + { + var chunk = bytes[(int)offset..(int)Math.Min(offset + PartBytes, bytes.LongLength)]; + var (status, body) = await PostPartAsync(client, "Showdown.room", offset, bytes.LongLength, chunk); + Assert.Equal(HttpStatusCode.OK, status); + Assert.Equal(offset + chunk.Length, body.GetProperty("received").GetInt64()); + Assert.Equal(bytes.LongLength, body.GetProperty("total").GetInt64()); + last = body; + } + + Assert.True(last.GetProperty("complete").GetBoolean()); + Assert.Equal("Showdown.room", last.GetProperty("name").GetString()); + Assert.Equal("room", last.GetProperty("kind").GetString()); + Assert.Equal( + SHA256.HashData(bytes), + SHA256.HashData(await File.ReadAllBytesAsync(Path.Join(storage.Root, "Showdown.room")))); + + // Nothing of the assembly is left lying around once the file is whole. + Assert.Empty(Directory.GetFiles(storage.Root, "*" + AdminUtils.PartSuffix)); + + await app.StopAsync(); + } + + /// A part that does not line up with what is stored is refused, and told where to continue, + /// so a part retried after a lost answer resumes instead of corrupting the file. + [Fact] + public async Task APartThatDoesNotLineUpIsRefusedWithWhereToContinue() + { + using var storage = new Storage(); + await using var app = BuildApp(); + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) }; var bytes = Bytes(10L * 1024 * 1024, seed: 5); + var first = bytes[..PartBytes]; + var (firstStatus, _) = await PostPartAsync(client, "Showdown.room", 0, bytes.LongLength, first); Assert.Equal(HttpStatusCode.OK, firstStatus); + // One part is stored, and the file it belongs to is not there yet. + Assert.Equal(PartBytes, new FileInfo(Path.Join(storage.Root, "Showdown.room" + AdminUtils.PartSuffix)).Length); + Assert.False(File.Exists(Path.Join(storage.Root, "Showdown.room"))); // Jumped ahead: the file holds one part, and the answer says so. + var ahead = bytes[(PartBytes * 2)..]; + var (aheadStatus, aheadBody) = await PostPartAsync(client, "Showdown.room", PartBytes * 2, bytes.LongLength, ahead); + Assert.Equal(HttpStatusCode.Conflict, aheadStatus); + Assert.Equal("part_out_of_order", aheadBody.GetProperty("error").GetString()); + Assert.Equal(PartBytes, aheadBody.GetProperty("stored").GetInt64()); + + // Re-sent from where the server actually is, it finishes cleanly. + var rest = bytes[PartBytes..]; + var (restStatus, restBody) = await PostPartAsync(client, "Showdown.room", aheadBody.GetProperty("stored").GetInt64(), bytes.LongLength, rest); + Assert.Equal(HttpStatusCode.OK, restStatus); + Assert.True(restBody.GetProperty("complete").GetBoolean()); + Assert.Equal( + SHA256.HashData(bytes), + SHA256.HashData(await File.ReadAllBytesAsync(Path.Join(storage.Root, "Showdown.room")))); + + await app.StopAsync(); + } + + /// A part of a file being uploaded is not an upload: it is hidden from every listing, and + /// only the finished file appears. + [Fact] + public async Task AFileStillBeingAssembledIsNotListedAsAnUpload() + { + using var storage = new Storage(); + await using var app = BuildApp(); + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) }; + + var bytes = Bytes(6L * 1024 * 1024, seed: 9); + var (partStatus, _) = await PostPartAsync(client, "Showdown_0_1.assetbundle", 0, bytes.LongLength, bytes[..PartBytes]); + Assert.Equal(HttpStatusCode.OK, partStatus); + + using var half = await client.GetAsync($"/admin/panel/uploads?key={Secret}"); + var halfBody = await half.Content.ReadAsStringAsync(); + Assert.DoesNotContain("Showdown_0_1.assetbundle", halfBody); + Assert.DoesNotContain(AdminUtils.PartSuffix, halfBody); + + var (restStatus, _) = await PostPartAsync(client, "Showdown_0_1.assetbundle", PartBytes, bytes.LongLength, bytes[PartBytes..]); + Assert.Equal(HttpStatusCode.OK, restStatus); + + using var whole = await client.GetAsync($"/admin/panel/uploads?key={Secret}"); + var wholeBody = await whole.Content.ReadAsStringAsync(); + Assert.Contains("Showdown_0_1.assetbundle", wholeBody); + Assert.DoesNotContain(AdminUtils.PartSuffix, wholeBody); + + await app.StopAsync(); + } + + /// A part cannot be stored under a partial name, and a file beyond the ceiling cannot be + /// imported this way at all - both are refused before anything is written. + [Fact] + public async Task APartialNameAndAnOversizedFileAreRefused() + { + using var storage = new Storage(); + await using var app = BuildApp(); + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) }; + + var chunk = Bytes(1024); + var (partialName, _) = await PostPartAsync(client, "Showdown.room.uploading", 0, 2048, chunk); + Assert.Equal(HttpStatusCode.BadRequest, partialName); + + var (tooBig, _) = await PostPartAsync(client, "Showdown.room", 0, AdminUtils.MaxUploadBytes + 1, chunk); + Assert.Equal(HttpStatusCode.BadRequest, tooBig); + + // A part carrying more than the file it belongs to says the import was discarded. + var (overrun, _) = await PostPartAsync(client, "Showdown.room", 0, 512, chunk); + Assert.Equal(HttpStatusCode.BadRequest, overrun); + + Assert.Empty(Directory.GetFiles(storage.Root)); + + await app.StopAsync(); + } + + /// Starting over at zero begins the file again rather than appending to an abandoned + /// attempt, so a retried import cannot end up as two files welded together. + [Fact] + public async Task StartingAgainAtZeroReplacesAnAbandonedAttempt() + { + using var storage = new Storage(); + await using var app = BuildApp(); + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) }; var bytes = Bytes(3L * 1024 * 1024, seed: 13); + // An import that got one part in and then stopped. + await PostPartAsync(client, "Showdown.room", 0, bytes.LongLength, Bytes(1024, seed: 99)); + Assert.Equal(1024, new FileInfo(Path.Join(storage.Root, "Showdown.room" + AdminUtils.PartSuffix)).Length); + + var (status, body) = await PostPartAsync(client, "Showdown.room", 0, bytes.LongLength, bytes); + Assert.Equal(HttpStatusCode.OK, status); + Assert.True(body.GetProperty("complete").GetBoolean()); + Assert.Equal( + SHA256.HashData(bytes), + SHA256.HashData(await File.ReadAllBytesAsync(Path.Join(storage.Root, "Showdown.room")))); + + await app.StopAsync(); + } + + /// The part route is as guarded as the routes it stands in for: no key, no write. + [Fact] + public async Task PartsWithoutTheAdminKeyAreRefused() + { + using var storage = new Storage(); + await using var app = BuildApp(); + await app.StartAsync(); + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) }; + + using var content = new ByteArrayContent(Bytes(1024)); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + using var response = await client.PostAsync("/admin/panel/upload/part?key=wrong&name=Showdown.room&offset=0&total=1024", content); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("Invalid secret", await response.Content.ReadAsStringAsync()); + Assert.Empty(Directory.GetFiles(storage.Root)); + + await app.StopAsync(); + } +} diff --git a/ServerTests/AdminUploadNameTests.cs b/ServerTests/AdminUploadNameTests.cs new file mode 100644 index 0000000..0e33c41 --- /dev/null +++ b/ServerTests/AdminUploadNameTests.cs @@ -0,0 +1,72 @@ +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +// An imported room save names its holotar and image blobs by their original +// rec.net names, and the 2023 client fetches exactly those (GET /data/.htr). +// The admin upload route used to rename every file to a fresh GUID, so all of +// them answered 404 and the rooms loaded with those objects missing. The route +// can now be told the name to store under - and must refuse anything that is not +// a bare file name, since it writes into the CDN directory. +public class AdminUploadNameTests +{ + [Fact] + public void NoNameRequested_LeavesTheNameToTheServer() + { + Assert.True(AdminUtils.TryResolveUploadName(null, out var name, out var error)); + Assert.Null(name); + Assert.Null(error); + + Assert.True(AdminUtils.TryResolveUploadName(" ", out name, out error)); + Assert.Null(name); + Assert.Null(error); + } + + [Theory] + [InlineData("315cb6bf6af14c0b83bfe2fdc8ef0c79.htr")] + [InlineData("f4ab6f0d2409427fa7b1b33587f4db67.room")] + [InlineData("DefaultProfileImage.jpg")] + [InlineData("room_image-2.png")] + public void ABlobNameIsKeptExactly(string requested) + { + Assert.True(AdminUtils.TryResolveUploadName(requested, out var name, out var error)); + Assert.Equal(requested, name); + Assert.Null(error); + } + + [Fact] + public void SurroundingWhitespaceIsNotPartOfTheName() + { + Assert.True(AdminUtils.TryResolveUploadName(" abc.htr ", out var name, out _)); + Assert.Equal("abc.htr", name); + } + + // Every one of these would place the write somewhere other than the CDN + // directory, or produce a name the client could never have requested. + [Theory] + [InlineData("../outside.htr")] + [InlineData("..\\outside.htr")] + [InlineData("sub/dir/blob.htr")] + [InlineData("sub\\dir\\blob.htr")] + [InlineData("/absolute/blob.htr")] + [InlineData("C:\\Windows\\blob.htr")] + [InlineData("blob..htr")] + [InlineData("blob name.htr")] + [InlineData("blob%2e%2e.htr")] + [InlineData("blob;rm -rf.htr")] + public void APathOrAnOddNameIsRefusedRatherThanRenamed(string requested) + { + Assert.False(AdminUtils.TryResolveUploadName(requested, out var name, out var error)); + Assert.Null(name); + Assert.NotNull(error); + } + + [Fact] + public void AnOverlongNameIsRefused() + { + Assert.False(AdminUtils.TryResolveUploadName(new string('a', 129) + ".htr", + out _, out var error)); + Assert.NotNull(error); + } +} diff --git a/ServerTests/AnnouncementWireTests.cs b/ServerTests/AnnouncementWireTests.cs new file mode 100644 index 0000000..1277e5e --- /dev/null +++ b/ServerTests/AnnouncementWireTests.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Notifications; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A saved announcement has to reach players without a restart. The only lever the client offers +/// for that is its notification table, which is keyed by name and drops any frame whose Id is not +/// the exact registered key - the same contract the community board and the progression frame live +/// under. The server used to push the numeric enum AnnouncementText (26), which matches no +/// registration, so an announcement only ever appeared on the next login. +/// +public class AnnouncementWireTests +{ + [Fact] + public void TheAnnouncementGoesOutUnderTheLiteralNameTheClientRegistersItsHandlerFor() + { + var frame = AnnouncementBroadcast.Update(new Announcement + { + AnnouncementId = 7, + Title = "Weekend event", + Body = "Double XP all weekend.", + Platform = Platforms.All, + }); + + Assert.Equal("AnnouncementUpdate", (string)frame.Id); + Assert.Contains("\"Id\":\"AnnouncementUpdate\"", frame.ToJson()); + + // Not the numeric enum (26): the client registers the announcement handler by name and + // drops any frame whose Id is not the exact key, which is why a saved announcement used to + // be invisible until the game was restarted. + Assert.DoesNotContain("\"Id\":\"26\"", frame.ToJson()); + } + + [Fact] + public void TheAnnouncementFrameCarriesTheSavedAnnouncement() + { + var frame = AnnouncementBroadcast.Update(new Announcement + { + AnnouncementId = 7, + Title = "Weekend event", + Body = "Double XP all weekend.", + }); + + using var doc = JsonDocument.Parse(frame.ToJson()); + var msg = doc.RootElement.GetProperty("Msg"); + + Assert.Equal(7L, msg.GetProperty("AnnouncementId").GetInt64()); + Assert.Equal("Weekend event", msg.GetProperty("Title").GetString()); + Assert.Equal("Double XP all weekend.", msg.GetProperty("Body").GetString()); + } + + [Fact] + public void ARemovedAnnouncementGoesOutUnderTheDeleteLiteralTheClientRegisters() + { + var frame = AnnouncementBroadcast.Delete(new Announcement + { + AnnouncementId = 7, + Title = "Weekend event", + Body = "Double XP all weekend.", + Platform = Platforms.All, + }); + + // A delete is its own registration on the client, so a removed announcement leaves the + // on-screen list on the spot instead of waiting for the next api/announcement/v1/get. + Assert.Equal("AnnouncementDelete", (string)frame.Id); + Assert.Contains("\"Id\":\"AnnouncementDelete\"", frame.ToJson()); + + using var doc = JsonDocument.Parse(frame.ToJson()); + Assert.Equal(7L, doc.RootElement.GetProperty("Msg").GetProperty("AnnouncementId").GetInt64()); + } +} diff --git a/ServerTests/AssemblyInfo.cs b/ServerTests/AssemblyInfo.cs new file mode 100644 index 0000000..7f62a9b --- /dev/null +++ b/ServerTests/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using Xunit; + +// RoomUtils and MatchmakingUtils hold their DbContext factories as process-wide statics, so +// two test classes running at once can serve each other's queries: a class that swaps the +// factory silently points every other in-flight test at an empty database. That made the +// suite pass or fail by scheduling - room-save and role tests failed only in full runs and +// passed on their own. The suite runs in about a second, so serializing it is cheaper than +// the flakiness. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/ServerTests/AuthOnboardingFlowTests.cs b/ServerTests/AuthOnboardingFlowTests.cs new file mode 100644 index 0000000..1773113 --- /dev/null +++ b/ServerTests/AuthOnboardingFlowTests.cs @@ -0,0 +1,1816 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.IdentityModel.Tokens; +using RadicadeServer; +using RadicadeServer.API.Accounts; +using RadicadeServer.Auth; +using RadicadeServer.API.Moderation; +using RadicadeServer.Auth.Cached; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth.Connection; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class AuthOnboardingFlowTests +{ + private const string PlatformId = "76561198000000000"; + private const string DeviceId = "device-onboarding-flow"; + + [Theory] + [InlineData("cached_login")] + [InlineData("refresh_token")] + [InlineData("create_account")] + public async Task StandaloneCannotAuthenticateWithOnlyDeviceOrPlatformIdentity(string grant) + { + await using var db = CreateDb(); + await SeedLinkedAccountAsync(db, true, true); + var request = RequestData(); + request.platform = Platforms.Standalone; + request.grant_type = grant; + request.account_id = 17; + request.refresh_token = Guid.NewGuid().ToString(); + var api = CreateApi(db); + api.Request.Headers.Authorization = "Bearer invalid"; + Assert.IsType((await api.ConnectWithToken(request)).Result); + Assert.Single(await db.CachedLogins.ToListAsync()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task StandalonePasswordResolvesAccountAndDoesNotTrustPostedIdentity(bool correctPassword) + { + var oldKey = DBConfig.JwtKey; + DBConfig.JwtKey = "standalone-test-signing-key-with-sufficient-length"; + try + { + await using var db = CreateDb(); + await SeedLinkedAccountAsync(db, true, true); + var player = await db.Players.SingleAsync(); + player.Hashword = new Microsoft.AspNetCore.Identity.PasswordHasher() + .HashPassword(player, "test-password-quest"); + await db.SaveChangesAsync(); + var request = RequestData(); + request.platform = Platforms.Standalone; + request.grant_type = "password"; + request.account_id = 123456; + request.username = "ExistingPlayer"; + request.password = correctPassword ? "test-password-quest" : "incorrect-password"; + var api = CreateApi(db); + api.Request.Headers.Authorization = "Bearer invalid"; + var result = await api.ConnectWithToken(request); + var response = Assert.IsType(Assert.IsType(result.Result).Value); + if (!correctPassword) + { + Assert.Null(response.access_token); + Assert.NotEmpty(response.error); + Assert.DoesNotContain(await db.CachedLogins.ToListAsync(), x => x.platform == Platforms.Standalone); + return; + } + Assert.Empty(response.error); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + var token = new JwtSecurityTokenHandler().ReadJwtToken(response.access_token); + Assert.Equal("17", token.Claims.Single(x => x.Type == "userId").Value); + Assert.Equal("7", token.Claims.Single(x => x.Type == "rn.plat").Value); + Assert.Equal(17, request.account_id); + Assert.Equal("radicade-quest:" + DeviceId, request.platform_id); + Assert.Contains(await db.CachedLogins.ToListAsync(), x => + x.accountId == 17 && x.platform == Platforms.Standalone && x.platformId == request.platform_id); + var lookup = new CachedLoginAPI(db); + var list = Assert.IsType(await lookup.GetCachedLoginsForPlatformId(Platforms.Standalone, request.platform_id)); + Assert.Empty(Assert.IsType>(list.Value)); + Assert.Empty(await lookup.GetCachedLoginsForPlatformIds(new[] { request.platform_id })); + } + finally { DBConfig.JwtKey = oldKey; } + } + + [Theory] + [InlineData("valid", true)] + [InlineData("tampered", false)] + [InlineData("device", false)] + [InlineData("account", false)] + [InlineData("revoked", false)] + [InlineData("password_changed", false)] + [InlineData("expired", false)] + [InlineData("platform", false)] + public async Task QuestRememberedLoginRequiresBoundSignedSession(string scenario, bool accepted) + { + var oldKey = DBConfig.JwtKey; + DBConfig.JwtKey = "quest-remembered-test-signing-key-with-sufficient-length"; + try + { + await using var db = CreateDb(); + await SeedLinkedAccountAsync(db, true, true); + var player = await db.Players.SingleAsync(); + player.Hashword = new Microsoft.AspNetCore.Identity.PasswordHasher() + .HashPassword(player, "quest-session-test-password"); + await db.SaveChangesAsync(); + var request = RequestData(); + request.platform = Platforms.Standalone; + request.grant_type = "password"; + request.username = "ExistingPlayer"; + request.password = "quest-session-test-password"; + var login = CreateApi(db); + var initial = Assert.IsType(Assert.IsType( + (await login.ConnectWithToken(request)).Result).Value); + Assert.False(string.IsNullOrEmpty(initial.access_token)); + var session = login.Response.Headers["X-Radicade-Quest-Session"].ToString(); + Assert.Equal(initial.access_token, session); + if (scenario is "expired" or "platform") + { + var original = new JwtSecurityTokenHandler().ReadJwtToken(session); + var claims = original.Claims.Where(c => c.Type is not "exp" and not "nbf" and not "iat" and not "iss").ToList(); + if (scenario == "platform") + { + claims.RemoveAll(c => c.Type == "rn.plat"); + claims.Add(new Claim("rn.plat", "1")); + } + session = new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken( + issuer: DBConfig.TokenIssuer, claims: claims, + notBefore: DateTime.UtcNow.AddDays(-2), + expires: scenario == "expired" ? DateTime.UtcNow.AddDays(-1) : DateTime.UtcNow.AddDays(1), + signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(DBConfig.JwtKey)), SecurityAlgorithms.HmacSha256))); + } + + var resume = RequestData(); + resume.platform = Platforms.Standalone; + resume.grant_type = "cached_login"; + resume.account_id = scenario == "account" ? 999 : 0; + if (scenario == "device") resume.device_id = "different-headset"; + if (scenario == "revoked") player.TokenVersion++; + if (scenario == "password_changed") player.Hashword = "different-password-hash"; + await db.SaveChangesAsync(); + var api = CreateApi(db); + api.Request.Headers["X-Radicade-Quest-Session"] = scenario == "tampered" ? "invalid." + session : session; + var result = (await api.ConnectWithToken(resume)).Result; + if (!accepted) Assert.IsType(result); + else + { + var response = Assert.IsType(Assert.IsType(result).Value); + Assert.False(string.IsNullOrEmpty(response.access_token)); + Assert.Equal(17, resume.account_id); + } + } + finally { DBConfig.JwtKey = oldKey; } + } + + [Fact] + public async Task CachedLoginForUnknownPlatformIdentityIsNotCreatedByTheLoginAttempt() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + try + { + await using var db = CreateDb(); + await SeedLinkedAccountAsync(db, flowFinished: true, complete: true); + var request = RequestData(); + request.grant_type = "cached_login"; + request.account_id = 17; + request.platform_id = "76561198999999999"; + var api = CreateApi(db, request.platform_id); + + var result = await api.ConnectWithToken(request); + + var refusal = Assert.IsType(result.Result); + Assert.Equal(400, refusal.StatusCode); + var body = Assert.IsType(refusal.Value); + Assert.Equal("invalid_grant", body.error); + Assert.Equal("no linked account for this platform identity", body.error_description); + Assert.Single(await db.CachedLogins.ToListAsync()); + Assert.DoesNotContain(await db.CachedLogins.ToListAsync(), login => + login.platform == Platforms.Steam && login.platformId == request.platform_id); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + } + } + + [Fact] + public async Task TitleScreenLoginIsRefusedWhenItNamesNoAccount() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + db.Profiles.Add(new Profile + { + accountId = 1, + profileImage = "default", + username = "SystemAccount", + displayName = "SystemAccount", + createdAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + + var api = CreateApi(db, PlatformId); + var result = await api.ConnectWithToken(RequestData()); + + var refusalResult = Assert.IsType(result.Result); + Assert.Equal(400, refusalResult.StatusCode); + var refusal = Assert.IsType(refusalResult.Value); + Assert.Equal("invalid_request", refusal.error); + Assert.True(string.IsNullOrWhiteSpace(refusal.access_token)); + + var unknown = RequestData(); + unknown.account_id = 4242; + var unknownResult = await api.ConnectWithToken(unknown); + var unknownRefusal = Assert.IsType(unknownResult.Result); + Assert.Equal(400, unknownRefusal.StatusCode); + Assert.Equal("invalid_grant", Assert.IsType(unknownRefusal.Value).error); + + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + Assert.Single(await db.Profiles.ToListAsync()); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + private sealed class TestAuthAPI : AuthAPI + { + private readonly string? authenticatedSteamId; + + public TestAuthAPI(RecDBContext db, string? authenticatedSteamId) + : base(db) + { + this.authenticatedSteamId = authenticatedSteamId; + } + + protected override Task AuthenticateSteamTicketAsync(string ticket, string appId, CancellationToken cancellationToken) + { + Assert.Equal("140test-ticket", ticket); + Assert.Equal("471710", appId); + return Task.FromResult(authenticatedSteamId); + } + } + + /// + /// AuthAPI with no override of the ticket check, so a test exercises the retry the server + /// actually runs rather than the harness's fixed answer. + /// + private sealed class RealSteamTicketAuthAPI : AuthAPI + { + public RealSteamTicketAuthAPI(RecDBContext db) + : base(db) + { + } + } + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + [Fact] + public async Task FirstRunGetsNoSessionUntilTheOnboardingFlowSignsUp() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + db.Profiles.Add(new Profile + { + accountId = 1, + profileImage = "default", + username = "SystemAccount", + displayName = "SystemAccount", + createdAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + + var lookupApi = new CachedLoginAPI(db) + { + ControllerContext = CreateControllerContext() + }; + var lookup = Assert.IsType( + await lookupApi.GetCachedLoginsForPlatformId(Platforms.Steam, PlatformId)); + Assert.Empty(Assert.IsAssignableFrom>(lookup.Value)); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + + var authApi = CreateApi(db, PlatformId); + + var refused = await authApi.ConnectWithToken(RequestData()); + var refusalResult = Assert.IsType(refused.Result); + Assert.Equal(400, refusalResult.StatusCode); + var refusal = Assert.IsType(refusalResult.Value); + Assert.Equal("invalid_request", refusal.error); + Assert.True(string.IsNullOrWhiteSpace(refusal.access_token)); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + Assert.Single(await db.Profiles.ToListAsync()); + + // The sign-up the flow ends on is what provisions the identity, and it keeps what the + // player chose - the birthday, name and password its pages collected - rather than + // replacing them with a generated name and an unset birthday. + var createRequest = RequestData(); + createRequest.grant_type = "create_account"; + createRequest.username = "ChosenOnboardingName"; + createRequest.password = "onboarding-password"; + createRequest.birthday = new DateTime(1999, 5, 4); + + var createResult = await authApi.ConnectWithToken(createRequest); + var response = Assert.IsType(Assert.IsType(createResult.Result).Value); + + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + var cachedLogin = await db.CachedLogins.SingleAsync(); + var player = await db.Players.SingleAsync(); + var profile = await db.Profiles.SingleAsync(candidate => candidate.accountId == cachedLogin.accountId); + var inventory = await db.Unlocks.SingleAsync(candidate => candidate.AccountId == player.AccountId); + Assert.NotEmpty(inventory.GetAvatarItems()); + var starterDescriptions = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.ParseCatalog( + RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.LoadStarterJson()) + .Select(item => item.AvatarItemDesc).ToHashSet(); + Assert.All(inventory.GetAvatarItems(), item => Assert.True(item.IsBaseAvatarItem || + starterDescriptions.Contains(RadicadeServer.RecNet.Avatar.AvatarItemDescription.Normalize(item.AvatarItemDesc)))); + Assert.Empty(inventory.GetEquipment()); + Assert.Empty(inventory.GetConsumables()); + Assert.Equal(PlatformId, cachedLogin.platformId); + Assert.Equal(Platforms.Steam, cachedLogin.platform); + Assert.Equal(cachedLogin.accountId, player.AccountId); + Assert.Equal(cachedLogin.accountId, profile.accountId); + Assert.Equal("ChosenOnboardingName", profile.username); + Assert.Equal("ChosenOnboardingName", profile.displayName); + Assert.Equal(new DateTime(1999, 5, 4), player.Birthday); + Assert.False(player.CodeOfConductAccepted); + Assert.True(PlayerUtils.BuildSelfAccount(profile, player).newPlayer); + + // The flow's own settings have to say it is under way and how far along: not that it never + // began (the client would restart it) and not that it finished (the client would skip into + // the game). The avatar customization and Code of Conduct pages are still ahead of the + // player, so those stay false until the client itself records them. + var onboarding = (await db.PlayerSettings.SingleAsync()).DeserializeSettings(); + string OnboardingSetting(string key) => onboarding.Single(setting => setting.Key == key).Value; + Assert.Equal("True", OnboardingSetting("Recroom.AccountCreation.HasStarted")); + Assert.Equal("True", OnboardingSetting("Recroom.AccountCreation.HasChosenUsername")); + Assert.Equal("True", OnboardingSetting("Recroom.AccountCreation.HasCreatedPassword")); + Assert.Equal("False", OnboardingSetting("Recroom.AccountCreation.HasFinished")); + Assert.Equal("False", OnboardingSetting("Recroom.AccountCreation.FinishedAvatarCustomization")); + Assert.Equal("False", OnboardingSetting("Recroom.AccountCreation.AcceptedCodeOfConduct")); + Assert.Equal("1", OnboardingSetting("Radicade.NeedsOrientation")); + + // The preliminary GET stays read-only, and now reports the link the sign-up created. + lookup = Assert.IsType( + await lookupApi.GetCachedLoginsForPlatformId(Platforms.Steam, PlatformId)); + Assert.Single(Assert.IsAssignableFrom>(lookup.Value)); + + // A sign-up retried after a dropped response has to resolve to the account it already made: + // a second account for one platform identity would let a player hold two, and a banned one + // hold a clean one. + var retryRequest = RequestData(); + retryRequest.grant_type = "create_account"; + var retry = await authApi.ConnectWithToken(retryRequest); + var retryResponse = Assert.IsType(Assert.IsType(retry.Result).Value); + Assert.True(string.IsNullOrEmpty(retryResponse.error), retryResponse.error_description); + Assert.False(string.IsNullOrWhiteSpace(retryResponse.access_token)); + Assert.Single(await db.CachedLogins.ToListAsync()); + Assert.Single(await db.Players.ToListAsync()); + Assert.Equal(cachedLogin.accountId, (await db.Players.SingleAsync()).AccountId); + + var tokenHandler = new JwtSecurityTokenHandler(); + var principal = tokenHandler.ValidateToken(response.access_token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(DBConfig.JwtKey)), + ValidateIssuer = false, + ValidateAudience = false, + ClockSkew = TimeSpan.Zero + }, out _); + Assert.Equal(cachedLogin.accountId.ToString(), principal.FindFirst(ClaimTypes.NameIdentifier)?.Value); + + var deviceIdMethod = typeof(PlayerReportingAPI).GetMethod(nameof(PlayerReportingAPI.PostDeviceId))!; + Assert.Contains(deviceIdMethod.GetCustomAttributes(inherit: true), attribute => attribute is AllowAnonymousAttribute); + var deviceApi = new PlayerReportingAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = principal } + } + }; + Assert.IsType(await deviceApi.PostDeviceId(new DeviceId + { + oldDeviceId = DeviceId, + newDeviceId = "device-after-authenticated-update" + })); + Assert.Equal("device-after-authenticated-update", (await db.Players.SingleAsync()).DeviceId); + + var anonymousDeviceApi = new PlayerReportingAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity()) } + } + }; + Assert.IsType(await anonymousDeviceApi.PostDeviceId(new DeviceId + { + oldDeviceId = DeviceId, + newDeviceId = "must-not-overwrite" + })); + Assert.Equal("device-after-authenticated-update", (await db.Players.SingleAsync()).DeviceId); + + var invalidBearerDeviceApi = new PlayerReportingAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity()) + } + } + }; + invalidBearerDeviceApi.Request.Headers.Authorization = "Bearer invalid"; + Assert.IsType(await invalidBearerDeviceApi.PostDeviceId(new DeviceId + { + oldDeviceId = DeviceId, + newDeviceId = "invalid-bearer-must-not-overwrite" + })); + Assert.Equal("device-after-authenticated-update", (await db.Players.SingleAsync()).DeviceId); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + [Fact] + public async Task FirstBootGetsASessionForTheAccountItCreates() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + db.Profiles.Add(new Profile + { + accountId = 17, + profileImage = "default", + username = "ReservedAccount", + displayName = "ReservedAccount", + createdAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + + var api = CreateApi(db, PlatformId); + + var boot = RequestData(); + boot.grant_type = "create_account"; + + var first = await api.ConnectWithToken(boot); + var response = Assert.IsType(Assert.IsType(first.Result).Value); + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + + var bootedId = boot.account_id; + Assert.NotEqual(17, bootedId); + Assert.NotEqual("ReservedAccount", (await db.Profiles.SingleAsync(x => x.accountId == bootedId)).username); + + var cachedLogin = await db.CachedLogins.SingleAsync(); + Assert.Equal(bootedId, cachedLogin.accountId); + Assert.Equal(PlatformId, cachedLogin.platformId); + + var lookupApi = new CachedLoginAPI(db) { ControllerContext = CreateControllerContext() }; + var lookup = Assert.IsType( + await lookupApi.GetCachedLoginsForPlatformId(Platforms.Steam, PlatformId)); + var links = Assert.IsAssignableFrom>(lookup.Value).ToList(); + Assert.Single(links); + Assert.Equal(bootedId, links[0].accountId); + + var onboarding = (await db.PlayerSettings.SingleAsync(x => x.AccountId == bootedId)).DeserializeSettings(); + string BootSetting(string key) => onboarding.Single(setting => setting.Key == key).Value; + // The client creates the account here and then walks its creation pages, so this marker + // has to read true: it is what puts the client into account creation instead of the + // cached-account route. False here is the state that skipped the welcome screen, the + // username and every other onboarding step. + Assert.Equal("True", BootSetting("Recroom.AccountCreation.HasStarted")); + Assert.Equal("False", BootSetting("Recroom.AccountCreation.HasFinished")); + Assert.Equal("0", BootSetting("Recroom.OOBE")); + + var retry = RequestData(); + retry.grant_type = "create_account"; + var again = await api.ConnectWithToken(retry); + Assert.IsType(again.Result); + Assert.Equal(bootedId, retry.account_id); + Assert.Single(await db.Profiles.Where(x => x.accountId != 17).ToListAsync()); + Assert.Single(await db.CachedLogins.ToListAsync()); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + [Fact] + public void SteamApiKeyLoadsFromRadicadeConfigurationAndTrimsQuotes() + { + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousAdminAccounts = DBConfig.AdminAccounts; + var previousInfluencerIds = DBConfig.TestInfluencerAccountIds; + var previousCoOwnerIds = DBConfig.TestCoOwnerAccountIds; + try + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Radicade:SteamApiKey"] = " \"steam-test-key\" " + }) + .Build(); + + DBConfig.LoadConfiguration(configuration); + + Assert.Equal("steam-test-key", DBConfig.SteamApiKey); + } + finally + { + DBConfig.SteamApiKey = previousSteamApiKey; + DBConfig.AdminAccounts = previousAdminAccounts; + DBConfig.TestInfluencerAccountIds = previousInfluencerIds; + DBConfig.TestCoOwnerAccountIds = previousCoOwnerIds; + } + } + + [Fact] + public async Task CachedLoginFailsClosedWithoutConfiguredSteamApiKey() + { + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousTrust = DBConfig.TrustUnverifiedSteamIdentity; + DBConfig.SteamApiKey = ""; + DBConfig.TrustUnverifiedSteamIdentity = false; + try + { + await using var db = CreateDb(); + var api = new TestAuthAPI(db, PlatformId) + { + ControllerContext = CreateControllerContext() + }; + + var result = await api.ConnectWithToken(RequestData()); + + Assert.IsType(result.Result); + Assert.Empty(await db.Profiles.ToListAsync()); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + } + finally + { + DBConfig.SteamApiKey = previousSteamApiKey; + DBConfig.TrustUnverifiedSteamIdentity = previousTrust; + } + } + + /// + /// A sign-up the server refuses has to be a FAILURE the client can see. Answering 200 puts + /// `access_token: null` in a body the client reads as a finished login: it holds no token, makes + /// no further request at all, resets its scene and retries - the exact shape of the 2026-09-30 + /// log from a machine with no account on this server. The client's own onboarding trace read that + /// same 200 + null token as "Login token captured", so the server was the only place the reason + /// existed. HTTP 400 with `error`/`error_description` is the shape the unlinked-identity refusal + /// already uses and the one the client acts on. + /// + [Fact] + public async Task RefusedSignUpIsABadRequest_NotA200WithANullToken() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + db.Profiles.Add(new Profile + { + accountId = 1, + profileImage = "default", + username = "SystemAccount", + displayName = "SystemAccount", + createdAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + + var api = CreateApi(db, PlatformId); + var request = RequestData(); + request.grant_type = "create_account"; + + request.username = "RefusedSignUpName"; + request.password = "refused-sign-up-password"; + request.birthday = new DateTime(1999, 5, 4); + request.device_id = ""; + + var result = await api.ConnectWithToken(request); + + var refusalResult = Assert.IsType(result.Result); + Assert.Equal(400, refusalResult.StatusCode); + var refusal = Assert.IsType(refusalResult.Value); + Assert.False(string.IsNullOrWhiteSpace(refusal.error_description)); + Assert.True(string.IsNullOrWhiteSpace(refusal.access_token)); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + [Fact] + public async Task DevelopmentBootWithoutASteamKeyStillLetsANewPlayerSignUp() + { + // What a local server runs on: no publisher key, so no ticket can be verified - and with the + // ticket required, a Steam client could neither sign up nor log in at all. The fallback only + // exists for that case (a production boot throws on the missing key), and the identity it binds + // is still the posted one, so this pins that the sign-up completes and nothing else loosens. + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousTrust = DBConfig.TrustUnverifiedSteamIdentity; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = ""; + DBConfig.TrustUnverifiedSteamIdentity = true; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + // Account 1 is the system account, which must never be handed a client token - so the + // generated ids have to start after it, exactly as they do on a seeded server. + db.Profiles.Add(new Profile + { + accountId = 1, + profileImage = "default", + username = "SystemAccount", + displayName = "SystemAccount", + createdAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + + // No TestAuthAPI ticket stub: the whole point is that nothing verifies a ticket here. + var api = new AuthAPI(db) + { + ControllerContext = CreateControllerContext() + }; + var request = RequestData(); + request.grant_type = "create_account"; + request.username = "LocalDevPlayer"; + + var result = await api.ConnectWithToken(request); + var response = Assert.IsType(Assert.IsType(result.Result).Value); + + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + var cachedLogin = await db.CachedLogins.SingleAsync(); + Assert.Equal(PlatformId, cachedLogin.platformId); + var profile = await db.Profiles.SingleAsync(candidate => candidate.accountId == cachedLogin.accountId); + Assert.Equal("LocalDevPlayer", profile.username); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + DBConfig.TrustUnverifiedSteamIdentity = previousTrust; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + [Fact] + public async Task CachedLoginRequiresAValidSteamOwnershipTicketBeforeProvisioning() + { + var previousSteamApiKey = DBConfig.SteamApiKey; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + try + { + await using var db = CreateDb(); + var api = CreateApi(db); + + var result = await api.ConnectWithToken(RequestData()); + + Assert.IsType(result.Result); + Assert.Empty(await db.Profiles.ToListAsync()); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + } + finally + { + DBConfig.SteamApiKey = previousSteamApiKey; + } + } + + [Fact] + public async Task CachedLoginRejectsSteamTicketForDifferentPlatformIdentity() + { + var previousSteamApiKey = DBConfig.SteamApiKey; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + try + { + await using var db = CreateDb(); + var api = CreateApi(db, "76561198000000001"); + + var result = await api.ConnectWithToken(RequestData()); + + Assert.IsType(result.Result); + Assert.Empty(await db.Profiles.ToListAsync()); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + } + finally + { + DBConfig.SteamApiKey = previousSteamApiKey; + } + } + + [Fact] + public async Task CreateAccountGrantRequiresAVerifiedPlatformTicketAndProvisionsNothing() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + try + { + await using var db = CreateDb(); + // Same request as the flow's sign-up, but with no ticket the server has verified: a sign-up + // binds a platform identity, so it cannot run on an identity nobody proved. + var api = CreateApi(db); + var request = RequestData(); + request.grant_type = "create_account"; + + var result = await api.ConnectWithToken(request); + + Assert.IsType(result.Result); + Assert.Empty(await db.Profiles.ToListAsync()); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(await db.CachedLogins.ToListAsync()); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + } + } + + [Fact] + public async Task CachedLoginLookupDoesNotOfferOrphanedOrIncompleteAccountLinks() + { + await using var db = CreateDb(); + db.Profiles.Add(new Profile + { + accountId = 17, + profileImage = "default", + username = "CompleteAccount", + displayName = "CompleteAccount", + createdAt = DateTime.UtcNow + }); + db.Players.Add(new PlayerData { AccountId = 17, DeviceId = DeviceId }); + db.Presences.Add(new RealPresence { playerId = 17, deviceClass = DeviceClass.Screen }); + db.Profiles.Add(new Profile + { + accountId = 18, + profileImage = "default", + username = "MissingPlayer", + displayName = "MissingPlayer", + createdAt = DateTime.UtcNow + }); + db.Players.Add(new PlayerData { AccountId = 19, DeviceId = DeviceId }); + db.CachedLogins.AddRange( + new CachedLogin { accountId = 17, platform = Platforms.Steam, platformId = PlatformId, lastLoginTime = DateTime.UtcNow }, + new CachedLogin { accountId = 18, platform = Platforms.Steam, platformId = PlatformId, lastLoginTime = DateTime.UtcNow.AddMinutes(1) }, + new CachedLogin { accountId = 19, platform = Platforms.Steam, platformId = PlatformId, lastLoginTime = DateTime.UtcNow.AddMinutes(2) }, + new CachedLogin { accountId = 20, platform = Platforms.Steam, platformId = PlatformId, lastLoginTime = DateTime.UtcNow.AddMinutes(3) }); + db.Presences.Add(new RealPresence { playerId = 19, deviceClass = DeviceClass.Screen }); + await db.SaveChangesAsync(); + + var api = new CachedLoginAPI(db) { ControllerContext = CreateControllerContext() }; + var singleLookup = Assert.IsType( + await api.GetCachedLoginsForPlatformId(Platforms.Steam, PlatformId)); + var singleLinks = Assert.IsAssignableFrom>(singleLookup.Value).ToList(); + Assert.Single(singleLinks); + Assert.Equal(17, singleLinks[0].accountId); + + var bulkLinks = await api.GetCachedLoginsForPlatformIds(new[] { PlatformId, PlatformId }); + Assert.Single(bulkLinks); + Assert.Equal(17, bulkLinks[0].accountId); + } + + [Fact] + public async Task CachedLoginForLinkedPlatformStillIssuesTokenWithoutCreatingAnotherIdentity() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + try + { + await using var db = CreateDb(); + db.Profiles.Add(new Profile + { + accountId = 17, + profileImage = "default", + username = "ExistingUser", + displayName = "ExistingUser", + createdAt = DateTime.UtcNow + }); + db.Players.Add(new PlayerData + { + AccountId = 17, + DeviceId = DeviceId, + DeviceClass = DeviceClass.Screen, + AppVersion = "20230831" + }); + db.CachedLogins.Add(new CachedLogin + { + accountId = 17, + platform = Platforms.Steam, + platformId = PlatformId, + lastLoginTime = DateTime.UtcNow + }); + db.Presences.Add(new RealPresence + { + playerId = 17, + deviceClass = DeviceClass.Screen + }); + await db.SaveChangesAsync(); + + var api = CreateApi(db, PlatformId); + var request = RequestData(); + request.account_id = 17; + var result = await api.ConnectWithToken(request); + var response = Assert.IsType(Assert.IsType(result.Result).Value); + + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + Assert.Single(await db.Profiles.ToListAsync()); + Assert.Single(await db.CachedLogins.ToListAsync()); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + } + } + + /// + /// The player's own case: the identity they sign up with already owns an account (their Steam + /// account was linked on an earlier boot), so the sign-up resumes THAT account - and the name, + /// birthday and password they typed on the flow's pages have to land on it. This is what used to + /// be dropped: the grant matched the existing link, issued a token for the account, and threw the + /// payload away, so the player finished the flow looking at a name they never chose. + /// + [Theory] + [InlineData("TheNameIChose", false)] + [InlineData("ExistingPlayer", false)] + [InlineData("TheNameIChose", true)] + public async Task CreateAccountOnALinkedIdentityFinishesThatAccountWithTheNameTheFlowCollected(string chosenName, bool missingSettings) + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + await SeedLinkedAccountAsync(db, flowFinished: false, complete: false); + if (missingSettings) + { + db.PlayerSettings.Remove(await db.PlayerSettings.SingleAsync()); + await db.SaveChangesAsync(); + } + + var api = CreateApi(db, PlatformId); + var request = RequestData(); + request.grant_type = "create_account"; + request.username = chosenName; + if (missingSettings) request.account_id = 17; + request.password = "the-password-from-the-flow"; + request.birthday = new DateTime(2011, 8, 30); + + var result = await api.ConnectWithToken(request); + var response = Assert.IsType(Assert.IsType(result.Result).Value); + + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + + // One identity, one account: the sign-up finished the account it was resuming rather than + // minting a second one behind it. + var cachedLogin = await db.CachedLogins.SingleAsync(); + Assert.Equal(17, cachedLogin.accountId); + Assert.Single(await db.Profiles.ToListAsync()); + + var profile = await db.Profiles.SingleAsync(); + Assert.Equal(chosenName, profile.username); + Assert.Equal(chosenName, profile.displayName); + + var player = await db.Players.SingleAsync(); + Assert.Equal(new DateTime(2011, 8, 30), player.Birthday); + Assert.False(player.IsJunior); + Assert.False(string.IsNullOrWhiteSpace(player.Hashword)); + var settings = (await db.PlayerSettings.SingleAsync()).DeserializeSettings(); + Assert.Contains(settings, setting => setting.Key == "Recroom.AccountCreation.HasChosenUsername" && setting.Value == "True"); + Assert.Contains(settings, setting => setting.Key == "Recroom.AccountCreation.HasCreatedPassword" && setting.Value == "True"); + Assert.DoesNotContain(settings, setting => setting.Key == "Recroom.AccountCreation.HasFinished" && setting.Value == "True"); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + /// + /// A finished account can still lack required details. A new-account request must not + /// reuse it, even when the client carries the previously selected account ID. + /// + [Fact] + public async Task CreateAccountDoesNotReuseAFinishedAccountWithMissingDetails() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + await SeedLinkedAccountAsync(db, flowFinished: true, complete: false); + + var player = await db.Players.SingleAsync(); + Assert.True(PlayerUtils.IsNewPlayer(player), "the fixture has to be an account the client still shows the flow for"); + + var api = CreateApi(db, PlatformId); + var request = RequestData(); + request.grant_type = "create_account"; + request.username = "NamedOnAResumedFlow"; + request.account_id = 17; + request.password = "the-password-from-the-flow"; + + var result = await api.ConnectWithToken(request); + var response = Assert.IsType(Assert.IsType(result.Result).Value); + + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + Assert.Equal(2, await db.CachedLogins.CountAsync()); + Assert.NotEqual(17, request.account_id); + Assert.Equal("NamedOnAResumedFlow", (await db.Profiles.SingleAsync(x => x.accountId == request.account_id)).username); + Assert.Equal("ExistingPlayer", (await db.Profiles.SingleAsync(x => x.accountId == 17)).username); + Assert.False(string.IsNullOrWhiteSpace((await db.Players.SingleAsync(x => x.AccountId == request.account_id)).Hashword)); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + /// + /// The other half of the rule: an account that is complete - birthday, password and accepted Code + /// of Conduct - is not renamed or re-keyed by a sign-up grant that arrives later. + /// The new sign-up receives its own account. + /// + [Fact] + public async Task CreateAccountLeavesACompleteAccountsNameAndPasswordAlone() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + await SeedLinkedAccountAsync(db, flowFinished: true, complete: true); + + var api = CreateApi(db, PlatformId); + var request = RequestData(); + request.grant_type = "create_account"; + request.username = "SomebodyElsesTurn"; + request.account_id = 17; + request.password = "a-password-from-a-later-flow"; + request.birthday = new DateTime(1999, 5, 4); + + var result = await api.ConnectWithToken(request); + var response = Assert.IsType(Assert.IsType(result.Result).Value); + + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + + Assert.NotEqual(17, request.account_id); + var profile = await db.Profiles.SingleAsync(x => x.accountId == 17); + Assert.Equal("ExistingPlayer", profile.username); + Assert.Equal("ExistingPlayer", profile.displayName); + + var player = await db.Players.SingleAsync(x => x.AccountId == 17); + Assert.Equal(new DateTime(2000, 1, 1), player.Birthday); + Assert.Equal("seeded-hash", player.Hashword); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + /// + /// + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task BareCreateAccountResolvesToTheLinkedAccountAndNeverMintsASecond(bool complete) + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + try + { + await using var db = factory.CreateDbContext(); + await SeedLinkedAccountAsync(db, flowFinished: true, complete: complete); + var api = CreateApi(db, PlatformId); + var request = RequestData(); + request.grant_type = "create_account"; + var result = await api.ConnectWithToken(request); + var response = Assert.IsType(Assert.IsType(result.Result).Value); + Assert.True(string.IsNullOrEmpty(response.error), response.error_description); + Assert.False(string.IsNullOrWhiteSpace(response.access_token)); + + Assert.Equal(17, request.account_id); + Assert.Single(await db.Profiles.ToListAsync()); + Assert.Single(await db.CachedLogins.ToListAsync()); + Assert.Single(await db.Players.ToListAsync()); + Assert.Equal("ExistingPlayer", (await db.Profiles.SingleAsync(x => x.accountId == 17)).username); + + var retry = RequestData(); + retry.grant_type = "create_account"; + await api.ConnectWithToken(retry); + Assert.Equal(17, retry.account_id); + Assert.Single(await db.Profiles.ToListAsync()); + + var login = RequestData(); + login.account_id = 17; + var loginResult = await api.ConnectWithToken(login); + var loginResponse = Assert.IsType(Assert.IsType(loginResult.Result).Value); + Assert.False(string.IsNullOrWhiteSpace(loginResponse.access_token)); + Assert.Equal(17, login.account_id); + Assert.Equal("ExistingPlayer", (await db.Profiles.SingleAsync(x => x.accountId == 17)).username); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + [Theory] + [InlineData("developer,influencer,betastudio", true)] + [InlineData("developer,influencer", false)] + [InlineData(" BETASTUDIO ", true)] + public async Task StudioRoleIsIncludedInTokensOnlyWhenAssigned(string roles, bool expected) + { + var previousKey = DBConfig.JwtKey; + var previousFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + RoomUtils.DbFactory = factory; + try + { + using var db = factory.CreateDbContext(); + await SeedLinkedAccountAsync(db, true, true); + var profile = await db.Profiles.SingleAsync(); + profile.role = roles; + await db.SaveChangesAsync(); + var generate = typeof(AuthAPI).GetMethod("GenerateToken", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; + var result = await (Task)generate.Invoke(null, new object[] { + db, 17L, "20230831", DeviceClass.Screen, Platforms.Steam, "studio:17" })!; + Assert.True(result.success); + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(Assert.IsType(result.value)); + Assert.Equal(expected, jwt.Claims.Any(c => c.Type == "role" && c.Value == "betastudio")); + } + finally + { + DBConfig.JwtKey = previousKey; + RoomUtils.DbFactory = previousFactory; + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task KeepsakeTokenRolesFollowFeatureWithoutGrantingDeveloperAccess(bool enabled) + { + var previousKey = DBConfig.JwtKey; + var previousFactory = RoomUtils.DbFactory; + var previousFeature = RadicadeServer.RecNet.Keepsakes.KeepsakeFeature.Override; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + RoomUtils.DbFactory = factory; + RadicadeServer.RecNet.Keepsakes.KeepsakeFeature.Override = enabled; + try + { + using var db = factory.CreateDbContext(); + await SeedLinkedAccountAsync(db, true, true); + var profile = await db.Profiles.SingleAsync(); + profile.role = ""; + await db.SaveChangesAsync(); + var generate = typeof(AuthAPI).GetMethod("GenerateToken", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; + var result = await (Task)generate.Invoke(null, new object[] { + db, 17L, "20230831", DeviceClass.Screen, Platforms.Steam, "keepsake:17" })!; + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(Assert.IsType(result.value)); + var roles = jwt.Claims.Where(c => c.Type == "role").Select(c => c.Value).ToArray(); + Assert.Equal(enabled, roles.Contains("keepsake")); + Assert.Equal(enabled, roles.Contains("livekeepsakeeventroomsaveoverride")); + Assert.DoesNotContain("developer", roles); + Assert.DoesNotContain("moderator", roles); + } + finally + { + DBConfig.JwtKey = previousKey; + RoomUtils.DbFactory = previousFactory; + RadicadeServer.RecNet.Keepsakes.KeepsakeFeature.Override = previousFeature; + } + } + + [Fact] + public async Task CreateAccountRouteAdoptsTheAccountTheBootWasHanded() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + + var bootApi = CreateApi(db, PlatformId); + var boot = RequestData(); + boot.grant_type = "create_account"; + Assert.IsType((await bootApi.ConnectWithToken(boot)).Result); + var bootedId = boot.account_id; + Assert.Equal(bootedId, (await db.CachedLogins.SingleAsync()).accountId); + + using var steam = new SteamStub(StubSteamTicket(PlatformId)); + + var context = CreateAccountContext(PlatformId, steamTicket: "140test-ticket"); + var api = new AccountsAPI(db) { ControllerContext = context }; + + var result = await api.CreateAccount(new AccountCreationData { platformId = PlatformId }); + + Assert.True(result.success, result.error); + Assert.NotNull(result.value); + + Assert.Single(await db.Profiles.ToListAsync()); + Assert.Single(await db.Players.ToListAsync()); + var cachedLogin = await db.CachedLogins.SingleAsync(); + Assert.Equal(bootedId, cachedLogin.accountId); + Assert.Equal(PlatformId, cachedLogin.platformId); + Assert.Equal(DeviceId, (await db.Players.SingleAsync()).DeviceId); + + var onboarding = (await db.PlayerSettings.SingleAsync()).DeserializeSettings(); + Assert.Equal("True", onboarding.Single(x => x.Key == "Recroom.AccountCreation.HasStarted").Value); + Assert.Equal("False", onboarding.Single(x => x.Key == "Recroom.AccountCreation.HasFinished").Value); + + var lookupApi = new CachedLoginAPI(db) { ControllerContext = CreateControllerContext() }; + var lookup = Assert.IsType( + await lookupApi.GetCachedLoginsForPlatformId(Platforms.Steam, PlatformId)); + var links = Assert.IsAssignableFrom>(lookup.Value).ToList(); + Assert.Single(links); + Assert.Equal(bootedId, links[0].accountId); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + [Fact] + public async Task CreateAccountRouteMakesASecondAccountWhenTheDevicesAccountIsFinished() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + await SeedLinkedAccountAsync(db, flowFinished: true, complete: true); + + using var steam = new SteamStub(StubSteamTicket(PlatformId)); + + var context = CreateAccountContext(PlatformId, steamTicket: "140test-ticket"); + var api = new AccountsAPI(db) { ControllerContext = context }; + + var result = await api.CreateAccount(new AccountCreationData { platformId = PlatformId }); + + Assert.True(result.success, result.error); + Assert.Equal(2, (await db.Profiles.ToListAsync()).Count); + Assert.NotEqual(17, (await db.Profiles.SingleAsync(x => x.accountId != 17)).accountId); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + [Fact] + public async Task CreateAccountRouteEstablishesTheAccountOnlyForAProvedSteamIdentity() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket(PlatformId)); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140test-ticket"); + + Assert.True(result.success, result.error); + Assert.Single(await db.Profiles.ToListAsync()); + Assert.Single(await db.Players.ToListAsync()); + Assert.Contains(steam.Handler.Requests, url => url.Contains("AuthenticateUserTicket")); + }); + + [Fact] + public async Task CreateAccountRouteRefusesAnIdentityWithNoSteamTicket() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket(PlatformId)); + + var result = await PostCreateAccount(db, PlatformId); + + Assert.False(result.success); + Assert.Contains("No Steam ticket", Assert.IsType(result.value)); + Assert.Empty(await db.Profiles.ToListAsync()); + Assert.Empty(await db.Players.ToListAsync()); + Assert.Empty(steam.Handler.Requests); + }); + + [Fact] + public async Task CreateAccountRouteRefusesATicketForAnotherSteamAccount() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket("76561198000000001")); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140someone-elses-ticket"); + + Assert.False(result.success); + Assert.Contains("76561198000000001", Assert.IsType(result.value)); + Assert.Empty(await db.Profiles.ToListAsync()); + }); + + [Fact] + public async Task CreateAccountRouteRefusesATicketSteamWillNotAccept() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubRejectedSteamTicket(101, "Invalid ticket")); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140forged-ticket"); + + Assert.False(result.success); + Assert.Contains("Invalid ticket", Assert.IsType(result.value)); + Assert.Empty(await db.Profiles.ToListAsync()); + }); + + [Fact] + public async Task CreateAccountRouteRefusesATicketIssuedForAnotherApp() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubRejectedSteamTicket(102, "The ticket is for another app")); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140other-app-ticket"); + + Assert.False(result.success); + Assert.Contains("not for an app this server accepts", Assert.IsType(result.value)); + Assert.Empty(await db.Profiles.ToListAsync()); + }); + + [Fact] + public async Task CreateAccountRouteAcceptsATicketCarriedInPlatformAuth() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket(PlatformId)); + + var result = await PostCreateAccount(db, PlatformId, + platformAuth: "{\"Ticket\":\"140test-ticket\",\"AppId\":\"471710\"}"); + + Assert.True(result.success, result.error); + Assert.Single(await db.Profiles.ToListAsync()); + }); + + [Fact] + public async Task CreateAccountRouteRefusesASteamAccountWithACommunityBan() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket(PlatformId, communityBanned: true)); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140test-ticket"); + + Assert.False(result.success); + Assert.Contains("community ban", Assert.IsType(result.value)); + Assert.Empty(await db.Profiles.ToListAsync()); + }); + + [Theory] + [InlineData(0)] + [InlineData(3)] + [InlineData(3650)] + public async Task CreateAccountRouteAcceptsASteamAccountOfAnyAge(int daysOld) => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket(PlatformId, + timeCreated: DateTimeOffset.UtcNow.AddDays(-daysOld).ToUnixTimeSeconds())); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140test-ticket"); + + Assert.True(result.success, result.error); + Assert.Single(await db.Profiles.ToListAsync()); + }); + + [Fact] + public async Task CreateAccountRouteRefusesASteamProfileThatIsNotSetUp() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket(PlatformId, profileState: 0)); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140test-ticket"); + + Assert.False(result.success); + Assert.Contains("set up", Assert.IsType(result.value)); + Assert.Empty(await db.Profiles.ToListAsync()); + }); + + [Fact] + public async Task CreateAccountRouteAllowsTheAccountWhenSteamCannotAnswerTheStandingCheck() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket(PlatformId, summariesUnavailable: true)); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140test-ticket"); + + Assert.True(result.success, result.error); + Assert.Single(await db.Profiles.ToListAsync()); + }); + + [Fact] + public async Task CreateAccountRouteRefusesSteamWhenThereIsNoKeyAndUnverifiedIdentitiesAreNotTrusted() => + await RunCreateAccountTest(async db => + { + using var steam = new SteamStub(StubSteamTicket(PlatformId)); + + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140test-ticket"); + + Assert.False(result.success); + Assert.Contains("no SteamApiKey", Assert.IsType(result.value)); + Assert.Empty(await db.Profiles.ToListAsync()); + Assert.Empty(steam.Handler.Requests); + }, steamApiKey: ""); + + [Fact] + public async Task CreateAccountRouteTrustsAnUnverifiedSteamIdentityOnlyWhenToldTo() => + await RunCreateAccountTest(async db => + { + var result = await PostCreateAccount(db, PlatformId, steamTicket: "140test-ticket"); + + Assert.True(result.success, result.error); + Assert.Single(await db.Profiles.ToListAsync()); + }, steamApiKey: "", trustUnverified: true); + + private static async Task RunCreateAccountTest(Func body, + string steamApiKey = "unit-test-steam-api-key", bool trustUnverified = false) + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousTrust = DBConfig.TrustUnverifiedSteamIdentity; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = steamApiKey; + DBConfig.TrustUnverifiedSteamIdentity = trustUnverified; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + await using var db = factory.CreateDbContext(); + await body(db); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + DBConfig.TrustUnverifiedSteamIdentity = previousTrust; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + private static ControllerContext CreateAccountContext(string platformId, string? steamTicket = null, string? platformAuth = null) + { + var context = CreateControllerContext(); + context.HttpContext.Request.ContentType = "application/x-www-form-urlencoded"; + var form = new Dictionary + { + ["platform"] = "0", + ["platformId"] = platformId, + ["device_id"] = DeviceId, + ["device_class"] = "0" + }; + if (platformAuth != null) form["platform_auth"] = platformAuth; + context.HttpContext.Request.Form = new FormCollection(form); + if (steamTicket != null) context.HttpContext.Request.Headers["x-steam-ticket"] = steamTicket; + return context; + } + + private static Task PostCreateAccount(RecDBContext db, string platformId, + string? steamTicket = null, string? platformAuth = null) + { + var api = new AccountsAPI(db) { ControllerContext = CreateAccountContext(platformId, steamTicket, platformAuth) }; + return api.CreateAccount(new AccountCreationData { platformId = platformId }); + } + + private static readonly long LongStandingSteamAccount = + DateTimeOffset.UtcNow.AddYears(-5).ToUnixTimeSeconds(); + + /// + /// The accepted-app-id retry on the token grant. TestAuthAPI overrides the ticket check, which is + /// exactly where that retry lives, so anything asserted through it would pass either way - these + /// two run the production implementation instead. The 2023 client identifies as app 480 while + /// platform_auth can claim 471710 (or the reverse), and Valve answers 102 for the mismatch. + /// + [Fact] + public async Task CreateAccountGrantRetriesAnAcceptedAppIdWhenValveSaysTheTicketIsForAnotherApp() + { + var previousJwtKey = DBConfig.JwtKey; + var previousSteamApiKey = DBConfig.SteamApiKey; + var previousPlayerFactory = PlayerUtils.DbFactory; + var previousRoomFactory = RoomUtils.DbFactory; + var factory = new TestDbContextFactory(); + DBConfig.JwtKey = "unit-test-signing-key-with-sufficient-length"; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + PlayerUtils.DbFactory = factory; + RoomUtils.DbFactory = factory; + + try + { + using var steam = new SteamStub(new SteamStubHandler(url => + url.Contains("appid=471710", StringComparison.Ordinal) + ? SteamJson("{\"response\":{\"error\":{\"errorcode\":102,\"errordesc\":\"The ticket is for another app\"}}}") + : SteamJson($"{{\"response\":{{\"params\":{{\"result\":\"OK\",\"steamid\":\"{PlatformId}\"}}}}}}"))); + + await using var db = factory.CreateDbContext(); + var api = new RealSteamTicketAuthAPI(db) { ControllerContext = CreateControllerContext() }; + var request = RequestData(); + request.grant_type = "create_account"; + request.device_id = "device-steam-appid-retry"; + + var result = await api.ConnectWithToken(request); + + Assert.IsType(result.Result); + Assert.Equal(2, steam.Handler.Requests.Count); + Assert.Contains("appid=480", steam.Handler.Requests[1], StringComparison.Ordinal); + Assert.Single(await db.Profiles.ToListAsync()); + } + finally + { + DBConfig.JwtKey = previousJwtKey; + DBConfig.SteamApiKey = previousSteamApiKey; + PlayerUtils.DbFactory = previousPlayerFactory; + RoomUtils.DbFactory = previousRoomFactory; + } + } + + [Fact] + public async Task CreateAccountGrantStillRefusesATicketNoAcceptedAppIdConfirms() + { + var previousSteamApiKey = DBConfig.SteamApiKey; + DBConfig.SteamApiKey = "unit-test-steam-api-key"; + try + { + using var steam = new SteamStub(StubRejectedSteamTicket(102, "The ticket is for another app")); + + await using var db = CreateDb(); + var api = new RealSteamTicketAuthAPI(db) { ControllerContext = CreateControllerContext() }; + var request = RequestData(); + request.grant_type = "create_account"; + request.device_id = "device-steam-appid-refused"; + + Assert.IsType((await api.ConnectWithToken(request)).Result); + Assert.Empty(await db.Profiles.ToListAsync()); + } + finally + { + DBConfig.SteamApiKey = previousSteamApiKey; + } + } + + private static SteamStubHandler StubSteamTicket(string steamId, long? timeCreated = null, + bool communityBanned = false, int profileState = 1, bool summariesUnavailable = false) + { + var created = timeCreated ?? LongStandingSteamAccount; + return new SteamStubHandler(url => + { + if (url.Contains("AuthenticateUserTicket")) + return SteamJson($"{{\"response\":{{\"params\":{{\"result\":\"OK\",\"steamid\":\"{steamId}\"}}}}}}"); + + if (url.Contains("GetPlayerSummaries")) + return summariesUnavailable + ? new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError) + : SteamJson($"{{\"response\":{{\"players\":[{{\"steamid\":\"{steamId}\"," + + $"\"timecreated\":{created},\"profilestate\":{profileState}}}]}}}}"); + + if (url.Contains("GetPlayerBans")) + return SteamJson($"{{\"players\":[{{\"SteamId\":\"{steamId}\",\"CommunityBanned\":" + + $"{(communityBanned ? "true" : "false")}}}]}}"); + + return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound); + }); + } + + private static SteamStubHandler StubRejectedSteamTicket(int errorCode, string description) => + new(url => url.Contains("AuthenticateUserTicket") + ? SteamJson($"{{\"response\":{{\"error\":{{\"errorcode\":{errorCode},\"errordesc\":\"{description}\"}}}}}}") + : new HttpResponseMessage(System.Net.HttpStatusCode.NotFound)); + + private static HttpResponseMessage SteamJson(string body) => + new(System.Net.HttpStatusCode.OK) { Content = new StringContent(body, Encoding.UTF8, "application/json") }; + + private sealed class SteamStubHandler(Func respond) : HttpMessageHandler + { + public List Requests { get; } = []; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var url = request.RequestUri?.ToString() ?? string.Empty; + Requests.Add(url); + return Task.FromResult(respond(url)); + } + } + + private sealed class SteamStub : IDisposable + { + private readonly HttpClient previous; + private readonly HttpClient created; + + public SteamStub(SteamStubHandler handler) + { + Handler = handler; + previous = PlatformIdentity.Client; + created = new HttpClient(handler); + PlatformIdentity.Client = created; + } + + public SteamStubHandler Handler { get; } + + public void Dispose() + { + PlatformIdentity.Client = previous; + created.Dispose(); + } + } + + /// + /// The client asks for an age only when the server says the age is unsettled, and it picks its + /// junior or non-junior username page from that same answer. pendingJunior was hardcoded false + /// next to a computed HasBirthday, so an account with no birthday still read as settled: the 2023 + /// title screen took the non-junior username page and skipped the BIRTHDAY page of its new-user + /// flow, which is why no account on this server ever ended up with a birthday. + /// + [Fact] + public async Task AnAccountWithoutABirthdayReportsAPendingJuniorSoTheClientAsksForAnAge() + { + await using var db = CreateDb(); + await SeedLinkedAccountAsync(db, flowFinished: false, complete: false); + await SeedReputationAndProgressionAsync(db); + + var player = await db.GetPlayer(17); + + Assert.NotNull(player); + Assert.False(player!.HasBirthday); + Assert.True(player.PendingJunior); + Assert.False(player.JuniorProfile); + } + + [Fact] + public async Task AnAccountWithABirthdayIsNotPendingSoTheAgePageIsNotOfferedAgain() + { + await using var db = CreateDb(); + await SeedLinkedAccountAsync(db, flowFinished: true, complete: true); + await SeedReputationAndProgressionAsync(db); + + var player = await db.GetPlayer(17); + + Assert.NotNull(player); + Assert.True(player!.HasBirthday); + Assert.False(player.PendingJunior); + } + + private static async Task SeedReputationAndProgressionAsync(RecDBContext db) + { + db.Reputations.Add(new Reputation { AccountId = 17 }); + db.Progression.Add(new ProgressionData + { + PlayerId = 17, + XP = 0, + Level = 1, + Progress = new ProgressionJson().ToJson() + }); + await db.SaveChangesAsync(); + } + + private static async Task SeedLinkedAccountAsync(RecDBContext db, bool flowFinished, bool complete) + { + db.Profiles.Add(new Profile + { + accountId = 17, + profileImage = "default", + username = "ExistingPlayer", + displayName = "ExistingPlayer", + createdAt = DateTime.UtcNow + }); + db.Players.Add(new PlayerData + { + AccountId = 17, + DeviceId = DeviceId, + DeviceClass = DeviceClass.Screen, + AppVersion = "20230831", + Birthday = complete ? new DateTime(2000, 1, 1) : DateTime.MinValue, + Hashword = complete ? "seeded-hash" : "", + CodeOfConductAccepted = complete + }); + db.CachedLogins.Add(new CachedLogin + { + accountId = 17, + platform = Platforms.Steam, + platformId = PlatformId, + lastLoginTime = DateTime.UtcNow + }); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = 17, + Settings = + "[{\"Key\":\"Recroom.AccountCreation.HasStarted\",\"Value\":\"true\"}," + + $"{{\"Key\":\"Recroom.AccountCreation.HasFinished\",\"Value\":\"{(flowFinished ? "true" : "false")}\"}}]" + }); + db.Presences.Add(new RealPresence + { + playerId = 17, + deviceClass = DeviceClass.Screen + }); + await db.SaveChangesAsync(); + } + + private static TokenConnectData RequestData() => new() + { + grant_type = "cached_login", + account_id = 0, + ver = "20230831", + device_class = DeviceClass.Screen, + device_id = DeviceId, + platform = Platforms.Steam, + platform_id = PlatformId, + client_id = "recroom", + platform_auth = "{\"Ticket\":\"140test-ticket\",\"AppId\":\"471710\"}" + }; + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new RecDBContext(options); + } + + private static AuthAPI CreateApi(RecDBContext db, string? authenticatedSteamId = null) => new TestAuthAPI(db, authenticatedSteamId) + { + ControllerContext = CreateControllerContext() + }; + + private static ControllerContext CreateControllerContext() + { + var httpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity()) + }; + httpContext.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback; + return new ControllerContext { HttpContext = httpContext }; + } +} diff --git a/ServerTests/AvatarCatalogParsingTests.cs b/ServerTests/AvatarCatalogParsingTests.cs new file mode 100644 index 0000000..ce70708 --- /dev/null +++ b/ServerTests/AvatarCatalogParsingTests.cs @@ -0,0 +1,100 @@ +using RadicadeServer.RecNet.Avatar; +using Xunit; + +namespace RadicadeServer.Tests; + +public class AvatarCatalogParsingTests +{ + [Fact] + public void V4UsesExactClientKeysAndNonNullStrings() + { + var item = new AvatarItem + { + AvatarItemDesc = "item,,,", FriendlyName = "Test", Rarity = 20, + ThumbnailImage = "abc123.png" + }; + var json = System.Text.Json.JsonSerializer.SerializeToElement(new AvatarItemV4(item)); + // PascalCase, matching a real capture of this route. The client has no case-insensitive + // fallback, so a lowercased name leaves AvatarItemDesc null and the item unresolvable. + Assert.Equal( + new[] + { + "AvatarItemDesc", "AvatarItemId", "AvatarItemType", "CreatedAt", "FriendlyName", + "IsBaseAvatarItem", "PlatformMask", "Rarity", "TagList", "ThumbnailImage", "Tooltip" + }, + json.EnumerateObject().Select(property => property.Name).OrderBy(name => name)); + Assert.Equal("item,,,", json.GetProperty("AvatarItemDesc").GetString()); + Assert.Equal("", json.GetProperty("TagList").GetString()); + Assert.Equal(20, json.GetProperty("Rarity").GetInt32()); + Assert.Equal("abc123.png", json.GetProperty("ThumbnailImage").GetString()); + // 0 here would read as "usable on no platform" and the wardrobe would hide the item. + Assert.Equal(-1, json.GetProperty("PlatformMask").GetInt32()); + var legacy = System.Text.Json.JsonSerializer.SerializeToElement(item); + Assert.True(legacy.TryGetProperty("AvatarItemDesc", out _)); + } + + [Fact] + public void RepositoryCatalogLoadsSupportedItemsWithoutLosingMetadata() + { + var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar.json")); + var expected = AvatarUnlockCatalog.KeepSupportedItems( + System.Text.Json.JsonSerializer.Deserialize>(json)); + var actual = AvatarUnlockCatalog.ParseCatalog(json); + Assert.NotEmpty(actual); + Assert.Equal(expected.Count, actual.Count); + Assert.Equal(expected.Select(x => x.AvatarItemDesc), actual.Select(x => x.AvatarItemDesc)); + Assert.Equal(expected.Select(x => x.FriendlyName), actual.Select(x => x.FriendlyName)); + Assert.Equal(expected.Select(x => x.Rarity), actual.Select(x => x.Rarity)); + } + + [Fact] + public void CatalogCanBeEnrichedWithItemIdsAndThumbnails() + { + var items = AvatarUnlockCatalog.ParseCatalog(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar.json"))); + var metadata = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "merged-avatar.json")); + var enriched = AvatarUnlockCatalog.EnrichWithMetadata(items, metadata); + var shirt = Assert.Single(enriched.Where(x => x.AvatarItemDesc == "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")); + Assert.Equal(1018, shirt.AvatarItemId); + Assert.False(string.IsNullOrWhiteSpace(shirt.ThumbnailImage)); + } + + [Fact] + public void BadEntriesDoNotDiscardValidItems() + { + var items = AvatarUnlockCatalog.ParseCatalog(""" + [ + null, 42, + {"AvatarItemDesc":"bad,,,","Rarity":"not-a-number"}, + {"AvatarItemDesc":"overflow,,,","AvatarItemId":9223372036854775807}, + {"FriendlyName":"Missing description"}, + {"AvatarItemDesc":"valid,,,","FriendlyName":"Valid item","Rarity":20} + ] + """); + var item = Assert.Single(items); + Assert.Equal("valid,", item.AvatarItemDesc); + Assert.Equal(-1, item.PlatformMask); + Assert.Equal("Valid item", item.FriendlyName); + } + + [Fact] + public void AcceptsCommentsTrailingCommasAndQuotedNumbers() + { + var items = AvatarUnlockCatalog.ParseCatalog(""" + [ // Hand-edited catalog + {"avatarItemDesc":"valid,,,","rarity":"20","platformMask":"-1",}, + ] + """); + Assert.Equal(20, Assert.Single(items).Rarity); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("null")] + [InlineData("{}")] + [InlineData("[broken")] + public void InvalidCatalogReturnsEmptyForFallback(string? json) + { + Assert.Empty(AvatarUnlockCatalog.ParseCatalog(json)); + } +} diff --git a/ServerTests/AvatarCatalogSourceTests.cs b/ServerTests/AvatarCatalogSourceTests.cs new file mode 100644 index 0000000..b47f016 --- /dev/null +++ b/ServerTests/AvatarCatalogSourceTests.cs @@ -0,0 +1,196 @@ +using RadicadeServer.RecNet.Avatar; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The loader prefers the fuller 2022-avatar.json and keeps avatar.json as a fallback, and it +/// only backfills metadata when the chosen catalog is missing it. These tests pin that +/// preference order so the served avatar list cannot silently fall back. +/// +public class AvatarCatalogSourceTests +{ + [Fact] + public void NewerCatalogIsPreferredOverTheFallback() + { + Assert.Equal("2022-avatar.json", DBConfig.AvatarCatalogFileNames[0]); + Assert.Contains("avatar.json", DBConfig.AvatarCatalogFileNames); + } + + [Fact] + public void CandidatePathsTryThePreferredFileInEveryDirectoryFirst() + { + var paths = DBConfig.AvatarCatalogCandidatePaths().ToArray(); + + Assert.NotEmpty(paths); + var firstFallback = Array.FindIndex(paths, path => path.EndsWith("avatar.json", StringComparison.OrdinalIgnoreCase) + && !path.EndsWith("2022-avatar.json", StringComparison.OrdinalIgnoreCase)); + var lastPreferred = Array.FindLastIndex(paths, path => path.EndsWith("2022-avatar.json", StringComparison.OrdinalIgnoreCase)); + + Assert.True(lastPreferred >= 0, "2022-avatar.json should be among the candidate paths."); + Assert.True(firstFallback > lastPreferred, + "every 2022-avatar.json location must be tried before any avatar.json location."); + } + + [Fact] + public void CompleteCatalogNeedsNoEnrichment() + { + var catalogJson = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar.json")); + var items = AvatarUnlockCatalog.ParseCatalog(catalogJson); + + // The compact avatar.json carries no item ids, so it is enriched from merged-avatar.json. + Assert.True(AvatarUnlockCatalog.NeedsMetadataEnrichment(items)); + Assert.False(AvatarUnlockCatalog.NeedsMetadataEnrichment(null)); + Assert.False(AvatarUnlockCatalog.NeedsMetadataEnrichment(Array.Empty())); + Assert.False(AvatarUnlockCatalog.NeedsMetadataEnrichment( + [new AvatarItem { AvatarItemDesc = "item,", AvatarItemId = 12, ThumbnailImage = "a.png" }])); + } + + [Fact] + public void ResolvableFilterDropsOnlyUnknownItems() + { + var original = DBConfig.ResolvableAvatarItemDescs; + try + { + DBConfig.ResolvableAvatarItemDescs = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "8aa79563-ace1-4ba7-ad0c-f3210a78142f," + }; + + var kept = AvatarUnlockCatalog.KeepResolvableItems( + [ + new AvatarItem { AvatarItemDesc = "8aa79563-ace1-4ba7-ad0c-f3210a78142f,,," }, + new AvatarItem { AvatarItemDesc = "not-in-the-client-database,,," } + ]); + + var item = Assert.Single(kept); + Assert.Equal("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", item.AvatarItemDesc); + } + finally + { + DBConfig.ResolvableAvatarItemDescs = original; + } + } + + [Fact] + public void ResolvableFilterIsANoOpWithoutALookup() + { + var original = DBConfig.ResolvableAvatarItemDescs; + try + { + DBConfig.ResolvableAvatarItemDescs = new HashSet(StringComparer.OrdinalIgnoreCase); + + var kept = AvatarUnlockCatalog.KeepResolvableItems( + [new AvatarItem { AvatarItemDesc = "anything," }]); + + Assert.Equal("anything,", Assert.Single(kept).AvatarItemDesc); + } + finally + { + DBConfig.ResolvableAvatarItemDescs = original; + } + } + + /// + /// The client only ships assets for items released before its build, so a newer row makes it + /// log "Unknown Material guid" and throw "Bad outfit string. No prefab found!". Rain Cloud + /// Hat (2023-04-07) is the item that actually crashed a boot in the 20230301 build. + /// + [Fact] + public void ItemsReleasedAfterTheClientBuildAreNotServed() + { + const string rainCloudHat = "24781418-38ad-411f-925e-2321fe616b51,"; + const string turkeySweater = "002a0f2f-1a24-4439-b578-470818ef8325,"; + + var kept = AvatarUnlockCatalog.KeepClientAvailableItems( + [ + new AvatarItem { AvatarItemDesc = rainCloudHat, CreatedAt = new DateTime(2023, 4, 7, 18, 16, 3, DateTimeKind.Utc) }, + new AvatarItem { AvatarItemDesc = turkeySweater, CreatedAt = new DateTime(2022, 11, 1, 17, 51, 50, DateTimeKind.Utc) }, + new AvatarItem { AvatarItemDesc = "no-release-date," }, + new AvatarItem { AvatarItemDesc = "offsetless,", CreatedAt = new DateTime(2023, 4, 7, 18, 16, 3, DateTimeKind.Unspecified) } + ]); + + Assert.DoesNotContain(kept, item => item.AvatarItemDesc == rainCloudHat); + Assert.DoesNotContain(kept, item => item.AvatarItemDesc == "offsetless,"); + Assert.Contains(kept, item => item.AvatarItemDesc == turkeySweater); + Assert.Contains(kept, item => item.AvatarItemDesc == "no-release-date,"); + } + + [Fact] + public void ReleaseCutoffIsTheClientBuildDate() + { + Assert.Equal(new DateTime(2023, 3, 1, 0, 0, 0, DateTimeKind.Utc), DBConfig.AvatarItemReleaseCutoffUtc); + } + + [Fact] + public void ShippedLookupCoversTheCatalogServedToClients() + { + var lookupPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar-item-lookup.json"); + var catalogPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json"); + if (!File.Exists(lookupPath) || !File.Exists(catalogPath)) + { + return; + } + + var descriptions = System.Text.Json.JsonDocument.Parse(File.ReadAllText(lookupPath)) + .RootElement.GetProperty("descriptions") + .EnumerateArray() + .Select(element => element.GetString()!) + .Where(description => !string.IsNullOrWhiteSpace(description)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.True(descriptions.Count > 2000, $"lookup only lists {descriptions.Count} descriptions"); + + var original = DBConfig.ResolvableAvatarItemDescs; + var originalCatalog = DBConfig.DefaultAvatarCatalog; + var originalFallback = DBConfig.DefaultAvatarUnlocks; + try + { + DBConfig.ResolvableAvatarItemDescs = descriptions; + DBConfig.DefaultAvatarCatalog = File.ReadAllText(catalogPath); + DBConfig.DefaultAvatarUnlocks = "[]"; + + var served = AvatarUnlockCatalog.GetSafeDefaultItems(); + + Assert.NotEmpty(served); + Assert.Equal(2990, descriptions.Count); + Assert.Equal(AvatarUnlockCatalog.ParseCatalog(DBConfig.DefaultAvatarCatalog).Count, served.Count); + Assert.DoesNotContain(served, item => !descriptions.Contains(item.AvatarItemDesc)); + Assert.DoesNotContain(served, item => AvatarItemDescription.HasEmptyMaterialSlot(item.AvatarItemDesc)); + Assert.DoesNotContain(served, item => + item.CreatedAt != default && item.CreatedAt.ToUniversalTime() >= DBConfig.AvatarItemReleaseCutoffUtc); + // This item is present in the September client, unlike the old March lookup. + Assert.Contains(served, item => + item.AvatarItemDesc.StartsWith("24781418-38ad-411f-925e-2321fe616b51", StringComparison.OrdinalIgnoreCase)); + } + finally + { + DBConfig.ResolvableAvatarItemDescs = original; + DBConfig.DefaultAvatarCatalog = originalCatalog; + DBConfig.DefaultAvatarUnlocks = originalFallback; + } + } + + [Fact] + public void ShippedCatalogHasUniqueServerIdsAndRetainsKnownMetadata() + { + var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json"); + if (!File.Exists(path)) + { + // The fixture is optional: without it the loader simply falls back to avatar.json. + return; + } + + var items = AvatarUnlockCatalog.ParseCatalog(File.ReadAllText(path)); + + Assert.NotEmpty(items); + Assert.DoesNotContain(items, item => AvatarItemDescription.HasEmptyMaterialSlot(item.AvatarItemDesc)); + Assert.DoesNotContain(items, item => item.AvatarItemId <= 0); + Assert.Equal(items.Count, items.Select(item => item.AvatarItemId).Distinct().Count()); + Assert.DoesNotContain(items, item => string.IsNullOrWhiteSpace(item.FriendlyName)); + Assert.Contains(items, item => item.FriendlyName == "NFL Houston Texans Hat" + && !string.IsNullOrWhiteSpace(item.ThumbnailImage)); + // New client-only entries have no known store thumbnail; do not invent image URLs. + Assert.True(AvatarUnlockCatalog.NeedsMetadataEnrichment(items)); + } +} diff --git a/ServerTests/AvatarGiftGenerateTests.cs b/ServerTests/AvatarGiftGenerateTests.cs new file mode 100644 index 0000000..09a28d0 --- /dev/null +++ b/ServerTests/AvatarGiftGenerateTests.cs @@ -0,0 +1,416 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.API.Avatars.V3; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Storefront; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +public class AvatarGiftGenerateTests +{ + private const long PlayerId = 6812; + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GenerateGiftAcceptsTheEconFormContractAndPersistsAnOpenableGift(bool useV3) + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var db = new RecDBContext(options); + + Controller controller = useV3 ? new AvatarV3(db) : new AvatarV2(db); + SetUserAndFormBody(controller, + $"MFPELEJLJLD={(int)GiftContext.Game_Drop}&CIEHFJEIOJJ={(int)GiftContext.GameRewards_Tokens}&HNKBFLPDLOC=true&DPAJDIEBNKM=earned+in+game&Xp=17"); + + var result = Assert.IsType(await (useV3 + ? ((AvatarV3)controller).GenerateGift() + : ((AvatarV2)controller).GenerateGift())); + var gift = Assert.IsType(result.Value); + Assert.True(gift.GiftDropId > 0); + Assert.Equal(PlayerId, gift.FromPlayerId); + Assert.Equal(GiftContext.Game_Drop, gift.Context); + Assert.Equal("earned in game", gift.Tooltip); + Assert.Equal(Rarity.Rare, gift.Rarity); + Assert.Equal(17, gift.Xp); + Assert.InRange(gift.Currency, 10, 500); + + var pending = await StorefrontShared.GetPendingGiftsAsync(db, PlayerId); + var stored = Assert.Single(pending); + Assert.Equal(gift.GiftDropId, stored.GiftDropId); + Assert.Equal(gift.GiftDropId, stored.ForClient().Id); + Assert.Equal(gift.Currency, stored.Currency); + Assert.Equal(gift.Rarity, stored.Rarity); + } + + [Fact] + public async Task GenerateGiftAcceptsFriendlyJsonAndRejectsUnknownContexts() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var db = new RecDBContext(options); + + var api = new AvatarV2(db); + SetUserAndJsonBody(api, "{\"GiftContext\":2,\"AlternateGiftContext\":51,\"IsGameGift\":false,\"Message\":\"daily reward\",\"Xp\":9}"); + + var result = Assert.IsType(await api.GenerateGift()); + var gift = Assert.IsType(result.Value); + Assert.Equal(GiftContext.Game_Drop, gift.Context); + Assert.Equal("daily reward", gift.Tooltip); + Assert.Equal(9, gift.Xp); + Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); + + var invalid = new AvatarV2(db); + SetUserAndFormBody(invalid, "MFPELEJLJLD=999999&DPAJDIEBNKM=bad"); + Assert.IsType(await invalid.GenerateGift()); + Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); + } + + /// + /// A gift box holding an item becomes that item, and it keeps its name. + /// + /// The unlock used to be written from the descriptor alone - new AvatarItem { AvatarItemDesc } + /// - so everything else about the item was missing: the wardrobe listed a nameless, id-less, + /// thumbnail-less entry. It read as a copy of the item rather than the item, because that is what + /// it was. The gift's own name and rarity are what an unlock this server can resolve has to + /// carry, and the catalogue's own record is used where the catalogue has one (see the test + /// below). + /// + [Fact] + public async Task AGrantedItemUnlockKeepsTheNameTheRewardWasGivenUnder() + { + await using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + var gift = new Gift + { + GiftDropId = 4711, + FriendlyName = "Bandana (Flowers, Black)", + Tooltip = "First Game of the Day", + AvatarItemDesc = "9ff1f0ff-a0d6-4b3c-9b7c-6c0e5a1b2c3d,44d2f8a1-0000-0000-0000-000000000000", + Rarity = Rarity.Uncommon, + CurrencyType = CurrencyType.Invalid + }; + + await StorefrontShared.GrantGiftAsync(db, PlayerId, gift); + await db.SaveChangesAsync(); + + var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == PlayerId); + var item = Assert.Single(unlocks!.GetAvatarItems(), x => x.AvatarItemDesc == gift.AvatarItemDesc); + + Assert.Equal("Bandana (Flowers, Black)", item.FriendlyName); + Assert.Equal("First Game of the Day", item.Tooltip); + Assert.Equal((int)Rarity.Uncommon, item.Rarity); + } + + /// + /// An item the shipped catalogue carries is granted as the catalogue's own record: the name, the + /// item id, the type and the thumbnail the client draws it from, not just the descriptor. This is + /// what "the ACTUAL item instead of a cloned item" is. + /// + [Fact] + public void TheCatalogueResolvesAGrantedItemToItsOwnRecord() + { + var catalogue = JsonSerializer.Serialize(new List + { + new() + { + AvatarItemId = 55, + AvatarItemDesc = "abc123,def456", + FriendlyName = "Bandana (Flowers, Black)", + Tooltip = "Bandana (Flowers, Black)", + Rarity = 20, + AvatarItemType = 1, + ThumbnailImage = "thumb.png", + CreatedAt = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc) + } + }); + + var item = AvatarUnlockCatalog.FindItem("abc123,def456", catalogue); + + Assert.NotNull(item); + Assert.Equal("abc123,def456", item!.AvatarItemDesc); + Assert.Equal("Bandana (Flowers, Black)", item.FriendlyName); + Assert.Equal(55, item.AvatarItemId); + Assert.Equal(20, item.Rarity); + Assert.Equal(1, item.AvatarItemType); + Assert.Equal("thumb.png", item.ThumbnailImage); + + // A descriptor the catalogue does not carry resolves to nothing, so the grant path falls back + // to the gift's own name rather than writing a nameless unlock. + Assert.Null(AvatarUnlockCatalog.FindItem("not-in-the-catalogue", catalogue)); + } + + /// + /// A generate for a reward the player is still holding hands over THAT item, not a token box. + /// + /// The client collects a finished activity through this route and never names the item - the + /// request carries the reward's context and nothing else - so the item is whatever the reward + /// selection is holding. Generating a fresh random token box here is why a completed Stunt + /// Runner run or quest paid tokens while its selection screen showed the right items. + /// + [Fact] + public async Task GenerateGiftHandsOverTheItemTheOutstandingRewardIsHolding() + { + await using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + var item = new Gift + { + GiftDropId = 9001, + FriendlyName = "Archer Hat (Tan)", + Tooltip = "Archer Hat (Tan)", + AvatarItemDesc = "d2d3264b-fbf6-40b5-9f10-c85ec737d112,9b35ae52-febe-486a-8557-ec7190a9756e", + CurrencyType = CurrencyType.Invalid, + Currency = 0, + Rarity = Rarity.Uncommon, + Context = GiftContext.Quest_Goblin_C + }; + var reward = new GameReward + { + RewardSelectionId = 4242, + Message = "First Game of the Day", + GiftContext = GiftContext.Quest_Goblin_C, + RewardType = RewardType.PostGameActivity, + GiftDrop1 = item, + GiftDrop2 = item, + GiftDrop3 = item, + CreatedAt = new DateTime(2026, 10, 5, 0, 0, 0, DateTimeKind.Utc) + }; + var json = new ProgressionJson { PendingGameRewards = new List { reward } }; + db.Progression.Add(new ProgressionData { PlayerId = PlayerId, Progress = json.ToJson() }); + await db.SaveChangesAsync(); + + var api = new AvatarV2(db); + SetUserAndFormBody(api, $"MFPELEJLJLD={(int)GiftContext.Quest_Goblin_C}&DPAJDIEBNKM=First+Game+of+the+Day"); + + var result = Assert.IsType(await api.GenerateGift()); + var gift = Assert.IsType(result.Value); + + // The item the selection was showing, not a package of Rec Center tokens. + Assert.Equal("Archer Hat (Tan)", gift.FriendlyName); + Assert.Equal(item.AvatarItemDesc, gift.AvatarItemDesc); + Assert.Equal(CurrencyType.Invalid, gift.CurrencyType); + Assert.Equal(0, gift.Currency); + Assert.Equal(GiftContext.Quest_Goblin_C, gift.Context); + + // The collection is taken, not copied: the reward is completed and remembers the box. + var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + var saved = Assert.Single(stored.GetProgressionJson()!.PendingGameRewards); + Assert.True(saved.Consumed); + Assert.Equal(gift.GiftDropId, saved.ConsumedGiftDropId); + + // And it is a pending box, so the consume the client sends next is what unlocks the item. + var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); + Assert.Equal(gift.GiftDropId, box.GiftDropId); + Assert.Equal(item.AvatarItemDesc, box.AvatarItemDesc); + } + + /// + /// A generate for a reward the selection route already filed as a gift box hands THAT box back. + /// + /// This is the shape the client actually collects a quest with: the selection route files the + /// chosen box as a pending gift box, and opening the watch asks for the gift its context names + /// rather than naming the box. The reward itself is completed by that selection, so a lookup that + /// only considers *outstanding* rewards finds nothing - and the collection was answered with a + /// fresh box of tokens while the player's own item sat pending a step away. + /// + [Fact] + public async Task GenerateGiftHandsBackTheBoxTheSelectionAlreadyFiled() + { + await using var db = NewDb(); + var box = RunnerBox(); + await SeedCollectedRewardAsync(db, box); + await StorefrontShared.PersistGiftBoxAsync(db, PlayerId, box, notify: false); + + var api = new AvatarV2(db); + SetUserAndFormBody(api, $"MFPELEJLJLD={(int)GiftContext.StuntRunner_TheMainEvent_C}"); + + var gift = Assert.IsType(Assert.IsType(await api.GenerateGift()).Value); + + Assert.Equal("Runner Harness (Green)", gift.FriendlyName); + Assert.Equal(box.AvatarItemDesc, gift.AvatarItemDesc); + Assert.Equal(CurrencyType.Invalid, gift.CurrencyType); + Assert.Equal(0, gift.Currency); + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, gift.Context); + } + + /// + /// The reward for an activity context is drawn from that activity's own pool, as an item. + /// + /// This is the choice the generate route has to make when nothing is on file for the context the + /// client named - the box of the run it has just finished. It is drawn from the pool that context + /// names and filtered by what the player already owns, and it is never a package of Rec Center + /// tokens: the tokens are what every completed game and quest used to be paid here. + /// + [Fact] + public async Task BuildRoomRewardSelectsAnUnownedItemFromTheRoomsOwnPool() + { + await using var db = NewDb(); + var catalogue = ShippedCatalogueJson(); + + var box = await GameRewardRedemption.BuildRoomRewardAsync( + db, PlayerId, GiftContext.StuntRunner_TheMainEvent_C, catalogueJson: catalogue); + + Assert.NotNull(box); + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, box!.Context); + Assert.Equal(CurrencyType.Invalid, box.CurrencyType); + Assert.Equal(0, box.Currency); + Assert.True(box.GiftDropId > 0); + Assert.Contains(RroRewardPools.Resolve(GiftContext.StuntRunner_TheMainEvent_C, catalogue), + item => item.AvatarItemDesc == box.AvatarItemDesc); + + // And the box the run generated is recorded as handed over, so the reward request that + // follows it cannot pay the same run a second time. + var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + var reward = Assert.Single(stored.GetProgressionJson()!.PendingGameRewards); + Assert.True(reward.Consumed); + Assert.Equal(box.GiftDropId, reward.ConsumedGiftDropId); + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, reward.GiftContext); + } + + /// + /// A rank the player has taken everything from falls back inside the same quest, then out of it. + /// + /// A quest never pays tokens and never reaches into another quest's pool: Stunt Runner S walks + /// down its own ranks, and only once the whole quest is spent does the reward leave it for the + /// ordinary non-exclusive pool. + /// + [Fact] + public async Task BuildRoomRewardFallsBackInsideTheSameQuestOnly() + { + await using var db = NewDb(); + var catalogue = ShippedCatalogueJson(); + + // The player owns everything Stunt Runner S pays. + var s = RroRewardPools.Resolve(GiftContext.StuntRunner_TheMainEvent_S, catalogue); + Assert.NotEmpty(s); + await OwnEverythingAsync(db, s); + + var box = await GameRewardRedemption.BuildRoomRewardAsync( + db, PlayerId, GiftContext.StuntRunner_TheMainEvent_S, catalogueJson: catalogue); + + Assert.NotNull(box); + // A lower rank of the same quest - the box keeps the context it was drawn from, and that + // context belongs to Stunt Runner. + Assert.NotEqual(GiftContext.StuntRunner_TheMainEvent_S, box!.Context); + Assert.Equal("StuntRunner", RroRewardPools.RoomForContext(box.Context)); + Assert.Contains(RroRewardPools.LowerRanksOf(GiftContext.StuntRunner_TheMainEvent_S), c => c == box.Context); + } + + /// + /// A generate for a finished activity is answered with a gift, never with an error. + /// + /// The client uses whatever this route returns as the box the player opens, so a 400 here is a + /// failed generate on the client - "Failed to generate new gift: An error occurred" - and a reward + /// the player never collects. Whether the box holds an item or the ordinary fallback, it is a box. + /// + [Fact] + public async Task GenerateGiftNeverAnswersAnActivityBoxWithAnError() + { + await using var db = NewDb(); + await SeedCollectedRewardAsync(db, RunnerBox()); + + var api = new AvatarV2(db); + SetUserAndFormBody(api, $"MFPELEJLJLD={(int)GiftContext.StuntRunner_TheMainEvent_C}"); + + var gift = Assert.IsType(Assert.IsType(await api.GenerateGift()).Value); + + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, gift.Context); + Assert.True(gift.GiftDropId > 0); + + // It is filed as the player's box, so opening it is what puts the reward in the locker, and + // the hand-over is recorded against it. + var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); + Assert.Equal(gift.GiftDropId, box.GiftDropId); + + var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + Assert.Contains(stored.GetProgressionJson()!.PendingGameRewards, + r => r.Consumed && r.ConsumedGiftDropId == gift.GiftDropId); + } + + private static async Task OwnEverythingAsync(RecDBContext db, IEnumerable items) + { + var owned = items.Select(item => new AvatarItem { AvatarItemDesc = item.AvatarItemDesc }).ToList(); + db.Unlocks.Add(new PlayerUnlocks { AccountId = PlayerId, AvatarItems = owned.ToJson() }); + await db.SaveChangesAsync(); + } + + private static string ShippedCatalogueJson() => + File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json")); + + private static RecDBContext NewDb() => new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static Gift RunnerBox() => new() + { + GiftDropId = 7331, + FriendlyName = "Runner Harness (Green)", + Tooltip = "Runner Harness (Green)", + AvatarItemDesc = "1b3f0c6e-9b21-4c77-9d5a-2f0a53b3d1a4,bb31f2e0-0f2f-4d4a-9c3e-6a0d6b1f4c88", + CurrencyType = CurrencyType.Invalid, + Currency = 0, + Rarity = Rarity.Uncommon, + Context = GiftContext.StuntRunner_TheMainEvent_C + }; + + private static async Task SeedCollectedRewardAsync(RecDBContext db, Gift box) + { + var reward = new GameReward + { + RewardSelectionId = 5150, + Message = "Stunt Runner: The Main Event", + GiftContext = GiftContext.StuntRunner_TheMainEvent_C, + RewardType = RewardType.PostQuestActivity, + GiftDrop1 = box, + GiftDrop2 = box, + GiftDrop3 = box, + CreatedAt = DateTime.UtcNow.AddMinutes(-5), + Consumed = true, + ConsumedGiftDropId = box.GiftDropId, + ConsumedAt = DateTime.UtcNow.AddMinutes(-1) + }; + var json = new ProgressionJson { PendingGameRewards = new List { reward } }; + db.Progression.Add(new ProgressionData { PlayerId = PlayerId, Progress = json.ToJson() }); + await db.SaveChangesAsync(); + } + + private static void SetUserAndFormBody(Controller controller, string body) + { + SetRequest(controller, "application/x-www-form-urlencoded", body); + } + + private static void SetUserAndJsonBody(Controller controller, string body) + { + SetRequest(controller, "application/json", body); + } + + private static void SetRequest(Controller controller, string contentType, string body) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString())); + var context = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }; + context.Request.ContentType = contentType; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + context.Request.ContentLength = context.Request.Body.Length; + controller.ControllerContext = new ControllerContext { HttpContext = context }; + } +} diff --git a/ServerTests/AvatarItemDescriptionTests.cs b/ServerTests/AvatarItemDescriptionTests.cs new file mode 100644 index 0000000..2a3e1b9 --- /dev/null +++ b/ServerTests/AvatarItemDescriptionTests.cs @@ -0,0 +1,97 @@ +using RadicadeServer.RecNet.Avatar; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The catalog descriptions we hand the client must match the shape its own +/// AvatarItemWardrobeRuntimeConfig uses. Live RecNet records only ever carry +/// "guid," (no combination) or "guid,mat1,mat2," (materialised), never empty slots. +/// An empty slot makes the client log "Browsable avatar item not found" and drop the +/// item from the avatar, so these tests pin both the rewrite and its no-op cases. +/// +public class AvatarItemDescriptionTests +{ + [Theory] + // The legacy "no combination" form loses its empty slots. + [InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,", "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")] + [InlineData("_OWVy3z6iU-M3-zbQgSLig,,,", "_OWVy3z6iU-M3-zbQgSLig,")] + // An empty slot in the middle of a material list is dropped, order is preserved. + [InlineData("d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,,0440f08f-ef1d-49d8-942b-523056e8bb45,", + "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,")] + // Surrounding whitespace is not part of the identifier. + [InlineData(" 8aa79563-ace1-4ba7-ad0c-f3210a78142f,, ", "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")] + // Well-formed descriptions are returned byte-for-byte, including the shapes live + // RecNet writes and the trailing comma the client's own serializer omits. + [InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")] + [InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c,", + "8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c,")] + [InlineData("d84993dd-4e15-4481-9450-f88a34cdbc82,FnhttSsCoUCSAW8j-stC5A", + "d84993dd-4e15-4481-9450-f88a34cdbc82,FnhttSsCoUCSAW8j-stC5A")] + [InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c", + "8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c")] + public void Normalize_ProducesClientShapedDescriptions(string input, string expected) + { + Assert.Equal(expected, AvatarItemDescription.Normalize(input)); + } + + [Fact] + public void Normalize_IsIdempotent() + { + var once = AvatarItemDescription.Normalize("8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,"); + Assert.Equal(once, AvatarItemDescription.Normalize(once)); + } + + [Fact] + public void Normalize_LeavesMissingDescriptionsAlone() + { + Assert.Equal("", AvatarItemDescription.Normalize("")); + Assert.Equal("", AvatarItemDescription.Normalize(null)); + Assert.Equal("", AvatarItemDescription.Normalize(" ")); + // A description with no item GUID at all is not something this can repair. + Assert.Equal(",,,", AvatarItemDescription.Normalize(",,,")); + } + + [Theory] + [InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,", true)] + [InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", false)] + [InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void HasEmptyMaterialSlot_DetectsOnlyLegacyRows(string? input, bool expected) + { + Assert.Equal(expected, AvatarItemDescription.HasEmptyMaterialSlot(input)); + } + + [Fact] + public void CatalogParsingRepairsLegacyRows() + { + var items = AvatarUnlockCatalog.ParseCatalog(""" + [{"AvatarItemDesc":"8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,","UnlockedLevel":0,"PlatformMask":-1, + "FriendlyName":"Rec Room Shirt (V-Neck, White)","Rarity":0}] + """); + + var item = Assert.Single(items); + Assert.Equal("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", item.AvatarItemDesc); + Assert.Equal("Rec Room Shirt (V-Neck, White)", item.FriendlyName); + } + + [Fact] + public void KeepSupportedItemsRepairsLegacyRows() + { + var copy = Assert.Single(AvatarUnlockCatalog.KeepSupportedItems( + [new AvatarItem { AvatarItemDesc = "_qXaNbm4yEeyalo372QbvA,,,", FriendlyName = "Biker Goggles" }])); + + Assert.Equal("_qXaNbm4yEeyalo372QbvA,", copy.AvatarItemDesc); + } + + [Fact] + public void ShippedCatalogNeverServesEmptyMaterialSlots() + { + var catalogJson = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar.json")); + var items = AvatarUnlockCatalog.ParseCatalog(catalogJson); + + Assert.NotEmpty(items); + Assert.DoesNotContain(items, item => AvatarItemDescription.HasEmptyMaterialSlot(item.AvatarItemDesc)); + } +} diff --git a/ServerTests/AvatarWireFormatTests.cs b/ServerTests/AvatarWireFormatTests.cs new file mode 100644 index 0000000..a5471f4 --- /dev/null +++ b/ServerTests/AvatarWireFormatTests.cs @@ -0,0 +1,57 @@ +using RadicadeServer.RecNet.Avatar; +using Xunit; + +namespace RadicadeServer.Tests; + +public class AvatarWireFormatTests +{ + [Theory] + [InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")] + [InlineData("prefab,material", "prefab,material")] + [InlineData("prefab,material,", "prefab,material,")] + [InlineData("prefab,first,second,", "prefab,first,second,")] + [InlineData("prefab,,second,", "prefab,,second,")] + public void V4PreservesTheExactMaterialCombinationKey(string catalogKey, string selectionKey) + { + var dto = new AvatarItemV4(new AvatarItem { AvatarItemDesc = catalogKey }); + var json = System.Text.Json.JsonSerializer.SerializeToElement(dto); + Assert.Equal(selectionKey, json.GetProperty("AvatarItemDesc").GetString()); + } + + [Fact] + public void ScreenshotShirtSurvivesCatalogToWireConversion() + { + var catalog = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json")); + var shirt = AvatarUnlockCatalog.ParseCatalog(catalog) + .Single(item => item.AvatarItemDesc == "8aa79563-ace1-4ba7-ad0c-f3210a78142f,"); + Assert.Equal("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", new AvatarItemV4(shirt).AvatarItemDesc); + Assert.True(shirt.AvatarItemId > 0); + } + + [Fact] + public void BaseEndpointDoesNotReclassifyClothingAsHiddenBodyParts() + { + var originalCatalog = DBConfig.DefaultAvatarCatalog; + var originalLookup = DBConfig.ResolvableAvatarItemDescs; + try + { + DBConfig.DefaultAvatarCatalog = """ + [{"AvatarItemDesc":"shirt,","IsBaseAvatarItem":false}, + {"AvatarItemDesc":"base-body,","IsBaseAvatarItem":true}] + """; + DBConfig.ResolvableAvatarItemDescs = new HashSet(); + // These endpoints do not access the database. + var api = new RadicadeServer.API.Avatars.V1.AvatarV1(null!); + var response = Assert.IsType(api.GetDefaultBaseAvatarItems()); + var baseItems = Assert.IsType>(response.Value); + Assert.Equal("base-body,", Assert.Single(baseItems).AvatarItemDesc); + Assert.DoesNotContain(api.GetDefaultUnlocks(), item => item.AvatarItemDesc == "shirt,"); + Assert.Contains(api.GetDefaultUnlocks(), item => item.AvatarItemDesc == "base-body,"); + } + finally + { + DBConfig.DefaultAvatarCatalog = originalCatalog; + DBConfig.ResolvableAvatarItemDescs = originalLookup; + } + } +} diff --git a/ServerTests/BanAppealResolveTests.cs b/ServerTests/BanAppealResolveTests.cs new file mode 100644 index 0000000..650c704 --- /dev/null +++ b/ServerTests/BanAppealResolveTests.cs @@ -0,0 +1,266 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using RadicadeServer.API.Moderation; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The support site closes a ticket by telling the game what it decided, so the +/// game endpoint behind that call is the only thing standing between a queue +/// item and a player's appeal state. +/// +/// A ban appeal used to have exactly two verdicts, accept and deny. That left a +/// ticket which could be neither accepted nor denied with no way out of the +/// queue, and left staff choosing between lying about a verdict or never +/// clearing the ticket. Reviewing is the third verdict: the ban stands, the +/// appeal stops being pending, and the cooldown starts so the player can appeal +/// again later instead of waiting on a decision that will never come. +/// +/// These tests pin the state each verdict leaves behind, because a wrong field +/// here is invisible until a player is locked out of appealing. +/// +[Collection(DbFactoryCollection.Name)] +public class BanAppealResolveTests +{ + private const long PlayerId = 4242; + private const string AppealCode = "NWVPZX5Q"; + private const string SupportKey = "support-test-key"; + + /* A named in-memory store plus its root, so separate contexts in one test + really do share a database. That is what makes the assertions below check + what was saved rather than what happens to be attached to one context. */ + private sealed class TestStore + { + private readonly string _name = Guid.NewGuid().ToString(); + private readonly InMemoryDatabaseRoot _root = new(); + + public RecDBContext Open() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(_name, _root) + .Options); + } + + private static ModerationDB Ban(string? appealText) => new() + { + PlayerId = PlayerId, + IsBan = true, + Message = "Cheating", + Category = ReportCategory.CoC_Trolling, + CreatedAt = DateTime.UtcNow.AddDays(-1), + ExpireDuration = DateTime.UtcNow.AddDays(30), + AppealCode = AppealCode, + AppealText = appealText + }; + + private static async Task SeedAsync(string? appealText) + { + var store = new TestStore(); + await using var db = store.Open(); + db.Moderation.Add(Ban(appealText)); + await db.SaveChangesAsync(); + return store; + } + + private static async Task> ResolveAsync(TestStore store, string? decision) + { + var previous = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = SupportKey; + + try + { + await using var db = store.Open(); + var controller = new BanAppealAPI(db); + return await controller.Resolve(AppealCode, SupportKey, decision); + } + finally + { + DBConfig.AdminApiKey = previous; + } + } + + private static async Task ReloadAsync(TestStore store) + { + await using var db = store.Open(); + return await db.Moderation.AsNoTracking().SingleAsync(x => x.PlayerId == PlayerId); + } + + /* AppealResponse ships an anonymous { success, error, value }, so the payload + is read reflectively rather than by adding a type just for tests. */ + private static (bool Success, string? Error, string? Value) Read(ActionResult result) + { + var payload = result.Result switch + { + ObjectResult objectResult => objectResult.Value, + _ => null + }; + + Assert.NotNull(payload); + + var type = payload!.GetType(); + var success = (bool)(type.GetProperty("success")!.GetValue(payload) ?? false); + var error = type.GetProperty("error")!.GetValue(payload) as string; + var value = type.GetProperty("value")!.GetValue(payload) as string; + return (success, error, value); + } + + [Fact] + public async Task ReviewingAnAppealLeavesTheBanAndEndsThePendingState() + { + var store = await SeedAsync(PlayerUtils.SupportSiteAppealText); + + var (success, error, value) = Read(await ResolveAsync(store, "reviewed")); + + Assert.True(success, error); + Assert.Equal("Reviewed", value); + + var ban = await ReloadAsync(store); + Assert.True(ban.IsBan); + Assert.Equal("Reviewed", ban.AppealResolution); + Assert.NotNull(ban.AppealResolvedAt); + + /* The appeal body stays as the player wrote it: it is the record of what + was reviewed, not a flag to be cleared. */ + Assert.Equal(PlayerUtils.SupportSiteAppealText, ban.AppealText); + } + + /* The point of the verdict: a reviewed appeal is no longer pending, which is + what the ban screen keys off, and the cooldown now applies so the player + gets a fresh code once it lapses. */ + [Fact] + public async Task AReviewedAppealNoLongerLooksPendingAndStartsTheCooldown() + { + var store = await SeedAsync(PlayerUtils.SupportSiteAppealText); + + Read(await ResolveAsync(store, "review")); + + var ban = await ReloadAsync(store); + var pending = ban.AppealText != null && ban.AppealResolution == null; + Assert.False(pending); + + await using var db = store.Open(); + var cooldown = await PlayerUtils.GetAppealCooldownErrorAsync(db, PlayerId); + Assert.NotNull(cooldown); + } + + [Fact] + public async Task AcceptingAnAppealLiftsTheBan() + { + var store = await SeedAsync(PlayerUtils.SupportSiteAppealText); + + var (success, error, value) = Read(await ResolveAsync(store, "accept")); + + Assert.True(success, error); + Assert.Equal("Accepted", value); + + var ban = await ReloadAsync(store); + Assert.False(ban.IsBan); + Assert.Equal("Accepted", ban.AppealResolution); + } + + [Fact] + public async Task DenyingAnAppealKeepsTheBanAndStartsTheCooldown() + { + var store = await SeedAsync(PlayerUtils.SupportSiteAppealText); + + var (success, error, value) = Read(await ResolveAsync(store, "deny")); + + Assert.True(success, error); + Assert.Equal("Rejected", value); + + var ban = await ReloadAsync(store); + Assert.True(ban.IsBan); + Assert.Equal("Rejected", ban.AppealResolution); + Assert.NotNull(ban.AppealResolvedAt); + } + + /* A second click, a retry after a timeout, or a decision the admin panel + already made must never overwrite the verdict that is already there. */ + [Fact] + public async Task AnExistingVerdictIsNeverOverwritten() + { + var store = await SeedAsync(PlayerUtils.SupportSiteAppealText); + + Read(await ResolveAsync(store, "deny")); + var (success, error, value) = Read(await ResolveAsync(store, "reviewed")); + + Assert.True(success, error); + Assert.Equal("Rejected", value); + Assert.Equal("Rejected", (await ReloadAsync(store)).AppealResolution); + } + + [Fact] + public async Task AnUnknownDecisionChangesNothing() + { + var store = await SeedAsync(PlayerUtils.SupportSiteAppealText); + + var (success, error, value) = Read(await ResolveAsync(store, "maybe")); + + Assert.False(success); + Assert.Equal("Unknown decision.", error); + Assert.Null(value); + + var ban = await ReloadAsync(store); + Assert.True(ban.IsBan); + Assert.Null(ban.AppealResolution); + Assert.Null(ban.AppealResolvedAt); + } + + [Fact] + public async Task TheWrongSecretIsRefusedWithoutTouchingTheAppeal() + { + var store = await SeedAsync(PlayerUtils.SupportSiteAppealText); + + var previous = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = SupportKey; + + ActionResult result; + + try + { + await using var db = store.Open(); + result = await new BanAppealAPI(db).Resolve(AppealCode, "not-the-key", "accept"); + } + finally + { + DBConfig.AdminApiKey = previous; + } + + Assert.IsType(result.Result); + + var ban = await ReloadAsync(store); + Assert.True(ban.IsBan); + Assert.Null(ban.AppealResolution); + } + + [Fact] + public async Task AnUnknownCodeIsRefusedWithoutTouchingAnything() + { + var store = await SeedAsync(PlayerUtils.SupportSiteAppealText); + + var previous = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = SupportKey; + + ActionResult result; + + try + { + await using var db = store.Open(); + result = await new BanAppealAPI(db).Resolve("NOSUCHCODE", SupportKey, "reviewed"); + } + finally + { + DBConfig.AdminApiKey = previous; + } + + var (success, error, _) = Read(result); + Assert.False(success); + Assert.Equal("Unknown appeal code.", error); + Assert.Null((await ReloadAsync(store)).AppealResolution); + } +} diff --git a/ServerTests/BanRelatedAccountTests.cs b/ServerTests/BanRelatedAccountTests.cs new file mode 100644 index 0000000..ee2c6f5 --- /dev/null +++ b/ServerTests/BanRelatedAccountTests.cs @@ -0,0 +1,109 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A ban that lands because the account is related to a banned one - a shared device or address - +/// prints the account it is related to on its own line under the reason. The moderationBlockDetails +/// screen has always done this; these lock in that the pushed ban body carries the same line, and +/// that a ban about the player themselves never names anybody. +/// +[Collection(DbFactoryCollection.Name)] +public class BanRelatedAccountTests +{ + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static async Task AddProfileAsync(RecDBContext db, long accountId, string username) + { + db.Profiles.Add(new Profile + { + accountId = accountId, + username = username, + displayName = username, + profileImage = DBConfig.DefaultProfileImage + }); + await db.SaveChangesAsync(); + } + + [Fact] + public void BuildBlockDetailsAppendsTheRelatedAccountWhenOneIsGiven() + { + var details = PlayerUtils.BuildBlockDetails(true, ReportCategory.Misc, "Sharing a banned device.", + DateTime.UtcNow.AddDays(1), relatedUsername: "HealthyMoose640"); + + Assert.Equal("Sharing a banned device.\nRelated account: @HealthyMoose640", details.Message); + } + + [Fact] + public void BuildBlockDetailsWithoutARelatedAccountIsJustTheReason() + { + var details = PlayerUtils.BuildBlockDetails(true, ReportCategory.Misc, "Harassment.", + DateTime.UtcNow.AddDays(1)); + + Assert.Equal("Harassment.", details.Message); + } + + [Fact] + public async Task ARelationBanNamesTheAccountItIsRelatedTo() + { + using var db = NewDb(); + await AddProfileAsync(db, 12, "HealthyMoose640"); + + var ban = new PlayerUtils.PlayerBanInfo("Hardware", "Sharing a banned device.", + DateTime.UtcNow.AddDays(1), "hw:1", ReportCategory.Misc, BannedAccountId: 12); + + var details = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, accountId: 42); + + Assert.True(details.IsBan); + Assert.Equal("Sharing a banned device.\nRelated account: @HealthyMoose640", details.Message); + Assert.Equal(details.Message, details.DisplayReason); + } + + [Fact] + public async Task ABanAboutThePlayerThemselvesNamesNobody() + { + using var db = NewDb(); + await AddProfileAsync(db, 42, "SoleBearer"); + + var ban = new PlayerUtils.PlayerBanInfo("Moderation", "Harassment.", + DateTime.UtcNow.AddDays(1), "mod:1", ReportCategory.Misc, BannedAccountId: 42); + + var details = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, accountId: 42); + + Assert.Equal("Harassment.", details.Message); + Assert.DoesNotContain("Related account", details.Message); + } + + [Fact] + public async Task ABanWithNoRelatedAccountIsJustTheReason() + { + using var db = NewDb(); + + var ban = new PlayerUtils.PlayerBanInfo("Moderation", "Cheating.", + DateTime.UtcNow.AddDays(1), "mod:2", ReportCategory.Cheating); + + var details = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, accountId: 7); + + Assert.Equal("Cheating.", details.Message); + } + + [Fact] + public async Task RelatedAccountUsernameIgnoresAMissingOrSelfReference() + { + using var db = NewDb(); + await AddProfileAsync(db, 12, "HealthyMoose640"); + + Assert.Null(await PlayerUtils.RelatedAccountUsernameAsync(db, null, 42)); + Assert.Null(await PlayerUtils.RelatedAccountUsernameAsync(db, 42, 42)); + Assert.Null(await PlayerUtils.RelatedAccountUsernameAsync(db, 999, 42)); + Assert.Equal("HealthyMoose640", await PlayerUtils.RelatedAccountUsernameAsync(db, 12, 42)); + } +} diff --git a/ServerTests/CampusCardTests.cs b/ServerTests/CampusCardTests.cs new file mode 100644 index 0000000..1ff486d --- /dev/null +++ b/ServerTests/CampusCardTests.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.API.CampusCard.V1; +using Xunit; + +namespace RadicadeServer.Tests; + +public class CampusCardTests +{ + [Fact] + public void SubscriptionReturnsActiveGoldYear() + { + var result = Assert.IsType(new CampusCardAPI().GetSubscription().Result); + var card = Assert.IsType(result.Value); + Assert.NotNull(card.subscription); + Assert.Equal(RadicadeServer.RecNet.Enums.Store.SKU.CampusCardLevel.Gold, card.subscription!.level); + Assert.Equal(RadicadeServer.RecNet.Enums.Store.SKU.CampusCardPeriod.Year, card.subscription.period); + Assert.True(card.subscription.expirationDate > DateTime.UtcNow.AddMonths(11)); + } + + [Fact] + public void SignUpBonusReturnsReferenceShape() + { + var result = Assert.IsType(new CampusCardAPI().SignUpBonus()); + Assert.Equal(3, result.Value!.GetType().GetProperty("RRPlusSignUpBonusId")!.GetValue(result.Value)); + } +} diff --git a/ServerTests/CarouselFeedShapeTests.cs b/ServerTests/CarouselFeedShapeTests.cs new file mode 100644 index 0000000..e24d61a --- /dev/null +++ b/ServerTests/CarouselFeedShapeTests.cs @@ -0,0 +1,146 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RoomServer; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The Play menu's carousel rows read rooms/carousel/{source}&skip=&take=. The +/// client's own template for that feed is paged, and every sibling paged rooms feed answers +/// { Results, TotalResults }; this one answered a bare array, which the client reported as +/// +/// Malformed Response: '[ { "RoomId": 167, "IsDorm": false, ... } ]' +/// +/// and then as Error refreshing rooms from source CarouselEndpoint - the row stayed empty. +/// +/// The route assertion is here too: a controller-level test calls the handler by name, so it +/// cannot see that the URL the client builds has no route at all (that is what the 404 was +/// before this route existed). +/// +[Collection(DbFactoryCollection.Name)] +public class CarouselFeedShapeTests +{ + private const long RoomId = 167; + private const long OtherRoomId = 211; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + return (factory, factory.CreateDbContext()); + } + + private static void SetUser(Controller controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + /// Two public, live rooms - the shape a carousel row draws from. + private static async Task SeedRoomsAsync(RecDBContext db) + { + db.Rooms.AddRange( + new RoomData + { + RoomId = RoomId, + Name = "Orientation", + CreatorAccountId = 1, + Accessibility = Accessibility.Public, + State = RoomState.Active, + MaxPlayers = 1, + CreatedAt = DateTime.UtcNow, + }, + new RoomData + { + RoomId = OtherRoomId, + Name = "RecCenter", + CreatorAccountId = 1, + Accessibility = Accessibility.Public, + State = RoomState.Active, + MaxPlayers = 12, + CreatedAt = DateTime.UtcNow, + }); + await db.SaveChangesAsync(); + } + + private static JsonElement ValueOf(IActionResult result) + { + var ok = Assert.IsType(result); + return JsonSerializer.SerializeToElement(ok.Value); + } + + [Fact] + public void SupportsThePagedCarouselRouteTheClientBuilds() + { + var method = typeof(RoomServerAPI).GetMethod(nameof(RoomServerAPI.GetCarouselRooms))!; + + var attributes = method + .GetCustomAttributes(typeof(HttpMethodAttribute), false) + .Cast() + .ToList(); + + Assert.Contains(attributes, x => x.Template == "rooms/carousel/{*source}"); + Assert.Contains(attributes, x => x is HttpGetAttribute); + } + + [Fact] + public async Task TheCarouselRowAnswersThePagedEnvelopeWithRoomsInIt() + { + var (_, db) = CreateContext(); + await using var _db = db; + await SeedRoomsAsync(db); + + var api = new RoomServerAPI(db); + SetUser(api, 1); + + var value = ValueOf(await api.GetCarouselRooms("foryou", skip: -1, take: 20)); + + Assert.Equal(JsonValueKind.Object, value.ValueKind); + Assert.Equal(2, value.GetProperty("TotalResults").GetInt32()); + var results = value.GetProperty("Results").EnumerateArray().ToArray(); + Assert.Equal(new long[] { RoomId, OtherRoomId }, + results.Select(room => room.GetProperty("RoomId").GetInt64()).ToArray()); + } + + [Fact] + public async Task ASourceThatMatchesNothingStillFillsTheRow() + { + var (_, db) = CreateContext(); + await using var _db = db; + await SeedRoomsAsync(db); + + var api = new RoomServerAPI(db); + SetUser(api, 1); + + // "rising" is one of the server's own ordering names, so it matches no room by name or + // tag. An empty row is not the answer: the client marks the source failed. + var value = ValueOf(await api.GetCarouselRooms("rising&skip=0&take=20", skip: -1, take: 20)); + + Assert.Equal(2, value.GetProperty("TotalResults").GetInt32()); + } +} diff --git a/ServerTests/CdnRangeRequestTests.cs b/ServerTests/CdnRangeRequestTests.cs new file mode 100644 index 0000000..2feb3b7 --- /dev/null +++ b/ServerTests/CdnRangeRequestTests.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.CDN; +using RadicadeServer.DB; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The client's Unity asset bundle download is resumable: its string table carries +/// "bytes={0}-", "bytes={0}-{1}", "Accept-Ranges" and "Content-Range", so it asks the CDN for +/// byte ranges rather than a whole file. Every route that serves those bytes must therefore +/// process ranges — a range answer that returns the entire body makes the client re-transfer +/// the whole file once per range, which is how a 73 MB Studio bundle came to spend 228 s in +/// AssetBundle_Download, outlive the room's Photon connection, and fail the load with +/// code 6706-3. Dropping enableRangeProcessing is invisible at runtime (the download +/// still completes, just ~20x slower and past the network timeout), so it is pinned here. +/// +[Collection(DbFactoryCollection.Name)] +public class CdnRangeRequestTests : IDisposable +{ + private const string BlobName = "recdump-0cfd85d85c7b905dfbb83a6f3d3c887b.room"; + + private readonly string root = Directory.CreateTempSubdirectory("radicade-cdn-range-test-").FullName; + private readonly string? previousStorage; + + public CdnRangeRequestTests() + { + previousStorage = DBConfig.CDNStorage; + DBConfig.CDNStorage = root; + File.WriteAllText(Path.Join(root, BlobName), "blob"); + } + + public void Dispose() + { + DBConfig.CDNStorage = previousStorage!; + + try + { + Directory.Delete(root, true); + } + catch (IOException) + { + } + } + + private static readonly string ImmutableCache = "public, max-age=31536000, immutable"; + private static readonly string ReplaceableCache = "public, max-age=86400"; + + private const string BundleName = "0f0f0f0f-0f0f-0f0f-0f0f-0f0f0f0f0f0f.assetbundle"; + + private static T WithHttpContext(T controller) where T : Controller + { + controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; + return controller; + } + + private static PhysicalFileResult RangeServed(IActionResult result) + { + var file = Assert.IsType(result); + Assert.True(file.EnableRangeProcessing, $"{file.FileName} is served without range processing"); + Assert.Equal("application/octet-stream", file.ContentType); + return file; + } + + [Fact] + public void TheDataRouteThatCarriesAssetBundlesProcessesRanges() + { + // /data/{blob} is where the bundle bytes land: /rooms/unity_assets/... hands the + // client a Filename and nothing else, so this is the URL the downloader ranges over. + var file = RangeServed(new DataCDN().GetDataBlob(BlobName)); + + Assert.Equal(Path.Join(root, BlobName), file.FileName); + } + + [Fact] + public void TheRoomAndUnityAssetRoutesProcessRanges() + { + RangeServed(new RoomsCDN().GetDataBlob(BlobName)); + RangeServed(new RoomsCDN().GetUnityAssetRaw(BlobName, 0, 1)); + RangeServed(new RoomsCDN().GetUnityAssetRaw(BlobName, null, null)); + } + + [Fact] + public void TheInventionRouteProcessesRanges() + { + RangeServed(new InventionCDN().GetDataBlob(BlobName)); + } + + [Fact] + public void TheBlobRoutesTellClientsTheyMayCacheWhatTheyDownloaded() + { + // A Studio room's bundle is the payload that decides whether the room loads at all. + // With no cache directive the client's HTTP stack is free to fetch all of it again + // on the next join, at ~100 ms RTT, and the room load is exactly as slow as it was + // the first time. + File.WriteAllText(Path.Join(root, BundleName), "bundle"); + + var data = WithHttpContext(new DataCDN()); + data.GetDataBlob(BundleName); + Assert.Equal(ImmutableCache, data.Response.Headers["Cache-Control"].ToString()); + + var rooms = WithHttpContext(new RoomsCDN()); + rooms.GetUnityAssetRaw(BundleName, 0, 1); + Assert.Equal(ImmutableCache, rooms.Response.Headers["Cache-Control"].ToString()); + + // Room saves are content-addressed too, but admin repairs replace one under its + // existing name, so they must expire rather than be pinned forever. + data = WithHttpContext(new DataCDN()); + data.GetDataBlob(BlobName); + Assert.Equal(ReplaceableCache, data.Response.Headers["Cache-Control"].ToString()); + + rooms = WithHttpContext(new RoomsCDN()); + rooms.GetDataBlob(BlobName); + Assert.Equal(ReplaceableCache, rooms.Response.Headers["Cache-Control"].ToString()); + } + + [Fact] + public void AnUnknownBlobStillAnswersNotFound() + { + // Range support must not turn a missing file into a body, the way an unhandled + // unsatisfiable range can. + Assert.IsType(new DataCDN().GetDataBlob("not-on-disk.room")); + Assert.IsType(new RoomsCDN().GetDataBlob("not-on-disk.room")); + Assert.IsType(new RoomsCDN().GetUnityAssetRaw("not-on-disk.room", 0, 1)); + Assert.IsType(new InventionCDN().GetDataBlob("not-on-disk.room")); + } +} diff --git a/ServerTests/ChatThreadTests.cs b/ServerTests/ChatThreadTests.cs new file mode 100644 index 0000000..63112f1 --- /dev/null +++ b/ServerTests/ChatThreadTests.cs @@ -0,0 +1,637 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Notifications; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The chat surface the client actually reads. Three shapes are load-bearing here, and each +/// of them was wrong before this suite existed: +/// +/// * a thread opened as a conversation must NEVER answer an empty `messages` array. The +/// client's `RRUI.Data.ChatThreadModel.FetchOlderMessages` pages history from the OLDEST +/// message the model already holds, so an empty list throws +/// `ArgumentOutOfRangeException: Index was out of range` out of the list build - once per +/// frame, from the recycling list's own refresh - and the conversation pane stays blank +/// forever. Real threads always open with a `-5` "Player <@U..> started a chat" +/// notice as their oldest message, which is why they never hit it. +/// * the thread LIST answers `latestMessage` and no `messages` array; the two shapes are how +/// the client tells a listed thread from an opened one. A thread with nothing in it is +/// still listed, rather than hidden. +/// * `GET /chat/thread/{id}/message` is a BARE ARRAY, newest first - the spelling the paging +/// client calls - while `GET /chat/thread/{id}` answers an object. +/// +public class ChatThreadTests +{ + private const long CallerId = 2; + private const long FriendId = 3; + private const long StrangerId = 999; + private const long ThreadId = 1; + + private static readonly JsonSerializerOptions WireJson = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + private static string Serialize(object? value) => JsonSerializer.Serialize(value, WireJson); + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + return new RecDBContext(options); + } + + private static void SetUser(Controller controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetFormBody(Controller controller, string body) + { + var request = controller.ControllerContext.HttpContext.Request; + request.ContentType = "application/x-www-form-urlencoded"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + } + + private static void SetQuery(Controller controller, string query) + => controller.ControllerContext.HttpContext.Request.QueryString = new QueryString(query); + + /// + /// A stored `messages` blob in the format `ChatThread.AddMessage`/`ToJson` writes, which + /// is the default serializer's shape - the model's own property names are already + /// camelCase, so no naming policy is applied on either side. + /// + private static string Messages(params (long Id, long Sender, string Contents)[] messages) + { + return JsonSerializer.Serialize(messages.Select(m => new ChatMessage + { + chatMessageId = m.Id, + chatThreadeId = ThreadId, + senderPlayerId = m.Sender, + timeSent = new DateTime(2026, 9, 27, 1, 0, 0, DateTimeKind.Utc).AddMinutes(m.Id), + contents = m.Contents, + }).ToList()); + } + + private static ChatThread Thread(long id, string playerIds, string messages) => new() + { + chatThreadId = id, + chatThreadName = "", + lastReadMessageId = 0, + playerIds = playerIds, + messages = messages, + }; + + private static long[] MessageIds(JsonElement thread) => thread + .GetProperty("messages") + .EnumerateArray() + .Select(m => m.GetProperty("chatMessageId").GetInt64()) + .ToArray(); + + [Fact] + public void PartyThreadRoute_UsesTheClientListShape() + { + var method = typeof(ThreadAPI).GetMethod(nameof(ThreadAPI.GetPartyThread))!; + Assert.Contains(method.GetCustomAttributes(), + attribute => attribute.Template == "thread/party"); + + using var db = CreateDb(); + var api = new ThreadAPI(db); + SetUser(api, CallerId); + + var result = Assert.IsType(api.GetPartyThread()); + using var json = JsonDocument.Parse(Serialize(result.Value)); + Assert.Equal(JsonValueKind.Array, json.RootElement.ValueKind); + Assert.Empty(json.RootElement.EnumerateArray()); + } + + // ---- Opening a conversation ------------------------------------------------- + + [Fact] + public async Task CreateThreadWithMembers_OpensWithTheSystemNotice_SoTheListIsNeverEmpty() + { + using var db = CreateDb(); + var api = new ThreadAPI(db); + SetUser(api, CallerId); + SetFormBody(api, $"ids={FriendId}&messageCount=50"); + + var ok = Assert.IsType(await api.CreateThreadWithMembers()); + + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + var thread = doc.RootElement; + + Assert.Equal(new[] { CallerId, FriendId }, thread.GetProperty("playerIds").EnumerateArray().Select(p => p.GetInt64()).ToArray()); + + // An unnamed thread carries an EMPTY name, never null: the client dereferences this + // member unchecked and dies on a DM served null (observed live as + // `Failed to GetChatBetweenPlayers` / `JSON parse error: Invalid value`). The + // captured real traffic's `chatThreadName: null` is another build's serializer, not + // this client's DTO. + Assert.Equal("", thread.GetProperty("chatThreadName").GetString()); + + // And the type is present on every thread: the client deserializes it into a + // non-nullable int and drops the whole response when the member is missing. + Assert.Equal(0, thread.GetProperty("chatThreadType").GetInt32()); + + var messages = thread.GetProperty("messages").EnumerateArray().ToArray(); + var notice = Assert.Single(messages); + Assert.Equal(-5, notice.GetProperty("senderPlayerId").GetInt64()); + // The notice's text rides INSIDE the JSON envelope, the only spelling this client's + // message model reads text out of: a bare sentence as `contents` is what the 2026-09-30 + // 18:14 run reported as `ArgumentException: JSON parse error: Invalid value`. + Assert.Equal( + """{"Type":0,"Version":1,"Data":"Player <@U3> started a chat"}""", + notice.GetProperty("contents").GetString()); + + // An OPENED thread carries `messages` and never `latestMessage` - the client is sent + // one spelling or the other - and the creator has already read the notice. + Assert.False(thread.TryGetProperty("latestMessage", out _)); + Assert.Equal(notice.GetProperty("chatMessageId").GetInt64(), thread.GetProperty("lastReadMessageId").GetInt64()); + Assert.Equal(0, notice.GetProperty("moderationState").GetInt32()); + } + + [Fact] + public async Task CreateThreadWithMembers_ReusesTheExistingThread_WithoutASecondNotice() + { + using var db = CreateDb(); + var api = new ThreadAPI(db); + SetUser(api, CallerId); + SetFormBody(api, $"ids={FriendId}"); + + var first = Assert.IsType(await api.CreateThreadWithMembers()); + using var firstDoc = JsonDocument.Parse(Serialize(first.Value)); + var threadId = firstDoc.RootElement.GetProperty("chatThreadId").GetInt64(); + + SetFormBody(api, $"ids={FriendId}"); + var second = Assert.IsType(await api.CreateThreadWithMembers()); + using var secondDoc = JsonDocument.Parse(Serialize(second.Value)); + + Assert.Equal(threadId, secondDoc.RootElement.GetProperty("chatThreadId").GetInt64()); + Assert.Single(secondDoc.RootElement.GetProperty("messages").EnumerateArray()); + Assert.Equal(1, await db.Chats.CountAsync()); + } + + /// + /// The ORDER, the presence and the nullability of these members are part of the wire + /// contract, not style, and the client says so loudly when they drift: `Malformed + /// Response: '[ { "id": 5, "Id": 5, "chatThreadId": 5, ...'` in the 2026-09-30 log is + /// what the leading `id`/`Id`/`name` alias members did to the thread list, and + /// `Failed to GetChatBetweenPlayers` with `JSON parse error: Invalid value` is what a + /// null `chatThreadName` and a missing `chatThreadType` did to an opened thread. These + /// are the working reference's orders (`.external/recflare-server/apps/chat`, whose + /// comments record each of those client throws), so a change to any of them has to be a + /// deliberate one. + /// + private static string[] FieldOrder(JsonElement element) => element + .EnumerateObject() + .Select(property => property.Name) + .ToArray(); + + [Fact] + public async Task WireShapes_BindPositionally_InTheReferenceOrder() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages( + (1, -5, """{"Type":0,"Version":1,"Data":"Player <@U3> started a chat"}"""), + (2, FriendId, """{"Type":0,"Version":1,"Data":"hi"}""")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + + // The thread list item: latestMessage leads, then the thread's own fields. + var list = Assert.IsType(await api.GetThread(50, QueryMode.Latest)); + using (var doc = JsonDocument.Parse(Serialize(list.Value))) + { + var item = doc.RootElement.EnumerateArray().Single(); + Assert.Equal( + new[] { "latestMessage", "chatThreadId", "playerIds", "lastReadMessageId", "chatThreadName", "chatThreadType", "snoozedUntil", "isFavorited" }, + FieldOrder(item)); + + // A listed thread carries the newest message ONLY - never a `messages` array. + Assert.False(item.TryGetProperty("messages", out _)); + Assert.Equal(2, item.GetProperty("latestMessage").GetProperty("chatMessageId").GetInt64()); + + // The named DM: an empty string, not a null the client would dereference. + Assert.Equal("", item.GetProperty("chatThreadName").GetString()); + Assert.Equal(0, item.GetProperty("chatThreadType").GetInt32()); + } + + // An opened thread: the thread's own fields, then the page of messages - and no + // `latestMessage`, which is the member `messages` replaces. + var opened = Assert.IsType(await api.GetThreadById(ThreadId)); + using (var doc = JsonDocument.Parse(Serialize(opened.Value))) + { + var thread = doc.RootElement; + Assert.Equal( + new[] { "chatThreadId", "playerIds", "lastReadMessageId", "chatThreadName", "chatThreadType", "snoozedUntil", "isFavorited", "messages" }, + FieldOrder(thread)); + Assert.False(thread.TryGetProperty("latestMessage", out _)); + Assert.Equal("", thread.GetProperty("chatThreadName").GetString()); + Assert.Equal(0, thread.GetProperty("chatThreadType").GetInt32()); + + var message = thread.GetProperty("messages").EnumerateArray().First(); + Assert.Equal( + new[] { "chatMessageId", "chatThreadId", "senderPlayerId", "timeSent", "contents", "moderationState" }, + FieldOrder(message)); + } + } + + [Fact] + public async Task OpenThread_PagesNewestFirst_WithoutTheListSpelling() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages( + (1, -5, """{"Type":0,"Version":1,"Data":"Player <@U3> started a chat"}"""), + (2, FriendId, """{"Type":0,"Version":1,"Data":"hi"}"""), + (3, CallerId, """{"Type":0,"Version":1,"Data":"yo"}""")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + SetQuery(api, "?messageCount=2"); + + var ok = Assert.IsType(await api.GetThreadById(ThreadId)); + + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + var thread = doc.RootElement; + + // Newest first, and only the page the caller asked for: the newest message leads the + // page, and there is no `latestMessage` alongside it. + Assert.Equal(new long[] { 3, 2 }, MessageIds(thread)); + Assert.False(thread.TryGetProperty("latestMessage", out _)); + + // The per-viewer fields the client renders the header and the star from. + Assert.Equal(JsonValueKind.Null, thread.GetProperty("snoozedUntil").ValueKind); + Assert.False(thread.GetProperty("isFavorited").GetBoolean()); + Assert.Equal("", thread.GetProperty("chatThreadName").GetString()); + + var newest = thread.GetProperty("messages").EnumerateArray().First(); + Assert.Equal("""{"Type":0,"Version":1,"Data":"yo"}""", newest.GetProperty("contents").GetString()); + Assert.Equal(CallerId, newest.GetProperty("senderPlayerId").GetInt64()); + } + + [Fact] + public async Task OpenThread_WithNoMessages_StillOpens() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", "[]")); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + + var ok = Assert.IsType(await api.GetThreadById(ThreadId)); + + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + Assert.Equal(JsonValueKind.Array, doc.RootElement.GetProperty("messages").ValueKind); + Assert.Empty(doc.RootElement.GetProperty("messages").EnumerateArray()); + } + + // ---- Paging history --------------------------------------------------------- + + [Fact] + public async Task ThreadMessages_IsABareArray_NewestFirst_AndPagesBackwards() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages( + (1, -5, "notice"), (2, FriendId, "hi"), (3, CallerId, "yo")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + + // The newest page - the spelling ChatThreadModel.FetchOlderMessages opens with. + SetQuery(api, "?MessageCount=2&Mode=0"); + var newest = Assert.IsType(await api.GetThreadMessages(ThreadId)); + using (var doc = JsonDocument.Parse(Serialize(newest.Value))) + { + Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind); + Assert.Equal(new long[] { 3, 2 }, doc.RootElement.EnumerateArray().Select(m => m.GetProperty("chatMessageId").GetInt64()).ToArray()); + } + + // Walking back from an anchor message. + SetQuery(api, "?MessageCount=2&Mode=2&MessageId=3"); + var older = Assert.IsType(await api.GetThreadMessages(ThreadId)); + using (var doc = JsonDocument.Parse(Serialize(older.Value))) + { + Assert.Equal(new long[] { 2, 1 }, doc.RootElement.EnumerateArray().Select(m => m.GetProperty("chatMessageId").GetInt64()).ToArray()); + } + + // forward from an anchor, newest first + SetQuery(api, "?MessageCount=10&Mode=1&MessageId=1"); + var newer = Assert.IsType(await api.GetThreadMessages(ThreadId)); + using (var doc = JsonDocument.Parse(Serialize(newer.Value))) + { + Assert.Equal(new long[] { 3, 2 }, doc.RootElement.EnumerateArray().Select(m => m.GetProperty("chatMessageId").GetInt64()).ToArray()); + } + } + + [Fact] + public async Task ThreadMessages_AnchoredOnReferenceMessageId_PagesOlderThanTheAnchor() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages( + (1, -5, "notice"), (2, FriendId, "hi"), (3, CallerId, "yo")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + + // The exact call the client's FetchOlderMessages makes. The anchor is the OLDEST + // message it holds, so nothing is older than it and the page is empty - answering + // the newest messages instead (which is what ignoring `referenceMessageId` did) makes + // the client re-request forever. + SetQuery(api, "?messageCount=8&mode=2&referenceMessageId=1"); + var empty = Assert.IsType(await api.GetThreadMessages(ThreadId)); + using (var doc = JsonDocument.Parse(Serialize(empty.Value))) + Assert.Empty(doc.RootElement.EnumerateArray()); + + // Anchored higher up it walks backwards from that message, newest first. + SetQuery(api, "?messageCount=8&mode=2&referenceMessageId=3"); + var older = Assert.IsType(await api.GetThreadMessages(ThreadId)); + using (var doc = JsonDocument.Parse(Serialize(older.Value))) + Assert.Equal(new long[] { 2, 1 }, doc.RootElement.EnumerateArray().Select(m => m.GetProperty("chatMessageId").GetInt64()).ToArray()); + } + + [Fact] + public async Task ReadMessage_AnswersTheBareChatResultNumber_AndMovesTheReadPointer() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages((1, -5, "notice"), (2, FriendId, "hi")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + + var ok = Assert.IsType(await api.ReadThreadMessage(ThreadId, 2)); + + // A BARE number: the client deserializes this body straight into its ChatResult enum, + // so the `{ chatResult: 0 }` envelope fails as "Failed to ReadMessage". + Assert.Equal(0, ok.Value); + Assert.Equal(2, (await db.Chats.FirstAsync(x => x.chatThreadId == ThreadId)).lastReadMessageId); + + // Reading backwards never moves the pointer. + Assert.Equal(0, Assert.IsType(await api.ReadThreadMessage(ThreadId, 1)).Value); + Assert.Equal(2, (await db.Chats.FirstAsync(x => x.chatThreadId == ThreadId)).lastReadMessageId); + + // Still a 200 with a result code, not a 404, for a non-member or a missing thread. + SetUser(api, StrangerId); + Assert.Equal(3, Assert.IsType(await api.ReadThreadMessage(ThreadId, 2)).Value); + Assert.Equal(2, Assert.IsType(await api.ReadThreadMessage(404, 2)).Value); + } + + [Fact] + public async Task ThreadMessages_WithAnUnparseablePageSize_FallsBackRatherThan400ing() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages((1, -5, "notice"), (2, FriendId, "hi")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + SetQuery(api, "?MessageCount=lots&Mode=whatever"); + + var ok = Assert.IsType(await api.GetThreadMessages(ThreadId)); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + Assert.Equal(2, doc.RootElement.GetArrayLength()); + } + + // ---- Sending --------------------------------------------------------------- + + [Fact] + public async Task JsonWithoutContentLength_CreatesConversationAndPersistsMessagesAcrossRequests() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + long threadId; + using (var db = new RecDBContext(options)) + { + var api = new ThreadAPI(db); + SetUser(api, CallerId); + api.Request.ContentType = "application/json"; + api.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"playerIds\":[3],\"messageContents\":\"hello\"}")); + Assert.Null(api.Request.ContentLength); + var result = Assert.IsType(await api.CreateThread()); + using var json = JsonDocument.Parse(Serialize(result.Value)); + Assert.Equal(0, json.RootElement.GetProperty("chatResult").GetInt32()); + threadId = json.RootElement.GetProperty("chatThread").GetProperty("chatThreadId").GetInt64(); + } + using (var db = new RecDBContext(options)) + { + var api = new ThreadAPI(db); + SetUser(api, FriendId); + var result = Assert.IsType(await api.GetThreadById(threadId)); + using var json = JsonDocument.Parse(Serialize(result.Value)); + // Posted as bare text (the real client posts the envelope; this test posts a plain + // sentence), and served wrapped: `contents` leaves here as the JSON envelope the + // client's message model parses, whether the row stored one or not. + Assert.Equal( + """{"Type":0,"Version":1,"Data":"hello"}""", + json.RootElement.GetProperty("messages").EnumerateArray().First().GetProperty("contents").GetString()); + api.Request.ContentType = "application/json"; + api.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"messageContents\":\"reply\"}")); + await api.SendMessageToThread(threadId); + } + using (var db = new RecDBContext(options)) + { + var messages = (await db.Chats.SingleAsync()).GetMessages()!; + Assert.Equal(new[] { "hello", "reply" }, messages.Where(x => x.senderPlayerId > 0).Select(x => x.contents)); + } + } + + [Fact] + public void AddMessage_PreservesTheEntireStoredHistory() + { + var thread = Thread(ThreadId, "[2,3]", Messages((1, CallerId, "first"))); + thread.messages = thread.AddMessage(new ChatMessage { chatMessageId = 2, contents = "second" }); + Assert.Equal(new[] { "first", "second" }, thread.GetMessages()!.Select(x => x.contents)); + } + + [Fact] + public async Task LegacySingleMessageHistory_CanBeOpenedAndListed() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, "[2,3]", JsonSerializer.Serialize(new ChatMessage + { + chatMessageId = 1, chatThreadeId = ThreadId, senderPlayerId = CallerId, + contents = "saved message", timeSent = DateTime.UtcNow + }))); + await db.SaveChangesAsync(); + var api = new ThreadAPI(db); + SetUser(api, FriendId); + Assert.IsType(await api.GetThread(50, QueryMode.Latest)); + var result = Assert.IsType(await api.GetThreadById(ThreadId)); + using var json = JsonDocument.Parse(Serialize(result.Value)); + // A row written before the envelope spelling (bare text) is wrapped on the way out, so + // the client reads its text instead of rejecting the whole thread. + Assert.Equal( + """{"Type":0,"Version":1,"Data":"saved message"}""", + json.RootElement.GetProperty("messages").EnumerateArray().First().GetProperty("contents").GetString()); + } + + [Fact] + public async Task SendMessage_AnswersTheWholeThread_AndCountsAsReading() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages((1, -5, "notice")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + var envelope = """{"Type":0,"Version":2,"Data":"@test"}"""; + SetFormBody(api, "messageContents=" + Uri.EscapeDataString(envelope)); + + var ok = Assert.IsType(await api.SendMessageToThread(ThreadId)); + + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + var root = doc.RootElement; + Assert.Equal(0, root.GetProperty("chatResult").GetInt32()); + + var thread = root.GetProperty("chatThread"); + Assert.Equal(new long[] { 2, 1 }, MessageIds(thread)); + Assert.Equal(envelope, thread.GetProperty("messages").EnumerateArray().First().GetProperty("contents").GetString()); + // Sending is reading: the sender's own pointer is already at the message just posted. + Assert.Equal(2, thread.GetProperty("lastReadMessageId").GetInt64()); + + var stored = await db.Chats.FirstAsync(x => x.chatThreadId == ThreadId); + Assert.Equal(2, stored.GetMessages()!.Max(m => m.chatMessageId)); + } + + [Fact] + public async Task SendMessage_PushesTheChatNotificationUnderItsRegisteredName_NotTheEnumNumber() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages((1, -5, "notice")))); + await db.SaveChangesAsync(); + + var recipient = new RecordingSingleClient(); + var hubClients = DispatchProxy.Create(); + ((HubClientsProxy)(object)hubClients).Single = recipient; + var hubContext = DispatchProxy.Create, HubContextProxy>(); + ((HubContextProxy)(object)hubContext).HubClients = hubClients; + + var previousContext = Notify.Context; + var previousClients = Notify.Clients; + try + { + Notify.Context = hubContext; + Notify.Clients = new List + { + new() { AccountId = FriendId, ConnectionId = "chat-test-recipient", Subscriptions = new List() }, + }; + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + SetFormBody(api, "messageContents=" + Uri.EscapeDataString("""{"Type":0,"Version":2,"Data":"hello"}""")); + + Assert.IsType(await api.SendMessageToThread(ThreadId)); + + var pushed = Assert.Single(recipient.Sent); + + // The client registers its chat handler under the literal NAME and has NO handler + // for the number - verified by hooking its dispatcher registrations, which print + // every accepted key at login. An id it has no handler for is dropped silently, so + // a numeric id here means the recipient's conversation never updates. + Assert.Contains("\"Id\":\"ChatMessageReceived\"", pushed); + Assert.DoesNotContain("\"Id\":\"90\"", pushed); + Assert.Contains("hello", pushed); + } + finally + { + Notify.Context = previousContext; + Notify.Clients = previousClients; + } + } + + public class HubContextProxy : DispatchProxy + { + public IHubClients HubClients = null!; + protected override object? Invoke(MethodInfo? method, object?[]? args) + => method?.Name == "get_Clients" ? HubClients : throw new NotSupportedException(); + } + + public class HubClientsProxy : DispatchProxy + { + public ISingleClientProxy Single = null!; + protected override object? Invoke(MethodInfo? method, object?[]? args) + => method?.Name == "Client" ? Single : throw new NotSupportedException(); + } + + private sealed class RecordingSingleClient : ISingleClientProxy + { + public List Sent { get; } = new(); + + public Task InvokeCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + Sent.Add(Assert.IsType(args[0])); + return Task.CompletedTask; + } + } + + [Fact] + public async Task BlankMessageContents_ReportsInvalidArguments_WithTheThreadAttached() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages((1, -5, "notice")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, CallerId); + SetFormBody(api, "messageContents="); + + // The client posts exactly this right after /thread/withmembers to "open" the thread, + // and the real server answers 200 + invalid-arguments rather than an error status. + var ok = Assert.IsType(await api.SendMessageToThread(ThreadId)); + + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + Assert.Equal(1, doc.RootElement.GetProperty("chatResult").GetInt32()); + Assert.Single(doc.RootElement.GetProperty("chatThread").GetProperty("messages").EnumerateArray()); + + SetFormBody(api, "messageContents="); + var create = Assert.IsType(await api.CreateThread()); + using var createDoc = JsonDocument.Parse(Serialize(create.Value)); + Assert.Equal(1, createDoc.RootElement.GetProperty("chatResult").GetInt32()); + } + + // ---- Membership ------------------------------------------------------------- + + [Fact] + public async Task NonMembers_GetNotFoundFromEveryThreadRoute() + { + using var db = CreateDb(); + db.Chats.Add(Thread(ThreadId, $"[{CallerId},{FriendId}]", Messages((1, -5, "notice")))); + await db.SaveChangesAsync(); + + var api = new ThreadAPI(db); + SetUser(api, StrangerId); + + Assert.IsType(await api.GetThreadById(ThreadId)); + Assert.IsType(await api.GetThreadMessages(ThreadId)); + + SetFormBody(api, "messageContents=" + Uri.EscapeDataString("""{"Type":0,"Version":1,"Data":"hi"}""")); + Assert.IsType(await api.SendMessageToThread(ThreadId)); + } +} diff --git a/ServerTests/ClubCategoryDiscoveryTests.cs b/ServerTests/ClubCategoryDiscoveryTests.cs new file mode 100644 index 0000000..1491865 --- /dev/null +++ b/ServerTests/ClubCategoryDiscoveryTests.cs @@ -0,0 +1,276 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Clubs; +using RadicadeServer.DB; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The Clubs screen asks GET clubs/club/categoryTags for its categories and then fetches +/// one row per category, each on its own: +/// +/// GET clubs/club/search?sort=0&category=Social&count=32 +/// +/// Every one of those requests used to come back with the whole club catalogue, so a single club +/// was drawn in every row - the "clubs are appearing in every single category" report. The +/// category named in the request is the only thing that separates the rows. +/// +/// The route assertions are here for the same reason as in : +/// a controller-level test calls the handler it wants by name, so it cannot see that the URL the +/// client actually uses has no endpoint at all (the club invitation accept that answered 405). +/// +[Collection(DbFactoryCollection.Name)] +public class ClubCategoryDiscoveryTests +{ + private const long ViewerId = 700; + private const long CreatorId = 701; + private const long EducationalClubId = 702; + private const long EntertainmentClubId = 703; + private const long CustomTagClubId = 704; + + private static RecDBContext CreateDb() => new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static void SetUser(Controller controller, long accountId, string queryString = "") + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + var context = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }; + context.Request.QueryString = new QueryString(queryString); + controller.ControllerContext = new ControllerContext { HttpContext = context }; + } + + private static async Task SeedClubsAsync(RecDBContext db) => await ClubStore.SaveAsync(db, new List + { + new() + { + clubId = EducationalClubId, + ownerPlayerId = CreatorId, + name = "Science club", + primaryTag = "Educational", + }, + new() + { + clubId = EntertainmentClubId, + ownerPlayerId = CreatorId, + name = "Movie club", + primaryTag = "Entertainment", + }, + new() + { + clubId = CustomTagClubId, + ownerPlayerId = CreatorId, + name = "Chess club", + primaryTag = "Social", + customTags = new[] { "Educational" }, + }, + }); + + /// The club names one search answered with, in the order the client would read them. + private static string[] ClubNamesOf(object response) + { + using var document = JsonDocument.Parse(JsonSerializer.Serialize(response)); + return document.RootElement.GetProperty("Clubs").EnumerateArray() + .Select(club => club.GetProperty("Name").GetString() ?? string.Empty) + .OrderBy(name => name, StringComparer.Ordinal) + .ToArray(); + } + + private static int TotalClubsOf(object response) + { + using var document = JsonDocument.Parse(JsonSerializer.Serialize(response)); + return document.RootElement.GetProperty("TotalClubs").GetInt32(); + } + + [Fact] + public async Task EveryCategoryRowGetsOnlyItsOwnClubs() + { + await using var db = CreateDb(); + await SeedClubsAsync(db); + + foreach (var (category, expected) in new[] + { + // A custom tag is enough to put "Chess club" in the Educational row as well as Social. + ("Social", new[] { "Chess club" }), + ("Educational", new[] { "Chess club", "Science club" }), + ("Entertainment", new[] { "Movie club" }), + ("Creative", Array.Empty()), + }) + { + var api = new ClubsAPI(db); + SetUser(api, ViewerId, $"?sort=0&category={category}&count=32"); + + var ok = Assert.IsType(await api.SearchClubs()); + + var expectedInOrder = expected.OrderBy(name => name, StringComparer.Ordinal).ToArray(); + Assert.Equal(expectedInOrder, ClubNamesOf(ok.Value!)); + Assert.Equal(expectedInOrder.Length, TotalClubsOf(ok.Value!)); + } + } + + [Fact] + public async Task ASearchWithoutACategoryStillReturnsTheWholeCatalogue() + { + await using var db = CreateDb(); + await SeedClubsAsync(db); + + var api = new ClubsAPI(db); + SetUser(api, ViewerId, "?sort=0&count=32"); + + var ok = Assert.IsType(await api.SearchClubs()); + + Assert.Equal(3, TotalClubsOf(ok.Value!)); + } + + [Fact] + public async Task ABoundCategoryParameterIsHonouredWhenTheQueryStringIsNotTheCarrier() + { + await using var db = CreateDb(); + await SeedClubsAsync(db); + + var api = new ClubsAPI(db); + SetUser(api, ViewerId); + + var ok = Assert.IsType(await api.SearchClubs(category: "Entertainment")); + + Assert.Equal(new[] { "Movie club" }, ClubNamesOf(ok.Value!)); + } + + [Fact] + public async Task TheMostActiveRowsFilterTheSameWayTheDiscoveryRowsDo() + { + await using var db = CreateDb(); + await SeedClubsAsync(db); + + var today = new ClubsAPI(db); + SetUser(today, ViewerId, "?category=Educational"); + var todayOk = Assert.IsType(await today.GetMostActiveToday()); + Assert.Equal(2, JsonSerializer.SerializeToElement(todayOk.Value).GetArrayLength()); + + var now = new ClubsAPI(db); + SetUser(now, ViewerId, "?category=Showcase"); + var nowOk = Assert.IsType(await now.GetMostActiveNow()); + Assert.Empty(JsonSerializer.SerializeToElement(nowOk.Value).EnumerateArray()); + } + + [Fact] + public void CategoryMatchingIsCaseInsensitiveAndBlankMeansEverything() + { + var club = new ClubRecord { primaryTag = "Educational", customTags = new[] { "Science" } }; + + Assert.True(ClubStore.MatchesCategory(club, "educational")); + Assert.True(ClubStore.MatchesCategory(club, " #Science ")); + Assert.False(ClubStore.MatchesCategory(club, "#Social")); + Assert.True(ClubStore.MatchesCategory(club, null)); + Assert.True(ClubStore.MatchesCategory(club, " ")); + Assert.False(ClubStore.MatchesCategory(null, "Educational")); + Assert.Single(ClubStore.FilterByCategory(new List { club }, "Educational")); + Assert.Empty(ClubStore.FilterByCategory(new List { club }, "Social")); + } + + [Fact] + public void CategoryTagsServesTheListTheClientBuildsItsRowsFrom() + { + using var db = CreateDb(); + + var ok = Assert.IsType(new ClubsAPI(db).GetCategoryTags()); + + Assert.Equal( + new[] { "Social", "Casual", "Competitive", "Creative", "Entertainment", "Educational", "Lifestyle" }, + Assert.IsType(ok.Value)); + } + + /// + /// The effective URLs of one action with its class routes expanded - the addresses the client + /// asks for, rather than the action's own templates. + /// + private static string[] RoutesOf(string actionName) + { + var classRoutes = typeof(ClubsAPI).GetCustomAttributes(inherit: false) + .Select(attribute => (attribute.Template ?? string.Empty).Trim('/')) + .ToArray(); + + var routes = new List(); + foreach (var attribute in typeof(ClubsAPI).GetMethod(actionName)! + .GetCustomAttributes(inherit: false)) + { + var template = attribute.Template ?? string.Empty; + + if (template.StartsWith("~/", StringComparison.Ordinal)) + { + routes.AddRange(attribute.HttpMethods.Select(verb => $"{verb} /{template[2..]}")); + continue; + } + + routes.AddRange(from prefix in classRoutes + from verb in attribute.HttpMethods + select $"{verb} /{prefix}/{template.TrimStart('/')}"); + } + + return routes.ToArray(); + } + + [Fact] + public void TheClientMemberActionsAreRegisteredAtTheUrlsItCalls() + { + Assert.Equal(new[] { "clubs/club", "api/clubs/v1/club", "api/clubs/v2/club" }, + typeof(ClubsAPI).GetCustomAttributes(inherit: false) + .Select(attribute => attribute.Template).ToArray()); + + var routes = new[] + { + "AcceptClubInvitation", "DeclineClubInvitation", "InviteMember", + "RemoveClubMember", "SetMinimumLevel", "ChangeMemberType", + }.SelectMany(RoutesOf).ToArray(); + + foreach (var expected in new[] + { + // The invitation a profile sends, and the accept the invitee clicks: the accept + // answered 405 because only .../members/accept was registered, never .../acceptinvite. + "PUT /clubs/club/{clubId:long}/members/invite", + "POST /clubs/club/{clubId:long}/members/invitemembers", + "POST /clubs/club/{clubId:long}/members/acceptinvite", + "PUT /clubs/club/{clubId:long}/members/acceptinvite", + "POST /clubs/club/{clubId:long}/members/declineinvite", + "POST /clubs/club/{clubId:long}/members/remove", + "DELETE /clubs/club/{clubId:long}/members/remove", + "PUT /clubs/club/{clubId:long}/minlevel", + "POST /clubs/club/{clubId:long}/members/changetype", + }) + { + Assert.Contains(expected, routes); + Assert.Single(routes, route => route == expected); + } + } + + [Fact] + public void NoClubMemberActionRegistersAnAbsoluteCopyOfItsClassRoute() + { + // "~/clubs/club/{clubId}/members/remove" next to the class-relative + // "{clubId}/members/remove" under [Route("clubs/club")] resolved to the same path twice, so + // ASP.NET threw AmbiguousMatchException (500) for every call to it. The absolute + // "~/club/..." aliases are a different path and stay. + foreach (var action in new[] + { + "AcceptClubInvitation", "DeclineClubInvitation", "InviteMember", + "RemoveClubMember", "SetMinimumLevel", "ChangeMemberType", + }) + { + var templates = typeof(ClubsAPI).GetMethod(action)! + .GetCustomAttributes(inherit: false) + .Select(attribute => attribute.Template ?? string.Empty) + .ToArray(); + + Assert.DoesNotContain(templates, + template => template.StartsWith("~/clubs/", StringComparison.Ordinal) || + template.StartsWith("~/api/clubs/", StringComparison.Ordinal)); + } + } +} diff --git a/ServerTests/ClubMatchmakingRouteTests.cs b/ServerTests/ClubMatchmakingRouteTests.cs new file mode 100644 index 0000000..06842b0 --- /dev/null +++ b/ServerTests/ClubMatchmakingRouteTests.cs @@ -0,0 +1,343 @@ +using System.Reflection; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Matchmaking; +using RadicadeServer.Clubs; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using System.Text.Json; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class ClubMatchmakingRouteTests +{ + private const long CallerId = 101; + private const long FriendId = 202; + private const long StrangerId = 303; + private const long RoomId = 404; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + return (factory, factory.CreateDbContext()); + } + + private static void SetUser(Controller controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetFormBody(Controller controller, string body) + { + var request = controller.HttpContext.Request; + request.ContentType = "application/x-www-form-urlencoded"; + request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(body)); + request.ContentLength = request.Body.Length; + } + + [Fact] + public void SupportsLegacyAndNewWatchClubJoinAliases() + { + var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.GoToClub))!; + var routes = method.GetCustomAttributes(typeof(HttpMethodAttribute), false) + .Cast() + .Select(x => x.Template ?? string.Empty) + .ToList(); + Assert.Contains("goto/club/{clubId}", routes); + Assert.Contains("matchmake/club/{clubId}", routes); + } + + [Fact] + public void RegistersFriendFollowingRoute() + { + var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.MatchmakePlayer))!; + var routes = method.GetCustomAttributes(typeof(HttpPostAttribute), false) + .Cast() + .Select(attribute => attribute.Template); + + Assert.Contains("matchmake/player/{targetId:long}", routes); + } + + [Fact] + public async Task FollowRequiresFriendshipAndEntersTheFriendsCurrentInstance() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + db.Rooms.Add(new RoomData + { + RoomId = RoomId, + Name = "Friend Room", + CreatorAccountId = FriendId, + Accessibility = Accessibility.Public, + CreatedAt = DateTime.UtcNow, + }); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + roomInstanceType = RoomInstanceType.Public, + maxCapacity = 10, + photonRoomId = "friend-photon-room", + }; + db.RoomInstances.Add(instance); + db.Presences.AddRange( + new RealPresence { playerId = CallerId }, + new RealPresence { playerId = FriendId, isOnline = true, roomInstance = instance.roomInstanceId }, + new RealPresence { playerId = StrangerId, isOnline = true, roomInstance = instance.roomInstanceId }); + db.Relationships.Add(new RadicadeServer.RecNet.Relationships.RealRelationship + { + AccountId = FriendId, + PlayerId = CallerId, + RelationshipType = RadicadeServer.RecNet.Relationships.RelationshipType.Friend, + }); + await db.SaveChangesAsync(); + + var api = new MatchmakingAPI(db); + SetUser(api, CallerId); + var followed = await api.MatchmakePlayer(FriendId, null); + + Assert.NotNull(followed); + Assert.Equal(JoinStatus.Success, followed!.errorCode); + Assert.Equal(instance.roomInstanceId, followed.roomInstance?.roomInstanceId); + Assert.Equal("friend-photon-room", followed.roomInstance?.photonRoomId); + + await using var verify = factory.CreateDbContext(); + Assert.Equal(instance.roomInstanceId, (await verify.Presences.SingleAsync(x => x.playerId == CallerId)).roomInstance); + + var nonFriendApi = new MatchmakingAPI(verify); + SetUser(nonFriendApi, CallerId); + var refused = await nonFriendApi.MatchmakePlayer(StrangerId, null); + Assert.NotNull(refused); + Assert.Equal(JoinStatus.NoSuchRoom, refused!.errorCode); + Assert.Null(refused.roomInstance); + + var selfFollow = await nonFriendApi.MatchmakePlayer(CallerId, null); + Assert.NotNull(selfFollow); + Assert.Equal(JoinStatus.NoSuchRoom, selfFollow!.errorCode); + Assert.Null(selfFollow.roomInstance); + } + + [Fact] + public async Task FollowRefusesRoomBans() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + db.Rooms.Add(new RoomData + { + RoomId = RoomId, + Name = "Friend Room", + CreatorAccountId = FriendId, + Accessibility = Accessibility.Public, + CreatedAt = DateTime.UtcNow, + }); + var instance = new RoomInstance { roomId = RoomId, maxCapacity = 10 }; + db.RoomInstances.Add(instance); + db.Presences.AddRange( + new RealPresence { playerId = CallerId }, + new RealPresence { playerId = FriendId, isOnline = true, roomInstance = instance.roomInstanceId }); + db.Relationships.Add(new RadicadeServer.RecNet.Relationships.RealRelationship + { + AccountId = CallerId, + PlayerId = FriendId, + RelationshipType = RadicadeServer.RecNet.Relationships.RelationshipType.Friend, + }); + db.RoomBans.Add(new RoomBan { roomId = RoomId, bannedPlayerId = CallerId, actorId = FriendId }); + await db.SaveChangesAsync(); + + var api = new MatchmakingAPI(db); + SetUser(api, CallerId); + var refused = await api.MatchmakePlayer(FriendId, null); + + Assert.NotNull(refused); + Assert.Equal(JoinStatus.BannedFromRoom, refused!.errorCode); + Assert.Null(refused.roomInstance); + await using var verify = factory.CreateDbContext(); + Assert.Null((await verify.Presences.SingleAsync(x => x.playerId == CallerId)).roomInstance); + } + + [Fact] + public async Task ClubOwnerIsCreatorMemberAndLegacyClubRoutesAreRegistered() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var db = new RecDBContext(options); + var club = await ClubStore.CreateAsync(db, CallerId, "My club", "Description"); + + var creator = Assert.Single(ClubStore.GetMembers(club), member => member.AccountId == CallerId); + Assert.Equal(ClubMembershipTypes.Creator, creator.MembershipType); + Assert.Equal(club.createdAt, creator.CreatedAt); + + var leave = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.LeaveClub))!; + var leaveRoutes = leave.GetCustomAttributes() + .Select(attribute => attribute.Template) + .ToArray(); + Assert.Contains("{clubId:long}/members/leave", leaveRoutes); + Assert.DoesNotContain("~/clubs/club/{clubId:long}/members/leave", leaveRoutes); + var minLevel = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.SetMinimumLevel))!; + var minLevelRoutes = minLevel.GetCustomAttributes() + .Select(attribute => attribute.Template) + .ToArray(); + Assert.Contains("{clubId:long}/minlevel", minLevelRoutes); + // The class route "clubs/club" already answers clubs/club/{clubId}/minlevel. The absolute + // copy of that same path registered the template twice, so every call to it threw + // AmbiguousMatchException (500) instead of reaching the handler. + Assert.DoesNotContain("~/clubs/club/{clubId:long}/minlevel", minLevelRoutes); + + await ClubStore.SetMemberTypeAsync(db, club.clubId, FriendId, + ClubMembershipTypes.Member, CallerId); + var leaveApi = new ClubsAPI(db); + SetUser(leaveApi, FriendId); + var left = Assert.IsType(await leaveApi.LeaveClub(club.clubId)); + Assert.NotNull(left.Value); + Assert.DoesNotContain(ClubStore.GetMembers(await ClubStore.GetAsync(db, club.clubId)), + member => member.AccountId == FriendId); + + var creatorLeave = new ClubsAPI(db); + SetUser(creatorLeave, CallerId); + Assert.IsType(await creatorLeave.LeaveClub(club.clubId)); + Assert.Contains(ClubStore.GetMembers(await ClubStore.GetAsync(db, club.clubId)), + member => member.AccountId == CallerId && member.MembershipType == ClubMembershipTypes.Creator); + } + + [Fact] + public async Task ClubMainImageCanBeReadAndUpdatedByCoowners() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var db = new RecDBContext(options); + var club = await ClubStore.CreateAsync(db, CallerId, "Image club", "Description"); + + var api = new ClubsAPI(db); + SetUser(api, CallerId); + var read = Assert.IsType(await api.GetClubMainImage(club.clubId)); + using (var initial = JsonDocument.Parse(JsonSerializer.Serialize(read.Value))) + { + Assert.Equal("DefaultImgPurple", initial.RootElement.GetProperty("value") + .GetProperty("Club").GetProperty("MainImageName").GetString()); + } + + SetFormBody(api, "imageName=2026-07-12%2Fclub.jpg"); + var updated = Assert.IsType(await api.SetClubMainImage(club.clubId)); + using (var response = JsonDocument.Parse(JsonSerializer.Serialize(updated.Value))) + { + Assert.Equal("2026-07-12/club.jpg", response.RootElement.GetProperty("value") + .GetProperty("Club").GetProperty("MainImageName").GetString()); + } + + var reread = Assert.IsType(await api.GetClubMainImage(club.clubId)); + using (var response = JsonDocument.Parse(JsonSerializer.Serialize(reread.Value))) + { + Assert.Equal("2026-07-12/club.jpg", response.RootElement.GetProperty("value") + .GetProperty("Club").GetProperty("MainImageName").GetString()); + } + + var missingImageApi = new ClubsAPI(db); + SetUser(missingImageApi, CallerId); + SetFormBody(missingImageApi, ""); + Assert.IsType(await missingImageApi.SetClubMainImage(club.clubId)); + + var memberApi = new ClubsAPI(db); + SetUser(memberApi, FriendId); + SetFormBody(memberApi, "imageName=not-allowed.jpg"); + var forbidden = Assert.IsType(await memberApi.SetClubMainImage(club.clubId)); + Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode); + + var getRouteTemplates = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.GetClubMainImage))! + .GetCustomAttributes().Select(attribute => attribute.Template).ToArray(); + Assert.Contains("{clubId:long}/mainimage", getRouteTemplates); + Assert.Contains("~/club/{clubId:long}/mainimage", getRouteTemplates); + var putRouteTemplates = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.SetClubMainImage))! + .GetCustomAttributes().Select(attribute => attribute.Template).ToArray(); + Assert.Contains("{clubId:long}/mainimage", putRouteTemplates); + Assert.Contains("~/club/{clubId:long}/mainimage", putRouteTemplates); + } + + [Fact] + public async Task ClubHomeAndSubscriptionShapesMatchTheReferenceContracts() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var db = new RecDBContext(options); + var club = await ClubStore.CreateAsync(db, CallerId, "Home club", "Description"); + await ClubStore.SetClubhouseAsync(db, club.clubId, RoomId); + await ClubStore.SetHomeClubAsync(db, CallerId, club.clubId); + + var home = await ClubStore.GetHomeClubAsync(db, CallerId); + Assert.Equal(club.clubId, home?.clubId); + using var clubJson = JsonDocument.Parse(JsonSerializer.Serialize(ClubStore.BuildClub(home, CallerId))); + Assert.Equal(JsonValueKind.Number, clubJson.RootElement.GetProperty("ClubId").ValueKind); + Assert.False(clubJson.RootElement.TryGetProperty("isOwner", out _)); + Assert.False(clubJson.RootElement.TryGetProperty("isMember", out _)); + Assert.False(clubJson.RootElement.TryGetProperty("createdAt", out _)); + var expectedClubKeys = new[] + { + "ClubId", "Name", "Description", "Category", "Visibility", "Joinability", "AllowJuniors", + "MainImageName", "ClubType", "ClubhouseRoomId", "CreatorAccountId", "IsRRO", "MinLevel", + "State", "MemberCount", + }; + Assert.Equal(expectedClubKeys.OrderBy(key => key, StringComparer.Ordinal), + clubJson.RootElement.EnumerateObject().Select(property => property.Name).OrderBy(key => key, StringComparer.Ordinal)); + + club.members.Add(new ClubMemberRecord + { + accountId = StrangerId, + membershipType = ClubMembershipTypes.PendingRequested, + inviterAccountId = CallerId, + }); + var clubMembers = ClubStore.GetMembers(club); + Assert.Contains(clubMembers, member => member.AccountId == StrangerId && + member.MembershipType == ClubMembershipTypes.PendingRequested); + var requestedMember = Assert.Single(clubMembers, member => member.AccountId == StrangerId); + using var memberJson = JsonDocument.Parse(JsonSerializer.Serialize( + ClubStore.BuildMemberListItem(requestedMember, club.clubId))); + var expectedMemberKeys = new[] { "ClubMemberId", "ClubId", "AccountId", "MembershipType", "CreatedAt" }; + Assert.Equal(expectedMemberKeys.OrderBy(key => key, StringComparer.Ordinal), + memberJson.RootElement.EnumerateObject().Select(property => property.Name).OrderBy(key => key, StringComparer.Ordinal)); + + var membersAction = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.GetClubMembers))!; + var memberRoutes = membersAction.GetCustomAttributes() + .Select(attribute => attribute.Template); + Assert.Contains("~/club/{clubId:long}/members", memberRoutes); + + using var subscription = JsonDocument.Parse(JsonSerializer.Serialize(ClubStore.BuildSubscriptionEnvelope(null, CallerId))); + Assert.False(subscription.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal(JsonValueKind.Null, subscription.RootElement.GetProperty("value").ValueKind); + + var accountRoutes = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.GetClubsForAccount))! + .GetCustomAttributes(typeof(HttpGetAttribute), false) + .Cast() + .Select(attribute => attribute.Template); + Assert.Contains("~/clubs/account/{accountId:long}/clubs", accountRoutes); + } +} diff --git a/ServerTests/ClubSubscriptionTests.cs b/ServerTests/ClubSubscriptionTests.cs new file mode 100644 index 0000000..7222139 --- /dev/null +++ b/ServerTests/ClubSubscriptionTests.cs @@ -0,0 +1,295 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Accounts; +using RadicadeServer.API.Notifications; +using RadicadeServer.Clubs; +using RadicadeServer.Clubs.Announcements; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Player subscriptions (`clubs.rec.net/subscription/*`) and the personal club id they hand back. +/// +/// The subscribe route only ever existed as a GET, so the client's `POST /subscription/{id}` fell off +/// the route table, and `GET /subscription/details/{accountId}` answered `clubId: 0`. That zero is the +/// id the client then posts its announcements to, which is why `POST /announcements/club/0` answered +/// 404 - the club record it named did not exist. These tests pin the whole loop: subscribe, read the +/// personal club id back, and post an announcement to it. +/// +[Collection(DbFactoryCollection.Name)] +public class ClubSubscriptionTests +{ + private const long SubscriberId = 71; + private const long CreatorId = 72; + private const long StrangerId = 73; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private static async Task<(RecDBContext Db, IDbContextFactory Previous)> CreateContextAsync() + { + var factory = new TestDbContextFactory(); + var previous = Notify.DbFactory; + Notify.DbFactory = factory; + + var db = factory.CreateDbContext(); + db.Profiles.AddRange(ProfileFor(SubscriberId), ProfileFor(CreatorId), ProfileFor(StrangerId)); + await db.SaveChangesAsync(); + return (db, previous); + } + + private static Profile ProfileFor(long id) => new() + { + accountId = id, + username = $"player{id}", + displayName = $"Player {id}", + profileImage = string.Empty, + createdAt = DateTime.UtcNow + }; + + private static void SetUser(Controller controller, long userId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetFormBody(Controller controller, string body) + { + var request = controller.HttpContext.Request; + request.ContentType = "application/x-www-form-urlencoded"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + request.ContentLength = request.Body.Length; + } + + private static void SetJsonBody(Controller controller, string body) + { + var request = controller.HttpContext.Request; + request.ContentType = "application/json"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + request.ContentLength = request.Body.Length; + } + + private static T Property(object? value, string name) + { + if (value is IDictionary dictionary && dictionary.TryGetValue(name, out var entry)) + return (T)entry!; + + var property = value?.GetType().GetProperty(name); + Assert.NotNull(property); + return (T)property!.GetValue(value)!; + } + + private static IEnumerable Ids(object? value) => Assert.IsAssignableFrom>(value); + + [Fact] + public async Task SubscribeRecordsTheRelationshipAndReportsTheCreatorsPersonalClub() + { + var (db, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new ClubSubscriptionAPI(db); + SetUser(api, SubscriberId); + + var personalClubId = PlayerSubscriptions.PersonalClubId(CreatorId); + Assert.True(personalClubId > 0); + + var subscribed = Assert.IsType(await api.Subscribe(CreatorId)); + Assert.Equal(personalClubId, Property(subscribed.Value, "value")); + + var subscriptions = Assert.IsType(await api.GetMySubscriptions()); + Assert.Equal(new[] { CreatorId }, Ids(subscriptions.Value).ToArray()); + + var details = Assert.IsType(await api.GetAccountSubscriptionDetails(CreatorId)); + Assert.Equal(personalClubId, Property(details.Value, "clubId")); + Assert.Equal(CreatorId, Property(details.Value, "accountId")); + Assert.Equal(1, Property(details.Value, "subscriberCount")); + + var subscribers = Assert.IsType(await api.GetMySubscribers()); + Assert.Empty(Ids(subscribers.Value)); + + var creatorView = new ClubSubscriptionAPI(db); + SetUser(creatorView, CreatorId); + var creatorSubscribers = Assert.IsType(await creatorView.GetMySubscribers()); + Assert.Equal(new[] { SubscriberId }, Ids(creatorSubscribers.Value).ToArray()); + + Assert.IsType(await api.Unsubscribe(CreatorId)); + Assert.Empty(Ids(Assert.IsType(await api.GetMySubscriptions()).Value)); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task BareSubscribeReadsThePlayerIdFromTheBody() + { + var (db, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new ClubSubscriptionAPI(db); + SetUser(api, SubscriberId); + SetJsonBody(api, $"{{\"playerId\":{CreatorId}}}"); + + var subscribed = Assert.IsType(await api.SubscribeBare()); + Assert.Equal(PlayerSubscriptions.PersonalClubId(CreatorId), Property(subscribed.Value, "value")); + + var subscriptions = Assert.IsType(await api.GetMySubscriptions()); + Assert.Equal(new[] { CreatorId }, Ids(subscriptions.Value).ToArray()); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task AnnouncementToClubZeroLandsOnTheCallersPersonalClub() + { + var (db, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new ClubAnnouncementsAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, "title=Hello&body=World"); + + var posted = Assert.IsType(await api.PostClubAnnouncements(0)); + var announcementId = Property(posted.Value, "value"); + Assert.True(announcementId > 0); + + var personalClubId = PlayerSubscriptions.PersonalClubId(CreatorId); + var feed = Assert.IsType(api.GetClubAnnouncements(personalClubId)); + using var document = JsonDocument.Parse(JsonSerializer.Serialize(feed.Value)); + + var value = document.RootElement.GetProperty("value"); + Assert.Equal(personalClubId, value.GetProperty("ClubId").GetInt64()); + var announcements = value.GetProperty("Announcements"); + Assert.Equal(1, announcements.GetArrayLength()); + Assert.Equal(announcementId, announcements[0].GetProperty("AnnouncementId").GetInt64()); + Assert.Equal(announcementId, value.GetProperty("LastAnnouncementId").GetInt64()); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task AnnouncementCannotBePostedToAnotherAccountsPersonalClub() + { + var (db, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new ClubAnnouncementsAPI(db); + SetUser(api, StrangerId); + SetFormBody(api, "title=Hi&body=There"); + + var result = await api.PostClubAnnouncements(PlayerSubscriptions.PersonalClubId(CreatorId)); + + Assert.Equal(StatusCodes.Status403Forbidden, Assert.IsType(result).StatusCode); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task BannerImageAcceptsTheNameFromAJsonBody() + { + var (db, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new AccountsAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, "{\"imageName\":\"banner-abc.jpg\"}"); + + var result = Assert.IsType(await api.SetBannerImage(null)); + + Assert.True(Property(result.Value, "success")); + Assert.Equal("banner-abc.jpg", + (await db.Profiles.AsNoTracking().SingleAsync(x => x.accountId == CreatorId)).bannerImage); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task BannerImageWithoutAReadableNameIsStillRefusedAndKeepsTheStoredBanner() + { + var (db, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + db.Profiles.Single(x => x.accountId == CreatorId).bannerImage = "keep-me.jpg"; + await db.SaveChangesAsync(); + + var api = new AccountsAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, "{}"); + + var result = await api.SetBannerImage(null); + + Assert.IsType(result); + Assert.Equal("keep-me.jpg", + (await db.Profiles.AsNoTracking().SingleAsync(x => x.accountId == CreatorId)).bannerImage); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task InvitingAnExistingMemberChangesTheirRoleInsteadOfRefusing() + { + var (db, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var club = await ClubStore.CreateAsync(db, CreatorId, "Test Club", string.Empty); + await ClubStore.InviteMemberAsync(db, club.clubId, StrangerId, ClubMembershipTypes.Member, CreatorId); + var accepted = await ClubStore.AcceptInvitationAsync(db, club.clubId, StrangerId); + Assert.NotNull(accepted); + Assert.Equal(ClubMembershipTypes.Member, ClubStore.GetMembershipType(accepted!, StrangerId)); + + var api = new ClubsAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, $"accountId={StrangerId}&membershipType={ClubMembershipTypes.Moderator}"); + + var result = await api.InviteMember(club.clubId); + + Assert.IsType(result); + var updated = await ClubStore.GetAsync(db, club.clubId); + Assert.Equal(ClubMembershipTypes.Moderator, ClubStore.GetMembershipType(updated!, StrangerId)); + } + finally + { + Notify.DbFactory = previous; + } + } +} diff --git a/ServerTests/CommunityBoardRealtimeTests.cs b/ServerTests/CommunityBoardRealtimeTests.cs new file mode 100644 index 0000000..e36f34e --- /dev/null +++ b/ServerTests/CommunityBoardRealtimeTests.cs @@ -0,0 +1,115 @@ +using System.Text.Json; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Board; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A board the admin panel saves has to reach players without a restart. The only lever the client +/// offers for that is its notification table, which is keyed by name and drops any frame whose Id is +/// not the exact registered key - the same contract the progression frame lives under. The server +/// used to announce a save as the numeric enum 95 with an empty payload, which no handler matches. +/// +public class CommunityBoardRealtimeTests +{ + private static CommunityBoard Board() => new() + { + FeaturedPlayer = new FeaturedPlayer + { + Id = 42, + TitleOverride = "Community Legend", + UrlOverride = "https://example/creator" + }, + FeaturedRoomGroup = new FeaturedRoomGroup + { + FeaturedRoomGroupId = 1, + Name = "Top Picks", + Rooms = new List + { + new() { RoomId = 1001, RoomName = "Dodgeball Arena", ImageName = "room-1.png" } + } + }, + CurrentAnnouncement = new BoardAnnouncement + { + Message = "See you in the Rec Center.", + MoreInfoUrl = "https://discord.gg/radicade" + }, + InstagramImages = new List + { + new() { ImageName = "cb-1.png", ImageUrl = "https://cdn/cb-1.png" } + }, + Videos = new List + { + new() + { + BlobName = "v.mp4", + Title = "Trailer", + Description = "Year in review", + ThumbnailBlobName = "t.png", + SourceUrl = "https://youtu.be/x" + } + } + }; + + [Fact] + public void TheBoardGoesOutUnderTheLiteralNameTheClientRegistersItsHandlerFor() + { + var frames = CommunityBoardBroadcast.BuildFrames(Board()); + var boardFrame = frames[0]; + + Assert.Equal("CommunityBoardUpdate", (string)boardFrame.Id); + Assert.Contains("\"Id\":\"CommunityBoardUpdate\"", boardFrame.ToJson()); + + // Not the numeric enum (95): the client registers the board handler by name and drops any + // frame whose Id is not the exact key, which is why a saved board used to be invisible until + // the game was restarted. + Assert.DoesNotContain("\"Id\":\"95\"", boardFrame.ToJson()); + } + + [Fact] + public void TheBoardFrameCarriesTheWholeBoardSoTheClientNeedNotRefetch() + { + var frames = CommunityBoardBroadcast.BuildFrames(Board()); + + using var doc = JsonDocument.Parse(frames[0].ToJson()); + var msg = doc.RootElement.GetProperty("Msg"); + + Assert.Equal("Community Legend", msg.GetProperty("FeaturedPlayer").GetProperty("TitleOverride").GetString()); + Assert.Equal("Top Picks", msg.GetProperty("FeaturedRoomGroup").GetProperty("Name").GetString()); + Assert.Equal("Dodgeball Arena", + msg.GetProperty("FeaturedRoomGroup").GetProperty("Rooms")[0].GetProperty("RoomName").GetString()); + Assert.Equal(1, msg.GetProperty("InstagramImages").GetArrayLength()); + Assert.Equal("Trailer", msg.GetProperty("Videos")[0].GetProperty("Title").GetString()); + } + + [Fact] + public void TheAnnouncementGoesOutUnderItsOwnNumericHandler() + { + var frames = CommunityBoardBroadcast.BuildFrames(Board()); + var announcementFrame = frames.Single(f => Equals(f.Id, "96")); + + Assert.Contains("\"Id\":\"96\"", announcementFrame.ToJson()); + + using var doc = JsonDocument.Parse(announcementFrame.ToJson()); + var msg = doc.RootElement.GetProperty("Msg"); + Assert.Equal("See you in the Rec Center.", msg.GetProperty("Message").GetString()); + Assert.Equal("https://discord.gg/radicade", msg.GetProperty("MoreInfoUrl").GetString()); + } + + [Fact] + public void AnEmptyBoardStillSendsAWellFormedAnnouncementFrame() + { + var frames = CommunityBoardBroadcast.BuildFrames(new CommunityBoard()); + + Assert.Equal(2, frames.Count); + using var doc = JsonDocument.Parse(frames[1].ToJson()); + Assert.Equal(string.Empty, doc.RootElement.GetProperty("Msg").GetProperty("Message").GetString()); + } + + [Fact] + public void ANullBoardPushesNothing() + { + Assert.Empty(CommunityBoardBroadcast.BuildFrames(null)); + } +} diff --git a/ServerTests/ConnectionRecoveryTests.cs b/ServerTests/ConnectionRecoveryTests.cs new file mode 100644 index 0000000..bcce827 --- /dev/null +++ b/ServerTests/ConnectionRecoveryTests.cs @@ -0,0 +1,110 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class ConnectionRecoveryTests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReconnectReplaysActiveBanWithoutRoomPresence(bool active) + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + await using var db = new RecDBContext(options); + db.Moderation.Add(new RadicadeServer.RecNet.Moderation.ModerationDB + { + PlayerId = 987654322, IsBan = true, Message = "test ban", + ExpireDuration = DateTime.UtcNow.AddHours(active ? 1 : -1) + }); + await db.SaveChangesAsync(); + var proxy = new RecordingClient(); + var clients = System.Reflection.DispatchProxy.Create(); + ((CallerClientsProxy)(object)clients).Client = proxy; + var previousContext = Notify.Context; + var previousFactory = Notify.DbFactory; + try + { + Notify.DbFactory = null!; + var hub = new NotifyHub(previousContext, db) + { + Context = new TestHubContext(987654322), Clients = clients + }; + await hub.OnConnectedAsync(); + Assert.Equal(active ? 1 : 0, proxy.Messages.Count); + if (active) + { + Assert.Equal("Notification", proxy.Messages[0].Method); + // The replayed kick must carry the client enum's NUMBER (22) inside a JSON + // string; the name parses and lands on no handler, and a bare number does not + // parse at all - both leave a banned player kicked with no ban screen. + Assert.Contains("\"Id\":\"22\"", proxy.Messages[0].Json); + Assert.DoesNotContain("ModerationKick", proxy.Messages[0].Json); + Assert.Contains("test ban", proxy.Messages[0].Json); + } + } + finally + { + lock (Notify.ClientsLock) Notify.Clients.RemoveAll(c => c.AccountId == 987654322); + Notify.Context = previousContext; + Notify.DbFactory = previousFactory; + } + } + + public class CallerClientsProxy : System.Reflection.DispatchProxy + { + public IClientProxy Client = null!; + protected override object? Invoke(System.Reflection.MethodInfo? method, object?[]? args) + => method?.Name == "get_Caller" ? Client : throw new NotSupportedException(); + } + + private sealed class RecordingClient : ISingleClientProxy + { + public Task InvokeCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + public List<(string Method, string Json)> Messages { get; } = new(); + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + Messages.Add((method, Assert.IsType(args[0]))); + return Task.CompletedTask; + } + } + + [Fact] + public async Task NotificationDisconnectDoesNotChangeGamePresence() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + await using var db = new RecDBContext(options); + var presence = new RealPresence { playerId = 987654321, isOnline = true, roomInstance = 42 }; + db.Presences.Add(presence); + await db.SaveChangesAsync(); + var hub = new NotifyHub(Notify.Context, db) { Context = new TestHubContext(presence.playerId) }; + + await hub.OnDisconnectedAsync(new IOException("Transport interrupted")); + + await db.Entry(presence).ReloadAsync(); + Assert.Equal(42L, presence.roomInstance); + Assert.True(presence.isOnline); + } + + private sealed class TestHubContext(long accountId) : HubCallerContext + { + public override string ConnectionId => "connection-recovery-test"; + public override string? UserIdentifier => accountId.ToString(); + public override ClaimsPrincipal User => new(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, accountId.ToString())], "test")); + public override IDictionary Items { get; } = new Dictionary(); + public override IFeatureCollection Features { get; } = new FeatureCollection(); + public override CancellationToken ConnectionAborted => CancellationToken.None; + public override void Abort() { } + } +} diff --git a/ServerTests/CustomAvatarItemRealtimeTests.cs b/ServerTests/CustomAvatarItemRealtimeTests.cs new file mode 100644 index 0000000..8d36200 --- /dev/null +++ b/ServerTests/CustomAvatarItemRealtimeTests.cs @@ -0,0 +1,305 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Primitives; +using RadicadeServer.API.Avatars; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Notifications; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A custom avatar item is worn, so writing one has to reach the client that is wearing it. +/// The routes only ever wrote the row: a shirt that was drawn, recolored, renamed or deleted +/// stayed as it was on screen until the player restarted, because nothing told the client +/// that the item had changed. These tests pin the missing half - each write pushes the +/// account update (the same notification cheers and the selected cheer badge use) to the +/// owner's own connection. +/// +/// The design filename changes on every redraw, so the account update makes clients resolve +/// the item again and fetch the new image rather than a cached one. Whether the client +/// redraws the worn shirt from that frame is a client-side question this suite cannot +/// answer; what is pinned here is that the server sends it. +/// +[Collection(DbFactoryCollection.Name)] +public class CustomAvatarItemRealtimeTests +{ + private const long OwnerId = 61; + private const string OwnerConnection = "custom-item-owner"; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private sealed class RecordingProxy : ISingleClientProxy + { + public List<(string Method, string Json)> Messages { get; } = new(); + + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + Messages.Add((method, Assert.IsType(args[0]))); + return Task.CompletedTask; + } + + public Task InvokeCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + } + + private sealed class RecordingClients : IHubClients + { + public RecordingProxy Broadcast { get; } = new(); + public Dictionary Connections { get; } = new(); + + public RecordingProxy For(string connectionId) + { + if (!Connections.TryGetValue(connectionId, out var proxy)) + Connections[connectionId] = proxy = new RecordingProxy(); + return proxy; + } + + public IClientProxy All => Broadcast; + public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) => Broadcast; + public IClientProxy Client(string connectionId) => For(connectionId); + public IClientProxy Clients(IReadOnlyList connectionIds) => Broadcast; + public IClientProxy Group(string groupName) => Broadcast; + public IClientProxy GroupExcept(string groupName, IReadOnlyList excludedConnectionIds) => Broadcast; + public IClientProxy Groups(IReadOnlyList groupNames) => Broadcast; + public IClientProxy User(string userId) => Broadcast; + public IClientProxy Users(IReadOnlyList userIds) => Broadcast; + } + + private sealed class RecordingHub : IHubContext + { + private readonly RecordingClients clients = new(); + + public RecordingClients Recorder => clients; + public IHubClients Clients => clients; + public IGroupManager Groups => throw new NotSupportedException(); + } + + private sealed class Fixture : IAsyncDisposable + { + public Fixture(RecDBContext db, TestDbContextFactory factory, RecordingHub hub) + { + Db = db; + this.factory = factory; + this.hub = hub; + } + + private readonly TestDbContextFactory factory; + private readonly RecordingHub hub; + private readonly IDbContextFactory previousFactory = Notify.DbFactory; + private readonly IHubContext previousContext = Notify.Context; + + public RecDBContext Db { get; } + public RecordingProxy Owner => hub.Recorder.For(OwnerConnection); + + public ValueTask DisposeAsync() + { + lock (Notify.ClientsLock) + Notify.Clients.RemoveAll(client => client.ConnectionId == OwnerConnection); + Notify.DbFactory = previousFactory; + Notify.Context = previousContext; + return Db.DisposeAsync(); + } + } + + /// + /// Opens a database holding the owner's profile and player row, registers one notification + /// connection for them, and points the notifier at both. has to + /// be set even though the controller already has a context: the notifier opens its own to read + /// what it sends. + /// + private static async Task OpenAsync() + { + var factory = new TestDbContextFactory(); + var hub = new RecordingHub(); + var db = factory.CreateDbContext(); + + db.Profiles.Add(new Profile + { + accountId = OwnerId, + username = $"player{OwnerId}", + displayName = $"Player {OwnerId}", + profileImage = string.Empty, + createdAt = DateTime.UtcNow + }); + db.Players.Add(new PlayerData { AccountId = OwnerId }); + await db.SaveChangesAsync(); + await db.DisposeAsync(); + + lock (Notify.ClientsLock) + Notify.Clients.Add(new HubClient { AccountId = OwnerId, ConnectionId = OwnerConnection }); + + Notify.DbFactory = factory; + Notify.Context = hub; + + return new Fixture(factory.CreateDbContext(), factory, hub); + } + + private static CustomAvatarItemDB ShirtFor(Guid itemId) => new() + { + CustomAvatarItemId = itemId, + CreatorAccountId = OwnerId, + Name = "Shirt", + Description = string.Empty, + BaseAvatarItemColor = "red", + CreatedAt = DateTime.UtcNow, + ModifiedAt = DateTime.UtcNow + }; + + private static void SetUser(Controller controller, long userId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetDesignForm(Controller controller, Guid itemId, string color, string fileName) + { + var request = controller.HttpContext.Request; + request.ContentType = "multipart/form-data; boundary=radicade-test"; + request.Form = new FormCollection( + new Dictionary + { + ["customAvatarItemId"] = itemId.ToString(), + ["customAvatarItemColor"] = color + }, + new FormFileCollection + { + new FormFile(new MemoryStream(Encoding.UTF8.GetBytes("png-bytes")), 0, 9, "design", fileName) + { + Headers = new HeaderDictionary(), + ContentType = "image/png" + } + }); + } + + private static void SetJsonBody(Controller controller, string json) + { + var request = controller.HttpContext.Request; + request.ContentType = "application/json"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json)); + request.ContentLength = request.Body.Length; + request.EnableBuffering(); + } + + private static void AssertAccountUpdateReachedTheOwner(RecordingProxy owner) + { + Assert.Contains(owner.Messages, frame => + frame.Method == "Notification" && frame.Json.Contains("\"Id\":\"AccountUpdate\"")); + } + + [Fact] + public async Task RedrawingAShirtPushesTheAccountUpdate() + { + await using var fixture = await OpenAsync(); + var itemId = Guid.NewGuid(); + fixture.Db.CustomAvatarItems.Add(ShirtFor(itemId)); + await fixture.Db.SaveChangesAsync(); + + var api = new CustomAvatarItemsAPI(fixture.Db); + SetUser(api, OwnerId); + SetDesignForm(api, itemId, "blue", "shirt.png"); + + Assert.IsType(await api.SaveDesign()); + + var stored = await fixture.Db.CustomAvatarItems.AsNoTracking().FirstAsync(x => x.CustomAvatarItemId == itemId); + Assert.Equal("blue", stored.BaseAvatarItemColor); + Assert.EndsWith(".png", stored.DesignFileName); + + AssertAccountUpdateReachedTheOwner(fixture.Owner); + } + + [Fact] + public async Task RenamingAnItemPushesTheAccountUpdate() + { + await using var fixture = await OpenAsync(); + var itemId = Guid.NewGuid(); + fixture.Db.CustomAvatarItems.Add(ShirtFor(itemId)); + await fixture.Db.SaveChangesAsync(); + + var api = new CustomAvatarItemsAPI(fixture.Db); + SetUser(api, OwnerId); + SetJsonBody(api, """{"Name":"Racing Shirt","BaseAvatarItemColor":"green"}"""); + + Assert.IsType(await api.Update(itemId)); + + var stored = await fixture.Db.CustomAvatarItems.AsNoTracking().FirstAsync(x => x.CustomAvatarItemId == itemId); + Assert.Equal("Racing Shirt", stored.Name); + Assert.Equal("green", stored.BaseAvatarItemColor); + + AssertAccountUpdateReachedTheOwner(fixture.Owner); + } + + [Fact] + public async Task CreatingAnItemPushesTheAccountUpdate() + { + await using var fixture = await OpenAsync(); + + var api = new CustomAvatarItemsAPI(fixture.Db); + SetUser(api, OwnerId); + SetJsonBody(api, """{"Name":"Fresh Shirt","Description":"","Price":0,"BaseAvatarItemId":1,"Accessibility":0}"""); + + Assert.IsType(await api.Create()); + + var stored = await fixture.Db.CustomAvatarItems.AsNoTracking() + .FirstAsync(x => x.CreatorAccountId == OwnerId); + Assert.Equal("Fresh Shirt", stored.Name); + + AssertAccountUpdateReachedTheOwner(fixture.Owner); + } + + [Fact] + public async Task DeletingAnItemPushesTheAccountUpdate() + { + await using var fixture = await OpenAsync(); + var itemId = Guid.NewGuid(); + fixture.Db.CustomAvatarItems.Add(ShirtFor(itemId)); + await fixture.Db.SaveChangesAsync(); + + var api = new CustomAvatarItemsAPI(fixture.Db); + SetUser(api, OwnerId); + + Assert.IsType(await api.Delete(itemId)); + + Assert.Empty(fixture.Db.CustomAvatarItems.Where(x => x.CustomAvatarItemId == itemId)); + AssertAccountUpdateReachedTheOwner(fixture.Owner); + } + + [Fact] + public async Task AWriteFromAnotherAccountDoesNotReachTheOwner() + { + await using var fixture = await OpenAsync(); + var itemId = Guid.NewGuid(); + fixture.Db.CustomAvatarItems.Add(ShirtFor(itemId)); + await fixture.Db.SaveChangesAsync(); + + var api = new CustomAvatarItemsAPI(fixture.Db); + SetUser(api, OwnerId + 1); + SetJsonBody(api, """{"Name":"Not Mine"}"""); + + var result = Assert.IsType(await api.Update(itemId)); + Assert.Equal(StatusCodes.Status403Forbidden, result.StatusCode); + Assert.Empty(fixture.Owner.Messages); + + var stored = await fixture.Db.CustomAvatarItems.AsNoTracking().FirstAsync(x => x.CustomAvatarItemId == itemId); + Assert.Equal("Shirt", stored.Name); + } +} diff --git a/ServerTests/DbFactoryCollection.cs b/ServerTests/DbFactoryCollection.cs new file mode 100644 index 0000000..09a2c30 --- /dev/null +++ b/ServerTests/DbFactoryCollection.cs @@ -0,0 +1,31 @@ +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// RoomUtils/MatchmakingUtils expose their DbContext factories as process-wide statics, so +/// two test classes that replace them cannot run at the same time - whichever wins would +/// answer the other's queries. xUnit parallelizes classes by default, which made the suite +/// fail depending on scheduling; membership in this collection serializes those classes. +/// +[CollectionDefinition(Name)] +public class DbFactoryCollection : ICollectionFixture +{ + public const string Name = "SharesDbFactoryStatics"; +} + +/// +/// The registration throttle is process-wide state. This suite creates dozens of accounts from +/// loopback using the same handful of device ids, so without this the run would spend itself +/// refusing its own fixtures and prove nothing about registration. The limits themselves are +/// covered on their own terms by AccountRegistrationTests, which sets its own and puts them back. +/// +public sealed class RegistrationThrottleFixture +{ + public RegistrationThrottleFixture() => + RegistrationGuard.Configure( + maxCreationsPerIp: int.MaxValue, + maxCreationsPerDevice: int.MaxValue, + minimumDeviceInterval: TimeSpan.Zero); +} diff --git a/ServerTests/DefaultRoomsResetTests.cs b/ServerTests/DefaultRoomsResetTests.cs new file mode 100644 index 0000000..313e3b3 --- /dev/null +++ b/ServerTests/DefaultRoomsResetTests.cs @@ -0,0 +1,262 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.CDN; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Leaderboards; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Showdown belongs in the shipped catalog, but it used to be built by an importer of its own: it +/// never got the image images.txt ships for it (it kept default.png), and because it sat outside +/// nothing that rebuilt the catalog would have brought it back. +/// These pin both halves of the repair - the catalog entry itself, and the one-time reset that +/// deletes every room in the table so the catalog can seed the shipped set from nothing. +/// +/// The reset is destructive by design, so the tests say what it takes: every room, the RecCenter +/// and every player's dorm included, with the rows that hang off them. What they check hardest is +/// that the database is left *usable* - the dorm template every player's own dorm is cloned from is +/// put back on the same boot, before anyone can join. +/// +public class DefaultRoomsResetTests +{ + private const long CoachId = 1; + private const long PlayerId = 2; + private const long DriftedShowdownId = 32; + private const long DriftedBowlingId = 12; + private const long RecCenterId = 3; + private const long DormId = 1; + private const long PlayerRoomId = 40; + + private const string DriftedShowdownTags = "rro,2|games,0|beta,1"; + + private static RecDBContext NewDb() => + new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static RoomData Room( + long id, string name, long creator, bool isDorm = false, string tags = "", + string image = "default.png", int maxPlayers = 10, + Accessibility accessibility = Accessibility.Public, bool cloningAllowed = false) => new() + { + RoomId = id, + Name = name, + Description = name, + ImageName = image, + Tags = tags, + CreatorAccountId = creator, + IsDorm = isDorm, + MaxPlayers = maxPlayers, + Accessibility = accessibility, + CloningAllowed = cloningAllowed, + State = RoomState.Active, + CreatedAt = DateTime.UtcNow, + }; + + private static SubRoom Sub( + long id, long roomId, string name = "Home", string scene = "old-scene", + string save = "", string bundle = "") => new() + { + SubRoomId = id, + RoomId = roomId, + Name = name, + DataBlob = save, + UnityAssetId = bundle, + UnitySceneId = scene, + MaxPlayers = 10, + Accessibility = Accessibility.Public, + SavedByAccountId = CoachId, + CreatedAt = DateTime.UtcNow, + }; + + /// + /// The shape the live database was in: the importer's Showdown with the default picture, a + /// catalog room that drifted, the RecCenter, the dorm template, a room a player made, and one + /// row in every table that hangs off a room - so the cleanup is checked table by table rather + /// than only for the room row. The alignment marker is already set, which is the trap: without + /// the reset clearing it, the catalog would decide this database was fine and do nothing. + /// + private static void SeedDriftedState(RecDBContext db) + { + db.Rooms.AddRange( + Room(DormId, "DormRoom", CoachId, isDorm: true, tags: "dormroom,0", + accessibility: Accessibility.Private, cloningAllowed: true), + Room(RecCenterId, "RecCenter", CoachId, tags: "rro,2", image: "RecCenter", maxPlayers: 12), + Room(DriftedShowdownId, "Showdown", CoachId, tags: DriftedShowdownTags, maxPlayers: 16, cloningAllowed: true), + Room(DriftedBowlingId, "Bowling", CoachId, tags: "rro,2|sports,0", maxPlayers: 99), + Room(PlayerRoomId, "MyCoolRoom", PlayerId, image: "myimage")); + + db.SubRooms.AddRange( + Sub(1, DormId, scene: SceneLocations.Dorm), + Sub(3, RecCenterId), + Sub(DriftedShowdownId, DriftedShowdownId, scene: SceneLocations.MakerRoom, + save: "Showdown.room", bundle: "Showdown_0_1.assetbundle"), + Sub(DriftedBowlingId, DriftedBowlingId), + Sub(PlayerRoomId, PlayerRoomId)); + + db.RoomStats.AddRange( + new RoomStats { RoomId = DormId }, + new RoomStats { RoomId = RecCenterId }, + new RoomStats { RoomId = DriftedShowdownId }, + new RoomStats { RoomId = PlayerRoomId }); + + db.RoomRoles.AddRange( + new RoomRole { RoomId = DriftedShowdownId, AccountId = CoachId, Role = RoleType.Creator }, + new RoomRole { RoomId = PlayerRoomId, AccountId = PlayerId, Role = RoleType.Creator }); + + db.RoomKeys.Add(new RoomKey { RoomId = DriftedShowdownId, ReplicationId = "key", Name = "Key", Description = "" }); + db.RoomBans.Add(new RoomBan { roomId = DriftedShowdownId, actorId = CoachId, bannedPlayerId = PlayerId }); + db.RoomComments.Add(new RoomComment { RoomId = DriftedShowdownId, CreatorAccountId = PlayerId, Comment = "on the wrong room" }); + db.RoomComments.Add(new RoomComment { RoomId = PlayerRoomId, CreatorAccountId = PlayerId, Comment = "kept for nobody" }); + db.RoomInteractions.Add(new PlayerInteractionDB { PlayerId = PlayerId, RoomId = DriftedShowdownId, Visited = true }); + db.RoomInteractions.Add(new PlayerInteractionDB { PlayerId = PlayerId, RoomId = PlayerRoomId, Visited = true }); + db.RoomInstances.Add(new RoomInstance { roomId = DriftedShowdownId, subRoomId = DriftedShowdownId }); + db.LeaderboardScores.Add(new LeaderboardScore { RoomId = DriftedShowdownId, PlayerId = PlayerId, Score = 12 }); + db.SubRoomPermissions.Add(new SubRoomPermission { subRoomId = DriftedShowdownId, permission = "AnyoneCanSave" }); + db.SubRoomHistory.Add(new RestorableSubRoom { RoomId = DriftedShowdownId, SubRoomId = DriftedShowdownId, DataBlob = "old" }); + + db.PlayerEvents.Add(new PlayerEventDB + { + PlayerEventId = 7, + Name = "Event in a room that is about to go", + CreatorPlayerId = PlayerId, + RoomId = DriftedShowdownId, + StartTime = DateTime.UtcNow, + EndTime = DateTime.UtcNow, + }); + db.PlayerEventResponses.Add(new PlayerEventResponse { PlayerEventId = 7, PlayerId = PlayerId }); + + // These three only point at a room, so their rows are kept and the reference cleared. + db.Images.Add(new ImageDB { Name = "photo-in-showdown", OwnerId = PlayerId, RoomId = DriftedShowdownId }); + db.Messages.Add(new Message { ToPlayerId = PlayerId, FromPlayerId = CoachId, RoomId = DriftedShowdownId, SentTime = DateTime.UtcNow }); + + // Already aligned: the trap the reset has to clear. + db.GameConfig.Add(new ConfigVal("DefaultRooms.AlignmentVersion", "1")); + + db.SaveChanges(); + } + + [Fact] + public void TheCatalogShipsShowdownWithItsOwnImageAndRoomFile() + { + var showdown = DefaultRooms.All.Single(room => room.Name == "Showdown"); + + Assert.Equal("Showdown", showdown.ImageName); + Assert.Equal(16, showdown.MaxPlayers); + Assert.True(showdown.CloningAllowed); + Assert.True(showdown.IsRRO); + Assert.Equal(Accessibility.Public, showdown.Accessibility); + Assert.Equal(SceneLocations.MakerRoom, showdown.SceneId); + Assert.Equal("Showdown.room", showdown.SaveName); + Assert.Equal("Showdown_0_1.assetbundle", showdown.BundleName); + // Spelled out rather than through RroWith, because "beta" is stored as an Auto tag. + Assert.Equal(DriftedShowdownTags, StringTools.CreateTagString(showdown.Tags.ToList())); + } + + [Fact] + public async Task TheResetEmptiesTheTableAndPutsBackTheDormTemplateAndTheShippedRooms() + { + var db = NewDb(); + SeedDriftedState(db); + + await DefaultRooms.EnsureAsync(db); + db.ChangeTracker.Clear(); + + // Every room went, with no exception list - the old ids are all gone and nothing is left + // in the tables that hung off them. + foreach (var oldId in new[] { DormId, RecCenterId, DriftedShowdownId, DriftedBowlingId, PlayerRoomId }) + Assert.False(await db.Rooms.AnyAsync(room => room.RoomId == oldId)); + + Assert.Empty(await db.RoomComments.ToListAsync()); + Assert.Empty(await db.RoomInteractions.ToListAsync()); + Assert.Empty(await db.RoomInstances.ToListAsync()); + Assert.Empty(await db.LeaderboardScores.ToListAsync()); + Assert.Empty(await db.RoomKeys.ToListAsync()); + Assert.Empty(await db.RoomBans.ToListAsync()); + Assert.Empty(await db.SubRoomPermissions.ToListAsync()); + Assert.Empty(await db.SubRoomHistory.ToListAsync()); + Assert.Empty(await db.PlayerEvents.ToListAsync()); + Assert.Empty(await db.PlayerEventResponses.ToListAsync()); + + // A photo, a message and a moderation record outlive the room, without the dead reference. + Assert.Null((await db.Images.SingleAsync(image => image.Name == "photo-in-showdown")).RoomId); + Assert.Null((await db.Messages.SingleAsync()).RoomId); + + // The dorm template is back before anyone can join - it is the row every player's own dorm + // is cloned from, so a server without it gives out no dorms at all. + var dorm = await db.Rooms.SingleAsync(room => room.IsDorm); + Assert.Equal("DormRoom", dorm.Name); + Assert.Equal("Dorm", dorm.ImageName); + Assert.Equal(CoachId, dorm.CreatorAccountId); + Assert.True(dorm.CloningAllowed); + var dormSub = await db.SubRooms.SingleAsync(sub => sub.RoomId == dorm.RoomId); + Assert.Equal("Home", dormSub.Name); + Assert.Equal(SceneLocations.Dorm, dormSub.UnitySceneId); + Assert.True(dormSub.IsSandbox); + Assert.True(await db.RoomStats.AnyAsync(stats => stats.RoomId == dorm.RoomId)); + + // ...and the whole shipped set was built from the catalog on the same boot, Showdown with + // its own picture and its room file attached. + var showdown = await db.Rooms.SingleAsync(room => room.Name == "Showdown"); + Assert.NotEqual(DriftedShowdownId, showdown.RoomId); + Assert.Equal("Showdown", showdown.ImageName); + Assert.Equal(16, showdown.MaxPlayers); + Assert.Equal(DriftedShowdownTags, showdown.Tags); + Assert.True(showdown.CloningAllowed); + Assert.True(showdown.IsRRO); + Assert.Equal(Accessibility.Public, showdown.Accessibility); + Assert.Equal(RoomState.Active, showdown.State); + Assert.Equal(CoachId, showdown.CreatorAccountId); + + var sub = await db.SubRooms.SingleAsync(room => room.RoomId == showdown.RoomId); + Assert.Equal("Home", sub.Name); + Assert.Equal(SceneLocations.MakerRoom, sub.UnitySceneId); + Assert.Equal("Showdown.room", sub.DataBlob); + Assert.Equal("Showdown_0_1.assetbundle", sub.UnityAssetId); + Assert.Equal(16, sub.MaxPlayers); + Assert.False(sub.IsSandbox); + Assert.True(await db.RoomStats.AnyAsync(stats => stats.RoomId == showdown.RoomId)); + Assert.True(await db.RoomRoles.AnyAsync(role => role.RoomId == showdown.RoomId && role.AccountId == CoachId)); + + var recCenter = await db.Rooms.SingleAsync(room => room.Name == "RecCenter"); + Assert.NotEqual(RecCenterId, recCenter.RoomId); + Assert.Equal("RecCenter", recCenter.ImageName); + var bowling = await db.Rooms.SingleAsync(room => room.Name == "Bowling"); + Assert.Equal("Bowling", bowling.ImageName); + Assert.Equal(8, bowling.MaxPlayers); + Assert.True(await db.Rooms.AnyAsync(room => room.Name == "Orientation")); + + // Exactly the shipped set, plus the one dorm template. + Assert.Equal(DefaultRooms.All.Count + 1, await db.Rooms.CountAsync()); + Assert.Equal("1", (await db.GameConfig.SingleAsync(config => config.Key == "DefaultRooms.AlignmentVersion")).Value); + Assert.Equal("3", (await db.GameConfig.SingleAsync(config => config.Key == "DefaultRooms.ShippedRoomResetVersion")).Value); + } + + [Fact] + public async Task TheResetRunsOnceAndDoesNotWipeTheRoomsItJustSeeded() + { + var db = NewDb(); + SeedDriftedState(db); + + await DefaultRooms.EnsureAsync(db); + var seeded = await db.Rooms.SingleAsync(room => room.Name == "Showdown"); + var dorm = await db.Rooms.SingleAsync(room => room.IsDorm); + + // A second boot: the marker stops the wipe, so the rooms the first one built are still + // there - not deleted and rebuilt every time the server starts. + await DefaultRooms.EnsureAsync(db); + db.ChangeTracker.Clear(); + + Assert.Equal(seeded.RoomId, (await db.Rooms.SingleAsync(room => room.Name == "Showdown")).RoomId); + Assert.Equal(dorm.RoomId, (await db.Rooms.SingleAsync(room => room.IsDorm)).RoomId); + Assert.Equal(DefaultRooms.All.Count + 1, await db.Rooms.CountAsync()); + } +} diff --git a/ServerTests/DiscordWebhookTests.cs b/ServerTests/DiscordWebhookTests.cs new file mode 100644 index 0000000..cd42684 --- /dev/null +++ b/ServerTests/DiscordWebhookTests.cs @@ -0,0 +1,90 @@ +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using RadicadeServer.Discord.Webhooks; +using RadicadeServer.Radicade.Discord; +using Xunit; + +namespace RadicadeServer.Tests; + +public sealed class DiscordWebhookTests +{ + [Fact] + public void WebhookConfig_LoadsLocalFileAndLetsConfigurationOverrideIt() + { + var root = Path.Combine(AppContext.BaseDirectory, $"webhook-config-test-{Guid.NewGuid():N}"); + var webhookDirectory = Path.Combine(root, "Discord", "Webhooks"); + Directory.CreateDirectory(webhookDirectory); + + try + { + File.WriteAllText(Path.Combine(webhookDirectory, "webhooks.json"), """ + { + "BanAppeals": "https://discord.com/api/webhooks/123/appeal", + "Reports": "https://discord.com/api/webhooks/123/file-report" + } + """); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Radicade:Discord:Webhooks:Reports"] = "https://discord.com/api/webhooks/456/config-report" + }) + .Build(); + + WebhookConfig.Load(configuration, root); + + Assert.Equal("https://discord.com/api/webhooks/123/appeal", WebhookConfig.BanAppealsUrl); + Assert.Equal("https://discord.com/api/webhooks/456/config-report", WebhookConfig.ReportsUrl); + } + finally + { + WebhookConfig.Load(new ConfigurationBuilder().Build(), Path.Combine(root, "missing")); + Directory.Delete(root, recursive: true); + } + } + + [Theory] + [InlineData("Club", "https://cdn.radicade.app/ClubReport.png")] + [InlineData("Player", "https://cdn.radicade.app/PlayerReport.png")] + [InlineData("Room", "https://cdn.radicade.app/RoomReport.png")] + [InlineData("Event", "https://cdn.radicade.app/EventReport.png")] + [InlineData("Invention", "https://cdn.radicade.app/InventionReport.png")] + public void ReportImage_MatchesReportCategory(string reportType, string expectedUrl) + => Assert.Equal(expectedUrl, WebhookImages.ForReport(reportType)); + + [Fact] + public void ReportImage_IsNotAssignedToUnspecifiedCategories() + => Assert.Null(WebhookImages.ForReport("Board")); + + [Fact] + public void CategoryArtwork_IsSerializedAsThumbnailRatherThanLargeImage() + { + var payload = new DiscordPayload + { + Title = "Bug report", + ThumbnailUrl = WebhookImages.BugReport + }; + + using var document = JsonDocument.Parse(DiscordPayloadJson.Build(payload)); + var embed = document.RootElement.GetProperty("embeds")[0]; + + Assert.Equal(WebhookImages.BugReport, embed.GetProperty("thumbnail").GetProperty("url").GetString()); + Assert.False(embed.TryGetProperty("image", out _)); + } + + [Fact] + public void UploadedBugScreenshot_RemainsTheLargeImageAlongsideCategoryThumbnail() + { + var payload = new DiscordPayload + { + Title = "Bug report", + ThumbnailUrl = WebhookImages.BugReport + }; + + using var document = JsonDocument.Parse(DiscordPayloadJson.Build(payload, embedImageUrl: "attachment://screenshot.png")); + var embed = document.RootElement.GetProperty("embeds")[0]; + + Assert.Equal("attachment://screenshot.png", embed.GetProperty("image").GetProperty("url").GetString()); + Assert.Equal(WebhookImages.BugReport, embed.GetProperty("thumbnail").GetProperty("url").GetString()); + } +} diff --git a/ServerTests/DorkNetCatalogTests.cs b/ServerTests/DorkNetCatalogTests.cs new file mode 100644 index 0000000..fab2ad8 --- /dev/null +++ b/ServerTests/DorkNetCatalogTests.cs @@ -0,0 +1,200 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The shipped consumable and equipment-skin catalogs come from DorkNet's extraction of the +/// 2023 client's own asset configs (tools/import_dorknet_catalogs.py). These tests keep the two +/// halves of each row honest: a consumable that has no client token spawns as +/// "Unable to find consumable", and a skin without an exact prefab + guid pair does not resolve +/// at all. +/// +public class DorkNetCatalogTests +{ + private static string Fixture(string name) => Path.Combine(AppContext.BaseDirectory, "Fixtures", name); + + private static List ShippedConsumables() + { + var path = Fixture("consumables.json"); + Assert.True(File.Exists(path), $"missing fixture {path}"); + // Rarity is written as a name in the catalog, which is what the admin panel's own + // options (JsonStringEnumConverter) accept. + return JsonSerializer.Deserialize>( + File.ReadAllText(path), + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }) + ?? new List(); + } + + private static List ShippedSkins() + { + var path = Fixture("equipment_skins.json"); + Assert.True(File.Exists(path), $"missing fixture {path}"); + return EquipmentSkinCatalog.Parse(File.ReadAllText(path)); + } + + [Fact] + public void ShippedConsumablesAllResolveToAClientToken() + { + var rows = ShippedConsumables(); + + Assert.True(rows.Count >= 53, $"only {rows.Count} consumable(s) shipped"); + Assert.DoesNotContain(rows, row => string.IsNullOrWhiteSpace(row.ConsumableItemDesc)); + + // ToClientDesc leaves unknown names alone, so a row that comes back unchanged would be + // served as its friendly name - exactly the bug that made gift boxes fail to spawn. + var unresolved = rows + .Where(row => ConsumableCatalog.ToClientDesc(row.ConsumableItemDesc) == row.ConsumableItemDesc) + .Select(row => row.ConsumableItemDesc) + .ToList(); + + Assert.Empty(unresolved); + } + + [Fact] + public void ShippedConsumablesAreDistinctAndCarryAFriendlyName() + { + var rows = ShippedConsumables(); + + Assert.Equal( + rows.Count, + rows.Select(row => row.ConsumableItemDesc).Distinct(StringComparer.OrdinalIgnoreCase).Count()); + Assert.DoesNotContain(rows, row => string.IsNullOrWhiteSpace(row.FriendlyName)); + Assert.Contains(rows, row => row.Rarity != Rarity.Common); + } + + [Fact] + public void ShippedSkinsAllCarryThePairTheClientResolvesThemFrom() + { + var skins = ShippedSkins(); + + Assert.True(skins.Count >= 194, $"only {skins.Count} skin(s) shipped"); + Assert.DoesNotContain(skins, skin => string.IsNullOrWhiteSpace(skin.PrefabName)); + Assert.DoesNotContain(skins, skin => string.IsNullOrWhiteSpace(skin.ModificationGuid)); + Assert.DoesNotContain(skins, skin => string.IsNullOrWhiteSpace(skin.DisplayName)); + Assert.Equal( + skins.Count, + skins.Select(skin => skin.ModificationGuid).Distinct(StringComparer.OrdinalIgnoreCase).Count()); + } + + [Fact] + public void SkinsBecomeEquipmentRowsNamedFromTheCatalog() + { + var skin = new EquipmentSkinItem + { + PrefabName = "[PaintballGun]", + ModificationGuid = "00000000-0000-0000-0000-000000000001", + DisplayName = "Paintball Gun Skin Test", + Tooltip = "Equipment skin for PaintballGun.", + Rarity = Rarity.Uncommon + }; + + var equipment = Assert.Single(EquipmentSkinCatalog.ToEquipment([skin])); + + Assert.Equal("[PaintballGun]", equipment.PrefabName); + Assert.Equal(skin.ModificationGuid, equipment.ModificationGuid); + Assert.Equal("Paintball Gun Skin Test", equipment.FriendlyName); + Assert.Equal("Equipment skin for PaintballGun.", equipment.Tooltip); + Assert.Equal(Rarity.Uncommon, equipment.Rarity); + } + + [Fact] + public void SkinsWithoutAPrefabOrGuidAreDropped() + { + var parsed = EquipmentSkinCatalog.Parse(""" + { "items": [ + { "prefab_name": "[Arrow]", "modification_guid": "aaa", "display_name": "Arrow Skin" }, + { "prefab_name": "", "modification_guid": "bbb", "display_name": "No prefab" }, + { "prefab_name": "[Basketball]", "modification_guid": "", "display_name": "No guid" } + ] } + """); + + Assert.Equal("[Arrow]", Assert.Single(parsed).PrefabName); + Assert.Empty(EquipmentSkinCatalog.Parse("not json at all")); + Assert.Empty(EquipmentSkinCatalog.Parse(null)); + } + + [Fact] + public void MergingSkinsAddsOnlyThePairsMissingFromTheCatalog() + { + const string equipmentJson = """ + [ { "PrefabName": "[Arrow]", "ModificationGuid": "b862383e-0de3-492d-8f72-25e756287687", + "FriendlyName": "[Arrow]", "Tooltip": "", "Rarity": 0 } ] + """; + const string skinsJson = """ + { "items": [ + { "prefab_name": "[Arrow]", "modification_guid": "b862383e-0de3-492d-8f72-25e756287687", + "display_name": "Arrow Skin Vitton", "rarity": 10 }, + { "prefab_name": "[Arrow]", "modification_guid": "aaaaaaaa-0000-0000-0000-000000000000", + "display_name": "Arrow Skin Fresh", "rarity": 10 } + ] } + """; + + var merged = JsonSerializer.Deserialize>( + EquipmentSkinCatalog.MergeInto(equipmentJson, skinsJson))!; + + // The existing row keeps its place: a skin contributes only pairs the catalog lacks. + Assert.Equal(2, merged.Count); + Assert.Equal("[Arrow]", merged[0].FriendlyName); + Assert.Equal("Arrow Skin Fresh", merged[1].FriendlyName); + Assert.Equal(Rarity.Uncommon, merged[1].Rarity); + + // Idempotent: merging the merged catalog again adds nothing. + var again = JsonSerializer.Deserialize>( + EquipmentSkinCatalog.MergeInto( + EquipmentSkinCatalog.MergeInto(equipmentJson, skinsJson), skinsJson))!; + Assert.Equal(2, again.Count); + } + + [Fact] + public void MergingAnUnreadableCatalogLeavesItAlone() + { + const string broken = "{ this is not an equipment list }"; + + Assert.Equal(broken, EquipmentSkinCatalog.MergeInto(broken, """{ "items": [ { "prefab_name": "[Arrow]", "modification_guid": "aaa" } ] }""")); + Assert.Equal("[]", EquipmentSkinCatalog.MergeInto("[]", "{}")); + Assert.Equal("[]", EquipmentSkinCatalog.MergeInto(null, null)); + } + + /// How many rows repeat a prefab/guid pair another row already carries. + private static int DuplicatePairs(List rows) => + rows.Count - rows + .Select(row => $"{row.PrefabName}\u001f{row.ModificationGuid}") + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count(); + + [Fact] + public void ShippedSkinsSurviveAMergeIntoTheShippedEquipmentCatalog() + { + var equipmentPath = Fixture("equipment.json"); + var skinsPath = Fixture("equipment_skins.json"); + if (!File.Exists(equipmentPath) || !File.Exists(skinsPath)) + { + // Both fixtures are optional here; the shipped files themselves are what matters. + return; + } + + var equipment = File.ReadAllText(equipmentPath); + var merged = JsonSerializer.Deserialize>( + EquipmentSkinCatalog.MergeInto(equipment, File.ReadAllText(skinsPath)))!; + var before = JsonSerializer.Deserialize>(equipment)!; + + Assert.True(merged.Count >= before.Count); + // The shipped equipment.json already repeats some prefab/guid pairs, so the invariant is + // that merging a skin never adds another row for a pair the catalog already had. + Assert.Equal(DuplicatePairs(before), DuplicatePairs(merged)); + foreach (var skin in ShippedSkins()) + { + Assert.Contains(merged, row => + string.Equals(row.PrefabName, skin.PrefabName, StringComparison.OrdinalIgnoreCase) + && string.Equals(row.ModificationGuid, skin.ModificationGuid, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/ServerTests/DormRoomImageTests.cs b/ServerTests/DormRoomImageTests.cs new file mode 100644 index 0000000..c43d45e --- /dev/null +++ b/ServerTests/DormRoomImageTests.cs @@ -0,0 +1,97 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A dorm is the one room every player is certain to see, and it had no picture of its own: the +/// row carried RoomData's "default.png", which is not a file on disk, so the client drew its own +/// placeholder and every dorm looked the same in the panel. images.txt ships a Dorm picture, so a +/// dorm with nothing set gets it - while a dorm whose picture was set on purpose keeps it. +/// +public class DormRoomImageTests +{ + private static RecDBContext NewDb() => + new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static RoomData Room(long id, string name, bool isDorm, string image, long creator = 1) => new() + { + RoomId = id, + Name = name, + Description = name, + ImageName = image, + CreatorAccountId = creator, + IsDorm = isDorm, + MaxPlayers = 10, + CreatedAt = DateTime.UtcNow, + }; + + [Fact] + public async Task ANewDormRoomGetsTheDormPicture() + { + var db = NewDb(); + + await db.AddDormRoom(); + + Assert.Equal("Dorm", (await db.Rooms.SingleAsync(room => room.IsDorm)).ImageName); + } + + [Fact] + public async Task DormsWithNoPictureOfTheirOwnGetTheDormPictureAndSetOnesAreLeftAlone() + { + var db = NewDb(); + db.Rooms.AddRange( + Room(1, "DormRoom", isDorm: true, image: "default.png"), + Room(2, "DormRoom", isDorm: true, image: " "), + Room(3, "DormRoom", isDorm: true, image: DBConfig.DefaultRoomImage), + Room(4, "DormRoom", isDorm: true, image: "MyCustomDormPicture"), + Room(5, "MyCoolRoom", isDorm: false, image: "default.png", creator: 2)); + db.SaveChanges(); + + await db.NormalizeDormRoomImagesAsync(); + + Assert.Equal("Dorm", (await db.Rooms.SingleAsync(room => room.RoomId == 1)).ImageName); + Assert.Equal("Dorm", (await db.Rooms.SingleAsync(room => room.RoomId == 2)).ImageName); + Assert.Equal("Dorm", (await db.Rooms.SingleAsync(room => room.RoomId == 3)).ImageName); + Assert.Equal("MyCustomDormPicture", (await db.Rooms.SingleAsync(room => room.RoomId == 4)).ImageName); + // Not a dorm: the room default there is somebody else's business. + Assert.Equal("default.png", (await db.Rooms.SingleAsync(room => room.RoomId == 5)).ImageName); + + // Runs on every boot, so a second pass must be a no-op. + await db.NormalizeDormRoomImagesAsync(); + Assert.Equal("MyCustomDormPicture", (await db.Rooms.SingleAsync(room => room.RoomId == 4)).ImageName); + } + + [Fact] + public void TheImagesSetShipsBothPictures() + { + var lines = File.ReadAllLines(Path.Combine(FindServerDirectory(), "images.txt")); + + Assert.Contains("Dorm=https://cdn.radicade.app/Dorm.png", lines); + Assert.Contains("Showdown=https://cdn.radicade.app/Showdown.png", lines); + } + + /// + /// images.txt lives next to the project file and is copied to the test output too, so walk up + /// from the test binary until the Server project's copy shows up. + /// + private static string FindServerDirectory() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, "Server", "images.txt"); + if (File.Exists(candidate)) + return Path.Combine(directory.FullName, "Server"); + + directory = directory.Parent; + } + + throw new InvalidOperationException("could not find Server/images.txt from the test output directory"); + } +} diff --git a/ServerTests/DormVisitTests.cs b/ServerTests/DormVisitTests.cs new file mode 100644 index 0000000..c126f2f --- /dev/null +++ b/ServerTests/DormVisitTests.cs @@ -0,0 +1,110 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class DormVisitTests +{ + private const long PlayerId = 701; + private const long DormRoomId = 81; + private const long DormInstanceId = 901; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private static async Task<(TestDbContextFactory Factory, RecDBContext Db)> CreateContextAsync() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + var db = factory.CreateDbContext(); + db.Rooms.Add(new RoomData + { + RoomId = DormRoomId, + Name = "DormRoom", + IsDorm = true, + CreatorAccountId = PlayerId, + Accessibility = Accessibility.Private, + CreatedAt = DateTime.UtcNow + }); + db.SubRooms.Add(new SubRoom + { + SubRoomId = 81, + RoomId = DormRoomId, + Name = "Home", + UnitySceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6", + MaxPlayers = 4, + Accessibility = Accessibility.Private + }); + db.RoomInstances.Add(new RoomInstance + { + roomInstanceId = DormInstanceId, + roomId = DormRoomId, + subRoomId = 81, + roomInstanceType = RoomInstanceType.Dormroom, + isPrivate = true, + name = "@tester's Dorm", + maxCapacity = 4 + }); + await db.SaveChangesAsync(); + return (factory, db); + } + + [Fact] + public async Task DormEntryCountsOneVisitAndHeartbeatRehydrationDoesNotAddMore() + { + var (factory, db) = await CreateContextAsync(); + await using var _db = db; + var presence = new RealPresence { playerId = PlayerId }; + db.Presences.Add(presence); + await db.SaveChangesAsync(); + + await MatchmakingUtils.EnsureDormPresenceAsync(db, PlayerId, presence); + await MatchmakingUtils.EnsureDormPresenceAsync(db, PlayerId, presence); + await db.SaveChangesAsync(); + + await using var verify = factory.CreateDbContext(); + var interaction = await verify.RoomInteractions.SingleAsync(x => x.RoomId == DormRoomId && x.PlayerId == PlayerId); + var stats = await verify.RoomStats.SingleAsync(x => x.RoomId == DormRoomId); + Assert.True(interaction.Visited); + Assert.Equal(1, interaction.VisitCount); + Assert.Equal(1, stats.VisitCount); + Assert.Equal(1, stats.VisitorCount); + } + + [Fact] + public async Task ReturningToDormCountsASecondVisit() + { + var (factory, db) = await CreateContextAsync(); + await using var _db = db; + var presence = new RealPresence { playerId = PlayerId }; + db.Presences.Add(presence); + await db.SaveChangesAsync(); + + await MatchmakingUtils.EnsureDormPresenceAsync(db, PlayerId, presence); + presence.roomInstance = null; + await db.SaveChangesAsync(); + await MatchmakingUtils.EnsureDormPresenceAsync(db, PlayerId, presence); + await db.SaveChangesAsync(); + + await using var verify = factory.CreateDbContext(); + var interaction = await verify.RoomInteractions.SingleAsync(x => x.RoomId == DormRoomId && x.PlayerId == PlayerId); + var stats = await verify.RoomStats.SingleAsync(x => x.RoomId == DormRoomId); + Assert.Equal(2, interaction.VisitCount); + Assert.Equal(2, stats.VisitCount); + Assert.Equal(1, stats.VisitorCount); + } +} diff --git a/ServerTests/EquipmentCatalogTests.cs b/ServerTests/EquipmentCatalogTests.cs new file mode 100644 index 0000000..70c544c --- /dev/null +++ b/ServerTests/EquipmentCatalogTests.cs @@ -0,0 +1,177 @@ +using System.Text.Json; +using RadicadeServer.RecNet.Avatar; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Guards the shipped equipment catalog (RadicadeServer-Config/equipment.json). +/// +/// That file is what every new account's equipment list is built from, and it reached this repo +/// in bad shape: 268 rows carrying 192 distinct prefab/guid pairs, 92 rows whose FriendlyName was +/// the bare prefab ("[PaintballAssaultRifle]") because the name never resolved, three guids +/// assigned to two different prefabs, and one guid that was a character too long. None of it +/// raised an error - the client just listed unnamed entries, and a skin attached to the wrong +/// prefab can never be applied - so the invariants are pinned here instead. +/// +/// The current file is a dump of the 2023 client's own equipment list: 271 rows over the same 37 +/// prefabs, every row named, every guid well formed and belonging to exactly one prefab. +/// +public class EquipmentCatalogTests +{ + private static string Fixture(string name) => Path.Combine(AppContext.BaseDirectory, "Fixtures", name); + + private static List ShippedCatalog() + { + var path = Fixture("equipment.json"); + Assert.True(File.Exists(path), $"missing fixture {path}"); + var rows = JsonSerializer.Deserialize>(File.ReadAllText(path)); + Assert.NotNull(rows); + return rows!; + } + + private static string Pair(Equipment row) => + $"{row.PrefabName}\u001f{row.ModificationGuid}"; + + [Fact] + public void ShippedCatalogCarriesEveryRowItDeclaresAndNoneOfThemTwice() + { + var rows = ShippedCatalog(); + + // A floor rather than an exact count: skins are data and will be added, but silently + // losing a chunk of the catalog is the failure worth catching. + Assert.True(rows.Count >= 271, $"equipment.json shrank to {rows.Count} rows"); + + var duplicates = rows + .GroupBy(Pair, StringComparer.OrdinalIgnoreCase) + .Where(group => group.Count() > 1) + .Select(group => $"{group.Key} x{group.Count()}") + .ToList(); + + // Service merges by pair, so a repeated row never reached the client - it just made the + // file lie about its own size. The catalog it replaced repeated 76 of them. + Assert.Empty(duplicates); + } + + [Fact] + public void EveryShippedGuidIsAWholeGuid() + { + var bad = ShippedCatalog() + .Where(row => !IsGuid(row.ModificationGuid)) + .Select(row => $"{row.PrefabName} {row.ModificationGuid}") + .ToList(); + + Assert.Empty(bad); + } + + /// + /// A RecNet modification guid is either the dashed 36-character form or the 22-character + /// base64url form the client emits. Anything else is a truncated or padded guid, which is how + /// the catalog this replaced carried "[Crossbow] ESQ0qOlgrkCc8MbUXGnF8Ag" - one character + /// longer than the real guid for that skin, so the Plaid crossbow could never resolve. + /// + private static bool IsGuid(string? guid) + { + if (guid == null) return false; + if (guid.Length == 36) + return Guid.TryParseExact(guid, "D", out _); + if (guid.Length != 22) return false; + return guid.All(c => char.IsAsciiLetterOrDigit(c) || c == '-' || c == '_'); + } + + [Fact] + public void EveryShippedGuidBelongsToOnePrefab() + { + var spanning = ShippedCatalog() + .GroupBy(row => row.ModificationGuid, StringComparer.OrdinalIgnoreCase) + .Where(group => group.Select(row => row.PrefabName).Distinct(StringComparer.OrdinalIgnoreCase).Count() > 1) + .Select(group => $"{group.Key} -> {string.Join(", ", group.Select(row => row.PrefabName).Distinct())}") + .ToList(); + + // A skin is the pair, so a guid that names two prefabs means one of the rows is wrong and + // that skin is attached to a tool the client will never look it up under. + Assert.Empty(spanning); + } + + [Fact] + public void EveryShippedRowHasARealName() + { + var unnamed = ShippedCatalog() + .Where(row => string.IsNullOrWhiteSpace(row.FriendlyName) + || row.FriendlyName.StartsWith('[')) + .Select(row => $"{row.PrefabName} {row.ModificationGuid}") + .ToList(); + + // A bare prefab as the name is what the equipment list renders verbatim, which is why + // 92 rows used to read "[QuestSword]" instead of "Sword (Extra)". + Assert.Empty(unnamed); + } + + [Fact] + public void EveryShippedRowIsAResolvableSkin() + { + var rows = ShippedCatalog(); + + foreach (var row in rows) + { + // Bracketed, not necessarily ending in one: "[PaintballGun] Confetti" is a real + // prefab of its own. + Assert.StartsWith("[", row.PrefabName); + Assert.Contains("]", row.PrefabName); + Assert.False(string.IsNullOrWhiteSpace(row.ModificationGuid)); + Assert.NotNull(row.Tooltip); + } + + // The prefab half has to be one of the client's own tool prefabs, so a typo like the + // "[Dodgeball]", "[SciFi_Grenade]" and "[PaintBallRifleScoped]" rows the old catalog + // carried shows up here rather than as a skin the player can own but never wear. Pinned + // as a set on purpose: a genuinely new tool prefab should be a deliberate addition, and + // the count alone would not notice one typo replacing another. + var prefabs = rows.Select(row => row.PrefabName).Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(name => name, StringComparer.OrdinalIgnoreCase).ToList(); + Assert.Equal(ShippedPrefabs.OrderBy(name => name, StringComparer.OrdinalIgnoreCase), prefabs); + } + + /// The 37 tool prefabs the 2023 client ships skins for. + private static readonly string[] ShippedPrefabs = + { + "[Arena_Grenade]", "[Basketball]", "[BowlingBall]", "[Bucket]", "[Crossbow]", + "[Crossbow_Hunter]", "[DiscGolfDisc]", "[DodgeballBall]", "[Grenade]", "[Longbow]", "[MakerPen]", + "[PaintballAssaultRifle]", "[PaintballGrenadeLauncher]", "[PaintballGun]", + "[PaintballGun] Confetti", "[PaintballRifleScoped]", "[PaintballShield]", + "[PaintballShotgun]", "[Paintball_PaintThrower]", "[QuestShield]", "[QuestSword]", + "[Quest_Goblin_Wand]", "[Quest_SciFi_AutomaticGun]", "[Quest_SciFi_Pistol]", + "[Quest_SciFi_RailGun]", "[Quest_SciFi_Shotgun]", "[RecCenter_Cafe_Mug]", + "[RecRoyale_Backpack]", "[Sandbox_D10]", "[Sandbox_D12]", "[Sandbox_D20]", + "[Sandbox_D4]", "[Sandbox_D6]", "[Sandbox_D8]", "[ShareCamera]", "[SoccerShield]", + "[Vehicle_RallyBuggy]" + }; + + [Fact] + public void TheServedCatalogKeepsEveryRowTheFileDeclares() + { + // What the equipment endpoint actually hands a player: the file merged with the DorkNet + // skin catalog, keyed by pair exactly as EquipmentV2 does it. + var equipmentPath = Fixture("equipment.json"); + var skinsPath = Fixture("equipment_skins.json"); if (!File.Exists(skinsPath)) return; + + + + var merged = JsonSerializer.Deserialize>( + EquipmentSkinCatalog.MergeInto(File.ReadAllText(equipmentPath), File.ReadAllText(skinsPath)))!; + + var served = merged + .Where(row => !string.IsNullOrWhiteSpace(row.PrefabName)) + .GroupBy(Pair, StringComparer.OrdinalIgnoreCase) + .ToList(); + + Assert.Equal(merged.Count, served.Count); + Assert.True(served.Count >= 271); + + // And the pairs the file names are the pairs the endpoint serves - no row is lost to the + // merge, and no merge-added row displaces one. + var declared = ShippedCatalog().Select(Pair).ToList(); + var servedPairs = served.Select(group => group.Key).ToList(); + Assert.All(declared, pair => Assert.Contains(pair, servedPairs, StringComparer.OrdinalIgnoreCase)); + } +} diff --git a/ServerTests/EquipmentInventoryTests.cs b/ServerTests/EquipmentInventoryTests.cs new file mode 100644 index 0000000..92b6e9d --- /dev/null +++ b/ServerTests/EquipmentInventoryTests.cs @@ -0,0 +1,483 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Avatars.V1; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; +using Xunit; + +namespace RadicadeServer.Tests; + +/// Inventory reads expose starter defaults and owned items, never the full catalogs. +[Collection(DbFactoryCollection.Name)] +public class EquipmentInventoryTests +{ + [Theory] + [InlineData("Unlocked Locker", true)] + [InlineData("moderator, unlocked locker", true)] + [InlineData("developer", false)] + [InlineData("Not Unlocked Locker", false)] + [InlineData(null, false)] + public async Task LockerRoleControlsAllThreeInventoriesWithoutPermanentlyGrantingItems(string? role, bool full) + { + var oldAvatar = DBConfig.DefaultAvatarCatalog; + var oldEquipment = DBConfig.DefaultEquipmentItems; + var oldSkins = DBConfig.DefaultEquipmentSkinItems; + var oldConsumables = DBConfig.DefaultConsumableItems; + var oldLookup = DBConfig.ResolvableAvatarItemDescs; + try + { + DBConfig.DefaultAvatarCatalog = """[{"AvatarItemDesc":"role-exclusive,","FriendlyName":"Role item"}]"""; + DBConfig.ResolvableAvatarItemDescs = new HashSet(); + DBConfig.DefaultEquipmentItems = """[{"PrefabName":"[PaintballGun]","ModificationGuid":"role-skin"}]"""; + DBConfig.DefaultEquipmentSkinItems = "[]"; + DBConfig.DefaultConsumableItems = ShipCatalog("consumables.json"); + using var db = NewDb(); + var profile = new RadicadeServer.RecNet.Auth.Profile + { + accountId = 7, username = "locker", displayName = "Locker", profileImage = "default", role = role + }; + db.Profiles.Add(profile); + db.Unlocks.Add(new PlayerUnlocks { AccountId = 7, AvatarItems = "[]" }); + await db.SaveChangesAsync(); + var avatar = new RadicadeServer.API.Avatars.V4.AvatarV4(db); + var equipment = new EquipmentV2(db); + var consumables = new ConsumablesV2(db); + SetUser(avatar, 7); + SetUser(equipment, 7); + SetUser(consumables, 7); + var items = Assert.IsType>(Assert.IsType((await avatar.GetLocalAvatarItems()).Result).Value); + Assert.Equal(full, items.Any(item => item.AvatarItemDesc == "role-exclusive,")); + Assert.Equal(full, equipment.GetUnlockedEquipment().Any(item => item.ModificationGuid == "role-skin")); + Assert.Equal(full, (await consumables.GetUnlockedConsumables())!.Any()); + + profile.role = null; + await db.SaveChangesAsync(); + items = Assert.IsType>(Assert.IsType((await avatar.GetLocalAvatarItems()).Result).Value); + Assert.DoesNotContain(items, item => item.AvatarItemDesc == "role-exclusive,"); + Assert.Empty(equipment.GetUnlockedEquipment()); + Assert.Empty((await consumables.GetUnlockedConsumables())!); + var stored = await db.Unlocks.AsNoTracking().SingleAsync(); + Assert.Empty(stored.GetAvatarItems()); + Assert.Empty(stored.GetEquipment()); + Assert.Empty(stored.GetConsumables()); + } + finally + { + DBConfig.DefaultAvatarCatalog = oldAvatar; + DBConfig.DefaultEquipmentItems = oldEquipment; + DBConfig.DefaultEquipmentSkinItems = oldSkins; + DBConfig.DefaultConsumableItems = oldConsumables; + DBConfig.ResolvableAvatarItemDescs = oldLookup; + } + } + + [Fact] + public async Task QuestAvatarInventoryDoesNotReturnDescriptorsMissingFromTheClientBuild() + { + var originalLookup = DBConfig.ResolvableAvatarItemDescs; + try + { + using var lookup = JsonDocument.Parse(ShipCatalog("avatar-item-lookup.json")); + DBConfig.ResolvableAvatarItemDescs = lookup.RootElement.GetProperty("descriptions") + .EnumerateArray().Select(element => element.GetString()!) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + const string unsupportedHairDye = "yTdx1nVVtECU8Vh7EC8dGA"; + Assert.DoesNotContain(unsupportedHairDye, DBConfig.ResolvableAvatarItemDescs); + var supportedDescription = DBConfig.ResolvableAvatarItemDescs.First(); + + using var db = NewDb(); + var unlocks = new PlayerUnlocks + { + AccountId = 7, + AvatarItems = JsonSerializer.Serialize(new[] + { + new AvatarItem { AvatarItemDesc = supportedDescription, FriendlyName = "Supported" }, + new AvatarItem { AvatarItemDesc = unsupportedHairDye, FriendlyName = "Unsupported" } + }) + }; + db.Unlocks.Add(unlocks); + await db.SaveChangesAsync(); + + var api = new RadicadeServer.API.Avatars.V4.AvatarV4(db); + SetUser(api, 7, (int)RadicadeServer.RecNet.Enums.Players.Platforms.Standalone); + var result = Assert.IsType>( + Assert.IsType((await api.GetLocalAvatarItems()).Result).Value); + + Assert.Contains(result, item => item.AvatarItemDesc == supportedDescription && item.FriendlyName == "Supported"); + Assert.DoesNotContain(result, item => item.AvatarItemDesc == unsupportedHairDye); + + SetUser(api, 7, (int)RadicadeServer.RecNet.Enums.Players.Platforms.Oculus); + result = Assert.IsType>( + Assert.IsType((await api.GetLocalAvatarItems()).Result).Value); + Assert.Contains(result, item => item.AvatarItemDesc == unsupportedHairDye); + } + finally + { + DBConfig.ResolvableAvatarItemDescs = originalLookup; + } + } + + [Fact] + public async Task NewInventoryContainsStarterClothingButNoCatalogSkinsOrConsumables() + { + var originalCatalog = DBConfig.DefaultAvatarCatalog; + var originalLookup = DBConfig.ResolvableAvatarItemDescs; + try + { + DBConfig.DefaultAvatarCatalog = ShipCatalog("2022-avatar.json"); + using var lookup = JsonDocument.Parse(ShipCatalog("avatar-item-lookup.json")); + DBConfig.ResolvableAvatarItemDescs = lookup.RootElement.GetProperty("descriptions") + .EnumerateArray().Select(x => x.GetString()!).ToHashSet(StringComparer.OrdinalIgnoreCase); + var starters = AvatarUnlockCatalog.GetStarterItems(); + Assert.NotEmpty(starters); + Assert.True(starters.Count < AvatarUnlockCatalog.ParseCatalog(DBConfig.DefaultAvatarCatalog).Count); + Assert.All(starters, item => Assert.Contains(item.AvatarItemDesc, DBConfig.ResolvableAvatarItemDescs)); + var allowed = AvatarUnlockCatalog.ParseCatalog(AvatarUnlockCatalog.LoadStarterJson()) + .Select(x => x.AvatarItemDesc).ToHashSet(); + Assert.All(starters, item => Assert.True(item.IsBaseAvatarItem || allowed.Contains(item.AvatarItemDesc))); + + using var db = NewDb(); + var inventory = new PlayerUnlocks { AccountId = 7, AvatarItems = starters.ToJson() }; + db.Unlocks.Add(inventory); + await db.SaveChangesAsync(); + var equipment = new EquipmentV2(db); + SetUser(equipment, 7); + Assert.Empty(equipment.GetUnlockedEquipment()); + var consumables = new ConsumablesV2(db); + SetUser(consumables, 7); + Assert.Empty((await consumables.GetUnlockedConsumables())!); + Assert.Empty(inventory.GetEquipment()); + Assert.Empty(inventory.GetConsumables()); + } + finally + { + DBConfig.DefaultAvatarCatalog = originalCatalog; + DBConfig.ResolvableAvatarItemDescs = originalLookup; + } + } + + [Fact] + public async Task ConsumingLastOwnedItemDoesNotRestoreItFromTheCatalog() + { + using var db = NewDb(); + var inventory = new PlayerUnlocks { AccountId = 7 }; + inventory.AddConsumable(new Consumable { Id = 42, ConsumableItemDesc = "Root Beer", Count = 1, InitialCount = 1 }); + db.Unlocks.Add(inventory); + await db.SaveChangesAsync(); + var api = new ConsumablesV2(db); + SetUser(api, 7); + api.Request.ContentType = "application/json"; + api.Request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("{\"Id\":42,\"DeltaCount\":1}")); + Assert.IsType(await api.ConsumeConsumable()); + db.ChangeTracker.Clear(); + Assert.Empty((await api.GetUnlockedConsumables())!); + Assert.Empty((await db.Unlocks.SingleAsync()).GetConsumables()); + } + + private static string Fixture(string name) => Path.Combine(AppContext.BaseDirectory, "Fixtures", name); + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static void SetUser(Controller controller, long accountId, int? platform = null) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + if (platform.HasValue) + identity.AddClaim(new Claim("rn.plat", platform.Value.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static string ShipCatalog(string fixture) => File.ReadAllText(Fixture(fixture)); + + [Fact] + public void TheEquipmentUpdateRouteAnswersUnderBothPrefixes() + { + var routes = typeof(EquipmentV1).GetCustomAttributes() + .Select(route => route.Template).ToList(); + + // The client calls it under `econ/`, like every other avatars route. Without this the + // request 404s even though the method is right there. + Assert.Contains("econ/api/equipment/v1", routes); + Assert.Contains("api/equipment/v1", routes); + } + + [Fact] + public void TheEquipmentUpdateRouteAnswersBothVerbs() + { + var method = typeof(EquipmentV1).GetMethod(nameof(EquipmentV1.UpdateEquipment))!; + + var posts = method.GetCustomAttributes().Select(a => a.Template).ToList(); + var puts = method.GetCustomAttributes().Select(a => a.Template).ToList(); + + // The 2023 client POSTs; the reference implementation documents PUT. Accepting only one + // is a bet, and the whole point of this endpoint is that it used to answer nothing. + Assert.Contains("update", posts); + Assert.Contains("update", puts); + } + + [Fact] + public async Task AFavouritedOwnedItemIsStoredAndServedBack() + { + var previous = DBConfig.DefaultEquipmentItems; + try + { + DBConfig.DefaultEquipmentItems = ShipCatalog("equipment.json"); + + using var db = NewDb(); + var owned = new PlayerUnlocks { AccountId = 7 }; + owned.AddEquipment(JsonSerializer.Deserialize>(DBConfig.DefaultEquipmentItems)![0]); + db.Unlocks.Add(owned); + await db.SaveChangesAsync(); + + var guid = JsonSerializer.Deserialize>(DBConfig.DefaultEquipmentItems)![0].ModificationGuid; + var controller = new EquipmentV1(db); + SetUser(controller, 7); + + var result = await controller.UpdateEquipment(JsonDocument.Parse( + $"[{{\"ModificationGuid\":\"{guid}\",\"Favorited\":true}}]").RootElement); + + Assert.IsType(result); + + var stored = (await db.Unlocks.FirstAsync(x => x.AccountId == 7)).GetEquipment()!; + var row = Assert.Single(stored); + Assert.Equal(guid, row.ModificationGuid); + Assert.True(row.Favorited); + + // And the flag has to survive the trip back out: getUnlocked merges the player's rows + // over the catalog, so the row above is what makes the favourite stick. + var readback = new EquipmentV2(db); + SetUser(readback, 7); + var served = readback.GetUnlockedEquipment(); + var servedRow = Assert.Single(served, item => item.ModificationGuid == guid); + Assert.True(servedRow.Favorited); + Assert.Equal(served.Count, served.Select(item => item.ModificationGuid).Distinct().Count()); + } + finally + { + DBConfig.DefaultEquipmentItems = previous; + } + } + + [Fact] + public async Task AFavouriteOnAnAccountWithNoUnlockRowDoesNotGrantTheSkin() + { + var previous = DBConfig.DefaultEquipmentItems; + try + { + DBConfig.DefaultEquipmentItems = ShipCatalog("equipment.json"); + + // Accounts in the wild exist without an unlock row - one turned up on the live + // database while this was being written. Without creating the row the write is + // accepted, dropped, and served back as an un-favourite. + using var db = NewDb(); + Assert.Empty(db.Unlocks); + + var guid = JsonSerializer.Deserialize>(DBConfig.DefaultEquipmentItems)![0].ModificationGuid; + var controller = new EquipmentV1(db); + SetUser(controller, 7); + + Assert.IsType(await controller.UpdateEquipment(JsonDocument.Parse( + $"[{{\"ModificationGuid\":\"{guid}\",\"Favorited\":true}}]").RootElement)); + + var unlocks = await db.Unlocks.FirstAsync(x => x.AccountId == 7); + Assert.Empty(unlocks.GetEquipment()); + } + finally + { + DBConfig.DefaultEquipmentItems = previous; + } + } + + [Fact] + public async Task UnfavouritingAnUntouchedItemStoresNothing() + { + var previous = DBConfig.DefaultEquipmentItems; + try + { + DBConfig.DefaultEquipmentItems = ShipCatalog("equipment.json"); + + using var db = NewDb(); + db.Unlocks.Add(new PlayerUnlocks { AccountId = 7 }); + await db.SaveChangesAsync(); + + var guid = JsonSerializer.Deserialize>(DBConfig.DefaultEquipmentItems)![0].ModificationGuid; + var controller = new EquipmentV1(db); + SetUser(controller, 7); + + var result = await controller.UpdateEquipment(JsonDocument.Parse( + $"[{{\"ModificationGuid\":\"{guid}\",\"Favorited\":false}}]").RootElement); + + Assert.IsType(result); + // Nothing to un-favourite means nothing to write. Otherwise every entry the client + // echoes would leave a row behind and a profile would grow a copy of the catalog. + Assert.Equal("[]", (await db.Unlocks.FirstAsync(x => x.AccountId == 7)).Equipment); + } + finally + { + DBConfig.DefaultEquipmentItems = previous; + } + } + + [Fact] + public async Task AMalformedUpdateBodyIsRefused() + { + using var db = NewDb(); + var controller = new EquipmentV1(db); + SetUser(controller, 7); + + Assert.IsType( + await controller.UpdateEquipment(JsonDocument.Parse("\"nope\"").RootElement)); + Assert.IsType( + await controller.UpdateEquipment(JsonDocument.Parse("{\"other\":[]}").RootElement)); + } + + [Fact] + public void EveryShippedConsumableIsGrantedAsAClientToken() + { + var granted = ConsumableCatalog.DefaultInstances(ShipCatalog("consumables.json")); + + Assert.Equal(87, granted.Count); + Assert.DoesNotContain(granted, item => string.IsNullOrWhiteSpace(item.ConsumableItemDesc)); + // A descriptor that is still a friendly name updates nothing in the client: it resolves + // consumables by token and logs "Unable to find consumable" on a miss. + Assert.DoesNotContain(granted, item => item.ConsumableItemDesc.Contains(' ')); + Assert.Equal(granted.Count, granted.Select(item => item.Id).Distinct().Count()); + Assert.All(granted, item => + { + Assert.Equal(ConsumableCatalog.DefaultCount, item.Count); + Assert.Equal(ConsumableCatalog.DefaultCount, item.InitialCount); + Assert.InRange(item.Id, 800_000_000L, 899_999_999L); + }); + } + + [Fact] + public void AGrantedIdIsTheSameOnEveryBoot() + { + var first = ConsumableCatalog.DefaultInstances(ShipCatalog("consumables.json")); + var second = ConsumableCatalog.DefaultInstances(ShipCatalog("consumables.json")); + + // Pinned so nobody swaps FNV for string.GetHashCode(), which is salted per process: the + // ids would change on restart and the client would see a different inventory each boot. + Assert.Equal( + first.ToDictionary(item => item.ConsumableItemDesc, item => item.Id), + second.ToDictionary(item => item.ConsumableItemDesc, item => item.Id)); + } + + [Fact] + public async Task TheCatalogIsNotGrantedToAPlayerWhoOwnsNothing() + { + var previous = DBConfig.DefaultConsumableItems; + try + { + DBConfig.DefaultConsumableItems = ShipCatalog("consumables.json"); + + using var db = NewDb(); + db.Unlocks.Add(new PlayerUnlocks { AccountId = 7 }); + await db.SaveChangesAsync(); + + var controller = new ConsumablesV2(db); + SetUser(controller, 7); + + var served = await controller.GetUnlockedConsumables(); + + Assert.NotNull(served); + Assert.Empty(served!); + Assert.Equal(served.Count, served.Select(item => item.ConsumableItemDesc).Distinct().Count()); + Assert.All(served, item => + { + Assert.NotEmpty(item.Ids); + // Ids and CreatedAts are parallel arrays to the client; a short one is a malformed + // response, not just an odd one. + Assert.Equal(item.Ids.Count, item.CreatedAts.Count); + Assert.True(item.Count > 0); + }); + } + finally + { + DBConfig.DefaultConsumableItems = previous; + } + } + + [Fact] + public async Task OnlyOwnedConsumableInstancesAreServed() + { + var previous = DBConfig.DefaultConsumableItems; + try + { + DBConfig.DefaultConsumableItems = ShipCatalog("consumables.json"); + + var token = ConsumableCatalog.ToClientDesc("Root Beer"); + using var db = NewDb(); + var unlocks = new PlayerUnlocks { AccountId = 7 }; + unlocks.AddConsumable(new Consumable + { + Id = 42, + ConsumableItemDesc = "Root Beer", + Count = 2, + InitialCount = 2, + CreatedAt = new DateTime(2024, 5, 1, 0, 0, 0, DateTimeKind.Utc) + }); + db.Unlocks.Add(unlocks); + await db.SaveChangesAsync(); + + var controller = new ConsumablesV2(db); + SetUser(controller, 7); + + var served = (await controller.GetUnlockedConsumables())!; + var rootBeer = Assert.Single(served, item => item.ConsumableItemDesc == token); + + // What they really have, not the grant plus their purchase - otherwise buying a second + // one would look like it did nothing. + Assert.Equal(new List { 42 }, rootBeer.Ids); + Assert.Equal(2, rootBeer.Count); + Assert.Single(served); + } + finally + { + DBConfig.DefaultConsumableItems = previous; + } + } + + [Fact] + public async Task TwoInstancesOfOneItemAreGroupedIntoOneEntry() + { + var previous = DBConfig.DefaultConsumableItems; + try + { + DBConfig.DefaultConsumableItems = ShipCatalog("consumables.json"); + + using var db = NewDb(); + var unlocks = new PlayerUnlocks { AccountId = 7 }; + unlocks.AddConsumable(new Consumable { Id = 11, ConsumableItemDesc = "Bubbly", Count = 1, InitialCount = 1 }); + unlocks.AddConsumable(new Consumable { Id = 12, ConsumableItemDesc = "Bubbly", Count = 3, InitialCount = 3 }); + db.Unlocks.Add(unlocks); + await db.SaveChangesAsync(); + + var controller = new ConsumablesV2(db); + SetUser(controller, 7); + + var served = (await controller.GetUnlockedConsumables())!; + var bubbly = Assert.Single(served, item => item.ConsumableItemDesc == ConsumableCatalog.ToClientDesc("Bubbly")); + + Assert.Equal(new List { 11, 12 }, bubbly.Ids); + Assert.Equal(4, bubbly.Count); + } + finally + { + DBConfig.DefaultConsumableItems = previous; + } + } +} diff --git a/ServerTests/EventTimeTests.cs b/ServerTests/EventTimeTests.cs new file mode 100644 index 0000000..344bc7e --- /dev/null +++ b/ServerTests/EventTimeTests.cs @@ -0,0 +1,194 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Events; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// An event's start and end are the wall clock its creator picked, and the client posts them +/// with no zone on them. Treating that value as the instant itself made both this server and the +/// client file a fresh event as already finished: the Upcoming filters compare event windows to +/// UTC, and a zone-less timestamp on the wire is read as UTC by the client, so anything the +/// player scheduled "in twenty minutes" came back six hours in the past and was hidden. These +/// tests pin the conversion on the way in and the `Z` form on the way out. +/// +public class EventTimeTests +{ + private const long CreatorId = 42; + + private static RecDBContext CreateDb() => new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static EventsAPI CreateController(RecDBContext db, string? jsonBody = null) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, CreatorId.ToString())); + + var controller = new EventsAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext + { + User = new ClaimsPrincipal(identity) + } + } + }; + + if (jsonBody != null) + { + var bytes = Encoding.UTF8.GetBytes(jsonBody); + controller.HttpContext.Request.Body = new MemoryStream(bytes); + controller.HttpContext.Request.ContentType = "application/json"; + controller.HttpContext.Request.ContentLength = bytes.Length; + } + + return controller; + } + + private static async Task SeedRoomAsync(RecDBContext db) + { + var room = new RoomData { Name = "Event Room" }; + db.Rooms.Add(room); + await db.SaveChangesAsync(); + return room.RoomId; + } + + private static async Task CreateAsync(RecDBContext db, long roomId, string startTime, string endTime) + { + var body = + $$""" + { + "Name": "Time check", + "RoomId": {{roomId}}, + "StartTime": "{{startTime}}", + "EndTime": "{{endTime}}" + } + """; + var created = Assert.IsType(await CreateController(db, body).CreateEventV2()); + using var document = JsonDocument.Parse(JsonSerializer.Serialize(created.Value)); + return document.RootElement.GetProperty("PlayerEvent").GetProperty("PlayerEventId").GetInt64(); + } + + /// An event time with no zone, the way the game posts it. + private static string WallClock(DateTime local) => local.ToString("yyyy-MM-dd'T'HH:mm:ss"); + + /// The same instant with the sub-second ticks the wall-clock form drops. + private static DateTime WholeSecond(DateTime value) + => new(value.Ticks - value.Ticks % TimeSpan.TicksPerSecond, value.Kind); + + [Fact] + public async Task APostedWallClockIsStoredAsTheUtcInstantItNames() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var picked = new DateTime(2026, 10, 8, 20, 10, 0, DateTimeKind.Unspecified); + + await CreateAsync(db, roomId, WallClock(picked), WallClock(picked.AddMinutes(30))); + + var stored = await db.PlayerEvents.SingleAsync(); + var expected = TimeZoneInfo.ConvertTimeToUtc(picked, TimeZoneInfo.Local); + Assert.Equal(expected, stored.StartTime); + Assert.Equal(expected.AddMinutes(30), stored.EndTime); + } + + [Fact] + public async Task APostedUtcTimestampIsHonouredAsTheInstantItNames() + { + // A client that starts sending real UTC must not be shifted by the server's zone. + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var instant = new DateTime(2026, 10, 9, 3, 10, 0, DateTimeKind.Utc); + + await CreateAsync(db, roomId, instant.ToString("o"), instant.AddMinutes(30).ToString("o")); + + var stored = await db.PlayerEvents.SingleAsync(); + Assert.Equal(instant, stored.StartTime); + } + + [Fact] + public async Task EventTimestampsGoOutAsUtcWithATrailingZ() + { + // Real RecNet serves every event time as `...Z`, and a zone-less value is read as UTC by + // the client - so the read shape, the write envelope and the notification frame must all + // carry the zone, or the client's own not-ended filter drops the event. + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var picked = WholeSecond(DateTime.Now.AddMinutes(20)); + var eventId = await CreateAsync(db, roomId, WallClock(picked), WallClock(picked.AddHours(1))); + + var controller = CreateController(db); + var read = await controller.GetEvent(eventId); + var ok = Assert.IsType(read.Result); + using var document = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value)); + + var startTime = Assert.IsType(document.RootElement.GetProperty("StartTime").GetString()); + var endTime = Assert.IsType(document.RootElement.GetProperty("EndTime").GetString()); + + Assert.EndsWith("Z", startTime); + Assert.EndsWith("Z", endTime); + Assert.Equal(TimeZoneInfo.ConvertTimeToUtc(picked, TimeZoneInfo.Local), + DateTime.Parse(startTime, null, System.Globalization.DateTimeStyles.RoundtripKind)); + } + + [Fact] + public async Task AnEventScheduledInTwentyMinutesIsUpcomingEverywhereTheClientLooks() + { + // The bug this guards: an event made "in twenty minutes" was stored as the wall clock and + // compared to UTC now, so it was six hours finished as far as every Upcoming-shaped query + // - and the client's own ended filter - were concerned. + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var picked = DateTime.Now.AddMinutes(20); + var eventId = await CreateAsync(db, roomId, WallClock(picked), WallClock(picked.AddHours(1))); + var controller = CreateController(db); + + Assert.Contains(await controller.GetBrowseFeed(0, 50), e => e.PlayerEventId == eventId); + Assert.Contains(await controller.GetUpcomingEvents(), e => e.PlayerEventId == eventId); + Assert.Contains(await controller.SearchEvents(null, null), e => e.PlayerEventId == eventId); + + // What the client itself checks: the timestamp parsed as UTC is still in the future. + var later = await controller.GetBrowseFeed(0, 50); + var onWire = later.Single(e => e.PlayerEventId == eventId).StartTime; + Assert.True(DateTimeOffset.Parse(EventClock.ToWire(onWire)) > DateTimeOffset.UtcNow); + } + + [Fact] + public async Task AnEventThatHasFinishedIsStillKeptOutOfTheUpcomingFeeds() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var picked = DateTime.Now.AddHours(-4); + var eventId = await CreateAsync(db, roomId, WallClock(picked), WallClock(picked.AddHours(2))); + var controller = CreateController(db); + + Assert.DoesNotContain(await controller.GetBrowseFeed(0, 50), e => e.PlayerEventId == eventId); + Assert.DoesNotContain(await controller.GetUpcomingEvents(), e => e.PlayerEventId == eventId); + } + + [Fact] + public void TheClockConvertsAWallClockAndPassesAUtcInstantThrough() + { + var wallClock = new DateTime(2026, 10, 8, 20, 10, 0, DateTimeKind.Unspecified); + + Assert.Equal(TimeZoneInfo.ConvertTimeToUtc(wallClock, TimeZoneInfo.Local), + EventClock.FromPostedTime(wallClock)); + + var utc = new DateTime(2026, 10, 9, 3, 10, 0, DateTimeKind.Utc); + Assert.Equal(utc, EventClock.FromPostedTime(utc)); + Assert.Equal("2026-10-09T03:10:00.0000000Z", EventClock.ToWire(utc)); + + // A value read back from the store carries no kind; it is already UTC and must not be + // converted a second time. + Assert.Equal("2026-10-09T03:10:00.0000000Z", + EventClock.ToWire(new DateTime(2026, 10, 9, 3, 10, 0, DateTimeKind.Unspecified))); + } +} diff --git a/ServerTests/EventsApiTests.cs b/ServerTests/EventsApiTests.cs new file mode 100644 index 0000000..b0e7fc7 --- /dev/null +++ b/ServerTests/EventsApiTests.cs @@ -0,0 +1,781 @@ +using System.IO; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Events; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// End-to-end tests for the game's event endpoints, exercising the real +/// EventsAPI controller against an in-memory database. +/// +public class EventsApiTests +{ + private const long CreatorId = 42; + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new RecDBContext(options); + } + + private static EventsAPI CreateController(RecDBContext db, long? userId = CreatorId) + { + var identity = new ClaimsIdentity("TestAuth"); + if (userId.HasValue) + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.Value.ToString())); + + return new EventsAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + } + + private static EventsAPI CreateController(RecDBContext db, long? userId, string jsonBody) + { + var controller = CreateController(db, userId); + SetJsonBody(controller, jsonBody); + return controller; + } + + private static void SetJsonBody(EventsAPI controller, string jsonBody) + { + var bytes = Encoding.UTF8.GetBytes(jsonBody); + controller.HttpContext.Request.Body = new MemoryStream(bytes); + controller.HttpContext.Request.ContentType = "application/json"; + controller.HttpContext.Request.ContentLength = bytes.Length; + } + + private static async Task CreateV2EventAsync(RecDBContext db, long roomId) + { + var createController = CreateController( + db, CreatorId, + $$"""{ "Name": "Club meetup", "RoomId": {{roomId}} }"""); + var create = Assert.IsType(await createController.CreateEventV2()); + using var document = System.Text.Json.JsonDocument.Parse( + System.Text.Json.JsonSerializer.Serialize(create.Value)); + return document.RootElement.GetProperty("PlayerEvent").GetProperty("PlayerEventId").GetInt64(); + } + + private static async Task UpdateEventV2Async( + RecDBContext db, long eventId, string route, string body) + { + var controller = CreateController(db); + SetJsonBody(controller, body); + controller.HttpContext.Request.Path = $"/api/playerevents/v2/{eventId}/{route}"; + var result = Assert.IsType(await controller.UpdateEventV2(eventId)); + return System.Text.Json.JsonDocument.Parse(System.Text.Json.JsonSerializer.Serialize(result.Value)); + } + + private static async Task SeedRoomAsync(RecDBContext db, string name = "Test Room") + { + var room = new RoomData { Name = name }; + db.Rooms.Add(room); + await db.SaveChangesAsync(); + return room.RoomId; + } + + private static async Task CreateEventAsync( + EventsAPI controller, + long roomId, + string name = "Launch Party") + { + var result = await controller.CreateEvent( + name, "A test event", roomId, + DateTime.UtcNow.AddHours(1), null, null, null); + + var ok = Assert.IsType(result.Result); + return Assert.IsType(ok.Value); + } + + [Fact] + public async Task CreateEvent_ThenAppearsInSearchAndRoomEvents() + { + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + + var created = await CreateEventAsync(controller, roomId, "Launch Party"); var search = await controller.SearchEvents("launch", null); + Assert.Single(search); + Assert.Contains(search, e => e.PlayerEventId == created.PlayerEventId); + + var roomEvents = await controller.GetEventsInRoom(roomId); + Assert.Contains(roomEvents, e => e.PlayerEventId == created.PlayerEventId); + } + + [Fact] + public async Task SearchEvents_OnlyReturnsPublicUpcomingEvents() + { + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + + await CreateEventAsync(controller, roomId, "Public Upcoming"); + db.PlayerEvents.Add(new PlayerEventDB + { + Name = "Ended", + Description = "", + ImageName = "", + StartTime = DateTime.UtcNow.AddHours(-4), + EndTime = DateTime.UtcNow.AddHours(-2), + CreatorPlayerId = CreatorId, + AttendeeCount = 1, + RoomId = roomId, + Accessibility = Accessibility.Public + }); + db.PlayerEvents.Add(new PlayerEventDB + { + Name = "Private", + Description = "", + ImageName = "", + StartTime = DateTime.UtcNow.AddHours(1), + EndTime = DateTime.UtcNow.AddHours(3), + CreatorPlayerId = CreatorId, + AttendeeCount = 1, + RoomId = roomId, + Accessibility = Accessibility.Private + }); + await db.SaveChangesAsync(); var search = await controller.SearchEvents(null, null); + Assert.Single(search); + Assert.Equal("Public Upcoming", search[0].Name); + } + + [Fact] + public async Task CreateEvent_BlankName_ReturnsBadRequest() + { + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + + var result = await controller.CreateEvent(" ", null, roomId, null, null, null, null); + + Assert.IsType(result.Result); + } + + [Fact] + public async Task CreateEvent_NonPositiveRoomId_ReturnsBadRequest() + { + var db = CreateDb(); + var controller = CreateController(db); + + var result = await controller.CreateEvent("Launch", null, 0, null, null, null, null); + + Assert.IsType(result.Result); + } + + [Fact] + public async Task CreateEvent_UnknownRoom_ReturnsBadRequest() + { + var db = CreateDb(); + var controller = CreateController(db); + + var result = await controller.CreateEvent("Launch", null, 9999, null, null, null, null); + + Assert.IsType(result.Result); + } + + [Fact] + public async Task CreateEvent_NoUser_ReturnsUnauthorized() + { + var db = CreateDb(); + var controller = CreateController(db, userId: null); + var roomId = await SeedRoomAsync(db); + + var result = await controller.CreateEvent("Launch", null, roomId, null, null, null, null); + + Assert.IsType(result.Result); + } + + [Fact] + public async Task CreateEvent_ImplicitlyRSVPsCreatorYes() + { + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + + var created = await CreateEventAsync(controller, roomId); + + var response = await db.PlayerEventResponses + .SingleAsync(r => r.PlayerEventId == created.PlayerEventId); + Assert.Equal(CreatorId, response.PlayerId); + Assert.Equal(EventResponses.Yes, response.Type); + Assert.Equal(1, created.AttendeeCount); + } + + [Fact] + public async Task GetCreatedEvents_ReturnsOnlyTheUsersEvents() + { + var db = CreateDb(); + var controller = CreateController(db, userId: 42); + var otherController = CreateController(db, userId: 43); + var roomId = await SeedRoomAsync(db); + + await CreateEventAsync(controller, roomId, "Mine"); + await CreateEventAsync(otherController, roomId, "Theirs"); + + var mine = await controller.GetCreatedEvents(); + + Assert.Single(mine); + Assert.Equal("Mine", mine[0].Name); + } + + [Fact] + public async Task GetAllEvents_ReturnsCreatedAndNoWireResponses() + { + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + await CreateEventAsync(controller, roomId, "Mine"); + + var all = await controller.GetAllEvents(); + + Assert.Single(all.Created); + // The 2023 client parses Responses entries with its typed MessagePack + // deserializer and a shape it does not fully understand deserializes to + // null, failing the boot-time events refresh. The RecFlare reference + // server always serves an empty Responses list, so Radicade does too. + Assert.Empty(all.Responses); + } + + [Fact] + public async Task GetAllEvents_NeverServesAnEmptyImageName() + { + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + db.PlayerEvents.Add(new PlayerEventDB + { + Name = "No image", + Description = "", + ImageName = "", + StartTime = DateTime.UtcNow.AddHours(1), + EndTime = DateTime.UtcNow.AddHours(2), + CreatorPlayerId = CreatorId, + AttendeeCount = 0, + RoomId = roomId, + Accessibility = Accessibility.Public + }); + await db.SaveChangesAsync(); + + var all = await controller.GetAllEvents(); + var created = Assert.Single(all.Created); + + // The 2023 client's RRUI events cache resolves an image for every event it + // lists and throws on an empty name before making any request, which fails + // the whole refresh and makes the client retry it in a tight loop. + Assert.Equal("default", created.ImageName); + } + + [Fact] + public async Task ClubEventFeedsReturnTheExpectedDifferentShapes() + { + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + var now = DateTime.UtcNow; + db.PlayerEvents.AddRange( + new PlayerEventDB + { + Name = "Club 12 event", Description = "", ImageName = "", RoomId = roomId, + CreatorPlayerId = CreatorId, StartTime = now.AddHours(1), EndTime = now.AddHours(2), + ClubId = 12, Accessibility = Accessibility.Public + }, + new PlayerEventDB + { + Name = "Club 13 event", Description = "", ImageName = "", RoomId = roomId, + CreatorPlayerId = CreatorId, StartTime = now.AddHours(2), EndTime = now.AddHours(3), + ClubId = 13, Accessibility = Accessibility.Public + }, + new PlayerEventDB + { + Name = "Unattached event", Description = "", ImageName = "", RoomId = roomId, + CreatorPlayerId = CreatorId, StartTime = now.AddHours(3), EndTime = now.AddHours(4), + Accessibility = Accessibility.Public + }); + await db.SaveChangesAsync(); + + var clubs = await controller.GetEventsByClubs([12, 13]); + Assert.Equal(new[] { "Club 12 event", "Club 13 event" }, clubs.Select(e => e.Name)); + Assert.Equal(12, clubs[0].ClubId); + + var single = Assert.IsType(await controller.GetEventsByClub(12)); + using var json = System.Text.Json.JsonDocument.Parse(System.Text.Json.JsonSerializer.Serialize(single.Value)); + Assert.Equal(string.Empty, json.RootElement.GetProperty("ContinuationToken").GetString()); + Assert.Equal("Club 12 event", json.RootElement.GetProperty("Events")[0].GetProperty("Name").GetString()); + } + + [Fact] + public async Task GetEvent_ReturnsEventOrNotFound() + { + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + var created = await CreateEventAsync(controller, roomId); + + var found = await controller.GetEvent(created.PlayerEventId); + var ok = Assert.IsType(found.Result); + Assert.Equal(created.PlayerEventId, Assert.IsType(ok.Value).PlayerEventId); + + var missing = await controller.GetEvent(9999); + Assert.IsType(missing.Result); + } + + [Fact] + public async Task GetEvent_SerializesAllClientRequiredFields() + { + // The client deserializes every key of its PlayerEvent model and logs + // "Failed to find key 'Tags'" (then "Malformed Response") when one is + // missing. Guard the full wire shape here. + var db = CreateDb(); + var controller = CreateController(db); + var roomId = await SeedRoomAsync(db); + var created = await CreateEventAsync(controller, roomId); + + var json = System.Text.Json.JsonSerializer.Serialize(created); + + Assert.Contains("\"Tags\"", json); + Assert.Contains("\"ClubId\"", json); + Assert.Contains("\"State\"", json); + Assert.Contains("\"CreatedAt\"", json); + } + + [Fact] + public void GetEventTagFilters_ReturnsUsableTagCatalog() + { + // The client refuses to save an event while every tag group is empty, so the + // endpoint serves the reference server's lowercase catalog instead of three + // empty sets. TrendingFilters is null exactly as the reference serves it - + // the client renders no trending row for null. + var db = CreateDb(); + var controller = CreateController(db); + + var filters = controller.GetEventTagFilters(); + + Assert.NotNull(filters.PinnedFilters); + Assert.NotNull(filters.PopularFilters); + Assert.Null(filters.TrendingFilters); + Assert.NotEmpty(filters.PinnedFilters); + Assert.NotEmpty(filters.PopularFilters); + Assert.All(filters.PinnedFilters, t => Assert.Equal(t, t.ToLowerInvariant())); + Assert.All(filters.PopularFilters, t => Assert.Equal(t, t.ToLowerInvariant())); + Assert.Contains("game", filters.PopularFilters); + Assert.Contains("meetup", filters.PinnedFilters); + } + + [Fact] + public async Task SetResponse_AddsRsvpAndUpdatesAttendeeCount() + { + var db = CreateDb(); + var creator = CreateController(db, userId: 42); + var guest = CreateController(db, userId: 43); + var roomId = await SeedRoomAsync(db); + var created = await CreateEventAsync(creator, roomId); + + var yes = await guest.SetResponse(created.PlayerEventId, EventResponses.Yes); + Assert.Equal( + EventResponses.Yes, + Assert.IsType(Assert.IsType(yes.Result).Value).Type); + + var ev = await db.PlayerEvents.SingleAsync(e => e.PlayerEventId == created.PlayerEventId); + Assert.Equal(2, ev.AttendeeCount); + + await guest.SetResponse(created.PlayerEventId, EventResponses.No); + ev = await db.PlayerEvents.SingleAsync(e => e.PlayerEventId == created.PlayerEventId); + Assert.Equal(1, ev.AttendeeCount); + } + + [Fact] + public void GetLegacyPlaceholderResponses_ReturnsAnEmptyList() + { + var controller = CreateController(CreateDb()); + var route = typeof(EventsAPI).GetMethod(nameof(EventsAPI.GetLegacyPlaceholderResponses))! + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.HttpGetAttribute), false) + .Cast() + .Single(); + Assert.Equal("v1/-1/responses", route.Template); + + var responses = Assert.IsType(controller.GetLegacyPlaceholderResponses()); + var list = Assert.IsAssignableFrom>(responses.Value); + Assert.Empty(list); + } + + [Fact] + public async Task GetResponsesAndAttendees_ReflectRsvps() + { + var db = CreateDb(); + var creator = CreateController(db, userId: 42); + var guest = CreateController(db, userId: 43); + var roomId = await SeedRoomAsync(db); + var created = await CreateEventAsync(creator, roomId); + await guest.SetResponse(created.PlayerEventId, EventResponses.Yes); + + var responses = await creator.GetResponses(created.PlayerEventId); + var list = Assert.IsType>( + Assert.IsType(responses.Result).Value); + Assert.Equal(2, list.Count); + + var attendees = await creator.GetAttendees(created.PlayerEventId); + var ids = Assert.IsType>( + Assert.IsType(attendees.Result).Value); + Assert.Contains(CreatorId, ids); + Assert.Contains(43, ids); + } + + [Fact] + public async Task DeleteEvent_CreatorCanDelete_NonCreatorCannot() + { + var db = CreateDb(); + var creator = CreateController(db, userId: 42); + var stranger = CreateController(db, userId: 99); + var roomId = await SeedRoomAsync(db); + var created = await CreateEventAsync(creator, roomId); + + var forbidden = await stranger.DeleteEvent(created.PlayerEventId); + Assert.IsType(forbidden); + Assert.NotNull(await db.PlayerEvents.FindAsync(created.PlayerEventId)); + + var ok = await creator.DeleteEvent(created.PlayerEventId); + Assert.IsType(ok); + Assert.Null(await db.PlayerEvents.FindAsync(created.PlayerEventId)); + Assert.Empty(await db.PlayerEventResponses + .Where(r => r.PlayerEventId == created.PlayerEventId).ToListAsync()); + } + + [Fact] + public async Task DeleteEvent_MissingEvent_ReturnsNotFound() + { + var db = CreateDb(); + var controller = CreateController(db); + + var result = await controller.DeleteEvent(9999); + + Assert.IsType(result); + } + + [Fact] + public async Task CreateEventV2_CreatesEventAndReturnsWrapper() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var body = + $$""" + { + "Name": "Launch Party", + "Description": "Big launch", + "RoomId": {{roomId}}, + "StartTime": "{{DateTime.UtcNow.AddHours(1):o}}", + "EndTime": "{{DateTime.UtcNow.AddHours(3):o}}", + "Accessibility": "public", + "SubRoomId": null, + "ImageName": "" + } + """; + var controller = CreateController(db, CreatorId, body); + + var result = await controller.CreateEventV2(); + + var ok = Assert.IsType(result); + var json = System.Text.Json.JsonSerializer.Serialize(ok.Value); + using var doc = System.Text.Json.JsonDocument.Parse(json); + + // The write envelope: { PlayerEvent, Result, TagModifyResult } (the shape the + // reference server answers and the client's typed deserializer requires). + Assert.Equal(0, doc.RootElement.GetProperty("Result").GetInt32()); + var tagModify = doc.RootElement.GetProperty("TagModifyResult"); + Assert.Equal(0, tagModify.GetProperty("Result").GetInt32()); + Assert.Equal(System.Text.Json.JsonValueKind.Array, tagModify.GetProperty("Tags").ValueKind); + + var created = doc.RootElement.GetProperty("PlayerEvent"); + Assert.Equal("Launch Party", created.GetProperty("Name").GetString()); + Assert.Equal(roomId, created.GetProperty("RoomId").GetInt64()); + Assert.Equal(Accessibility.Public, (Accessibility)created.GetProperty("Accessibility").GetInt32()); + + var eventId = created.GetProperty("PlayerEventId").GetInt64(); + var ev = await db.PlayerEvents.SingleAsync(e => e.PlayerEventId == eventId); + Assert.Equal(1, ev.AttendeeCount); + var rsvp = await db.PlayerEventResponses.SingleAsync(r => r.PlayerEventId == eventId); + Assert.Equal(CreatorId, rsvp.PlayerId); + Assert.Equal(EventResponses.Yes, rsvp.Type); + } + + [Fact] + public async Task CreateEventV2_UnknownRoom_ReturnsBadRequest() + { + var db = CreateDb(); + var body = """{ "Name": "Launch", "RoomId": 9999 }"""; + var controller = CreateController(db, CreatorId, body); + + var result = await controller.CreateEventV2(); + + Assert.IsType(result); + } + + [Fact] + public async Task UpdateEventV2StoresClubAndEventSettings() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var eventId = await CreateV2EventAsync(db, roomId); + var payload = """{ "ClubId": 27 }"""; + + using var updatedJson = await UpdateEventV2Async(db, eventId, "club", payload); + var playerEvent = updatedJson.RootElement.GetProperty("PlayerEvent"); + Assert.Equal(27, playerEvent.GetProperty("ClubId").GetInt64()); + Assert.False(playerEvent.GetProperty("IsMultiInstance").GetBoolean()); + + var routeAttributes = typeof(EventsAPI).GetMethod(nameof(EventsAPI.UpdateEventV2))! + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), false) + .Cast(); + Assert.Contains(routeAttributes, attribute => attribute.Template == "v2/{eventId:long}/club"); + } + + [Fact] + public async Task UpdateEventV2ClubRouteSupportsNullableClubId() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var eventId = await CreateV2EventAsync(db, roomId); + + using var assigned = await UpdateEventV2Async(db, eventId, "club", """{ "ClubId": 27 }"""); + using var cleared = await UpdateEventV2Async(db, eventId, "club", """{ "ClubId": null }"""); + + Assert.Equal(27, assigned.RootElement.GetProperty("PlayerEvent").GetProperty("ClubId").GetInt64()); + Assert.Equal(System.Text.Json.JsonValueKind.Null, + cleared.RootElement.GetProperty("PlayerEvent").GetProperty("ClubId").ValueKind); + } + + [Fact] + public async Task UpdateEventV2MultiInstanceRouteReadsItsFocusedPayload() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var eventId = await CreateV2EventAsync(db, roomId); + var body = """{ "IsMultiInstance": true, "SupportMultiInstanceRoomChat": true, "DefaultBroadcastPermissions": 3, "CanRequestBroadcastPermissions": 7 }"""; + + using var updated = await UpdateEventV2Async(db, eventId, "multiinstance", body); + var playerEvent = updated.RootElement.GetProperty("PlayerEvent"); + Assert.True(playerEvent.GetProperty("IsMultiInstance").GetBoolean()); + Assert.True(playerEvent.GetProperty("SupportMultiInstanceRoomChat").GetBoolean()); + Assert.Equal(3, playerEvent.GetProperty("DefaultBroadcastPermissions").GetInt32()); + Assert.Equal(7, playerEvent.GetProperty("CanRequestBroadcastPermissions").GetInt32()); + } + + [Fact] + public async Task UpdateEventV2StoresStructuredTagsInTheClientShape() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var eventId = await CreateV2EventAsync(db, roomId); + var body = """{ "Tags": [{ "Tag": "meetup", "Type": 1 }] }"""; + + using var updated = await UpdateEventV2Async(db, eventId, "tags", body); + var tag = updated.RootElement.GetProperty("PlayerEvent").GetProperty("Tags")[0]; + Assert.Equal("meetup", tag.GetProperty("Tag").GetString()); + Assert.Equal(1, tag.GetProperty("Type").GetInt32()); + Assert.Equal("[{\"Tag\":\"meetup\",\"Type\":1}]", + (await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == eventId)).TagsJson); + } + + [Fact] + public async Task DeleteEventV2_RemovesTheEvent() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var controller = CreateController( + db, CreatorId, $$"""{ "Name": "To Delete", "RoomId": {{roomId}} }"""); + + var create = await controller.CreateEventV2(); + long createdId; + using (var doc = System.Text.Json.JsonDocument.Parse( + System.Text.Json.JsonSerializer.Serialize(Assert.IsType(create).Value))) + createdId = doc.RootElement.GetProperty("PlayerEvent").GetProperty("PlayerEventId").GetInt64(); + + var delete = await controller.DeleteEventV2(createdId); + + Assert.IsType(delete); + Assert.Null(await db.PlayerEvents.FindAsync(createdId)); + Assert.Empty(await db.PlayerEventResponses + .Where(r => r.PlayerEventId == createdId).ToListAsync()); + } + + [Fact] + public async Task DeleteEventV2_AlreadyGoneEvent_AnswersSuccessInsteadOfNotFound() + { + // The client clears a card only when the 82 frame says so, so a list still showing an + // event whose row is gone is otherwise stuck: every retry is a 404 it can only render as + // an error and no frame is ever pushed again. A repeat delete answers success and puts + // the frame back on the wire, which is what lets the retry clear the card. + var db = CreateDb(); + var controller = CreateController(db); + + var result = await controller.DeleteEventV2(9999); + + var ok = Assert.IsType(result); + using var document = System.Text.Json.JsonDocument.Parse( + System.Text.Json.JsonSerializer.Serialize(ok.Value)); + Assert.Equal(0, document.RootElement.GetProperty("Result").GetInt32()); + Assert.Equal(9999, + document.RootElement.GetProperty("PlayerEvent").GetProperty("PlayerEventId").GetInt64()); + } + + [Fact] + public async Task DeleteEventV2_OtherPlayersEvent_IsStillForbidden() + { + // Answering "already gone" must not widen who may delete a row that is still there. + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var createdId = await CreateV2EventAsync(db, roomId); + + var stranger = CreateController(db, userId: 99); + Assert.IsType(await stranger.DeleteEventV2(createdId)); + Assert.NotNull(await db.PlayerEvents.FindAsync(createdId)); + } + + private static async Task MakeMultiInstanceAsync(RecDBContext db, long eventId) + { + using var updated = await UpdateEventV2Async( + db, eventId, "multiinstance", """{ "IsMultiInstance": true }"""); + return eventId; + } + + [Fact] + public async Task BroadcastRoomInstance_SetsAndClearsTheBroadcastingInstance() + { + // The client posts `BroadcastRoomInstanceRequest { PlayerEventId, RoomInstanceId }` + // to api/playerevents/v1/broadcast when a broadcaster goes live, and again with a + // null instance when they stop. The server never had the route, so the call 404ed. + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var eventId = await MakeMultiInstanceAsync(db, await CreateV2EventAsync(db, roomId)); + var instance = new RoomInstance { roomId = roomId, subRoomId = 1, eventId = eventId }; + db.RoomInstances.Add(instance); + await db.SaveChangesAsync(); + + var start = CreateController(db, CreatorId, + $$"""{ "PlayerEventId": {{eventId}}, "RoomInstanceId": {{instance.roomInstanceId}} }"""); + var started = Assert.IsType(await start.BroadcastRoomInstance()); + using (var json = System.Text.Json.JsonDocument.Parse( + System.Text.Json.JsonSerializer.Serialize(started.Value))) + { + Assert.Equal(0, json.RootElement.GetProperty("Result").GetInt32()); + Assert.Equal(instance.roomInstanceId, + json.RootElement.GetProperty("PlayerEvent").GetProperty("BroadcastingRoomInstanceId").GetInt64()); + } + Assert.Equal(instance.roomInstanceId, + (await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == eventId)).BroadcastingRoomInstanceId); + + var stop = CreateController(db, CreatorId, + $$"""{ "PlayerEventId": {{eventId}}, "RoomInstanceId": null }"""); + var stopped = Assert.IsType(await stop.BroadcastRoomInstance()); + using (var json = System.Text.Json.JsonDocument.Parse( + System.Text.Json.JsonSerializer.Serialize(stopped.Value))) + { + Assert.Equal(System.Text.Json.JsonValueKind.Null, + json.RootElement.GetProperty("PlayerEvent").GetProperty("BroadcastingRoomInstanceId").ValueKind); + } + Assert.Null((await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == eventId)).BroadcastingRoomInstanceId); + } + + [Fact] + public async Task BroadcastRoomInstance_NonMultiInstanceEvent_AnswersNotMultiInstance() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var eventId = await CreateV2EventAsync(db, roomId); + var controller = CreateController(db, CreatorId, + $$"""{ "PlayerEventId": {{eventId}}, "RoomInstanceId": 7 }"""); + + var result = Assert.IsType(await controller.BroadcastRoomInstance()); + + using var json = System.Text.Json.JsonDocument.Parse( + System.Text.Json.JsonSerializer.Serialize(result.Value)); + Assert.Equal(27, json.RootElement.GetProperty("Result").GetInt32()); + Assert.Equal(System.Text.Json.JsonValueKind.Null, + json.RootElement.GetProperty("PlayerEvent").GetProperty("BroadcastingRoomInstanceId").ValueKind); + Assert.Null((await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == eventId)).BroadcastingRoomInstanceId); + } + + [Fact] + public async Task BroadcastRoomInstance_NonCreator_IsForbidden() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var eventId = await MakeMultiInstanceAsync(db, await CreateV2EventAsync(db, roomId)); + var stranger = CreateController(db, userId: 99, + $$"""{ "PlayerEventId": {{eventId}}, "RoomInstanceId": 7 }"""); + + Assert.IsType(await stranger.BroadcastRoomInstance()); + } + + [Fact] + public async Task BroadcastRoomInstance_RoundTripsThroughTheReadShapes() + { + // Viewers find the broadcast from the event feed, so the persisted instance has to + // survive into every read, not just the write envelope. + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var eventId = await MakeMultiInstanceAsync(db, await CreateV2EventAsync(db, roomId)); + db.PlayerEvents.Single(x => x.PlayerEventId == eventId).BroadcastingRoomInstanceId = 1234; + await db.SaveChangesAsync(); + + var feed = await CreateController(db).GetBrowseFeed(); + var feedEvent = Assert.Single(feed, e => e.PlayerEventId == eventId); + Assert.Equal(1234, feedEvent.BroadcastingRoomInstanceId); + + var byId = await CreateController(db).GetEvent(eventId); + var ok = Assert.IsType(byId.Result); + Assert.Equal(1234, Assert.IsType(ok.Value).BroadcastingRoomInstanceId); + } + + [Fact] + public async Task DeleteEventResponse_RemovesTheCallersRsvpAndRecomputesAttendees() + { + var db = CreateDb(); + var roomId = await SeedRoomAsync(db); + var creator = CreateController(db, CreatorId); + var created = await CreateEventAsync(creator, roomId); + var guest = CreateController(db, userId: 43); + await guest.SetResponse(created.PlayerEventId, EventResponses.Yes); + Assert.Equal(2, (await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == created.PlayerEventId)).AttendeeCount); + + var delete = CreateController(db, userId: 43, + $$"""{ "PlayerEventId": {{created.PlayerEventId}} }"""); + var result = Assert.IsType(await delete.DeleteEventResponse()); + + using var json = System.Text.Json.JsonDocument.Parse( + System.Text.Json.JsonSerializer.Serialize(result.Value)); + Assert.Equal(0, json.RootElement.GetProperty("Result").GetInt32()); + Assert.False(await db.PlayerEventResponses + .AnyAsync(r => r.PlayerEventId == created.PlayerEventId && r.PlayerId == 43)); + Assert.Equal(1, (await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == created.PlayerEventId)).AttendeeCount); + } + + [Fact] + public void BroadcastAndDeleteResponseRoutes_MatchTheClientRouteTable() + { + // The 2023 client's route table (global-metadata literals #39422 and #39427) posts to + // api/playerevents/v1/broadcast and api/playerevents/v1/deleteResponse. Both routes + // used to 404, which is why event broadcasting never started. + static IEnumerable<(string Method, string Template)> Routes(string methodName) + => typeof(EventsAPI).GetMethod(methodName)! + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), false) + .Cast() + .SelectMany(a => a.HttpMethods.Select(m => (m, a.Template))); + + Assert.Contains(("POST", "v1/broadcast"), Routes(nameof(EventsAPI.BroadcastRoomInstance))); + Assert.Contains(("POST", "v1/deleteResponse"), Routes(nameof(EventsAPI.DeleteEventResponse))); + } +} diff --git a/ServerTests/FailureEnvelopeTests.cs b/ServerTests/FailureEnvelopeTests.cs new file mode 100644 index 0000000..30aae41 --- /dev/null +++ b/ServerTests/FailureEnvelopeTests.cs @@ -0,0 +1,141 @@ +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using RadicadeServer.Middleware; +using RadicadeServer.RecNet.Responses; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The failure envelope, pinned end to end: a bodyless 4xx or 5xx must leave the pipeline +/// carrying `{ "success": false, "error": "<why>" }` under its own status. +/// +/// This is the wire contract the client's error path reads. The 2023-09-06 client takes the +/// body's `error` as a string and asserts on it (`ArgumentException: Error message cannot be +/// null`), and it raises that inside the caller's task continuation - so an empty body does not +/// surface as a visible failure, it silently cancels whatever the caller was doing. Player.log +/// shows the crash behind every bodyless 404 this server used to serve: `/clubs/club/home/me` +/// and `/econ/api/subscriptionseasons/v1/seasons/current` (both deliberate 404s) and the 404 a +/// private dorm answers when the caller is not its owner (docs/2023-0831-CLIENT-SWITCH.md, +/// invite section). +/// +public class FailureEnvelopeTests +{ + private static async Task<(int Status, string ContentType, string Body)> Run(RequestDelegate next) + { + var context = new DefaultHttpContext + { + // WriteAsJsonAsync resolves its serializer options from here; a bare DefaultHttpContext + // has no service provider. + RequestServices = new ServiceCollection().BuildServiceProvider(), + }; + context.Response.Body = new MemoryStream(); + + await new FailureEnvelopeMiddleware(next).InvokeAsync(context); + + context.Response.Body.Position = 0; + using var reader = new StreamReader(context.Response.Body, Encoding.UTF8); + return (context.Response.StatusCode, context.Response.ContentType ?? string.Empty, await reader.ReadToEndAsync()); + } + + private static RequestDelegate BareStatus(int status) => context => + { + context.Response.StatusCode = status; + return Task.CompletedTask; + }; + + [Fact] + public async Task ABareNotFoundAnswersTheFailureEnvelope() + { + var (status, contentType, body) = await Run(BareStatus(StatusCodes.Status404NotFound)); + + Assert.Equal(StatusCodes.Status404NotFound, status); + Assert.StartsWith("application/json", contentType); + + var json = JsonSerializer.Deserialize(body); + Assert.False(json.GetProperty("success").GetBoolean()); + Assert.Equal("not found", json.GetProperty("error").GetString()); + } + + [Theory] + [InlineData(400, "bad request")] + [InlineData(401, "unauthorized")] + [InlineData(403, "forbidden")] + [InlineData(405, "method not allowed")] + [InlineData(413, "request too large")] + [InlineData(500, "internal server error")] + [InlineData(503, "service unavailable")] + public async Task EveryBodylessFailureStatusIsSpelled(int statusCode, string expected) + { + var (status, _, body) = await Run(BareStatus(statusCode)); + + Assert.Equal(statusCode, status); + var json = JsonSerializer.Deserialize(body); + Assert.False(json.GetProperty("success").GetBoolean()); + Assert.Equal(expected, json.GetProperty("error").GetString()); + } + + [Fact] + public async Task AnUnknownFailureStatusStillCarriesANonNullError() + { + // 418 and friends: the wording is nobody's contract, but a NULL error is, and the client + // asserts on it whatever the status was. + var (_, _, body) = await Run(BareStatus(418)); + + var json = JsonSerializer.Deserialize(body); + Assert.False(json.GetProperty("success").GetBoolean()); + Assert.False(string.IsNullOrWhiteSpace(json.GetProperty("error").GetString())); + } + + [Fact] + public async Task AnAnswerThatAlreadyHasABodyIsLeftAlone() + { + var (status, _, body) = await Run(async context => + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + await context.Response.WriteAsJsonAsync(new Failure("No such room")); + }); + + Assert.Equal(StatusCodes.Status404NotFound, status); + Assert.Equal("No such room", JsonSerializer.Deserialize(body).GetProperty("error").GetString()); + } + + [Fact] + public async Task SuccessfulAndEmptyAnswersAreLeftAlone() + { + var (ok, okContentType, okBody) = await Run(BareStatus(StatusCodes.Status200OK)); + Assert.Equal(StatusCodes.Status200OK, ok); + Assert.Empty(okBody); + Assert.Empty(okContentType); + + var (noContent, _, noContentBody) = await Run(BareStatus(StatusCodes.Status204NoContent)); + Assert.Equal(StatusCodes.Status204NoContent, noContent); + Assert.Empty(noContentBody); + } + + [Fact] + public async Task UpgradesAndRedirectsAreLeftAlone() + { + // 101 is a WebSocket taking over the response: writing into it would corrupt the socket, + // and the hub is on this pipeline. + var (upgrade, _, upgradeBody) = await Run(BareStatus(101)); + Assert.Equal(101, upgrade); + Assert.Empty(upgradeBody); + + var (redirect, _, redirectBody) = await Run(BareStatus(StatusCodes.Status302Found)); + Assert.Equal(StatusCodes.Status302Found, redirect); + Assert.Empty(redirectBody); + } + + [Fact] + public void TheFailureEnvelopeNeverSpellsAnEmptyError() + { + // `error` is what the client turns into an exception message; blank is as unusable as null. + var json = JsonSerializer.Serialize(new Failure("")); + Assert.Equal("request failed", JsonSerializer.Deserialize(json).GetProperty("error").GetString()); + + Assert.Equal("not found", FailureEnvelopeMiddleware.Why(StatusCodes.Status404NotFound)); + } +} diff --git a/ServerTests/Fixtures/split-tests-20230301.json b/ServerTests/Fixtures/split-tests-20230301.json new file mode 100644 index 0000000..d5aa5a8 --- /dev/null +++ b/ServerTests/Fixtures/split-tests-20230301.json @@ -0,0 +1,86 @@ +{ + "Example_Test_2020_06_06": [ + "On", + "Off" + ], + "RoomRecommendations_2020_05_06": [ + "On", + "Off" + ], + "RoomRecommendationsType_2020_08_14": [ + "June22", + "Aug14JuniorSpecificModel", + "Aug14JuniorAccessibleModelForAll", + "Aug14MinVisitors350", + "Aug14MinVisitors3500", + "Aug14MinVisitors35000" + ], + "GottaGoFastNUX_2020_09_04": [ + "Off", + "On" + ], + "PlayMenuRRUI_HotRoomsCarousel_2021_02_09": [ + "Off", + "On" + ], + "PlayMenuRRUI_CarouselOrdering_2021_02_09": [ + "PlayMenuCarouselOrdering_1", + "PlayMenuCarouselOrdering_2", + "PlayMenuCarouselOrdering_3" + ], + "MutualFriends_2021_04_22": [ + "Off", + "On" + ], + "CurrentContestWinnersCarouselVisibility_2021_04_15": [ + "Off", + "On" + ], + "PlayMenuRRUI_DefaultTab_2021_05_13": [ + "Highlight", + "RRO", + "Hot" + ], + "ProfileOnClick_2021_04_30": [ + "Off", + "On" + ], + "DefaultNewPlayersToPlayMenu_2021_05_24": [ + "Off", + "On" + ], + "ParallelNetworkingInitialRoomLoad_2021_06_28": [ + "SequentialLoad", + "ParallelLoad" + ], + "PlayMenuRRUI_TopCreatorsCarousel_2021_09_01": [ + "Off", + "On" + ], + "FriendImportLogin_2021_09_23": [ + "Off", + "On" + ], + "RoomSimilarities_2021_10_29": [ + "On", + "Off" + ], + "PlayMenuBanner_2021_11_01": [ + "Off", + "On" + ], + "PlayMenuCarouselOrdering_2021_11_01": [ + "PlayMenuCarouselOrdering_1", + "PlayMenuCarouselOrdering_2", + "PlayMenuCarouselOrdering_3" + ], + "RudderStackAnalyticsDestinations_2022_02_10": [ + "Disabled", + "RudderStackOnly", + "Both" + ], + "ObjectModel_EnableForRoom_2022_07_01": [ + "Disallowed", + "Allowed" + ] +} diff --git a/ServerTests/FriendotronTests.cs b/ServerTests/FriendotronTests.cs new file mode 100644 index 0000000..9494219 --- /dev/null +++ b/ServerTests/FriendotronTests.cs @@ -0,0 +1,313 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Compatibility; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The Friendotron: the free gift-for-friends button in the Rec Center +/// (POST /econ/api/freegifts/v1/sendmultiple). +/// +/// The route used to answer a success flag and nothing else - no box, no item - so a send looked +/// like it worked and no friend ever received anything. A send now hands every recipient a gift +/// box holding a random one to three star consumable, labelled with the sender's message or +/// "A gift for you <3" when they left it empty, and a player may only send once a day. +/// +public class FriendotronTests +{ + private const long SenderId = 61; + private const long FirstFriendId = 62; + private const long SecondFriendId = 63; + + /// + /// The boxes are rolled from the catalogue the server ships, so these tests read the same + /// files it does - once per run rather than once per test, because LoadFiles rewrites the + /// process-wide catalogue every other catalog test in this suite reads. + /// + private static readonly object CatalogLock = new(); + private static bool catalogLoaded; + + private static void LoadShippedCatalog() + { + lock (CatalogLock) + { + if (catalogLoaded) return; + DBConfig.LoadFiles(); + catalogLoaded = true; + } + } + + private static async Task<(SqliteConnection Connection, RecDBContext Db)> OpenDbAsync(params long[] accountIds) + { + var connection = new SqliteConnection("DataSource=:memory:"); + await connection.OpenAsync(); + var options = TestDbOptions.Create().UseSqlite(connection).Options; + var db = new RecDBContext(options); + await db.Database.EnsureCreatedAsync(); + + foreach (var accountId in accountIds) + db.Players.Add(new PlayerData { AccountId = accountId, Hashword = "x" }); + await db.SaveChangesAsync(); + + return (connection, db); + } + + private static void SetUser(ControllerBase controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetJsonBody(ControllerBase controller, string body) + { + var request = controller.HttpContext.Request; + request.ContentType = "application/json"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + request.ContentLength = request.Body.Length; + } + + /// The client's request DTO: the recipients, the lid message, and the gift context. + private static string SendBody(IEnumerable recipients, string? message) => + new JsonObject + { + ["ToPlayerIds"] = new JsonArray(recipients.Select(id => (JsonNode)JsonValue.Create(id)!).ToArray()), + ["Message"] = message ?? string.Empty, + ["GiftContext"] = (int)GiftContext.Friendotron_Gift + }.ToJsonString(); + + private static async Task<(bool Success, string Error)> SendAsync( + CompatibilityAPI api, long senderId, IEnumerable recipients, string? message = null) + { + SetUser(api, senderId); + SetJsonBody(api, SendBody(recipients, message)); + + var result = Assert.IsType(await api.SendFreeGifts()); + using var document = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)); + return ( + document.RootElement.GetProperty("ALOCDGMHFKA").GetBoolean(), + document.RootElement.GetProperty("FBILGKFKFJE").GetString() ?? string.Empty); + } + + private static HashSet ShippedConsumableTokens() => + ConsumableCatalog.ParseItems(DBConfig.DefaultConsumableItems) + .Select(item => ConsumableCatalog.ToClientDesc(item.ConsumableItemDesc)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + [Fact] + public async Task ASendPutsABoxInEveryRecipientsHands() + { + LoadShippedCatalog(); + var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId, SecondFriendId); + await using var disposeConnection = connection; + await using var disposeDb = db; + + var tokens = ShippedConsumableTokens(); + Assert.NotEmpty(tokens); + + var (success, error) = await SendAsync(new CompatibilityAPI(db), SenderId, new[] { FirstFriendId, SecondFriendId }); + + Assert.True(success, error); + Assert.Equal(string.Empty, error); + + foreach (var recipient in new[] { FirstFriendId, SecondFriendId }) + { + var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, recipient)); + Assert.Equal(SenderId, box.FromPlayerId); + Assert.Equal(GiftContext.Friendotron_Gift, box.Context); + // One to three stars on the box, and a real catalogue consumable inside it. + Assert.Contains(box.Rarity, FriendotronService.StarTiers); + Assert.Contains(box.ConsumableItemDesc, tokens); + Assert.Equal("A gift for you <3", box.Tooltip); + } + + // Nobody else was sent anything. + Assert.Empty(await StorefrontShared.GetPendingGiftsAsync(db, SenderId)); + } + + [Fact] + public async Task OpeningTheBoxPutsTheConsumableInTheRecipientsInventory() + { + LoadShippedCatalog(); + var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId); + await using var disposeConnection = connection; + await using var disposeDb = db; + + Assert.True((await SendAsync(new CompatibilityAPI(db), SenderId, new[] { FirstFriendId })).Success); + var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId)); + + // The claim route's own sequence: grant the gift, then retire the pending box. + var strategy = db.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + await StorefrontShared.GrantGiftAsync(db, FirstFriendId, box); + await StorefrontShared.RemovePendingGiftAsync(db, FirstFriendId, box.GiftDropId); + await tx.CommitAsync(); + }); + + var unlocks = await db.Unlocks.AsNoTracking().FirstAsync(x => x.AccountId == FirstFriendId); + var owned = unlocks.GetConsumables() ?? new List(); + Assert.Contains(owned, item => item.ConsumableItemDesc == box.ConsumableItemDesc); + Assert.Empty(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId)); + } + + [Fact] + public async Task TheSendersMessageRidesAlongOnTheBox() + { + LoadShippedCatalog(); + var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId); + await using var disposeConnection = connection; + await using var disposeDb = db; + + var (success, error) = await SendAsync(new CompatibilityAPI(db), SenderId, new[] { FirstFriendId }, "Monkey-Jelly"); + + Assert.True(success, error); + var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId)); + Assert.Equal("Monkey-Jelly", box.Tooltip); + } + + [Fact] + public async Task ASecondSendWithinADayIsRefused() + { + LoadShippedCatalog(); + var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId); + await using var disposeConnection = connection; + await using var disposeDb = db; + + var api = new CompatibilityAPI(db); + Assert.True((await SendAsync(api, SenderId, new[] { FirstFriendId })).Success); + + var (success, error) = await SendAsync(api, SenderId, new[] { FirstFriendId }); + + Assert.False(success); + Assert.Contains("another friend box", error); + // The refusal did not slip a second box in behind it. + Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId)); + } + + [Fact] + public async Task ASendIsAllowedAgainOnceTheDayIsUp() + { + LoadShippedCatalog(); + var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId); + await using var disposeConnection = connection; + await using var disposeDb = db; + + var api = new CompatibilityAPI(db); + Assert.True((await SendAsync(api, SenderId, new[] { FirstFriendId })).Success); + + // Stand the stamp back a day and a bit: the cooldown is 24 hours, not a session or a lock. + var key = FriendotronService.CooldownKey(SenderId); + var stamp = await db.ServerConfig.FirstAsync(x => x.Key == key); + stamp.Value = DateTime.UtcNow.AddHours(-25).ToString("O"); + await db.SaveChangesAsync(); + + Assert.True((await SendAsync(api, SenderId, new[] { FirstFriendId })).Success); + Assert.Equal(2, (await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId)).Count); + } + + [Fact] + public async Task SelfGiftingFollowsTheConfigFlag() + { + LoadShippedCatalog(); + var (connection, db) = await OpenDbAsync(SenderId); + await using var disposeConnection = connection; + await using var disposeDb = db; + + var api = new CompatibilityAPI(db); + + // The seeded config allows self gifting, so a box to yourself is a box. + Assert.True((await SendAsync(api, SenderId, new[] { SenderId })).Success); + Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, SenderId)); + + db.GameConfig.Add(new ConfigVal("Friendotron_AllowSelfGifting", "false")); + var stamp = await db.ServerConfig.FirstAsync(x => x.Key == FriendotronService.CooldownKey(SenderId)); + stamp.Value = DateTime.UtcNow.AddHours(-25).ToString("O"); + await db.SaveChangesAsync(); + + var (success, error) = await SendAsync(api, SenderId, new[] { SenderId }); + + Assert.False(success); + Assert.Contains("yourself", error); + Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, SenderId)); + } + + [Fact] + public async Task AnUnknownRecipientIsRefusedAndNothingIsSent() + { + LoadShippedCatalog(); + var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId); + await using var disposeConnection = connection; + await using var disposeDb = db; + + var (success, error) = await SendAsync(new CompatibilityAPI(db), SenderId, new[] { FirstFriendId, 999 }); + + Assert.False(success); + Assert.Contains("999", error); + // All or nothing: the friend who does exist gets no box from a send that was refused. + Assert.Empty(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId)); + Assert.Empty(db.ServerConfig); + } + + [Fact] + public async Task ASendWithNoRecipientsIsRefused() + { + LoadShippedCatalog(); + var (connection, db) = await OpenDbAsync(SenderId); + await using var disposeConnection = connection; + await using var disposeDb = db; + + var (success, error) = await SendAsync(new CompatibilityAPI(db), SenderId, Array.Empty()); + + Assert.False(success); + Assert.Contains("recipients", error); + } + + [Fact] + public void ARolledBoxIsAlwaysAOneTwoOrThreeStarConsumable() + { + LoadShippedCatalog(); + var tokens = ShippedConsumableTokens(); + var random = new Random(20261012); + + var tiers = new HashSet(); + for (var roll = 0; roll < 200; roll++) + { + var item = FriendotronService.RollConsumable(DBConfig.DefaultConsumableItems, out var starTier, random); + + Assert.NotNull(item); + Assert.Contains(starTier, FriendotronService.StarTiers); + Assert.Contains(ConsumableCatalog.ToClientDesc(item!.ConsumableItemDesc), tokens); + tiers.Add(starTier); + } + + // All three star levels are reachable, not just the tier every shipped consumable sits in. + Assert.Equal(FriendotronService.StarTiers.OrderBy(x => x).ToArray(), tiers.OrderBy(x => x).ToArray()); + } + + [Fact] + public void TheDefaultLidMessageIsUsedOnlyWhenTheSenderTypedNothing() + { + Assert.Equal("A gift for you <3", FriendotronService.MessageOr(null)); + Assert.Equal("A gift for you <3", FriendotronService.MessageOr(" ")); + Assert.Equal("Monkey-Jelly", FriendotronService.MessageOr(" Monkey-Jelly ")); + } +} diff --git a/ServerTests/GameConfigSeederTests.cs b/ServerTests/GameConfigSeederTests.cs new file mode 100644 index 0000000..a8ecad2 --- /dev/null +++ b/ServerTests/GameConfigSeederTests.cs @@ -0,0 +1,121 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The startup GameConfig passes share one rule: a row an earlier pass added to the SAME +/// DbContext is not in the database yet, so a plain query for its key answers null - and the +/// "make sure this key exists" step that follows then adds a SECOND instance of it. EF +/// refuses that the moment the duplicate is tracked, and the refusal is a crash, not a +/// warning. It is how the first start against an empty database died: +/// +/// Unhandled exception. System.InvalidOperationException: The instance of entity type +/// 'ConfigVal' cannot be tracked because another instance with the same key value for +/// {'Key'} is already being tracked. +/// at Program.<<Main>$>g__EnsureSplitTestHardOverrides|0_32(RecDBContext dbContext) +/// +/// `EnsureRequiredGameConfigs` upserts `splitTestHardOverrides` and the ensure pass two lines +/// later re-added it. GameConfigSeeder is the tracked-aware lookup that closes the gap; these +/// cases hold it in place, and the same trap was armed behind it for +/// `splitTestsAvailable`/`splitTestSegmentProbabilities` (RepairSplitTestConfigs) and the +/// `UGC.Room2*` batch (EnableBetaCreativeToolsConfigs). +/// +public class GameConfigSeederTests +{ + private const string HardOverridesKey = "splitTestHardOverrides"; + + /// Shared so two contexts can mean the same store, the way two passes of one start do. + private static readonly InMemoryDatabaseRoot Root = new(); + + private static RecDBContext CreateDb(string name) => new( + TestDbOptions.Create() + .UseInMemoryDatabase(name, Root) + .Options); + + private static RecDBContext CreateDb() => CreateDb(Guid.NewGuid().ToString()); + + [Fact] + public async Task AddingTheSameKeyAfterAQuery_IsTheConflictTheSeederExistsToPrevent() + { + using var db = CreateDb(); + await db.GameConfig.AddAsync(new ConfigVal(HardOverridesKey, "A=1")); + + // The trap: a row this context added but has not saved is invisible to a query. + Assert.Null(await db.GameConfig.FirstOrDefaultAsync(x => x.Key == HardOverridesKey)); + + // And the duplicate a plain lookup would then add is fatal, not cosmetic. + await Assert.ThrowsAsync(async () => + await db.GameConfig.AddAsync(new ConfigVal(HardOverridesKey, "A=1;B=2"))); + } + + [Fact] + public async Task FindAsync_SeesARowThisContextAdded_WithoutSavingIt() + { + using var db = CreateDb(); + await db.GameConfig.AddAsync(new ConfigVal(HardOverridesKey, "A=1")); + + var found = await GameConfigSeeder.FindAsync(db, HardOverridesKey); + + Assert.NotNull(found); + Assert.Equal("A=1", found!.Value); + Assert.Same(db.GameConfig.Local.Single(), found); + } + + [Fact] + public async Task UpsertAsync_TwiceInOnePass_TracksOneRowAndKeepsBothWrites() + { + // The literal shape of the crash: one pass upserts the key, the next looks it up and + // (before the seeder) would have added a second ConfigVal carrying the same key. + using var db = CreateDb(); + await GameConfigSeeder.UpsertAsync(db, HardOverridesKey, "A=1"); + var existing = await GameConfigSeeder.FindAsync(db, HardOverridesKey); + + Assert.NotNull(existing); + existing!.Value = "A=1;B=2"; + await db.SaveChangesAsync(); + + Assert.Single(db.ChangeTracker.Entries()); + Assert.Equal("A=1;B=2", (await db.GameConfig.SingleAsync()).Value); + } + + [Fact] + public async Task FindAsync_ReadsARowAnotherContextStored() + { + var name = Guid.NewGuid().ToString(); + using (var seeding = CreateDb(name)) + { + await GameConfigSeeder.UpsertAsync(seeding, HardOverridesKey, "A=1"); + await seeding.SaveChangesAsync(); + } + + using var db = CreateDb(name); + Assert.Null(db.GameConfig.Local.FirstOrDefault()); // nothing tracked yet + + var found = await GameConfigSeeder.FindAsync(db, HardOverridesKey); + + Assert.NotNull(found); + Assert.Equal("A=1", found!.Value); + } + + [Fact] + public async Task UpsertAsync_FillsABlankStoredValue_ButLeavesAnEditedOneAlone() + { + using var db = CreateDb(); + + await GameConfigSeeder.UpsertAsync(db, "splitTestSoftOverrides", "A=1"); + await GameConfigSeeder.UpsertAsync(db, "splitTestSoftOverrides", "B=2"); + Assert.Equal("A=1", (await GameConfigSeeder.FindAsync(db, "splitTestSoftOverrides"))!.Value); + + await GameConfigSeeder.UpsertAsync(db, "UGC.MaxChipsVisible", ""); + await GameConfigSeeder.UpsertAsync(db, "UGC.MaxChipsVisible", "5000"); + Assert.Equal("5000", (await GameConfigSeeder.FindAsync(db, "UGC.MaxChipsVisible"))!.Value); + + await GameConfigSeeder.UpsertAsync(db, "UGC.MaxChipsVisible", "9000"); + Assert.Equal("5000", (await GameConfigSeeder.FindAsync(db, "UGC.MaxChipsVisible"))!.Value); + } +} diff --git a/ServerTests/GameRewardFlowTests.cs b/ServerTests/GameRewardFlowTests.cs new file mode 100644 index 0000000..17f866f --- /dev/null +++ b/ServerTests/GameRewardFlowTests.cs @@ -0,0 +1,706 @@ +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.API.GameRewards.V1; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RecNet.Storefront; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// What a completion is filed as, and what the player is handed when they choose. +/// +/// Three things were wrong together: the first activity of the day was not treated as a completion +/// at all, so it paid tokens; a game whose own pool could not pay was paid tokens and nothing else; +/// and completing a selection credited the item without telling the client, so the selection could +/// not be replayed and the reward only showed after a relog. +/// +public class GameRewardFlowTests +{ + private const long PlayerId = 77; + + private static RecDBContext NewDb() => + new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + /// + /// The same store, with the one console warning the consume route would otherwise raise turned + /// off: it opens a transaction, which the in-memory provider ignores - and reports as an error - + /// rather than supporting. The route's own behaviour is what is under test, not that warning. + /// + private static RecDBContext NewTransactionalDb() => + new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ConfigureWarnings(warnings => warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options); + + private static GameRewardsAPI AsPlayer(RecDBContext db, long playerId = PlayerId) => new(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, playerId.ToString())], "test")) + } + } + }; + + /// + /// Points the player's last visited room at a game room. The activity a completion is filed under + /// is named by the room the player was in, and that is the last room the server recorded a visit + /// to - there is no other way for a test to say which game was played. + /// + private static async Task SeedLastVisitedRoomAsync(RecDBContext db, string roomName) + { + var room = new RoomData { Name = roomName }; + db.Rooms.Add(room); + await db.SaveChangesAsync(); + db.RoomInteractions.Add(new PlayerInteractionDB { PlayerId = PlayerId, RoomId = room.RoomId, Visited = true }); + await db.SaveChangesAsync(); + } + + private static async Task RequestRewardAsync(GameRewardsAPI api, RewardType type, GiftContext? giftContext = null) + { + await RequestOnlyAsync(api, type, giftContext); + return await LatestPendingAsync(api); + } + + /// + /// Files a reward request and asserts only that it was accepted. + /// + /// A quest's reward does not reach /pending: it is handed over as the run's own box with no + /// selection screen, so the tests that check what a quest pays read the player's gifts instead. + /// + private static async Task RequestOnlyAsync(GameRewardsAPI api, RewardType type, GiftContext? giftContext = null) + { + var body = Encoding.UTF8.GetBytes(giftContext.HasValue + ? $"{{\"rewardType\":{(int)type},\"giftContext\":{(int)giftContext.Value}}}" + : $"{{\"rewardType\":{(int)type}}}"); + api.ControllerContext.HttpContext.Request.Body = new MemoryStream(body); + api.ControllerContext.HttpContext.Request.ContentType = "application/json"; + + var result = await api.RequestReward(); + Assert.True(result.Result is OkObjectResult, $"{(result.Result as ObjectResult)?.StatusCode.ToString() ?? "no result"}"); + + // The route answers the shared success object, not the reward: the client's own wrapper for it + // parses one of those, and the reward reaches the watch through the box the route hands over. + var ok = Assert.IsType(((OkObjectResult)result.Result!).Value); + Assert.True(ok.Success); + } + + /// + /// Requests a quest completion and returns the box it paid. + /// + /// A quest asks the player to choose nothing, so the reward never reaches /pending as a selection: + /// it is filed as the run's own box for the watch to open. These tests read that box, and the empty + /// /pending is asserted here because a quest that left a selection behind would be a quest offering + /// a choice it does not have. + /// + private static async Task RequestAndTakeQuestBoxAsync( + GameRewardsAPI api, RecDBContext db, GiftContext claimed, long playerId = PlayerId) + { + await RequestOnlyAsync(api, RewardType.PostGameActivity, claimed); + Assert.Empty(await api.GetPendingGameRewards()); + + var boxes = await StorefrontShared.GetPendingGiftsAsync(db, playerId); + return Assert.Single(boxes); + } + + private static async Task LatestPendingAsync(GameRewardsAPI api) + { + var pending = await api.GetPendingGameRewards(); + Assert.NotEmpty(pending); + return pending[^1]; + } + + private static GameRewardsAPI AsSelecting(RecDBContext db, long selectionId, long giftDropId) + { + var api = AsPlayer(db); + var body = Encoding.UTF8.GetBytes( + $"{{\"rewardSelectionId\":{selectionId},\"giftDropId\":{giftDropId}}}"); + api.ControllerContext.HttpContext.Request.Body = new MemoryStream(body); + api.ControllerContext.HttpContext.Request.ContentType = "application/json"; + return api; + } + + // --------------------------------------------------------------------------------- + // Which type a completion is filed under + // --------------------------------------------------------------------------------- + + private static readonly DateTime Monday = new(2026, 10, 5, 12, 0, 0, DateTimeKind.Utc); + + [Fact] + public void OnlyTheFirstCompletionOfTheDayIsTheFirstActivityOfTheDay() + { + var ledger = RewardDay.NewDay(Monday); + + // The client asks for FirstActivityOfDay on every completion. Honouring that claim every time + // is what printed "First Game of the Day" on every reward all day. + Assert.Equal(RewardType.FirstActivityOfDay, RewardDay.CompletionType(ledger, RewardType.FirstActivityOfDay)); + Assert.True(RewardDay.TryTakeFirstActivity(ledger)); + Assert.Equal(RewardType.PostGameActivity, RewardDay.CompletionType(ledger, RewardType.FirstActivityOfDay)); + Assert.False(RewardDay.TryTakeFirstActivity(ledger)); + + // The first box of the day is its own: it does not eat into the twelve the games pay. + Assert.Equal(0, ledger.ActivityBoxes); + } + + [Fact] + public void AFirstCompletionIsTheFirstActivityOfTheDayEvenWhenTheClientCallsItAPostGameReward() + { + var ledger = RewardDay.NewDay(Monday); + + Assert.Equal(RewardType.FirstActivityOfDay, RewardDay.CompletionType(ledger, RewardType.PostGameActivity)); + RewardDay.TryTakeFirstActivity(ledger); + Assert.Equal(RewardType.PostGameActivity, RewardDay.CompletionType(ledger, RewardType.PostGameActivity)); + } + + [Fact] + public void AQuestKeepsItsOwnTypeAndStillTakesTheDay() + { + var ledger = RewardDay.NewDay(Monday); + + Assert.Equal(RewardType.PostQuestActivity, RewardDay.CompletionType(ledger, RewardType.PostQuestActivity)); + RewardDay.TryTakeFirstActivity(ledger); + // The quest was the day's activity, so the game played after it is an ordinary game reward. + Assert.Equal(RewardType.PostGameActivity, RewardDay.CompletionType(ledger, RewardType.FirstActivityOfDay)); + } + + /// + /// The budget: one first-of-the-day box, twelve game boxes, and a daily-challenge box of its own. + /// + [Fact] + public void TheDayPaysFourteenBoxesAndNoMore() + { + var ledger = RewardDay.NewDay(Monday); + + Assert.True(RewardDay.TryTakeFirstActivity(ledger)); + Assert.False(RewardDay.TryTakeFirstActivity(ledger)); + + for (var i = 0; i < RewardDay.ActivityBoxesPerDay; i++) + Assert.True(RewardDay.TryTakeActivityBox(ledger), $"activity box {i + 1} was refused"); + Assert.False(RewardDay.TryTakeActivityBox(ledger)); + + Assert.True(RewardDay.TryTakeDailyChallengeBox(ledger)); + Assert.False(RewardDay.TryTakeDailyChallengeBox(ledger)); + + Assert.Equal(14, RewardDay.BoxesPerDay); + } + + /// + /// The ledger is the player's for the day, so a day rolls over - and a second day starts its own + /// count rather than inheriting yesterday's. + /// + [Fact] + public void ANewDayStartsANewLedgerAndTheSameDayDoesNot() + { + var json = new ProgressionJson(); + + var monday = RewardDay.For(json, Monday); + Assert.True(RewardDay.TryTakeFirstActivity(monday)); + RewardDay.TryTakeActivityBox(monday); + + // The same day is the same ledger, whatever moment it is asked at. + Assert.Same(monday, RewardDay.For(json, Monday.AddHours(6))); + Assert.True(monday.FirstActivityPaid); + Assert.Equal(1, monday.ActivityBoxes); + + // A new day is a new count. + var tuesday = RewardDay.For(json, Monday.AddDays(1)); + Assert.NotSame(monday, tuesday); + Assert.False(tuesday.FirstActivityPaid); + Assert.Equal(0, tuesday.ActivityBoxes); + } + + // --------------------------------------------------------------------------------- + // What a completion is filed as + // --------------------------------------------------------------------------------- + + [Fact] + public async Task TheFirstGameOfTheDayIsFiledAsOneAndTheSecondIsNot() + { + await using var db = NewDb(); + await SeedLastVisitedRoomAsync(db, "RecRoyaleSolos"); + var api = AsPlayer(db); + + var first = await RequestRewardAsync(api, RewardType.FirstActivityOfDay); + var second = await RequestRewardAsync(api, RewardType.FirstActivityOfDay); + + Assert.Equal(RewardType.FirstActivityOfDay, first.RewardType); + Assert.Equal(RewardType.PostGameActivity, second.RewardType); + } + + [Fact] + public async Task ARewardThatIsNotACompletionStillPaysTokens() + { + await using var db = NewDb(); + var api = AsPlayer(db); + + var reward = await RequestRewardAsync(api, RewardType.Levelup); + + Assert.Equal(RewardType.Levelup, reward.RewardType); + Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 }, + box => Assert.Equal(CurrencyType.RecCenterTokens, box.CurrencyType)); + // The boxes are titled with the amount they hold rather than all being "Rec Center Tokens", + // which made a 100 and a 300 read as the same reward twice. + Assert.Equal("100 Tokens", reward.GiftDrop1.FriendlyName); + Assert.Equal("200 Tokens", reward.GiftDrop2.FriendlyName); + Assert.Equal("300 Tokens", reward.GiftDrop3.FriendlyName); + } + + // --------------------------------------------------------------------------------- + // The day's boxes: one first of the day, twelve games, one daily challenge + // --------------------------------------------------------------------------------- + + /// + /// A completion that pays no box, because the day's are spent. The route still answers success: + /// the client asked for a game reward and the request was accepted, the player is simply not + /// owed another box today. + /// + private static async Task RequestRewardWithNoBoxAsync(GameRewardsAPI api, RewardType type) + { + var before = (await api.GetPendingGameRewards()).Count; + var body = Encoding.UTF8.GetBytes($"{{\"rewardType\":{(int)type}}}"); + api.ControllerContext.HttpContext.Request.Body = new MemoryStream(body); + api.ControllerContext.HttpContext.Request.ContentType = "application/json"; + + var result = await api.RequestReward(); + Assert.True(Assert.IsType(((OkObjectResult)result.Result!).Value).Success); + Assert.Equal(before, (await api.GetPendingGameRewards()).Count); + } + + [Fact] + public async Task TheDayStopsPayingAfterItsFirstBoxAndTwelveGames() + { + await using var db = NewDb(); + await SeedLastVisitedRoomAsync(db, "RecRoyaleSolos"); + var api = AsPlayer(db); + + var first = await RequestRewardAsync(api, RewardType.FirstActivityOfDay); + Assert.Equal(RewardType.FirstActivityOfDay, first.RewardType); + + for (var game = 0; game < RewardDay.ActivityBoxesPerDay; game++) + { + var box = await RequestRewardAsync(api, RewardType.FirstActivityOfDay); + Assert.Equal(RewardType.PostGameActivity, box.RewardType); + } + + // Thirteen boxes: the first of the day and the twelve games. + Assert.Equal(RewardDay.BoxesPerDay - 1, (await api.GetPendingGameRewards()).Count); + + // The fourteenth completion of the day is answered, and pays nothing. + await RequestRewardWithNoBoxAsync(api, RewardType.FirstActivityOfDay); + } + + [Fact] + public async Task TheFirstGameOfTheDayIsNotPaidAgainAfterARestart() + { + await using var db = NewDb(); + await SeedLastVisitedRoomAsync(db, "RecRoyaleSolos"); + + // A second API over the same progression is what a server restart looks like from here. The + // day used to be counted in memory, so the first completion after a restart was the first of + // the day again and the player was handed that box a second time. + var beforeRestart = await RequestRewardAsync(AsPlayer(db), RewardType.FirstActivityOfDay); + Assert.Equal(RewardType.FirstActivityOfDay, beforeRestart.RewardType); + Assert.Equal("First Game of the Day", beforeRestart.Message); + + var afterRestart = await RequestRewardAsync(AsPlayer(db), RewardType.FirstActivityOfDay); + Assert.Equal(RewardType.PostGameActivity, afterRestart.RewardType); + + // And the title is the server's. The client sends "First Game of the Day" as the message on + // every completion, which is why the screen still read that on the day's fifth box. + Assert.Equal("Game Reward", afterRestart.Message); + } + + [Fact] + public async Task TheDailyChallengePaysOneBoxADay() + { + await using var db = NewDb(); + var api = AsPlayer(db); + + var reward = await RequestRewardAsync(api, RewardType.DailyChallengesComplete); + + Assert.Equal(RewardType.DailyChallengesComplete, reward.RewardType); + // The daily challenge is a box of its own, filed under the context the client's own enum + // gives it: it is not a game and it names no room. + Assert.Equal(GiftContext.Daily_Challenge_Complete, reward.GiftContext); + + // And one a day - the same completion reported again pays nothing. + await RequestRewardWithNoBoxAsync(api, RewardType.DailyChallengesComplete); + } + + // --------------------------------------------------------------------------------- + // Which activity the reward is answered from + // --------------------------------------------------------------------------------- + + /// + /// The reward request arrives after the player has left the game: a finished Stunt Runner run's + /// request came in with the player already back in the Rec Center, so the last visited room was + /// the Rec Center, which pays nothing, and every completion was paid the token package. The + /// context the client claims is what survives the player moving on, and it is what decides the + /// activity when the room they last visited does not pay it. + /// + [Fact] + public async Task TheClaimedContextDecidesTheActivityWhenTheLastRoomIsNotOne() + { + await using var db = NewDb(); + await SeedLastVisitedRoomAsync(db, "RecCenter"); + var api = AsPlayer(db); + + var box = await RequestAndTakeQuestBoxAsync(api, db, GiftContext.StuntRunner_TheMainEvent_C); + + // Before the claim was consulted the Rec Center named the activity, which is none, and the + // reward was the token package filed under GameRewards_Tokens. A quest asks the player to + // choose nothing, so what the completion pays is the run's own box - not a selection. + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, box.Context); + Assert.NotEqual(GiftContext.GameRewards_Tokens, box.Context); + } + + [Fact] + public async Task ALasertagRewardIsPaidTicketsEvenWhenTheLastRoomIsNotOne() + { + await using var db = NewDb(); + await SeedLastVisitedRoomAsync(db, "RecCenter"); + var api = AsPlayer(db); + + // Tickets are the one reward that needs no catalogue to tell apart from the token package, + // so this is what shows the claim took over the room without depending on the shipped items. + var reward = await RequestRewardAsync(api, RewardType.PostGameActivity, GiftContext.Lasertag); + + Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 }, + box => Assert.Equal(CurrencyType.LaserTagTickets, box.CurrencyType)); + Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 }, + box => Assert.Equal("Laser Tag Tickets", box.FriendlyName)); + } + + [Fact] + public async Task AClaimTheServerDoesNotKnowStillLeavesTheRoomInCharge() + { + await using var db = NewDb(); + await SeedLastVisitedRoomAsync(db, "RecCenter"); + var api = AsPlayer(db); + + var reward = await RequestRewardAsync(api, RewardType.PostGameActivity, GiftContext.Default); + + // A claim that names no activity leaves the room in charge, and the room pays nothing, so the + // reward is the token package - filed under the claim it was asked for, which names no game. + Assert.Null(RroRewardPools.RoomForContext(reward.GiftContext)); + Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 }, + box => Assert.Equal(CurrencyType.RecCenterTokens, box.CurrencyType)); + // Each box is titled with its own amount, not all with "Rec Center Tokens". + Assert.Equal("50 Tokens", reward.GiftDrop1.FriendlyName); + Assert.Equal("100 Tokens", reward.GiftDrop2.FriendlyName); + Assert.Equal("150 Tokens", reward.GiftDrop3.FriendlyName); + } + + /// + /// The quest's item is what the whole collection flow leaves in the player's inventory. + /// + /// This is the half that was broken. The selection was right and so was the box it held, and then + /// the item became a box of Rec Center tokens at the moment it was collected, because the collect + /// - a generate naming the reward's context, exactly as the client sends it - asked the server to + /// invent a gift instead of handing over the one the reward was holding. The last assertion is the + /// one that matters: the quest's own descriptor and name, in the player's own unlocks. + /// + [Fact] + public async Task AQuestsItemIsWhatTheGenerateAndConsumeFlowLeavesInTheInventory() + { + await using var db = NewTransactionalDb(); + + var item = new Gift + { + GiftDropId = 6104, + FriendlyName = "Runner Harness (Green)", + Tooltip = "Stunt Runner: The Main Event", + AvatarItemDesc = "d2d3264b-fbf6-40b5-9f10-c85ec737d112,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + CurrencyType = CurrencyType.Invalid, + Currency = 0, + Rarity = Rarity.Uncommon, + Context = GiftContext.StuntRunner_TheMainEvent_C + }; + var reward = new GameReward + { + RewardSelectionId = 8877, + Message = "Stunt Runner: The Main Event", + GiftContext = GiftContext.StuntRunner_TheMainEvent_C, + RewardType = RewardType.PostQuestActivity, + GiftDrop1 = item, + GiftDrop2 = item, + GiftDrop3 = item, + CreatedAt = DateTime.UtcNow + }; + var json = new ProgressionJson { PendingGameRewards = new List { reward } }; + db.Progression.Add(new ProgressionData { PlayerId = PlayerId, Progress = json.ToJson() }); + await db.SaveChangesAsync(); + + // The collect the client sends: a generate naming the reward's context, then a consume of the + // box that answered it. Neither names an item; both have to end up naming this one. + var generated = Assert.IsType(Assert.IsType( + await AsAvatarV2(db, $"MFPELEJLJLD={(int)GiftContext.StuntRunner_TheMainEvent_C}&DPAJDIEBNKM=Stunt+Runner") + .GenerateGift()).Value); + + Assert.Equal("Runner Harness (Green)", generated.FriendlyName); + Assert.Equal(item.AvatarItemDesc, generated.AvatarItemDesc); + Assert.Equal(CurrencyType.Invalid, generated.CurrencyType); + Assert.Equal(0, generated.Currency); + + var consume = AsAvatarV2(db, $"{{\"giftDropId\":{generated.GiftDropId}}}"); + Assert.IsType(await consume.ConsumeGiftBody()); + + var unlocks = await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == PlayerId); + var unlocked = Assert.Single(unlocks!.GetAvatarItems(), x => x.AvatarItemDesc == item.AvatarItemDesc); + Assert.Equal("Runner Harness (Green)", unlocked.FriendlyName); + } + + private static AvatarV2 AsAvatarV2(RecDBContext db, string body) + { + var api = new AvatarV2(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString())], "test")) + } + } + }; + api.ControllerContext.HttpContext.Request.ContentType = "application/json"; + api.ControllerContext.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + api.ControllerContext.HttpContext.Request.ContentLength = api.ControllerContext.HttpContext.Request.Body.Length; + return api; + } + + /// + /// Records a room visit at a point in time, which is what says which game was played and when. + /// + private static async Task SeedVisitAsync(RecDBContext db, string roomName, DateTime visitedAt) + { + var room = await db.Rooms.FirstOrDefaultAsync(x => x.Name == roomName); + if (room == null) + { + room = new RoomData { Name = roomName }; + db.Rooms.Add(room); + await db.SaveChangesAsync(); + } + + db.RoomInteractions.Add(new PlayerInteractionDB + { + PlayerId = PlayerId, + RoomId = room.RoomId, + Visited = true, + LastVisted = visitedAt, + }); + await db.SaveChangesAsync(); + } + + /// + /// What the log showed: the reward request for a finished game arrives with the player already + /// back in the Rec Center and a body that names no activity at all. The room they walked into is + /// the newest visit and pays nothing, so the activity has to come from the newest visit that + /// does - the game they were actually placed into. + /// + [Fact] + public async Task ACompletionThatNamesNoActivityIsAnsweredFromTheNewestActivityThePlayerWasPlacedInto() + { + await using var db = NewDb(); + var now = DateTime.Now; + await SeedVisitAsync(db, "Dodgeball", now.AddMinutes(-5)); + await SeedVisitAsync(db, "RecCenter", now.AddSeconds(-5)); + var api = AsPlayer(db); + + var reward = await RequestRewardAsync(api, RewardType.PostGameActivity); + + // Before the room history was consulted the Rec Center named the activity, which is none, + // and the reward was the token package filed under GameRewards_Tokens. + Assert.Equal(GiftContext.Dodgeball, reward.GiftContext); + Assert.NotEqual(GiftContext.GameRewards_Tokens, reward.GiftContext); + } + + /// + /// The claim outranks the room history: a client that names the activity is believed, so the game + /// played most recently does not answer for the one the completion belongs to. + /// + [Fact] + public async Task TheClaimedActivityOutranksTheNewestOneThePlayerWasPlacedInto() + { + await using var db = NewDb(); + var now = DateTime.Now; + await SeedVisitAsync(db, "Dodgeball", now.AddMinutes(-30)); + await SeedVisitAsync(db, "RecCenter", now.AddSeconds(-5)); + var api = AsPlayer(db); + + var box = await RequestAndTakeQuestBoxAsync(api, db, GiftContext.StuntRunner_TheMainEvent_C); + + // The quest the claim names, not the game played most recently: a Stunt Runner run's box is + // filed under Stunt Runner even though a Dodgeball game is what the room history remembers. + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, box.Context); + } + + /// + /// Only recent visits count: a game played hours ago must not answer for a game the server cannot + /// place, or yesterday's activity pays for today's reward. + /// + [Fact] + public async Task AStaleActivityDoesNotAnswerForACompletionThatNamesNothing() + { + await using var db = NewDb(); + var now = DateTime.Now; + await SeedVisitAsync(db, "Dodgeball", now.AddHours(-8)); + await SeedVisitAsync(db, "RecCenter", now.AddSeconds(-5)); + var api = AsPlayer(db); + + var reward = await RequestRewardAsync(api, RewardType.PostGameActivity); + + Assert.Equal(GiftContext.GameRewards_Tokens, reward.GiftContext); + } + + // --------------------------------------------------------------------------------- + // What is not a completion + // --------------------------------------------------------------------------------- + + [Fact] + public async Task ALevelUpRequestIsNotPaidTheGameCompletedXp() + { + await using var db = NewDb(); + // A player of its own, so this account starts with no XP. + var api = AsPlayer(db, 9201); + + var reward = await RequestRewardAsync(api, RewardType.Levelup); + + Assert.Equal(RewardType.Levelup, reward.RewardType); + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == 9201); + Assert.Equal(0, progression?.XP ?? 0); + } + + [Fact] + public async Task ACompletionIsPaidTheGameCompletedXp() + { + await using var db = NewDb(); + var api = AsPlayer(db, 9202); + + // No room visited, so the reward is the token package - and it is still a completion, so it + // is still paid the game-completed XP. + await RequestRewardAsync(api, RewardType.PostGameActivity); + + var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == 9202); + Assert.Equal(ProgressionAwardService.XpPerGameCompleted, progression?.XP ?? 0); + } + + [Fact] + public async Task ALevelUpPackageIsFiledUnderItsOwnContext() + { + await using var db = NewDb(); + var api = AsPlayer(db, 9203); + + // A bonus is not a game: the game context the client sent with it does not become the context + // of the boxes, which is what the client paints and groups a gift by. + var reward = await RequestRewardAsync(api, RewardType.Levelup, GiftContext.StuntRunner_TheMainEvent_C); + + Assert.Equal(GiftContext.LevelUp, reward.GiftContext); + Assert.All(new[] { reward.GiftDrop1, reward.GiftDrop2, reward.GiftDrop3 }, + box => Assert.Equal(GiftContext.LevelUp, box.Context)); + } + + // --------------------------------------------------------------------------------- + // Choosing, and choosing again + // --------------------------------------------------------------------------------- + + [Fact] + public async Task ChoosingHandsOverAGiftBoxAndRetryingIsNotAFailure() + { + await using var db = NewDb(); + const long selectionId = 280843441; + const long itemDropId = 2045372488; + var item = new Gift + { + GiftDropId = itemDropId, + FriendlyName = "Archer Hat", + AvatarItemDesc = "Archer Hat (Tan)", + Context = GiftContext.Quest_Goblin_C, + CurrencyType = CurrencyType.Invalid + }; + db.Progression.Add(new ProgressionData + { + PlayerId = PlayerId, + Progress = new ProgressionJson + { + PendingGameRewards = + [ + new GameReward + { + RewardSelectionId = selectionId, + RewardType = RewardType.PostGameActivity, + GiftContext = GiftContext.Quest_Goblin_C, + GiftDrop1 = item, + GiftDrop2 = item, + GiftDrop3 = item + } + ] + }.ToJson() + }); + await db.SaveChangesAsync(); + + var api = AsSelecting(db, selectionId, itemDropId); + var first = await api.SelectReward(); + Assert.True(Assert.IsType(((OkObjectResult)first.Result!).Value).Success); + + // Handed over, not credited here: what the player opens is the box, and the drop keeps the id + // the client already knows because that is the id its own consume call names. + var boxes = await StorefrontShared.GetPendingGiftsAsync(db, PlayerId); + var box = Assert.Single(boxes); + Assert.Equal(itemDropId, box.GiftDropId); + Assert.Equal("Archer Hat (Tan)", box.AvatarItemDesc); + Assert.Empty(await api.GetPendingGameRewards()); + + // The retry the client makes after a slow answer: the same success, and no second box. + var retryApi = AsSelecting(db, selectionId, itemDropId); + var retry = await retryApi.SelectReward(); + Assert.True(Assert.IsType(((OkObjectResult)retry.Result!).Value).Success); + Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); + + var persisted = (await db.Progression.SingleAsync()).GetProgressionJson()!; + var completed = Assert.Single(persisted.PendingGameRewards); + Assert.True(completed.Consumed); + Assert.Equal(itemDropId, completed.ConsumedGiftDropId); + } + + [Fact] + public async Task AnUnrecognisedBoxIsStillTheRewardAndSaysSo() + { + await using var db = NewDb(); + const long selectionId = 99; + var item = new Gift { GiftDropId = 5, FriendlyName = "Knight Cape", AvatarItemDesc = "Knight Cape (Blue)" }; + db.Progression.Add(new ProgressionData + { + PlayerId = PlayerId, + Progress = new ProgressionJson + { + PendingGameRewards = [new GameReward { RewardSelectionId = selectionId, GiftDrop1 = item }] + }.ToJson() + }); + await db.SaveChangesAsync(); + + var api = AsSelecting(db, selectionId, giftDropId: 123456); + var result = await api.SelectReward(); + + Assert.True(Assert.IsType(((OkObjectResult)result.Result!).Value).Success); + Assert.Equal(5, Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)).GiftDropId); + } +} diff --git a/ServerTests/GameRewardPersistenceTests.cs b/ServerTests/GameRewardPersistenceTests.cs new file mode 100644 index 0000000..0348134 --- /dev/null +++ b/ServerTests/GameRewardPersistenceTests.cs @@ -0,0 +1,72 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.GameRewards.V1; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Storefront; +using Xunit; + +namespace RadicadeServer.Tests; + +public class GameRewardPersistenceTests +{ + [Fact] + public async Task CorrectedGiftIdsSurviveReloadAndRepeatedPendingRequests() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + const long selectionId = 5425026455704055785; + await using (var seed = new RecDBContext(options)) + { + seed.Progression.Add(new ProgressionData + { + PlayerId = 2, + Progress = new ProgressionJson + { + XpEarnedToday = 75, + PendingGameRewards = [new GameReward + { + RewardSelectionId = selectionId, + GiftDrop1 = new Gift { GiftDropId = long.MaxValue }, + GiftDrop2 = new Gift { GiftDropId = long.MinValue }, + GiftDrop3 = new Gift { GiftDropId = 123 } + }] + }.ToJson() + }); + await seed.SaveChangesAsync(); + } + + static GameRewardsAPI CreateApi(RecDBContext db) => new(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, "2")], "test")) + } + } + }; + + await using var firstDb = new RecDBContext(options); + var first = Assert.Single(await CreateApi(firstDb).GetPendingGameRewards()); + Assert.InRange(first.GiftDrop1!.GiftDropId, 1L, int.MaxValue); + Assert.InRange(first.GiftDrop2!.GiftDropId, 1L, int.MaxValue); + Assert.Equal(123L, first.GiftDrop3!.GiftDropId); + Assert.Equal(selectionId, first.RewardSelectionId); + + await using var nextDb = new RecDBContext(options); + var persisted = (await nextDb.Progression.SingleAsync()).GetProgressionJson()!; + Assert.Equal(75, persisted.XpEarnedToday); + var saved = Assert.Single(persisted.PendingGameRewards); + Assert.Equal(first.GiftDrop1.GiftDropId, saved.GiftDrop1!.GiftDropId); + Assert.Equal(first.GiftDrop2.GiftDropId, saved.GiftDrop2!.GiftDropId); + var second = Assert.Single(await CreateApi(nextDb).GetPendingGameRewards()); + Assert.Equal(first.GiftDrop1.GiftDropId, second.GiftDrop1!.GiftDropId); + Assert.Equal(first.GiftDrop2.GiftDropId, second.GiftDrop2!.GiftDropId); + } +} diff --git a/ServerTests/GameUpdateVersionCheckTests.cs b/ServerTests/GameUpdateVersionCheckTests.cs new file mode 100644 index 0000000..0a80a27 --- /dev/null +++ b/ServerTests/GameUpdateVersionCheckTests.cs @@ -0,0 +1,115 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.VersionCheck; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A Game Update is not a notification. The client registers no handler for 26 or 27 - and none for +/// ModerationUpdateRequired (21) either - so a frame under any of those ids is dropped in silence. +/// What the client does read is its REST version check, api/versioncheck/v4, and that answer is what +/// raises its own "Update Required" screen. So a published Game Update row has to change this +/// answer, and these tests pin both halves: the status flip, and the no-game-update case staying +/// exactly the shape it has always been. +/// +public class GameUpdateVersionCheckTests +{ + private const string Live = "[{\"id\":1,\"type\":\"Game Update\"}]"; + private const string Expired = "[{\"id\":1,\"type\":\"Game Update\",\"expires_at\":\"2000-01-01T00:00:00Z\"}]"; + private const string Future = "[{\"id\":1,\"type\":\"Game Update\",\"expires_at\":\"2999-01-01T00:00:00Z\"}]"; + private const string Regular = "[{\"id\":1,\"type\":\"Regular Announcement\"}]"; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + [Fact] + public void NoAnnouncementConfigLeavesTheVersionCheckAlone() + { + var result = GameUpdateVersionCheck.Resolve(null, DateTime.UtcNow); + + Assert.Equal(VersionCheckStatus.ValidForPlay, result.VersionStatus); + Assert.Equal(0, result.UpdateNotificationStage); + } + + [Theory] + [InlineData("")] + [InlineData("[]")] + [InlineData(Regular)] + [InlineData("not json at all")] + [InlineData("{\"type\":\"Game Update\"}")] + [InlineData(Expired)] + public void AnythingThatIsNotALiveGameUpdateKeepsPlayingAllowed(string json) + { + var result = GameUpdateVersionCheck.Resolve(json, DateTime.UtcNow); + + Assert.Equal(VersionCheckStatus.ValidForPlay, result.VersionStatus); + } + + [Theory] + [InlineData(Live)] + [InlineData(Future)] + [InlineData("[{\"id\":1,\"type\":\"game update\"}]")] + [InlineData("[{\"id\":1,\"type\":\"Regular Announcement\"},{\"id\":2,\"type\":\"Game Update\"}]")] + public void ALiveGameUpdateAsksTheClientToUpdate(string json) + { + var result = GameUpdateVersionCheck.Resolve(json, DateTime.UtcNow); + + Assert.Equal(VersionCheckStatus.UpdateRequired, result.VersionStatus); + // Not 0: 0 is what a healthy boot already gets, so it is the one value that could also mean + // "nothing to show". + Assert.True(result.UpdateNotificationStage > 0); + } + + [Fact] + public async Task TheVersionCheckAnswerCarriesUpdateRequiredWhileAGameUpdateIsLive() + { + using var db = NewDb(); + db.ServerConfig.Add(new ServerConfigVal(GameUpdateVersionCheck.AnnouncementConfigKey, Live)); + await db.SaveChangesAsync(); + + var api = new VersionCheckAPI(db); + var ok = Assert.IsType(await api.Get("20230831", "20230831")); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value)); + + Assert.Equal((int)VersionCheckStatus.UpdateRequired, doc.RootElement.GetProperty("VersionStatus").GetInt32()); + Assert.True(doc.RootElement.GetProperty("UpdateNotificationStage").GetInt32() > 0); + Assert.Equal(0, doc.RootElement.GetProperty("ValidVersion").GetInt32()); + Assert.False(doc.RootElement.GetProperty("IsVersionIslanded").GetBoolean()); + Assert.False(doc.RootElement.GetProperty("IsCrossPlayDisabled").GetBoolean()); + + // The key set is what the client's strict lookups depend on: ValidVersion must be present or + // the client aborts boot. It must not grow or shrink either. + Assert.Equal(5, doc.RootElement.EnumerateObject().Count()); + } + + [Fact] + public async Task TheVersionCheckAnswerIsUnchangedWhenThereIsNoGameUpdate() + { + using var db = NewDb(); + db.ServerConfig.Add(new ServerConfigVal(GameUpdateVersionCheck.AnnouncementConfigKey, Regular)); + await db.SaveChangesAsync(); + + var api = new VersionCheckAPI(db); + var ok = Assert.IsType(await api.Get("20230831", "20230831")); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value)); + + Assert.Equal(0, doc.RootElement.GetProperty("VersionStatus").GetInt32()); + Assert.Equal(0, doc.RootElement.GetProperty("UpdateNotificationStage").GetInt32()); + } + + [Fact] + public void TheLiveRecheckFrameGoesOutUnderTheNameTheClientRegisters() + + { + // The one game-update-relevant key in the client's handler table; 26 and 27 are not in it. + Assert.Equal("AppVersionUpdate", GameUpdateVersionCheck.AppVersionUpdateNotificationId); + } +} diff --git a/ServerTests/GiftClusterTests.cs b/ServerTests/GiftClusterTests.cs new file mode 100644 index 0000000..e5c0e3d --- /dev/null +++ b/ServerTests/GiftClusterTests.cs @@ -0,0 +1,146 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Compatibility; +using RadicadeServer.API.Config; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Store; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The free-gift button and the "Star Boxes" in the Rec Center storefront. +/// +/// Three separate causes sat behind the gift complaints: the free-gift button config answered +/// false (the client's "this feature has been temporarily disabled" screen), sendmultiple +/// answered an empty array where the client reads a two-field result (so a 200 still surfaced as +/// "Failed to SendFreeGift"), and a star box - a drop that names a rarity instead of an item - was +/// granted empty, so buying the 2/3/4-star box did nothing at all. +/// +public class GiftClusterTests +{ + private const long BuyerId = 55; + + private static DbContextOptions MemoryOptions() => + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + /// + /// The purchase path opens its own transaction, which the in-memory provider refuses, so this + /// suite uses SQLite - the same reason the registration tests do. + /// + /// + /// The star boxes are resolved from the avatar catalogue the server ships, so the test loads the + /// same files the server does. Without it the catalogue is the one-item starter fallback and no + /// box could resolve anything, which would make the assertion below pass for the wrong reason. + /// + private static void LoadShippedCatalog() => DBConfig.LoadFiles(); + + private static async Task<(SqliteConnection Connection, RecDBContext Db)> OpenDbAsync() + { + var connection = new SqliteConnection("DataSource=:memory:"); + await connection.OpenAsync(); + var options = TestDbOptions.Create().UseSqlite(connection).Options; + var db = new RecDBContext(options); + await db.Database.EnsureCreatedAsync(); + return (connection, db); + } + + [Fact] + public void TheFreeGiftButtonIsReportedAsEnabled() + { + using var db = new RecDBContext(MemoryOptions()); + + var result = Assert.IsType(new ConfigAPI(db).GetFreeGiftButtonConfig()); + + Assert.Equal(true, result.Value); + } + + [Fact] + public async Task SendMultipleAnswersTheClientsTwoFieldResult() + { + // The response DTO (RecNet.OMJLLFDFEJL) is a success flag and an error string, so even a + // refusal has to come back in that shape - the empty array this route once answered could + // not be read into it and surfaced as "Failed to SendFreeGift" against a 200. The send + // itself (boxes, items, the daily cooldown) is covered by FriendotronTests. + using var db = new RecDBContext(MemoryOptions()); + var api = new CompatibilityAPI(db); + + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, BuyerId.ToString())); + api.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + + var result = Assert.IsType(await api.SendFreeGifts()); + using var document = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)); + + Assert.False(document.RootElement.GetProperty("ALOCDGMHFKA").GetBoolean()); + Assert.Equal("No recipients were given.", document.RootElement.GetProperty("FBILGKFKFJE").GetString()); + } + + [Fact] + public async Task SendMultipleRefusesACallWithNoResolvedAccount() + { + using var db = new RecDBContext(MemoryOptions()); + var api = new CompatibilityAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity()) } + } + }; + + Assert.IsType(await api.SendFreeGifts()); + } + + [Theory] + [InlineData(541, 10)] + [InlineData(538, 20)] + [InlineData(539, 30)] + public async Task AStarBoxGrantsARandomCatalogueItemOfThatRarity(int purchasableItemId, int expectedRarity) + { + LoadShippedCatalog(); + + var (connection, db) = await OpenDbAsync(); + await using var disposeConnection = connection; + await using var disposeDb = db; + + db.Currency.Add(new Currency { AccountId = BuyerId, RecTokens = 100_000 }); + await db.SaveChangesAsync(); + + var outcome = await StorefrontShared.ExecutePurchaseAsync( + db, BuyerId, StorefrontTypes.RecCenter, purchasableItemId, + CurrencyType.RecCenterTokens, null, null); + + Assert.True(outcome.Success, outcome.ErrorDetail); + var update = Assert.Single(outcome.Response!.BalanceUpdates); + Assert.Equal(UpdateResponseTypes.OK, update.UpdateResponse); + + var gifted = Assert.Single(update.Data); + Assert.False(gifted.IsQuery); + Assert.False(string.IsNullOrWhiteSpace(gifted.FriendlyName)); + Assert.Equal(expectedRarity, (int)gifted.Rarity); + Assert.False(string.IsNullOrWhiteSpace(gifted.AvatarItemDesc)); + + // The box must hand over a real catalogued item of that rarity - not an empty drop. + var catalogueItem = AvatarUnlockCatalog.FindItem(gifted.AvatarItemDesc); + Assert.NotNull(catalogueItem); + Assert.Equal(expectedRarity, catalogueItem!.Rarity); + + var row = await db.Gifts.AsNoTracking().SingleAsync(x => x.AccountId == BuyerId); + Assert.Contains(StorefrontShared.ParseGiftList(row.Data), + pending => pending.AvatarItemDesc == gifted.AvatarItemDesc); + } +} diff --git a/ServerTests/HubRealtimeTests.cs b/ServerTests/HubRealtimeTests.cs new file mode 100644 index 0000000..db13d6b --- /dev/null +++ b/ServerTests/HubRealtimeTests.cs @@ -0,0 +1,370 @@ +using System.Reflection; +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API; +using RadicadeServer.API.Notifications; +using RadicadeServer.CDN; +using RadicadeServer.DB; +using RadicadeServer.RecNet; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Realtime is one socket, and it fails silently in every direction: a URL that lands +/// somewhere unmapped, or a socket the server cannot tie to an account, leaves the +/// connection looking healthy while every notification for that player is dropped - no ban +/// kick, no invite, no presence. These cover the two halves that made that possible: the +/// set of addresses the hub answers on, and putting the player's token on a socket the +/// client builds without one. +/// +[Collection(DbFactoryCollection.Name)] +public class HubRealtimeTests +{ + private sealed class TestDbContextFactory(bool failToCreate = false) : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() + { + if (failToCreate) throw new InvalidOperationException("Database unavailable"); + return new RecDBContext(options); + } + } + + private sealed class TestHubContext : IHubContext + { + public IHubClients Clients => null!; + public IGroupManager Groups => null!; + } + + private static readonly string[] HealthServiceNames = + [ + "auth", "ai", "data", "www", "cards", "notify", "images", "cdn", "commerce", + "datacollection", "discovery", "matchmaking", "storage", "chat", "geo", + "leaderboard", "accounts", "link", "locker", "moderation", "roomcomments", + "roomieintegrations", "clubs", "rooms", "econ", "bugreporting", "cms", + "gamelogs", "platformnotifications", "stringscdn", "strings", "lists", + "studio", "studiocdn", "thorn", "videos" + ]; + + private static async Task GetStatusAsync( + string service, + IDbContextFactory? dbFactory, + bool hubAvailable = true) + { + var previousFactory = Notify.DbFactory; + var previousContext = Notify.Context; + try + { + Notify.DbFactory = dbFactory!; + Notify.Context = hubAvailable ? new TestHubContext() : null!; + var api = new ServiceRootHealthAPI(dbFactory!); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = "/" + service; + api.ControllerContext = new ControllerContext { HttpContext = httpContext }; + return await api.GetServiceStatus(); + } + finally + { + Notify.DbFactory = previousFactory; + Notify.Context = previousContext; + } + } + + private static JsonElement ResponseBody(IActionResult result) + { + var value = Assert.IsAssignableFrom(result).Value; + using var document = JsonDocument.Parse(JsonSerializer.Serialize(value)); + return document.RootElement.Clone(); + } + + [Fact] + public async Task EveryNameserverServiceRootHasAnonymousHealthyStatusJson() + { + var method = typeof(ServiceRootHealthAPI).GetMethod(nameof(ServiceRootHealthAPI.GetServiceStatus))!; + Assert.NotEmpty(method.GetCustomAttributes()); + var routeTemplates = method.GetCustomAttributes() + .Select(attribute => attribute.Template) + .ToArray(); + var advertised = typeof(NameServerResponse).GetProperties() + .ToDictionary(property => property.Name, + property => new Uri((string)property.GetValue(DBConfig.NameServer)!)); + + // Every advertised endpoint is a host of its own under the service domain and carries + // no path at all: the client appends its own path literal to the base it reads here, + // and the host label is what NameserverRedirect turns back into a route prefix. The + // label is the route unless the server says otherwise, which is why the comparison + // below goes through ServiceRoute: notifications is served at /notify. + Assert.All(advertised.Values, url => + { + Assert.Equal(DBConfig.ServiceDomain, url.Host.Split('.', 2).ElementAtOrDefault(1)); + Assert.Equal("/", url.AbsolutePath); + }); + + var advertisedServices = advertised + .Where(pair => pair.Key is not nameof(NameServerResponse.API) and not nameof(NameServerResponse.PlayerSettings)) + .Select(pair => DBConfig.ServiceRoute(pair.Value.Host.Split('.')[0])) + .OrderBy(service => service, StringComparer.Ordinal) + .ToArray(); + var expectedRoutes = HealthServiceNames + .Where(service => service != "data") + .Select(service => "~/" + service) + .OrderBy(route => route, StringComparer.Ordinal) + .ToArray(); + + Assert.Equal(HealthServiceNames.OrderBy(service => service, StringComparer.Ordinal), advertisedServices); + Assert.Equal(expectedRoutes, routeTemplates.OrderBy(route => route, StringComparer.Ordinal)); + Assert.Contains("~/platformnotifications", routeTemplates); + Assert.DoesNotContain(routeTemplates, route => route!.Contains('*')); + Assert.DoesNotContain(routeTemplates, route => route is "~/" or "~/ns"); + + var factory = new TestDbContextFactory(); + foreach (var service in HealthServiceNames) + { + if (service == "data") + { + var dataRoot = typeof(DataCDN).GetMethod(nameof(DataCDN.Index))!; + Assert.Contains(dataRoot.GetCustomAttributes(), attribute => string.IsNullOrEmpty(attribute.Template)); + Assert.Contains("data/", typeof(DataCDN).GetCustomAttributes().Select(attribute => attribute.Template)); + continue; + } + + Assert.Contains("~/" + service, routeTemplates); + var result = Assert.IsType(await GetStatusAsync(service, factory)); + var body = ResponseBody(result); + Assert.Equal(service, body.GetProperty("service").GetString()); + Assert.Equal("ok", body.GetProperty("status").GetString()); + } + } + + // The nameserver names each service on its own host, and the patch has to turn that host + // back into the route this server serves it under: the client appends its own path literal + // to the advertised base, so a host that lands anywhere else is a service that answers + // nothing - with no error anywhere to say so. This reads the advertised names, the + // redirect and the server's own routes together, so the three cannot drift apart: a name + // misspelled in either list fails here rather than in a client that half-works. + [Fact] + public void EveryAdvertisedServiceHostLandsOnAServiceRootTheServerServes() + { + var roots = typeof(ServiceRootHealthAPI) + .GetMethod(nameof(ServiceRootHealthAPI.GetServiceStatus))! + .GetCustomAttributes() + .Select(attribute => attribute.Template!.TrimStart('~', '/').ToLowerInvariant()) + .ToHashSet(StringComparer.Ordinal); + // data answers from its own controller at /data rather than from the health roots. + roots.Add("data"); + + foreach (var property in typeof(NameServerResponse).GetProperties()) + { + var advertised = (string)property.GetValue(DBConfig.NameServer)!; + var label = new Uri(advertised).Host.Split('.')[0]; + var rewritten = new Uri(NameserverRedirect.Rewrite(advertised + "/probe", DBConfig.ServerUrl)); + var segment = rewritten.AbsolutePath.TrimStart('/').Split('/')[0].ToLowerInvariant(); + + Assert.Equal(new Uri(DBConfig.ServerUrl).Host, rewritten.Host); + + // api is the bare base every unprefixed call already went to, so it must not gain + // a segment at all. + if (property.Name == nameof(NameServerResponse.API)) + { + Assert.Equal("probe", rewritten.AbsolutePath.TrimStart('/')); + continue; + } + + Assert.Equal(DBConfig.ServiceRoute(label), segment); + + // playersettings is advertised for completeness; the client's settings calls go to + // /api/settings/v2, so there is no root of that name to land on. + if (property.Name == nameof(NameServerResponse.PlayerSettings)) + continue; + + Assert.Contains(segment, roots); + } + } + + [Fact] + public async Task NotificationServiceRootReturnsAnErrorCodeWhenTheHubIsUnavailable() + { + var result = Assert.IsType(await GetStatusAsync("notify", new TestDbContextFactory(), hubAvailable: false)); + + Assert.Equal(StatusCodes.Status503ServiceUnavailable, result.StatusCode); + var body = ResponseBody(result); + Assert.Equal("notify", body.GetProperty("service").GetString()); + Assert.Equal("NOTIFY_HUB_UNAVAILABLE", body.GetProperty("status").GetString()); + } + + [Fact] + public async Task ServiceRootsReturnTheirErrorCodeWhenTheDatabaseIsUnavailable() + { + var result = Assert.IsType(await GetStatusAsync("moderation", new TestDbContextFactory(failToCreate: true))); + + Assert.Equal(StatusCodes.Status503ServiceUnavailable, result.StatusCode); + var body = ResponseBody(result); + Assert.Equal("moderation", body.GetProperty("service").GetString()); + Assert.Equal("MODERATION_DATABASE_UNAVAILABLE", body.GetProperty("status").GetString()); + } + + [Fact] + public async Task NotificationServiceRootReturnsAnErrorCodeWhenItsDatabaseIsNotConfigured() + { + var result = Assert.IsType(await GetStatusAsync("notify", null)); + + Assert.Equal(StatusCodes.Status503ServiceUnavailable, result.StatusCode); + Assert.Equal("NOTIFY_DATABASE_NOT_CONFIGURED", ResponseBody(result).GetProperty("status").GetString()); + } + + [Fact] + public void DataServiceRootReportsHealthyAndUnavailableStorage() + { + var previousStorage = DBConfig.CDNStorage; + var root = Directory.CreateTempSubdirectory("radicade-data-health-test-"); + try + { + DBConfig.CDNStorage = root.FullName; + var healthy = Assert.IsType(new DataCDN().Index()); + var healthyBody = ResponseBody(healthy); + Assert.Equal("data", healthyBody.GetProperty("service").GetString()); + Assert.Equal("ok", healthyBody.GetProperty("status").GetString()); + + DBConfig.CDNStorage = Path.Combine(root.FullName, "missing"); + var unavailable = Assert.IsType(new DataCDN().Index()); + Assert.Equal(StatusCodes.Status503ServiceUnavailable, unavailable.StatusCode); + var errorBody = ResponseBody(unavailable); + Assert.Equal("data", errorBody.GetProperty("service").GetString()); + Assert.Equal("DATA_STORAGE_UNAVAILABLE", errorBody.GetProperty("status").GetString()); + } + finally + { + DBConfig.CDNStorage = previousStorage; + root.Delete(recursive: true); + } + } + + // The client builds the hub URL as the nameserver response's "Notifications" base plus + // the client's own "hub/v1" literal, and the two ways of joining them disagree about the + // slash. Every shape that join can produce has to answer, or a URL-shape mismatch kills + // realtime while every REST call keeps working. + [Theory] + [InlineData("/notify/hub/v1")] + [InlineData("/notify/hub")] + [InlineData("/notifyhub/v1")] + [InlineData("/hub/v1")] + [InlineData("/hub")] + public void EveryHubAddressShapeTheClientCanBuildAnswers(string path) + { + Assert.Contains(path, NotifyHubRoutes.Paths); + Assert.True(NotifyHubRoutes.IsHubPath(path)); + + // SignalR negotiates over HTTP first, then upgrades. + Assert.True(NotifyHubRoutes.IsHubPath(path + "/negotiate")); + + // Path matching never depends on the casing a proxy or a client happened to use. + Assert.True(NotifyHubRoutes.IsHubPath(path.ToUpperInvariant())); + } + + [Theory] + [InlineData("/notify")] + [InlineData("/notify/")] + [InlineData("/rooms/rooms/1")] + [InlineData("/matchmaking/player")] + [InlineData("/hubris")] + public void UnrelatedAddressesAreNotHubAddresses(string path) + => Assert.False(NotifyHubRoutes.IsHubPath(path)); + + // The socket the client opens, exactly as its own log shows it. It carries the + // connection id and nothing else: no access_token, and no Authorization header, because + // SignalR's WebSocket transport does not copy the one the game uses for REST calls. That + // is what left the server with an anonymous connection whose every notification was + // dropped. + [Theory] + [InlineData("wss://ns.radicade.app/notify/hub/v1?&id=abc")] + [InlineData("wss://ns.radicade.app/notify/hub/v1?id=abc")] + [InlineData("wss://ns.radicade.app/hub/v1?id=abc")] + [InlineData("wss://ns.radicade.app/notifyhub/v1?id=abc")] + [InlineData("wss://ns.radicade.app/hub?id=abc")] + [InlineData("https://ns.radicade.app/notify/hub/v1/negotiate")] + public void HubAddressesAreRecognised(string url) => Assert.True(NameserverRedirect.IsHubUrl(url)); + + [Theory] + [InlineData("https://ns.radicade.app/rooms/rooms/1")] + [InlineData("https://ns.radicade.app/hubris")] + [InlineData("https://ns.radicade.app/notify/")] + [InlineData("https://ns.radicade.app/api/settings/v2/")] + [InlineData("https://ns.radicade.app/accounts/account/me")] + public void OtherAddressesAreNotHubAddresses(string url) => Assert.False(NameserverRedirect.IsHubUrl(url)); + + // The address the client actually opens, rebuilt the canonical way round: the player's + // token first, the connection id kept. + [Fact] + public void TheClientsOwnHubAddressGetsThePlayersToken() + { + var opened = "wss://ns.radicade.app/notify/hub/v1?&id=z9bNeechDZOGXdxy5lQp0Q"; + + var authenticated = NameserverRedirect.EnsureHubToken(opened, "eyJhbGciOi.probe.token"); + + Assert.Equal("wss://ns.radicade.app/notify/hub/v1?access_token=eyJhbGciOi.probe.token&id=z9bNeechDZOGXdxy5lQp0Q", authenticated); + Assert.True(NameserverRedirect.HasHubToken(authenticated)); + + // Idempotent: a second pass must not stack a second copy on the address. + Assert.Equal(authenticated, NameserverRedirect.EnsureHubToken(authenticated, "eyJhbGciOi.probe.token")); + } + + [Theory] + [InlineData("/notify/hub/v1?id=abc")] + [InlineData("/notify/hub/v1")] + [InlineData("/hub")] + public void EveryHubShapeCanCarryTheToken(string path) + { + var authenticated = NameserverRedirect.EnsureHubToken("wss://host" + path, "token-value"); + + Assert.Contains("access_token=token-value", authenticated); + Assert.True(NameserverRedirect.HasHubToken(authenticated)); + } + + [Fact] + public void ASocketThatAlreadyCarriesATokenIsLeftAlone() + { + var already = "wss://host/hub/v1?access_token=theirs&id=abc"; + + Assert.Equal(already, NameserverRedirect.EnsureHubToken(already, "ours")); + Assert.True(NameserverRedirect.HasHubToken(already)); + } + + [Fact] + public void TheBearerPrefixIsNotPartOfTheToken() + { + Assert.Equal("wss://host/hub/v1?access_token=raw", + NameserverRedirect.EnsureHubToken("wss://host/hub/v1", "Bearer raw")); + } + + // Nothing may be added to an ordinary API call: a token in the wrong URL is a token + // leaked into logs and into whatever the address is handed to. + [Fact] + public void OrdinaryCallsAreNeverTouched() + { + const string api = "https://host/rooms/rooms/1?include=all"; + Assert.Equal(api, NameserverRedirect.EnsureHubToken(api, "token-value")); + Assert.Equal("wss://host/hub/v1", NameserverRedirect.EnsureHubToken("wss://host/hub/v1", null)); + Assert.Equal("wss://host/hub/v1", NameserverRedirect.EnsureHubToken("wss://host/hub/v1", "")); + Assert.Equal("wss://host/hub/v1", NameserverRedirect.EnsureHubToken("wss://host/hub/v1", "Bearer ")); + } + + // The stock client's hub host is the real RecNet's; redirecting it and authenticating it + // are two steps of the same journey and must compose. + [Fact] + public void AStockClientsHubSocketIsRedirectedAndThenAuthenticated() + { + var redirected = NameserverRedirect.RewriteWebSocket("wss://notify.rec.net/hub/v1?id=abc", "https://ns.radicade.app"); + Assert.Equal("wss://ns.radicade.app/notify/hub/v1?id=abc", redirected); + + var authenticated = NameserverRedirect.EnsureHubToken(redirected, "token-value"); + Assert.Equal("wss://ns.radicade.app/notify/hub/v1?access_token=token-value&id=abc", authenticated); + } +} diff --git a/ServerTests/ImageAssetResolutionTests.cs b/ServerTests/ImageAssetResolutionTests.cs new file mode 100644 index 0000000..3022ddf --- /dev/null +++ b/ServerTests/ImageAssetResolutionTests.cs @@ -0,0 +1,129 @@ +using System; +using System.IO; +using RadicadeServer.Images; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The image route answers by name, and the name a client asks for is not always the name on +/// disk. Two real cases drove this: a custom-avatar design is stored as +/// <guid>.bin while its bytes are a PNG, and the default room image ships as +/// DefaultRoomImage.png while the server asks for the bare DefaultRoomImage. +/// Both resolved to nothing, and nothing is not "no picture" -- it is an empty 404 body, which +/// the 2023 client turns into a broken surface (and, for a room image, a fatal load failure). +/// +public class ImageAssetResolutionTests : IDisposable +{ + private readonly string root = Directory.CreateTempSubdirectory("radicade-images-test-").FullName; + + public void Dispose() + { + try + { + Directory.Delete(root, true); + } + catch (IOException) + { + } + } + + private string Write(string name, string content = "x") + { + var path = Path.Join(root, name); + File.WriteAllText(path, content); + return path; + } + + [Fact] + public void TheRequestedNameIsFoundFirst() + { + var expected = Write("photo.png"); + + Assert.Equal(expected, ImageAPI.ResolveAsset("photo.png", new[] { root })); + } + + [Fact] + public void ADesignStoredAsBinIsFoundByTheNameTheClientAsksFor() + { + // Exactly what the customizer wrote before designs moved to the image CDN: the bytes + // are a PNG, the name on disk ends in .bin, and the client asks for that name. + var expected = Write("a07188bb-9f99-442d-a2ff-2aebad2ca7fe.bin"); + + Assert.Equal(expected, ImageAPI.ResolveAsset("a07188bb-9f99-442d-a2ff-2aebad2ca7fe.bin", new[] { root })); + } + + [Fact] + public void AMissingExtensionIsTriedOnTheSameStem() + { + // The default-image case: the server asks for "DefaultRoomImage", the deployment ships + // "DefaultRoomImage.png", and the extension search used to be skipped for this name. + var expected = Write("DefaultRoomImage.png"); + + Assert.Equal(expected, ImageAPI.ResolveAsset("DefaultRoomImage", new[] { root })); + } + + [Fact] + public void AnExtensionThatDoesNotDescribeTheContentStillFindsTheFile() + { + var expected = Write("design.png"); + + Assert.Equal(expected, ImageAPI.ResolveAsset("design.bin", new[] { root })); + Assert.Equal(expected, ImageAPI.ResolveAsset("design.jpeg", new[] { root })); + } + + [Fact] + public void RootsAreSearchedInOrder() + { + var first = Directory.CreateDirectory(Path.Join(root, "images")).FullName; + var second = Directory.CreateDirectory(Path.Join(root, "custom")).FullName; + var expected = Path.Join(first, "shirt.png"); + File.WriteAllText(expected, "x"); + File.WriteAllText(Path.Join(second, "shirt.png"), "x"); + + Assert.Equal(expected, ImageAPI.ResolveAsset("shirt.png", new[] { first, second })); + } + + [Fact] + public void ANamedRootIsPreferredOverALaterRootsVariant() + { + var images = Directory.CreateDirectory(Path.Join(root, "images")).FullName; + var custom = Directory.CreateDirectory(Path.Join(root, "custom")).FullName; + var expected = Path.Join(custom, "shirt.bin"); + File.WriteAllText(expected, "x"); + File.WriteAllText(Path.Join(images, "shirt.png"), "x"); + + Assert.Equal(expected, ImageAPI.ResolveAsset("shirt.bin", new[] { images, custom })); + } + + [Theory] + [InlineData("")] + [InlineData(".")] + [InlineData("..")] + [InlineData("../outside.png")] + [InlineData("..\\outside.png")] + [InlineData("sub/../outside.png")] + public void NamesThatDoNotNameAFileInsideARootResolveToNothing(string name) + { + Assert.Null(ImageAPI.ResolveAsset(name, new[] { root })); + } + + [Fact] + public void AnUnknownNameResolvesToNothing() + { + Assert.Null(ImageAPI.ResolveAsset("definitely-missing.png", new[] { root })); + Assert.Null(ImageAPI.ResolveAsset("definitely-missing.bin", new[] { root })); + } + + [Fact] + public void TheServedRootsStillIncludeTheDesignRoot() + { + // A design written before designs moved to the image CDN only stays reachable while + // CDN/Custom is still searched, so dropping it here would silently blank old shirts. + var roots = ImageAPI.AssetRoots(); + + Assert.Contains(roots, r => r.EndsWith(Path.Join("CDN", "Images"))); + Assert.Contains(roots, r => r.EndsWith(Path.Join("CDN", "Custom"))); + Assert.Contains(roots, r => r.EndsWith(Path.Join("CDN", "Storage"))); + } +} diff --git a/ServerTests/ImageSignatureRequestTests.cs b/ServerTests/ImageSignatureRequestTests.cs new file mode 100644 index 0000000..86135c0 --- /dev/null +++ b/ServerTests/ImageSignatureRequestTests.cs @@ -0,0 +1,127 @@ +using System.Text; +using Xunit; + +namespace RadicadeServer.Tests; + +public class ImageSignatureRequestTests +{ + private static byte[] Metadata(params string[] literals) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.Write(0xFAB11BAFu); + writer.Write(27); + writer.Write(24); + writer.Write(literals.Length * 8); + writer.Write(24 + literals.Length * 8); + writer.Write(literals.Sum(s => Encoding.UTF8.GetByteCount(s))); + int index = 0; + foreach (var literal in literals) + { + int size = Encoding.UTF8.GetByteCount(literal); + writer.Write(size); + writer.Write(index); + index += size; + } + foreach (var literal in literals) writer.Write(Encoding.UTF8.GetBytes(literal)); + return stream.ToArray(); + } + + [Theory] + [InlineData(".rec.net")] + [InlineData(".radicade.internal")] + public void ClientMetadataControlsHeaderEvenOnWrongRoute(string tail) + { + var key = ImageSignatureRequest.DetectKeyId(Metadata("https://img.rec.net", tail)); + Assert.Equal("KEY:RSA:p1" + tail, key); + var url = NameserverRedirect.Rewrite("https://img.rec.net/photo?width=64&sig=p1", "https://private.example"); + url = ImageSignatureRequest.AddKeyId(url, "https://private.example", key); + Assert.Contains("width=64&sig=p1&rcsig=" + Uri.EscapeDataString(key), url); + var header = RadicadeServer.Images.ImageAPI.SignatureFor(key, "bare", RadicadeServer.Images.ImageAPI.StockKeyId); + Assert.StartsWith("key-id=" + key + "; data=", header); + Assert.Equal(url, ImageSignatureRequest.AddKeyId(url, "https://private.example", key)); + } + + // Rec Room's own metadata does not use the plain v27 header: the four literal-table + // fields are shuffled into other 32-bit slots, so the reader discovers the layout. + // This writes the same shape (the 20230831 build puts table offset at 0x74, table + // size at 0x24, data offset at 0xA0, data size at 0x3C) at 0x100-aligned offsets. + private static byte[] ShuffledMetadata(params string[] literals) + { + // Real metadata files carry tens of thousands of literals; the reader refuses + // tables that are too small to be one, so the fixture is padded past that floor. + var padded = literals.Concat(Enumerable.Range(0, 40).Select(i => $"filler literal {i}")).ToArray(); + int tableOffset = 0x100; + int tableSize = padded.Length * 8; + int dataOffset = tableOffset + tableSize; + int dataSize = padded.Sum(s => Encoding.UTF8.GetByteCount(s)); + var file = new byte[dataOffset + dataSize]; + BitConverter.GetBytes(0x2EBE0u).CopyTo(file, 0); + BitConverter.GetBytes(tableSize).CopyTo(file, 0x24); + BitConverter.GetBytes(dataSize).CopyTo(file, 0x3C); + BitConverter.GetBytes(tableOffset).CopyTo(file, 0x74); + BitConverter.GetBytes(dataOffset).CopyTo(file, 0xA0); + int index = 0; + for (int i = 0; i < padded.Length; i++) + { + int size = Encoding.UTF8.GetByteCount(padded[i]); + BitConverter.GetBytes(size).CopyTo(file, tableOffset + 8 * i); + BitConverter.GetBytes(index).CopyTo(file, tableOffset + 8 * i + 4); + Encoding.UTF8.GetBytes(padded[i]).CopyTo(file, dataOffset + index); + index += size; + } + return file; + } + + [Theory] + [InlineData(".rec.net")] + [InlineData(".radicade.internal")] + public void ShuffledHeaderMetadataIsDiscoveredStructurally(string tail) + { + var metadata = ShuffledMetadata("https://img.rec.net", tail, "another literal"); + Assert.Equal("KEY:RSA:p1" + tail, ImageSignatureRequest.DetectKeyIdFromGameMetadata(metadata)); + Assert.Equal("KEY:RSA:p1" + tail, ImageSignatureRequest.DetectKeyIdFromGameMetadata(metadata)); + // A plain v27 file keeps working through the same entry point. + Assert.Equal("KEY:RSA:p1" + tail, ImageSignatureRequest.DetectKeyIdFromGameMetadata(Metadata(tail))); + } + + [Fact] + public void ShuffledHeaderAmbiguityAndGarbageStayUndecided() + { + Assert.Null(ImageSignatureRequest.DetectKeyIdFromGameMetadata( + ShuffledMetadata(".rec.net", ".radicade.internal", "x"))); + Assert.Null(ImageSignatureRequest.DetectKeyIdFromGameMetadata( + ShuffledMetadata("https://img.rec.net", "no key tail here"))); + Assert.Null(ImageSignatureRequest.DetectKeyIdFromGameMetadata(new byte[512])); + var random = new byte[4096]; + new Random(7).NextBytes(random); + Assert.Null(ImageSignatureRequest.DetectKeyIdFromGameMetadata(random)); + } + + [Fact] + public void InvalidOrAmbiguousMetadataDoesNotGuess() + { + Assert.Null(ImageSignatureRequest.DetectKeyId(new byte[2])); + Assert.Null(ImageSignatureRequest.DetectKeyId(Metadata(".rec.net", ".radicade.internal"))); + Assert.Null(ImageSignatureRequest.DetectKeyId(Metadata("https://img.rec.net"))); + var invalid = Metadata(".rec.net"); + Array.Copy(BitConverter.GetBytes(int.MaxValue), 0, invalid, 12, 4); + Assert.Null(ImageSignatureRequest.DetectKeyId(invalid)); + } + + [Theory] + [InlineData("https://other.example/images/photo")] + [InlineData("https://private.example.evil/images/photo")] + [InlineData("https://private.example/api/photo")] + [InlineData("http://private.example/images/photo")] + public void OnlyConfiguredServerImagesReceiveHint(string url) + => Assert.Equal(url, ImageSignatureRequest.AddKeyId(url, "https://private.example", "KEY:RSA:p1.rec.net")); + + [Fact] + public void PatchedRouteAndServerBasePathKeepParametersAndReplaceStaleHint() + { + var url = ImageSignatureRequest.AddKeyId("https://private.example/base/images/img/photo?rcsig=old&cropSquare=1#f", + "https://private.example/base/", "KEY:RSA:p1.radicade.internal"); + Assert.Equal("https://private.example/base/images/img/photo?cropSquare=1&rcsig=KEY%3ARSA%3Ap1.radicade.internal#f", url); + } +} diff --git a/ServerTests/ImagesAndRelationshipsTests.cs b/ServerTests/ImagesAndRelationshipsTests.cs new file mode 100644 index 0000000..08d2c09 --- /dev/null +++ b/ServerTests/ImagesAndRelationshipsTests.cs @@ -0,0 +1,325 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Relationships.V1; +using RadicadeServer.API.Relationships.V2; +using RadicadeServer.DB; +using RadicadeServer.RecNet.CDN; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Images; +using RadicadeServer.RecNet.Relationships; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Wire-contract tests for the images and relationships endpoints: the key +/// spellings the 2023 client deserializes, the cheer-state isolation between +/// images, and the pending-request guard on accepting friend requests. +/// +public class ImagesAndRelationshipsTests +{ + private const long CallerId = 42; + private const long OtherId = 77; + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new RecDBContext(options); + } + + private static T CreateController(RecDBContext db, long? userId = CallerId) where T : Controller + { + var identity = new ClaimsIdentity("TestAuth"); + if (userId.HasValue) + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.Value.ToString())); + + var controller = (T)Activator.CreateInstance(typeof(T), db)!; + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + return controller; + } + + private static ImageDB Photo(long id, long owner, SavedImageAccessibility access, + SavedImageType type, long? roomId = null, string? tags = null) => new() + { + Id = id, + Name = $"img{id}.png", + OwnerId = owner, + Accessibility = access, + ImageType = type, + RoomId = roomId, + PlayerIds = tags ?? "[]", + CreatedAt = new DateTime(2026, 1, 1).AddHours(id) + }; + + // ---- images ------------------------------------------------------------ + + [Fact] + public async Task PlayerPhotoList_ServesDualKeyProjection() + { + using var db = CreateDb(); + db.Images.AddRange( + Photo(1, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera), + Photo(2, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera), + Photo(3, CallerId, SavedImageAccessibility.Private, SavedImageType.ShareCamera)); + await db.SaveChangesAsync(); + + var api = CreateController(db); + var result = await api.GetImagesFromPlayer(CallerId); + + Assert.Equal(2, result.Count); + + // Newest first: image 2 leads, image 1 follows. + Assert.Equal(2L, (long)result[0].GetType().GetProperty("Id")!.GetValue(result[0])!); + + var first = JsonSerializer.Serialize(result[1]); + using var doc = JsonDocument.Parse(first); + var root = doc.RootElement; + + // Both key spellings must be present: the raw projection (Id/Type) that + // captured 2022 responses used, and the ImagesPlayer projection + // (SavedImageId/SavedImageType) the 2023 client's photo grids read. + Assert.Equal(1, root.GetProperty("Id").GetInt64()); + Assert.Equal(1, root.GetProperty("SavedImageId").GetInt64()); + Assert.Equal(1, root.GetProperty("Type").GetInt32()); + Assert.Equal(1, root.GetProperty("SavedImageType").GetInt32()); + Assert.Equal("img1.png", root.GetProperty("ImageName").GetString()); + } + + [Fact] + public async Task CheerImage_IsIsolatedPerImage() + { + using var db = CreateDb(); + db.Images.AddRange( + Photo(1, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera), + Photo(2, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera)); + await db.SaveChangesAsync(); + + var api = CreateController(db); + api.ControllerContext.HttpContext.Request.ContentType = "application/json"; + api.ControllerContext.HttpContext.Request.Body = + new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(new ImageCheerRequest { SavedImageId = 1, Cheer = true })); + + var result = await api.CheerImage(new ImageCheerRequest { SavedImageId = 1, Cheer = true }); + Assert.True(result.success); + + // One row, tied to image 1 only - the old code matched on the player + // alone, so this cheer would have clobbered cheers on every other image. + var interactions = await db.ImageInteractions.ToListAsync(); + var interaction = Assert.Single(interactions); + Assert.Equal(1, interaction.SavedImageId); + Assert.True(interaction.IsCheered); + + // Cheering image 2 now adds a second row rather than stealing the first. + api.ControllerContext.HttpContext.Request.Body = + new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(new ImageCheerRequest { SavedImageId = 2, Cheer = true })); + await api.CheerImage(new ImageCheerRequest { SavedImageId = 2, Cheer = true }); + + var all = await db.ImageInteractions.ToListAsync(); + Assert.Equal(2, all.Count); + Assert.Contains(all, x => x.SavedImageId == 1 && x.IsCheered); + Assert.Contains(all, x => x.SavedImageId == 2 && x.IsCheered); + } + + [Fact] + public async Task CheerLookup_ReportsCallerStateInRequestOrder() + { + using var db = CreateDb(); + db.Images.AddRange( + Photo(1, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera), + Photo(2, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera)); + db.ImageInteractions.Add(new CheeredImage { SavedImageId = 2, PlayerId = CallerId, IsCheered = true }); + await db.SaveChangesAsync(); + + var api = CreateController(db); + var result = await api.GetBulkImageCheers(new long[] { 1, 2 }); + + Assert.Equal(2, result.Count); + var first = Assert.IsType(result[0].GetType().GetProperty("SavedImageId")!.GetValue(result[0])); + Assert.Equal(1, first); + Assert.False((bool)result[0].GetType().GetProperty("IsCheered")!.GetValue(result[0])!); + Assert.True((bool)result[1].GetType().GetProperty("IsCheered")!.GetValue(result[1])!); + } + + [Fact] + public async Task PlayerFeed_IncludesTaggedPhotos() + { + using var db = CreateDb(); + db.Images.AddRange( + Photo(1, OtherId, SavedImageAccessibility.Public, SavedImageType.ShareCamera, tags: "[42]"), + Photo(2, OtherId, SavedImageAccessibility.Public, SavedImageType.ShareCamera, tags: "[99]"), + Photo(3, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera)); + await db.SaveChangesAsync(); + + var api = CreateController(db); + var feed = await api.GetPlayerFeed(CallerId); + + Assert.Equal(2, feed.Count); // own photo + tagged photo, not the other tag + } + + // ---- relationships ------------------------------------------------------- + + [Fact] + public async Task RelationshipWireShape_UsesPlayerID() + { + using var db = CreateDb(); + db.Relationships.Add(new RealRelationship + { + AccountId = CallerId, + PlayerId = OtherId, + RelationshipType = RelationshipType.Friend, + Favorited = ReciprocalStatus.Local + }); + await db.SaveChangesAsync(); + + var api = CreateController(db); + var relationships = await api.GetRelationships(); + + var relationship = Assert.Single(relationships); + var json = JsonSerializer.Serialize(relationship); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // The captured real RecNet shape: { Id, PlayerID, RelationshipType, ... }. + // The old PlayerId spelling failed to bind on the client, blanking the + // friends list. + Assert.True(root.TryGetProperty("PlayerID", out var playerId)); + Assert.Equal(OtherId, playerId.GetInt64()); + Assert.True(root.TryGetProperty("Id", out _)); + Assert.Equal(3, root.GetProperty("RelationshipType").GetInt32()); + Assert.False(root.TryGetProperty("PlayerId", out _)); + } + + [Fact] + public async Task SendThenAcceptFriendRequest_BefriendsBothSides() + { + using var db = CreateDb(); + + var sender = CreateController(db, CallerId); + await sender.SendFriendRequest(OtherId); + + var senderRel = await db.Relationships.SingleAsync(x => x.AccountId == CallerId && x.PlayerId == OtherId); + var receiverRel = await db.Relationships.SingleAsync(x => x.AccountId == OtherId && x.PlayerId == CallerId); + Assert.Equal(RelationshipType.FriendRequestSent, senderRel.RelationshipType); + Assert.Equal(RelationshipType.FriendRequestReceived, receiverRel.RelationshipType); + + // A third party cannot accept a request addressed to someone else. + var stranger = CreateController(db, 999); + await stranger.AcceptFriendRequest(CallerId); + receiverRel = await db.Relationships.SingleAsync(x => x.AccountId == OtherId && x.PlayerId == CallerId); + Assert.Equal(RelationshipType.FriendRequestReceived, receiverRel.RelationshipType); + + // The actual receiver accepts: both sides flip to Friend. + var receiver = CreateController(db, OtherId); + await receiver.AcceptFriendRequest(CallerId); + + senderRel = await db.Relationships.SingleAsync(x => x.AccountId == CallerId && x.PlayerId == OtherId); + receiverRel = await db.Relationships.SingleAsync(x => x.AccountId == OtherId && x.PlayerId == CallerId); + Assert.Equal(RelationshipType.Friend, senderRel.RelationshipType); + Assert.Equal(RelationshipType.Friend, receiverRel.RelationshipType); + } + + [Fact] + public async Task AcceptWithoutPendingRequest_DoesNothing() + { + using var db = CreateDb(); + var receiver = CreateController(db, OtherId); + await receiver.AcceptFriendRequest(CallerId); + + // No rows were created at all: accepting out of the blue is a no-op, + // not a silent friendship. + Assert.Empty(await db.Relationships.ToListAsync()); + } + + [Fact] + public async Task RemoveFriend_ClearsBothSides() + { + using var db = CreateDb(); + db.Relationships.AddRange( + new RealRelationship { AccountId = CallerId, PlayerId = OtherId, RelationshipType = RelationshipType.Friend }, + new RealRelationship { AccountId = OtherId, PlayerId = CallerId, RelationshipType = RelationshipType.Friend }); + await db.SaveChangesAsync(); + + var api = CreateController(db); + await api.RemoveFriend(OtherId); + + Assert.All(await db.Relationships.ToListAsync(), + x => Assert.Equal(RelationshipType.None, x.RelationshipType)); + } + + [Fact] + public async Task FriendsList_OnlyServesAcceptedFriendships() + { + using var db = CreateDb(); + db.Relationships.AddRange( + new RealRelationship { AccountId = CallerId, PlayerId = OtherId, RelationshipType = RelationshipType.Friend }, + new RealRelationship { AccountId = CallerId, PlayerId = 5, RelationshipType = RelationshipType.FriendRequestSent }); + await db.SaveChangesAsync(); + + var api = CreateController(db); + var friends = await api.GetFriends(); + + var friend = Assert.Single(friends); + Assert.Equal(OtherId, friend.PlayerID); + } + + [Fact] + public async Task RelationshipWith_ServesRowOrBareNone() + { + using var db = CreateDb(); + var api = CreateController(db); + + // No history yet: a bare None projection, not an error. + var result = await api.RelationshipWith(OtherId); + var ok = Assert.IsType(result); + using var noneDoc = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value)); + Assert.Equal(0, noneDoc.RootElement.GetProperty("RelationshipType").GetInt32()); + Assert.Equal(OtherId, noneDoc.RootElement.GetProperty("PlayerID").GetInt64()); + + db.Relationships.Add(new RealRelationship + { + AccountId = CallerId, + PlayerId = OtherId, + RelationshipType = RelationshipType.Friend + }); + await db.SaveChangesAsync(); + + var friendResult = await api.RelationshipWith(OtherId); + var ok2 = Assert.IsType(friendResult); + using var friendDoc = JsonDocument.Parse(JsonSerializer.Serialize(ok2.Value)); + Assert.Equal(OtherId, friendDoc.RootElement.GetProperty("PlayerID").GetInt64()); + Assert.Equal(3, friendDoc.RootElement.GetProperty("RelationshipType").GetInt32()); + } + + // ---- photo tagging --------------------------------------------------- + + [Fact] + public async Task PhotoTaggingSetting_RoundTrips() + { + using var db = CreateDb(); + var api = CreateController(db); + + // Unset reads 0 as a bare integer. + var initial = await api.GetPhotoTaggingSetting(); + var initialOk = Assert.IsType(initial); + Assert.Equal(0, initialOk.Value); + + // Write 2 through a JSON body, then read it back. + api.ControllerContext.HttpContext.Request.ContentType = "application/json"; + api.ControllerContext.HttpContext.Request.Body = + new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(new { Setting = 2 })); + var written = await api.SetPhotoTaggingSetting(); + Assert.Equal(2, Assert.IsType(written).Value); + + api.ControllerContext.HttpContext.Request.Body = new MemoryStream(); + var read = await api.GetPhotoTaggingSetting(); + Assert.Equal(2, Assert.IsType(read).Value); + } +} diff --git a/ServerTests/InventionOwnershipTests.cs b/ServerTests/InventionOwnershipTests.cs new file mode 100644 index 0000000..080d6c1 --- /dev/null +++ b/ServerTests/InventionOwnershipTests.cs @@ -0,0 +1,127 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Inventions.V1; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Inventions; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The two invention contracts the store reads a player's owned state through. +/// +/// `api/inventions/v1/dormskinsfromids` answers the 20230831 client's +/// `HCNIEKLCFOG(IEnumerable<long>) -> Response<IReadOnlyList<long>>`, so the body is a bare array of +/// numbers and never a list of invention objects - an object body makes the client's number reader +/// throw ("expected:'Number Token', actual:'{'") instead of resolving anything. +/// +/// `api/inventions/v1/fulllineageowner` answers whether the player owns the lineage, which has to be +/// true for an invention they *bought*: the purchase path writes nothing but the id into +/// PlayerUnlocks.Inventions, and the route used to ignore that list entirely (and overwrote its +/// answer on every id it was asked about, so only the last id ever decided). +/// +public class InventionOwnershipTests +{ + private const long Buyer = 501; + private const long Creator = 502; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static InventionsV1 Api(RecDBContext db, long playerId) => new(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, playerId.ToString())], "test")) + } + } + }; + + private static InventionDB Invention(long id, long creator, bool hidden = false) => new() + { + InventionId = id, + CreatorPlayerId = creator, + Name = "Invention " + id, + IsPublished = true, + HideFromPlayer = hidden, + BlobName = "blob-" + id + }; + + [Fact] + public async Task DormSkinsFromIds_AnswersABareArrayOfNumbers() + { + await using var db = NewDb(); + db.Inventions.AddRange(Invention(11, Creator), Invention(12, Creator), Invention(13, Creator, hidden: true)); + await db.SaveChangesAsync(); + + var result = await Api(db, Buyer).GetDormSkinsFromIds(["11,12,13", "-1"]); + + Assert.Equal(new List { 11, 12 }, result); + Assert.Equal("[11,12]", JsonSerializer.Serialize(result)); + } + + [Fact] + public async Task DormSkinsFromIds_AnswersInTheCallersOrder() + { + await using var db = NewDb(); + db.Inventions.AddRange(Invention(61, Creator), Invention(62, Creator)); + await db.SaveChangesAsync(); + + var result = await Api(db, Buyer).GetDormSkinsFromIds(["62", "9999", "61"]); + + Assert.Equal(new List { 62, 61 }, result); + } + + [Fact] + public async Task FullLineageOwner_IsTrueForABoughtInvention() + { + await using var db = NewDb(); + db.Inventions.Add(Invention(21, Creator)); + db.Unlocks.Add(new PlayerUnlocks { AccountId = Buyer, Inventions = "[21]" }); + await db.SaveChangesAsync(); + + Assert.True(await Api(db, Buyer).OwnsInventionLineage([21])); + } + + [Fact] + public async Task FullLineageOwner_IsFalseForAnInventionThePlayerNeitherMadeNorBought() + { + await using var db = NewDb(); + db.Inventions.Add(Invention(31, Creator)); + await db.SaveChangesAsync(); + + Assert.False(await Api(db, Buyer).OwnsInventionLineage([31])); + } + + [Fact] + public async Task FullLineageOwner_AccumulatesAcrossTheIdsItIsAsked() + { + await using var db = NewDb(); + db.Inventions.AddRange(Invention(41, Creator), Invention(42, Creator)); + db.Unlocks.Add(new PlayerUnlocks { AccountId = Buyer, Inventions = "[42]" }); + await db.SaveChangesAsync(); + + Assert.False(await Api(db, Buyer).OwnsInventionLineage([41])); + Assert.True(await Api(db, Buyer).OwnsInventionLineage([41, 42])); + Assert.True(await Api(db, Buyer).OwnsInventionLineage([42, 41])); + } + + [Fact] + public async Task FullLineageOwner_IsTrueForTheCreator() + { + await using var db = NewDb(); + db.Inventions.Add(Invention(51, Buyer)); + await db.SaveChangesAsync(); + + Assert.True(await Api(db, Buyer).OwnsInventionLineage([51])); + } +} diff --git a/ServerTests/InviteFlowTests.cs b/ServerTests/InviteFlowTests.cs new file mode 100644 index 0000000..56d1ef6 --- /dev/null +++ b/ServerTests/InviteFlowTests.cs @@ -0,0 +1,1694 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Config; +using RadicadeServer.API.Events; +using RadicadeServer.API.Matchmaking; +using RadicadeServer.API.Notifications; +using RadicadeServer.API.Relationships; +using RadicadeServer.API.Relationships.V2; +using RadicadeServer.Clubs; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Matchmaking; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Relationships; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RoomServer; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class InviteFlowTests +{ + private const long CreatorId = 42; + private const long InviteeId = 43; + private const long StrangerId = 44; + private const long RoomId = 45; + private const long EventId = 46; + private const long ClubId = 47; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private sealed record FactorySnapshot( + IDbContextFactory Notify, + IDbContextFactory Room, + IDbContextFactory Matchmaking); + + private static FactorySnapshot SetFactories(IDbContextFactory factory) + { + var previous = new FactorySnapshot(Notify.DbFactory, RoomUtils.DbFactory, MatchmakingUtils.DbFactory); + Notify.DbFactory = factory; + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + return previous; + } + + private static void RestoreFactories(FactorySnapshot previous) + { + Notify.DbFactory = previous.Notify; + RoomUtils.DbFactory = previous.Room; + MatchmakingUtils.DbFactory = previous.Matchmaking; + } + + private static async Task<(RecDBContext Db, FactorySnapshot Previous)> CreateContextAsync() + { + var factory = new TestDbContextFactory(); + var previous = SetFactories(factory); + var db = factory.CreateDbContext(); + + db.Profiles.AddRange(ProfileFor(CreatorId), ProfileFor(InviteeId), ProfileFor(StrangerId)); + db.Players.AddRange( + new PlayerData { AccountId = CreatorId, Hashword = "test" }, + new PlayerData { AccountId = InviteeId, Hashword = "test" }, + new PlayerData { AccountId = StrangerId, Hashword = "test" }); + await db.SaveChangesAsync(); + return (db, previous); + } + + private static void SetUser(Controller controller, long userId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetFormBody(Controller controller, string body) + { + var request = controller.HttpContext.Request; + request.ContentType = "application/x-www-form-urlencoded"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + request.ContentLength = request.Body.Length; + } + + private static Profile ProfileFor(long id) => new() + { + accountId = id, + username = $"player{id}", + displayName = $"Player {id}", + profileImage = string.Empty, + createdAt = DateTime.UtcNow + }; + + private static async Task SeedRoomAsync(RecDBContext db, Accessibility accessibility = Accessibility.Public) + { + db.Rooms.Add(new RoomData + { + RoomId = RoomId, + Name = "Invite test room", + CreatorAccountId = CreatorId, + Accessibility = accessibility, + MaxPlayers = 8, + CreatedAt = DateTime.UtcNow + }); + db.SubRooms.Add(new SubRoom + { + RoomId = RoomId, + SubRoomId = 1, + Name = "Main", + MaxPlayers = 8, + Accessibility = Accessibility.Public, + UnitySceneId = "invite-test-scene" + }); + await db.SaveChangesAsync(); + } + + private static async Task SeedEventAsync( + RecDBContext db, + Accessibility eventAccess = Accessibility.Private, + Accessibility roomAccess = Accessibility.Public) + { + await SeedRoomAsync(db, roomAccess); + var ev = new PlayerEventDB + { + PlayerEventId = EventId, + Name = "Invite test event", + CreatorPlayerId = CreatorId, + RoomId = RoomId, + StartTime = DateTime.UtcNow.AddMinutes(-10), + EndTime = DateTime.UtcNow.AddHours(1), + Accessibility = eventAccess + }; + db.PlayerEvents.Add(ev); + db.Presences.Add(new RealPresence { playerId = CreatorId, isOnline = true }); + db.Presences.Add(new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + return ev; + } + + [Fact] + public async Task BulkEventInvitePersistsPendingResponseAndMessageAndAllowsPrivateEventJoin() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedEventAsync(db, Accessibility.Private, Accessibility.Private); + var controller = new EventsAPI(db); + SetUser(controller, CreatorId); + SetFormBody(controller, $"PlayerEventId={EventId}&InvitedPlayerIds%5B%5D={InviteeId}"); + + var result = await controller.BulkInvite(); + + Assert.IsType(result); + var response = await db.PlayerEventResponses.SingleAsync(x => x.PlayerEventId == EventId && x.PlayerId == InviteeId); + Assert.Equal(EventResponses.Pending, response.Type); + var message = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + Assert.Equal(MessageType.PlayerEventInvitation, message.Type); + Assert.Equal(EventId, message.PlayerEventId); + Assert.Equal(CreatorId, message.FromPlayerId); + Assert.Equal(EventResponses.Pending, + (await db.PlayerEventResponses.SingleAsync(x => x.PlayerEventId == EventId && x.PlayerId == InviteeId)).Type); + + var invitee = new EventsAPI(db); + SetUser(invitee, InviteeId); + Assert.IsType((await invitee.RespondToEventV2(EventId)).Result); + + var match = await MatchmakingUtils.MatchmakeEvent(InviteeId, EventId); + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.Equal(EventId, match.roomInstance?.eventId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task PrivateEventRejectsUninvitedRsvpAndDeclinedInviteeCannotReenter() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedEventAsync(db); + + var strangerRsvp = new EventsAPI(db); + SetUser(strangerRsvp, StrangerId); + Assert.IsType((await strangerRsvp.RespondToEventV2(EventId)).Result); + + var invitedRsvp = new EventsAPI(db); + SetUser(invitedRsvp, InviteeId); + Assert.IsType((await invitedRsvp.RespondToEventV2(EventId)).Result); + + db.Messages.Add(new Message + { + ToPlayerId = InviteeId, + FromPlayerId = CreatorId, + Type = MessageType.PlayerEventInvitation, + PlayerEventId = EventId + }); + db.PlayerEventResponses.Add(new PlayerEventResponse + { + PlayerEventId = EventId, + PlayerId = InviteeId, + Type = EventResponses.No + }); + await db.SaveChangesAsync(); + + var match = await MatchmakingUtils.MatchmakeEvent(InviteeId, EventId); + Assert.Equal(JoinStatus.EventIsPrivate, match.errorCode); + + var resend = new EventsAPI(db); + SetUser(resend, CreatorId); + SetFormBody(resend, $"PlayerEventId={EventId}&InvitedPlayerIds%5B%5D={InviteeId}"); + Assert.IsType(await resend.BulkInvite()); + Assert.Equal(2, await db.Messages.CountAsync(x => x.ToPlayerId == InviteeId && x.PlayerEventId == EventId)); + + var response = await db.PlayerEventResponses.SingleAsync(x => x.PlayerEventId == EventId && x.PlayerId == InviteeId); + Assert.Equal(EventResponses.Pending, response.Type); + var renewedRsvp = new EventsAPI(db); + SetUser(renewedRsvp, InviteeId); + Assert.IsType((await renewedRsvp.RespondToEventV2(EventId)).Result); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task GameInviteCarriesInstanceAndRoomIdsAndGrantsOnlyRecipientPrivateAccess() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "private-instance" + }; + db.RoomInstances.Add(instance); + await db.SaveChangesAsync(); + db.Presences.Add(new RealPresence + { + playerId = CreatorId, + roomInstance = instance.roomInstanceId, + isOnline = true + }); + db.Presences.Add(new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + + var inviter = new MatchmakingAPI(db); + SetUser(inviter, CreatorId); + Assert.True((await inviter.InvitePlayer(InviteeId, instance.roomInstanceId)).success); + + var invite = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + Assert.Equal(MessageType.GameInviteV2, invite.Type); + Assert.Equal(instance.roomInstanceId, RoomInviteDetails.GetInstanceId(invite)); + Assert.Equal(RoomId, invite.RoomId); + + // The client's V2 parser requires this JSON string before its button can join. + using var data = JsonDocument.Parse(invite.Data); + Assert.Equal(invite.Id, data.RootElement.GetProperty("InviteId").GetInt64()); + Assert.Equal("Invite test room", data.RootElement.GetProperty("Name").GetString()); + Assert.Equal(0, data.RootElement.GetProperty("InviteMode").GetInt32()); + var inbox = new RadicadeServer.API.Messages.MessagesAPI(db); + SetUser(inbox, InviteeId); + await inbox.DeleteMessage(new DeleteMessageRequestDTO { MessageIds = new() { invite.Id } }); + Assert.Empty((await inbox.GetMessages())!); + Assert.True((await db.Messages.SingleAsync(x => x.Id == invite.Id)).IsDismissed); + + var joiner = new MatchmakingAPI(db); + SetUser(joiner, InviteeId); + var accepted = await joiner.AcceptInviteByMessageId( + data.RootElement.GetProperty("InviteId").GetInt64(), null); + Assert.Equal(JoinStatus.Success, accepted!.errorCode); + Assert.Equal(instance.photonRoomId, accepted.roomInstance!.photonRoomId); + var joined = await joiner.GoToRoomInstance(instance.roomInstanceId, null); + Assert.Equal(JoinStatus.Success, joined.errorCode); + Assert.Equal(instance.roomInstanceId, + (await db.Presences.SingleAsync(x => x.playerId == InviteeId)).roomInstance); + + var forged = new Message + { + FromPlayerId = StrangerId, + ToPlayerId = StrangerId, + Type = MessageType.GameInvite, + Data = instance.roomInstanceId.ToString(), + RoomId = RoomId + }; + db.Messages.Add(forged); + await db.SaveChangesAsync(); + var forgedJoiner = new MatchmakingAPI(db); + SetUser(forgedJoiner, StrangerId); + var forgedDenied = await forgedJoiner.GoToRoomInstance(instance.roomInstanceId, null); + Assert.Equal(JoinStatus.RoomInstanceIsPrivate, forgedDenied.errorCode); + + db.Messages.Remove(invite); + db.Messages.Remove(forged); + await db.SaveChangesAsync(); + var nextPlayer = new MatchmakingAPI(db); + SetUser(nextPlayer, StrangerId); + var denied = await nextPlayer.GoToRoomInstance(instance.roomInstanceId, null); + Assert.Equal(JoinStatus.RoomInstanceIsPrivate, denied.errorCode); + + var uninvited = new MatchmakingAPI(db); + SetUser(uninvited, StrangerId); + var unauthorizedInvite = await uninvited.InvitePlayer(InviteeId, instance.roomInstanceId); + Assert.False(unauthorizedInvite.success); + } + finally + { + RestoreFactories(previousFactories); + } + } + + /// + /// The room half of an invite. Answering a game invite starts with fetching the room it + /// names (`GET rooms/rooms/{id}?include=5421...` and `rooms/rooms/bulk?id=` for its card), + /// and a private room used to answer 404 / [] to everyone but its owner - so the one player + /// who had just been invited could not see the room they were invited to, and the prompt had + /// nothing to join from. The invite is the access it grants and nothing more: a stranger + /// still gets 404, and a sender who has left the room stops granting it. + /// + [Fact] + public async Task InviteAlsoMakesThePrivateRoomFetchableForTheInviteeOnly() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + + // A dorm is the shape the live report has: the friend invited from their own dorm, + // and that is the private room a stranger's fetch answers 404 for. + var room = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); + room.IsDorm = true; + await db.SaveChangesAsync(); + + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "private-instance" + }; + db.RoomInstances.Add(instance); + await db.SaveChangesAsync(); + db.Presences.Add(new RealPresence + { + playerId = CreatorId, + roomInstance = instance.roomInstanceId, + isOnline = true + }); + db.Presences.Add(new RealPresence { playerId = InviteeId, isOnline = true }); + db.Presences.Add(new RealPresence { playerId = StrangerId, isOnline = true }); + await db.SaveChangesAsync(); + + var inviter = new MatchmakingAPI(db); + SetUser(inviter, CreatorId); + Assert.True((await inviter.InvitePlayer(InviteeId, instance.roomInstanceId)).success); + + // The room itself, through the same handler the client's fetch lands on. + var inviteeRooms = new RoomServerAPI(db); + SetUser(inviteeRooms, InviteeId); + Assert.IsType((await inviteeRooms.GetRoomById(RoomId)).Result); + + var strangerRooms = new RoomServerAPI(db); + SetUser(strangerRooms, StrangerId); + Assert.IsType((await strangerRooms.GetRoomById(RoomId)).Result); + + // The card the prompt renders from is the same rule, so the bulk answer carries the + // room for the invitee and stays empty for everyone else. + var inviteeBulk = await inviteeRooms.GetBulkRooms(null, new List { RoomId }); + Assert.Single(inviteeBulk!); + var strangerBulk = await strangerRooms.GetBulkRooms(null, new List { RoomId }); + Assert.Empty(strangerBulk!); + + // The inviter leaving takes the invite's worth with it: there is no instance left to + // be placed in, so the room goes back to being invisible. + (await db.Presences.SingleAsync(x => x.playerId == CreatorId)).roomInstance = null; + await db.SaveChangesAsync(); + Assert.IsType((await inviteeRooms.GetRoomById(RoomId)).Result); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task AcceptedInviteKeepsDormLoadableAfterMessageDeletion(bool chatInvite) + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + var room = await db.Rooms.SingleAsync(); + room.IsDorm = true; + var instance = new RoomInstance + { + roomId = RoomId, subRoomId = 1, isPrivate = true, + maxCapacity = 8, photonRoomId = "invite-room-load" + }; + db.RoomInstances.Add(instance); + await db.SaveChangesAsync(); + db.Presences.AddRange( + new RealPresence { playerId = CreatorId, roomInstance = instance.roomInstanceId, isOnline = true }, + new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + var inviter = new MatchmakingAPI(db); + SetUser(inviter, CreatorId); + Assert.True((await inviter.InvitePlayer(InviteeId, instance.roomInstanceId)).success); + var invite = await db.Messages.SingleAsync(); + var accept = new MatchmakingAPI(db); + SetUser(accept, InviteeId); + if (chatInvite) + { + db.Chats.Add(new ChatThread + { + chatThreadId = 70, chatThreadName = "", + playerIds = $"[{InviteeId},{CreatorId}]", + messages = JsonSerializer.Serialize(new[] { new ChatMessage + { + chatMessageId = 2, chatThreadeId = 70, senderPlayerId = CreatorId, + timeSent = DateTime.UtcNow, + contents = "{\"Type\":1,\"Version\":1,\"Data\":\"Party invite\"}" + } }) + }); + await db.SaveChangesAsync(); + Assert.Equal(JoinStatus.Success, (await accept.MatchmakeChatInvite(70, 2, null))!.errorCode); + } + else + Assert.Equal(JoinStatus.Success, (await accept.AcceptInviteByMessageId(invite.Id, null))!.errorCode); + + // Inbox cleanup and the host leaving must not undo an accepted room admission. + db.Messages.Remove(invite); + (await db.Presences.SingleAsync(x => x.playerId == CreatorId)).roomInstance = null; + await db.SaveChangesAsync(); + var rooms = new RoomServerAPI(db); + SetUser(rooms, InviteeId); + var loaded = Assert.IsType((await rooms.GetRoomById(RoomId)).Result); + Assert.NotEmpty(Assert.IsType(loaded.Value).SubRooms); + Assert.Single((await rooms.GetBulkRooms(null, new List { RoomId }))!); + SetUser(rooms, StrangerId); + Assert.IsType((await rooms.GetRoomById(RoomId)).Result); + + var presence = await db.Presences.SingleAsync(x => x.playerId == InviteeId); + presence.isOnline = false; + await db.SaveChangesAsync(); + SetUser(rooms, InviteeId); + Assert.IsType((await rooms.GetRoomById(RoomId)).Result); + presence.isOnline = true; + presence.roomInstance = null; + await db.SaveChangesAsync(); + Assert.IsType((await rooms.GetRoomById(RoomId)).Result); + } + finally { RestoreFactories(previousFactories); } + } + + [Fact] + public async Task ExistingMemberCanForwardPrivateRoomInviteAndTheInviteeCanJoin() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "forwarded-private-instance" + }; + db.RoomInstances.Add(instance); + db.Presences.AddRange( + new RealPresence { playerId = CreatorId, roomInstance = instance.roomInstanceId, isOnline = true }, + new RealPresence { playerId = InviteeId, roomInstance = instance.roomInstanceId, isOnline = true }, + new RealPresence { playerId = StrangerId, isOnline = true }); + await db.SaveChangesAsync(); + db.Messages.Add(new Message + { + ToPlayerId = InviteeId, + FromPlayerId = CreatorId, + Type = MessageType.GameInvite, + Data = instance.roomInstanceId.ToString(), + RoomId = RoomId + }); + await db.SaveChangesAsync(); + + var inviter = new MatchmakingAPI(db); + SetUser(inviter, InviteeId); + Assert.True((await inviter.InvitePlayer(StrangerId, instance.roomInstanceId)).success); + var forwardedInvite = await db.Messages.SingleAsync(x => x.ToPlayerId == StrangerId); + Assert.Equal(InviteeId, forwardedInvite.FromPlayerId); + + var joiner = new MatchmakingAPI(db); + SetUser(joiner, StrangerId); + var joined = await joiner.GoToRoomInstance(instance.roomInstanceId, null); + Assert.Equal(JoinStatus.Success, joined.errorCode); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task PlayerInPrivateInstanceOfPublicRoomCanInvitePartyMemberToJoin() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Public); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "player-private-instance" + }; + db.RoomInstances.Add(instance); + db.Presences.Add(new RealPresence + { + playerId = StrangerId, + roomInstance = instance.roomInstanceId, + isOnline = true + }); + db.Presences.Add(new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + + var inviter = new MatchmakingAPI(db); + SetUser(inviter, StrangerId); + Assert.True((await inviter.InvitePlayer(InviteeId, instance.roomInstanceId)).success); + + var invite = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + Assert.Equal(StrangerId, invite.FromPlayerId); + Assert.Equal(instance.roomInstanceId, RoomInviteDetails.GetInstanceId(invite)); + Assert.Equal(RoomId, invite.RoomId); + + var joiner = new MatchmakingAPI(db); + SetUser(joiner, InviteeId); + var joined = await joiner.GoToRoomInstance(instance.roomInstanceId, null); + Assert.Equal(JoinStatus.Success, joined.errorCode); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task CoOwnerRoleInviteCanBeAcceptedByInviteeButNotForgedByAnotherPlayer() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, "role=30"); + var inviteResult = await api.InvitePlayerToRole(RoomId, InviteeId); + Assert.True(inviteResult!.success); + + var inviteMessage = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + Assert.Equal(MessageType.RoomCoOwnerInvited, inviteMessage.Type); + Assert.Equal(RoomId, inviteMessage.RoomId); + Assert.Equal(RoleType.None, + (await db.RoomRoles.SingleAsync(x => x.RoomId == RoomId && x.AccountId == InviteeId)).Role); + + var forged = new RoomServerAPI(db); + SetUser(forged, StrangerId); + SetFormBody(forged, "role=30"); + Assert.False((await forged.SetPlayerRole(RoomId, InviteeId))!.success); + + var acceptance = new RoomServerAPI(db); + SetUser(acceptance, InviteeId); + SetFormBody(acceptance, ""); + Assert.True((await acceptance.SetPlayerRole(RoomId, InviteeId))!.success); + Assert.Equal(RoleType.CoOwner, + (await db.RoomRoles.SingleAsync(x => x.RoomId == RoomId && x.AccountId == InviteeId)).Role); + Assert.Contains(await db.Messages.ToListAsync(), x => x.ToPlayerId == InviteeId && x.Type == MessageType.RoomCoOwnerAdded); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task ClubInviteAddsConfiguredMembershipAndRequiresCoownerAuthority() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await ClubStore.SaveAsync(db, new List + { + new() { clubId = ClubId, ownerPlayerId = CreatorId, name = "Test club" } + }); + + var unauthorized = new ClubsAPI(db); + SetUser(unauthorized, InviteeId); + SetFormBody(unauthorized, $"accountId={StrangerId}&membershipType=10"); + var denied = await unauthorized.InviteMember(ClubId); + Assert.IsType(denied); + Assert.Equal(StatusCodes.Status403Forbidden, ((ObjectResult)denied).StatusCode); + + var api = new ClubsAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, $"accountId={InviteeId}&membershipType=20"); + var result = await api.InviteMember(ClubId); + Assert.IsType(result); + + var updated = await ClubStore.GetAsync(db, ClubId); + Assert.Equal(ClubMembershipTypes.PendingInvited, ClubStore.GetMembershipType(updated, InviteeId)); + Assert.Equal(1, ClubStore.GetMemberCount(updated)); + var pendingMember = Assert.Single(ClubStore.GetMembers(updated), member => member.AccountId == InviteeId); + Assert.Equal(CreatorId, pendingMember.InviterAccountId); + Assert.Equal(ClubMembershipTypes.PendingInvited, pendingMember.MembershipType); + var pendingWireMember = Assert.IsType>( + ClubStore.BuildMember(pendingMember, updated, ProfileFor(InviteeId))); + Assert.Equal(0, pendingWireMember["membershipStatus"]); + Assert.Equal(CreatorId, pendingWireMember["inviterAccountId"]); + + var message = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + Assert.Equal(MessageType.ClubModeratorInvited, message.Type); + Assert.Equal(ClubId.ToString(), message.Data); + Assert.Equal(ClubMembershipTypes.Moderator.ToString(), message.Details); + + // Only the invitee can activate the stored tier; before acceptance the + // pending row isn't counted as a member or given the invited role. + var forgedAcceptance = new ClubsAPI(db); + SetUser(forgedAcceptance, StrangerId); + Assert.IsType(await forgedAcceptance.AcceptClubInvitation(ClubId)); + + var acceptance = new ClubsAPI(db); + SetUser(acceptance, InviteeId); + var accepted = Assert.IsType(await acceptance.AcceptClubInvitation(ClubId)); + var acceptedClub = await ClubStore.GetAsync(db, ClubId); + Assert.Equal(ClubMembershipTypes.Moderator, ClubStore.GetMembershipType(acceptedClub, InviteeId)); + Assert.Equal(2, ClubStore.GetMemberCount(acceptedClub)); + var activeMember = Assert.Single(ClubStore.GetMembers(acceptedClub), member => member.AccountId == InviteeId); + var activeWireMember = Assert.IsType>( + ClubStore.BuildMember(activeMember, acceptedClub, ProfileFor(InviteeId))); + Assert.Equal(1, activeWireMember["membershipStatus"]); + Assert.Contains("Moderator", JsonSerializer.Serialize(accepted.Value)); + } + finally + { + RestoreFactories(previousFactories); + } + } + + // ---- The spellings a 20230831 client can post --------------------------------- + + /// + /// The invite surface is pinned by shape because a 404 is what the player reads as + /// `Invite Failed!`: the bare form spelling the September client provably posts (its own + /// log shows `POST /matchmaking/invite` with a form body on an Invite press), the path + /// spelling beside it for a build that puts the target in the path, and the offline variant + /// beside the api routes. + /// + /// The client's `matchmake/invite/{0}` is NOT a send spelling. It sits in the contiguous + /// `matchmake/*` JOIN family, so it serves the accept-by-invite-id spelling + /// (), beside the chat-party + /// acceptance `matchmake/chatinvite/{threadId}/{messageId}`. Treating the path as a send + /// would invite whatever account happened to hold the same number as an invite's own + /// message id. + /// + [Fact] + public void InviteIsServedUnderEverySpellingTheClientsPost() + { + var sends = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.Invite))! + .GetCustomAttributes() + .Select(attribute => attribute.Template!) + .ToArray(); + + Assert.Contains("invite", sends); + Assert.Contains("invite/{playerId:long}", sends); + Assert.DoesNotContain(sends, route => route.StartsWith("matchmake/", StringComparison.Ordinal)); + + Assert.Equal("matchmake/invite/{inviteId:long}", + Assert.Single(typeof(MatchmakingAPI) + .GetMethod(nameof(MatchmakingAPI.AcceptInviteByMessageId))! + .GetCustomAttributes()).Template); + Assert.Equal("matchmake/chatinvite/{threadId:long}/{messageId:long}", + Assert.Single(typeof(MatchmakingAPI) + .GetMethod(nameof(MatchmakingAPI.MatchmakeChatInvite))! + .GetCustomAttributes()).Template); + + var offline = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.SendOfflineInvite))!; + Assert.Equal("~/api/offlineinvite/v1/send", + Assert.Single(offline.GetCustomAttributes()).Template); + + // The party-invite link lifetime rides the settings routes, as a bare single-key object: + // the api host the sender's client reads, and the chat host the receiver's card fetches + // when a party invite message arrives. + var partyInviteRoutes = typeof(PlayerSettingsAPI) + .GetMethod(nameof(PlayerSettingsAPI.GetPartyInviteSettings))! + .GetCustomAttributes() + .Select(attribute => attribute.Template!) + .ToArray(); + Assert.Contains("partyinvite", partyInviteRoutes); + Assert.Contains("~/chat/settings/partyinvite", partyInviteRoutes); + Assert.Contains("api/settings", typeof(PlayerSettingsAPI) + .GetCustomAttributes().Select(attribute => attribute.Template)); + } + + [Fact] + public void PartyInviteSettingsAnswerTheBareSingleKeyObject() + { + using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + + var result = Assert.IsType(new PlayerSettingsAPI(db).GetPartyInviteSettings()); + + using var json = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)); + var property = Assert.Single(json.RootElement.EnumerateObject()); + Assert.Equal("InviteLinkLifetimeInMinutes", property.Name); + Assert.Equal( + JsonValueKind.Number, + property.Value.ValueKind); + } + + /// + /// The path spelling carries no instance at all, so the invite has to mean "the instance I am + /// standing in" - the caller's own presence row, which is the same row the invite insists the + /// inviter be live in. + /// + [Fact] + public async Task PathShapedInviteWithoutAnInstanceInvitesIntoTheCallersCurrentInstance() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "path-shaped-invite" + }; + db.RoomInstances.Add(instance); + db.Presences.AddRange( + new RealPresence { playerId = CreatorId, roomInstance = instance.roomInstanceId, isOnline = true }, + new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + + var api = new MatchmakingAPI(db); + SetUser(api, CreatorId); + api.Request.RouteValues = new RouteValueDictionary { ["playerId"] = InviteeId.ToString() }; + + var result = await api.Invite(); + + Assert.True(result!.success); + var invite = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + Assert.Equal(MessageType.GameInviteV2, invite.Type); + Assert.Equal(CreatorId, invite.FromPlayerId); + Assert.Equal(instance.roomInstanceId, RoomInviteDetails.GetInstanceId(invite)); + Assert.Equal(RoomId, invite.RoomId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + /// + /// No instance in the request AND none on the caller is the one case worth refusing loudly: + /// the invite would otherwise be stored pointing at nothing, and the recipient's client would + /// read a join prompt for a room that does not exist. + /// + [Fact] + public async Task InviteWithNoInstanceAndNoPresenceIsRefusedWithoutStoringAnything() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new MatchmakingAPI(db); + SetUser(api, CreatorId); + api.Request.RouteValues = new RouteValueDictionary { ["playerId"] = InviteeId.ToString() }; + + var result = await api.Invite(); + + Assert.False(result!.success); + Assert.Empty(await db.Messages.ToListAsync()); + } + finally + { + RestoreFactories(previousFactories); + } + } + + /// + /// An offline invite is the same stored game invite: the target simply has no hub frame to + /// receive, and reads the stored message when they next sign in. Nothing about the invite is + /// weakened for them - the instance and the room still name where to go. + /// + [Fact] + public async Task OfflineInviteStoresTheSameGameInviteForATargetWithNoPresence() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = false, + maxCapacity = 8, + photonRoomId = "offline-invite" + }; + db.RoomInstances.Add(instance); + db.Presences.Add(new RealPresence + { + playerId = CreatorId, + roomInstance = instance.roomInstanceId, + isOnline = true + }); + await db.SaveChangesAsync(); + + var api = new MatchmakingAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, $"invitedPlayerId={InviteeId}&roomInstanceId={instance.roomInstanceId}"); + + var result = await api.SendOfflineInvite(); + + Assert.True(result.success); + var invite = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + Assert.Equal(MessageType.GameInviteV2, invite.Type); + Assert.Equal(CreatorId, invite.FromPlayerId); + Assert.Equal(instance.roomInstanceId, RoomInviteDetails.GetInstanceId(invite)); + Assert.Equal(RoomId, invite.RoomId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + // ---- Accepting --------------------------------------------------------------- + + /// + /// The client's accept spelling for a game invite is its own `matchmake/invite/{0}`, the id + /// being the invite message the hub pushed. The invite authorizes that exact instance for + /// its recipient - a private instance is what an invite exists to get into - and the move + /// is the same presence write a join performs, so the client's follow-up sees the recipient + /// standing in the sender's instance. + /// + [Fact] + public async Task AcceptingAStoredGameInviteMovesTheInviteeIntoTheInviterInstance() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "accepted-game-invite" + }; + db.RoomInstances.Add(instance); + db.Presences.AddRange( + new RealPresence { playerId = CreatorId, roomInstance = instance.roomInstanceId, isOnline = true }, + new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + + db.Messages.Add(new Message + { + FromPlayerId = CreatorId, + ToPlayerId = InviteeId, + Type = MessageType.GameInvite, + Data = instance.roomInstanceId.ToString(), + RoomId = RoomId + }); + await db.SaveChangesAsync(); + var invite = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + + var accept = new MatchmakingAPI(db); + SetUser(accept, InviteeId); + var result = await accept.AcceptInviteByMessageId(invite.Id, null); + + Assert.NotNull(result); + Assert.Equal(JoinStatus.Success, result!.errorCode); + Assert.Equal(instance.roomInstanceId, result.roomInstance?.roomInstanceId); + var presence = await db.Presences.SingleAsync(x => x.playerId == InviteeId); + Assert.Equal(instance.roomInstanceId, presence.roomInstance); + Assert.True(presence.isOnline); + Assert.Equal(JoinStatus.Success, presence.errorCode); + } + finally + { + RestoreFactories(previousFactories); + } + } + + /// + /// Every way a game invite can be stale or forged is answered with the code the client + /// already has for it, rather than with a status it reads as a network failure: someone + /// else's invite is expired, a note the caller wrote themselves is not an invite, a sender + /// with no live session is not online, a sender who moved on makes the invite stale, and an + /// instance that is gone names nothing to join. + /// + [Fact] + public async Task AcceptingAGameInviteRefusesStaleForgedAndOfflineInvitesByName() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "refused-game-invite" + }; + db.RoomInstances.Add(instance); + db.Presences.AddRange( + new RealPresence { playerId = CreatorId, roomInstance = instance.roomInstanceId, isOnline = true }, + new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + + Message Store(long to, long from, string data) + { + var message = new Message + { + FromPlayerId = from, + ToPlayerId = to, + Type = MessageType.GameInvite, + Data = data, + RoomId = RoomId + }; + db.Messages.Add(message); + db.SaveChanges(); + return message; + } + + async Task AcceptAs(long caller, long inviteId) + { + var api = new MatchmakingAPI(db); + SetUser(api, caller); + return await api.AcceptInviteByMessageId(inviteId, null); + } + + // Addressed to someone else: the caller has no such invite. + var foreign = Store(StrangerId, CreatorId, instance.roomInstanceId.ToString()); + var foreignResult = await AcceptAs(InviteeId, foreign.Id); + Assert.Equal(JoinStatus.RoomInviteExpired, foreignResult!.errorCode); + Assert.Null(foreignResult.roomInstance); + + // Written by the caller themselves. + var selfSent = Store(InviteeId, InviteeId, instance.roomInstanceId.ToString()); + Assert.Equal(JoinStatus.ChatMessageNotAnInvite, (await AcceptAs(InviteeId, selfSent.Id))!.errorCode); + + // The sender has no live session to be standing in the instance. + var fromOffline = Store(InviteeId, StrangerId, instance.roomInstanceId.ToString()); + Assert.Equal(JoinStatus.PlayerNotOnline, (await AcceptAs(InviteeId, fromOffline.Id))!.errorCode); + + // The sender moved to another instance, so the invite no longer names where they are. + var elsewhere = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "somewhere-else" + }; + db.RoomInstances.Add(elsewhere); + await db.SaveChangesAsync(); + (await db.Presences.SingleAsync(x => x.playerId == CreatorId)).roomInstance = elsewhere.roomInstanceId; + await db.SaveChangesAsync(); + var moved = Store(InviteeId, CreatorId, instance.roomInstanceId.ToString()); + Assert.Equal(JoinStatus.RoomInviteExpired, (await AcceptAs(InviteeId, moved.Id))!.errorCode); + + // The named instance is no longer live. + (await db.Presences.SingleAsync(x => x.playerId == CreatorId)).roomInstance = 999_999; + await db.SaveChangesAsync(); + var gone = Store(InviteeId, CreatorId, "999999"); + Assert.Equal(JoinStatus.RoomIsNotActive, (await AcceptAs(InviteeId, gone.Id))!.errorCode); + } + finally + { + RestoreFactories(previousFactories); + } + } + + /// + /// The chat-party acceptance the client spells `matchmake/chatinvite/{threadId}/{messageId}`: + /// when the message is stored, its sender is the inviter and their live instance is the + /// destination - the same stored thread the chat surface serves, read here for who wrote + /// the invite rather than for what it says. + /// + [Fact] + public async Task AcceptingAChatInviteResolvesTheInviterFromTheStoredMessage() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "accepted-chat-invite" + }; + db.RoomInstances.Add(instance); + db.Presences.AddRange( + new RealPresence { playerId = CreatorId, roomInstance = instance.roomInstanceId, isOnline = true }, + new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + + const long ThreadId = 70; + db.Chats.Add(new ChatThread + { + chatThreadId = ThreadId, + chatThreadName = "", + playerIds = $"[{InviteeId},{CreatorId}]", + messages = JsonSerializer.Serialize(new List + { + new() + { + chatMessageId = 2, + chatThreadeId = ThreadId, + senderPlayerId = CreatorId, + timeSent = DateTime.UtcNow, + contents = "\"come hang out\"" + } + }) + }); + await db.SaveChangesAsync(); + + var accept = new MatchmakingAPI(db); + SetUser(accept, InviteeId); + var result = await accept.MatchmakeChatInvite(ThreadId, 2, null); + + Assert.NotNull(result); + Assert.Equal(JoinStatus.Success, result!.errorCode); + Assert.Equal(instance.roomInstanceId, result.roomInstance?.roomInstanceId); + Assert.Equal(instance.roomInstanceId, + (await db.Presences.SingleAsync(x => x.playerId == InviteeId)).roomInstance); + } + finally + { + RestoreFactories(previousFactories); + } + } + + /// + /// A message id this server never stored falls back to the thread's single other member - + /// the only person a party invite in that conversation could be from. A caller who is not + /// in the thread is refused by name, and a stored message that is too old (or written by + /// the caller) is refused with the code the client already has for it. + /// + [Fact] + public async Task ChatInviteAcceptFallsBackToTheThreadsOtherMemberAndRefusesTheRest() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedRoomAsync(db, Accessibility.Private); + var instance = new RoomInstance + { + roomId = RoomId, + subRoomId = 1, + isPrivate = true, + maxCapacity = 8, + photonRoomId = "chat-invite-fallback" + }; + db.RoomInstances.Add(instance); + db.Presences.AddRange( + new RealPresence { playerId = CreatorId, roomInstance = instance.roomInstanceId, isOnline = true }, + new RealPresence { playerId = InviteeId, isOnline = true }); + await db.SaveChangesAsync(); + + const long ThreadId = 71; + const long ExpiredThreadId = 72; + const long ForeignThreadId = 73; + db.Chats.AddRange( + new ChatThread + { + chatThreadId = ThreadId, + chatThreadName = "", + playerIds = $"[{InviteeId},{CreatorId}]", + messages = "[]" + }, + new ChatThread + { + chatThreadId = ExpiredThreadId, + chatThreadName = "", + playerIds = $"[{InviteeId},{CreatorId}]", + messages = JsonSerializer.Serialize(new List + { + new() + { + chatMessageId = 5, + chatThreadeId = ExpiredThreadId, + senderPlayerId = CreatorId, + timeSent = DateTime.UtcNow.AddMinutes(-(PlayerSettingsAPI.InviteLinkLifetimeInMinutes + 5)), + contents = "\"too old\"" + } + }) + }, + new ChatThread + { + chatThreadId = ForeignThreadId, + chatThreadName = "", + playerIds = $"[{CreatorId},{StrangerId}]", + messages = "[]" + }); + await db.SaveChangesAsync(); + + // Not stored, exactly one other member: they are the inviter. + var fallback = new MatchmakingAPI(db); + SetUser(fallback, InviteeId); + var fallbackResult = await fallback.MatchmakeChatInvite(ThreadId, 77, null); + Assert.Equal(JoinStatus.Success, fallbackResult!.errorCode); + Assert.Equal(instance.roomInstanceId, + (await db.Presences.SingleAsync(x => x.playerId == InviteeId)).roomInstance); + + var expired = new MatchmakingAPI(db); + SetUser(expired, InviteeId); + Assert.Equal(JoinStatus.RoomInviteExpired, + (await expired.MatchmakeChatInvite(ExpiredThreadId, 5, null))!.errorCode); + + var foreign = new MatchmakingAPI(db); + SetUser(foreign, InviteeId); + Assert.Equal(JoinStatus.ChatPartyInviteNotFound, + (await foreign.MatchmakeChatInvite(ForeignThreadId, 1, null))!.errorCode); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task FriendInviteCreatesReciprocalPendingRowsAndDurableMessage() + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var sender = new RelationshipsV2(db); + SetUser(sender, CreatorId); + + Assert.IsType(await sender.SendFriendRequest(InviteeId)); + + Assert.Equal(RelationshipType.FriendRequestSent, + (await db.Relationships.SingleAsync(x => x.AccountId == CreatorId && x.PlayerId == InviteeId)).RelationshipType); + Assert.Equal(RelationshipType.FriendRequestReceived, + (await db.Relationships.SingleAsync(x => x.AccountId == InviteeId && x.PlayerId == CreatorId)).RelationshipType); + var invite = await db.Messages.SingleAsync(x => x.ToPlayerId == InviteeId); + Assert.Equal(MessageType.FriendInvite, invite.Type); + Assert.Equal(CreatorId, invite.FromPlayerId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task TheBroadcasterKeepsTheirOwnInstanceAndAViewerIsMatchedElsewhere() + { + // The instance a broadcaster registers is their own room. A viewer who may join the + // event is still not placed in it - a broadcast advertises the session, it does not + // open it - so matchmaking hands them the event's ordinary instance instead. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + ev.IsMultiInstance = true; + + var ordinary = new RoomInstance { roomInstanceId = 500, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + var broadcast = new RoomInstance { roomInstanceId = 501, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + db.RoomInstances.AddRange(ordinary, broadcast); + await db.SaveChangesAsync(); + + ev.BroadcastingRoomInstanceId = broadcast.roomInstanceId; + await db.SaveChangesAsync(); + + var host = await MatchmakingUtils.MatchmakeEvent(CreatorId, EventId); + Assert.Equal(JoinStatus.Success, host.errorCode); + Assert.Equal(broadcast.roomInstanceId, host.roomInstance?.roomInstanceId); + + var viewer = await MatchmakingUtils.MatchmakeEvent(InviteeId, EventId); + Assert.Equal(JoinStatus.Success, viewer.errorCode); + Assert.NotEqual(broadcast.roomInstanceId, viewer.roomInstance?.roomInstanceId); + Assert.Equal(ordinary.roomInstanceId, viewer.roomInstance?.roomInstanceId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task AnInvitedViewerIsStillNotPlacedInTheBroadcastersInstance() + { + // An invitation grants the event, not a place in the broadcaster's own room: an invited + // viewer is matched into an ordinary instance exactly like an uninvited one. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + ev.IsMultiInstance = true; + db.PlayerEventResponses.Add(new PlayerEventResponse + { + PlayerEventId = EventId, PlayerId = InviteeId, Type = EventResponses.Yes + }); + + var ordinary = new RoomInstance { roomInstanceId = 500, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + var broadcast = new RoomInstance { roomInstanceId = 501, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + db.RoomInstances.AddRange(ordinary, broadcast); + await db.SaveChangesAsync(); + + ev.BroadcastingRoomInstanceId = broadcast.roomInstanceId; + await db.SaveChangesAsync(); + + var match = await MatchmakingUtils.MatchmakeEvent(InviteeId, EventId); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.Equal(ordinary.roomInstanceId, match.roomInstance?.roomInstanceId); + Assert.NotEqual(broadcast.roomInstanceId, match.roomInstance?.roomInstanceId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task AViewerGetsTheirOwnInstanceWhenTheBroadcastIsTheOnlyOne() + { + // With nothing else to share, the viewer is given a fresh instance of the event's room - + // the broadcast instance is never reused - and it is public, so the next viewer lands + // beside them instead of causing a third instance. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + ev.IsMultiInstance = true; + db.Presences.Add(new RealPresence { playerId = StrangerId, isOnline = true }); + + var broadcast = new RoomInstance { roomInstanceId = 501, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + db.RoomInstances.Add(broadcast); + await db.SaveChangesAsync(); + + ev.BroadcastingRoomInstanceId = broadcast.roomInstanceId; + await db.SaveChangesAsync(); + + var first = await MatchmakingUtils.MatchmakeEvent(InviteeId, EventId); + + Assert.Equal(JoinStatus.Success, first.errorCode); + Assert.NotNull(first.roomInstance); + Assert.NotEqual(broadcast.roomInstanceId, first.roomInstance!.roomInstanceId); + Assert.Equal(EventId, first.roomInstance.eventId); + Assert.False(first.roomInstance.isPrivate); + + var second = await MatchmakingUtils.MatchmakeEvent(StrangerId, EventId); + + Assert.Equal(JoinStatus.Success, second.errorCode); + Assert.Equal(first.roomInstance.roomInstanceId, second.roomInstance?.roomInstanceId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task AViewerIsNotPlacedWithTheBroadcasterWhenTheRegisteredInstanceIsStale() + { + // Re-entering the event hands the broadcaster a fresh instance and nothing re-registers + // it, so the id the event still carries can name a room nobody is in. The instance the + // broadcaster is actually standing in is off-limits for the same reason the registered + // one is - otherwise the protection lapses the first time they walk back in. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + ev.IsMultiInstance = true; + + var registered = new RoomInstance { roomInstanceId = 501, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + var standingIn = new RoomInstance { roomInstanceId = 502, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + db.RoomInstances.AddRange(registered, standingIn); + await db.SaveChangesAsync(); + + ev.BroadcastingRoomInstanceId = registered.roomInstanceId; + // The broadcaster has since re-entered the event: their presence names 502 while the + // event still advertises the instance they registered, 501. + db.Presences.Single(x => x.playerId == CreatorId).roomInstance = standingIn.roomInstanceId; + await db.SaveChangesAsync(); + + var viewer = await MatchmakingUtils.MatchmakeEvent(InviteeId, EventId); + + Assert.Equal(JoinStatus.Success, viewer.errorCode); + Assert.NotNull(viewer.roomInstance); + Assert.NotEqual(registered.roomInstanceId, viewer.roomInstance!.roomInstanceId); + Assert.NotEqual(standingIn.roomInstanceId, viewer.roomInstance.roomInstanceId); + Assert.Equal(EventId, viewer.roomInstance.eventId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task CreatorCanJoinOwnEventLongBeforeItStarts() + { + // The host walks into their own event whenever they want, so the room they are + // advertising is already stood up (and they can be standing in it) hours early. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db); + ev.StartTime = DateTime.UtcNow.AddHours(6); + ev.EndTime = DateTime.UtcNow.AddHours(7); + await db.SaveChangesAsync(); + + var match = await MatchmakingUtils.MatchmakeEvent(CreatorId, EventId); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.Equal(EventId, match.roomInstance?.eventId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task NonCreatorIsStillHeldToTheEventStartTime() + { + // The early gate is the host's alone. An invited guest who shows up before the + // start time is turned away rather than dropped into an event that has not begun. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + ev.StartTime = DateTime.UtcNow.AddHours(6); + ev.EndTime = DateTime.UtcNow.AddHours(7); + await db.SaveChangesAsync(); + + var match = await MatchmakingUtils.MatchmakeEvent(InviteeId, EventId); + + Assert.Equal(JoinStatus.EventNotStarted, match.errorCode); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task CreatorCannotJoinAnEventThatHasAlreadyFinished() + { + // The exemption opens the door early, not indefinitely: once the window closes the + // event is closed to its own creator too. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db); + ev.StartTime = DateTime.UtcNow.AddHours(-2); + ev.EndTime = DateTime.UtcNow.AddHours(-1); + await db.SaveChangesAsync(); + + var match = await MatchmakingUtils.MatchmakeEvent(CreatorId, EventId); + + Assert.Equal(JoinStatus.EventAlreadyFinished, match.errorCode); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Theory] + [InlineData(false, 3)] + [InlineData(true, 6)] + public async Task EventInstanceWireTypeMatchesMultiInstanceSetting(bool multiInstance, int wireType) + { + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + ev.IsMultiInstance = multiInstance; + db.RoomInstances.Add(new RoomInstance + { + roomInstanceId = 501, roomId = RoomId, subRoomId = 1, + eventId = EventId, maxCapacity = 8, + roomInstanceType = RadicadeServer.RecNet.Enums.Rooms.RoomInstanceType.Event + }); + await db.SaveChangesAsync(); + + // Reusing a room created before multi-instance was enabled must repair its type. + var existing = await MatchmakingUtils.MatchmakeEvent(CreatorId, EventId); + Assert.Equal(JoinStatus.Success, existing.errorCode); + Assert.Equal(501, existing.roomInstance!.roomInstanceId); + Assert.Equal(wireType, (int)existing.roomInstance.roomInstanceType); + db.ChangeTracker.Clear(); + Assert.Equal(wireType, (int)(await db.RoomInstances.SingleAsync()).roomInstanceType); + + // Fresh private event instances need the same type; privacy is a separate flag. + var created = await MatchmakingUtils.MatchmakeEvent(CreatorId, EventId, JoinMode.PrivateNewInstance); + Assert.Equal(JoinStatus.Success, created.errorCode); + Assert.True(created.roomInstance!.isPrivate); + var json = System.Text.Json.JsonSerializer.SerializeToElement(created.roomInstance); + Assert.Equal(wireType, json.GetProperty("roomInstanceType").GetInt32()); + } + finally { RestoreFactories(previousFactories); } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task PrivateModeOnAnEventCreatesThePlayersOwnPrivateInstance(bool sourceStillOccupied) + { + // PRIVATE MODE on the event screen asks for the player's own copy of the event's + // room. It has to be created even while the event has a shared instance and a live + // broadcast instance to hand out - and it must be neither of those, or pressing the + // button would move the player into somebody else's room. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var ev = await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + ev.IsMultiInstance = true; + + var shared = new RoomInstance { roomInstanceId = 501, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + var broadcast = new RoomInstance { roomInstanceId = 502, roomId = RoomId, subRoomId = 1, eventId = EventId, maxCapacity = 8 }; + db.RoomInstances.AddRange(shared, broadcast); + await db.SaveChangesAsync(); + ev.BroadcastingRoomInstanceId = broadcast.roomInstanceId; + if (sourceStillOccupied) + { + var presenter = await db.Presences.SingleAsync(p => p.playerId == InviteeId); + presenter.isOnline = true; + presenter.roomInstance = broadcast.roomInstanceId; + } + await db.SaveChangesAsync(); + + var match = await MatchmakingUtils.MatchmakeEvent( + CreatorId, EventId, JoinMode.PrivateNewInstance); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.NotNull(match.roomInstance); + Assert.NotEqual(shared.roomInstanceId, match.roomInstance.roomInstanceId); + Assert.NotEqual(broadcast.roomInstanceId, match.roomInstance.roomInstanceId); + Assert.Equal(EventId, match.roomInstance.eventId); + // Event rooms stay event instances; `isPrivate` is what makes this one the + // player's own. + Assert.True(match.roomInstance.isPrivate); + // Matchmaking joined the player through its own context, so read the presence back + // fresh rather than through this context's tracked copy. + db.ChangeTracker.Clear(); + Assert.Equal(sourceStillOccupied ? (long?)broadcast.roomInstanceId : null, + (await db.PlayerEvents.SingleAsync(e => e.PlayerEventId == EventId)).BroadcastingRoomInstanceId); + Assert.Equal(match.roomInstance.roomInstanceId, + (await db.Presences.AsNoTracking().SingleAsync(x => x.playerId == CreatorId)).roomInstance); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task EachPlayersPrivateModeGetsItsOwnInstance() + { + // Two people pressing PRIVATE MODE on the same event do not end up together: every + // request is a new room, so neither is handed the other's private instance. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + + var host = await MatchmakingUtils.MatchmakeEvent( + CreatorId, EventId, JoinMode.PrivateNewInstance); + var guest = await MatchmakingUtils.MatchmakeEvent( + InviteeId, EventId, JoinMode.PrivateNewInstance); + + Assert.Equal(JoinStatus.Success, host.errorCode); + Assert.Equal(JoinStatus.Success, guest.errorCode); + Assert.NotNull(host.roomInstance); + Assert.NotNull(guest.roomInstance); + Assert.True(host.roomInstance!.isPrivate); + Assert.True(guest.roomInstance!.isPrivate); + Assert.NotEqual(host.roomInstance.roomInstanceId, guest.roomInstance.roomInstanceId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task PrivateModeLeavesNoInstanceBehindForSomeoneWhoMayNotBeInTheRoom() + { + // The event's room is private and this player is not the host, not a co-owner and has + // no invitation, so PRIVATE MODE is refused - refused before anything is created, so + // a refused press does not leave an empty private instance in the room. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedEventAsync(db, Accessibility.Public, Accessibility.Private); + db.Presences.Add(new RealPresence { playerId = StrangerId, isOnline = true }); + await db.SaveChangesAsync(); + var before = await db.RoomInstances.CountAsync(); + + var match = await MatchmakingUtils.MatchmakeEvent( + StrangerId, EventId, JoinMode.PrivateNewInstance); + + Assert.Equal(JoinStatus.RoomIsPrivate, match.errorCode); + Assert.Equal(before, await db.RoomInstances.CountAsync()); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task EventRouteHonoursTheClientsPrivateMode() + { + // Both event routes take the same GoToRequest as every other go-to call, and the join + // mode on it is what the event screen's PRIVATE MODE button sends. Without it the + // route answered with a shared instance and the client waited on a private room that + // was never created. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedEventAsync(db, Accessibility.Public, Accessibility.Public); + var controller = new MatchmakingAPI(db); + SetUser(controller, CreatorId); + + var privateJoin = await controller.GoToEvent(EventId, + new GoToRequest { JoinMode = JoinMode.PrivateNewInstance }); + + Assert.Equal(JoinStatus.Success, privateJoin!.errorCode); + Assert.True(privateJoin.roomInstance!.isPrivate); + + // And the ordinary join is still the shared one: the private instance above is + // never handed out by the plain lookup. + var ordinaryJoin = await controller.GoToEvent(EventId, + new GoToRequest { JoinMode = JoinMode.PublicMatchmaking }); + + Assert.Equal(JoinStatus.Success, ordinaryJoin!.errorCode); + Assert.False(ordinaryJoin.roomInstance!.isPrivate); + Assert.NotEqual(privateJoin.roomInstance.roomInstanceId, + ordinaryJoin.roomInstance.roomInstanceId); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task EventParticipantsMayInviteAndAStrangerIsRefusedWithTheCreatorNamed() + { + // The gate used to be creator-only. The client offers the invite button to players who are + // already on the event, so an attendee was answered an anonymous 403 for a press the client + // had invited them to make. + const long OutsiderId = 99; + + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedEventAsync(db); + db.PlayerEventResponses.Add(new PlayerEventResponse + { + PlayerEventId = EventId, + PlayerId = InviteeId, + Type = EventResponses.Yes + }); + await db.SaveChangesAsync(); + + var participant = new EventsAPI(db); + SetUser(participant, InviteeId); + SetFormBody(participant, $"PlayerEventId={EventId}&InvitedPlayerIds%5B%5D={StrangerId}"); + Assert.IsType(await participant.BulkInvite()); + Assert.Equal(EventResponses.Pending, (await db.PlayerEventResponses + .SingleAsync(x => x.PlayerEventId == EventId && x.PlayerId == StrangerId)).Type); + + var outsider = new EventsAPI(db); + SetUser(outsider, OutsiderId); + SetFormBody(outsider, $"PlayerEventId={EventId}&InvitedPlayerIds%5B%5D={InviteeId}"); + + var refused = Assert.IsType(await outsider.BulkInvite()); + + Assert.Equal(StatusCodes.Status403Forbidden, refused.StatusCode); + Assert.Contains($"\"CreatorPlayerId\":{CreatorId}", JsonSerializer.Serialize(refused.Value)); + } + finally + { + RestoreFactories(previousFactories); + } + } + + [Fact] + public async Task AnEventWhoseCreatorIsGoneMayBeInvitedToByAnyone() + { + // A wiped-and-reseeded database leaves events whose creating account no longer exists. Nobody + // is left to invite for them, so refusing every caller made the event permanently unusable. + var (db, previousFactories) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + await SeedEventAsync(db); + var orphan = await db.PlayerEvents.SingleAsync(x => x.PlayerEventId == EventId); + orphan.CreatorPlayerId = 987654; + await db.SaveChangesAsync(); + + var controller = new EventsAPI(db); + SetUser(controller, StrangerId); + SetFormBody(controller, $"PlayerEventId={EventId}&InvitedPlayerIds%5B%5D={InviteeId}"); + + Assert.IsType(await controller.BulkInvite()); + } + finally + { + RestoreFactories(previousFactories); + } + } +} diff --git a/ServerTests/KeepsakeRoomConfigTests.cs b/ServerTests/KeepsakeRoomConfigTests.cs new file mode 100644 index 0000000..0cfae34 --- /dev/null +++ b/ServerTests/KeepsakeRoomConfigTests.cs @@ -0,0 +1,332 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Keepsakes; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Keepsakes; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Keepsake rooms: which rooms the server tells the client it may place keepsakes in. +/// +/// The client's room-role permissions have no keepsake entry and its room DTO carries no keepsake +/// field, so the room's KeepsakeRoomListId - the thing it reports as missing with +/// "This room does not have keepsake permissions." - can only arrive with +/// GET /api/keepsakes/rooms/{id}. A configured room therefore answers with the room fields +/// even while it holds nothing, and an unconfigured one keeps the old NO CONTENT. +/// +public class KeepsakeRoomConfigTests +{ + private const long Room = 25; + + private static RecDBContext NewDb() + => new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + + private static KeepsakesAPI CreateApi(RecDBContext? db) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "2")); + var context = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }; + return new KeepsakesAPI(db!) { ControllerContext = new ControllerContext { HttpContext = context } }; + } + + /// + /// Runs one endpoint call against a config. The seam is process-wide and the endpoint reads it + /// per call, so the override only has to be in place for the call itself. + /// + private static async Task WithConfigAsync(KeepsakeRoomConfig config, Func> call) + { + var previous = KeepsakeRoomConfig.Override; + KeepsakeRoomConfig.Override = config; + try + { + return await call(); + } + finally + { + KeepsakeRoomConfig.Override = previous; + } + } + + [Fact] + public void NothingConfiguredIsNothingConfigured() + { + var config = KeepsakeRoomConfig.Parse(null); + + Assert.False(config.Any); + Assert.False(config.IsKeepsakeRoom(Room)); + Assert.Equal(KeepsakeRoomConfig.DefaultRoomListId, config.RoomListId); + Assert.Equal(KeepsakeRoomConfig.DefaultRoomLimit, config.RoomLimit); + } + + [Fact] + public void ExplicitEnvironmentRoomsReplaceAllRoomsFromFile() + { + var config = KeepsakeRoomConfig.Parse("""{"AllRooms":true}""", rooms: "25"); + Assert.True(config.IsKeepsakeRoom(25)); + Assert.False(config.IsKeepsakeRoom(26)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(4294967297L)] + public void InvalidRoomLimitsUseDefault(long limit) + { + var config = KeepsakeRoomConfig.Parse($$"""{"RoomLimit":{{limit}}}"""); + Assert.Equal(KeepsakeRoomConfig.DefaultRoomLimit, config.RoomLimit); + } + + [Fact] + public void AllRoomsFlagMakesEveryRoomAKeepsakeRoom() + { + var config = KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""); + + Assert.True(config.Any); + Assert.True(config.IsKeepsakeRoom(1)); + Assert.True(config.IsKeepsakeRoom(Room)); + + // No useful list of "every room" exists, so the payload advertises the room asked about. + Assert.Equal(new[] { Room }, config.AdvertisedRooms(Room)); + } + + [Fact] + public void RoomIdsAcceptNumbersAndStrings() + { + var config = KeepsakeRoomConfig.Parse("""{ "RoomIds": [3, "25"], "RoomListId": 7, "RoomLimit": 5 }"""); + + Assert.True(config.IsKeepsakeRoom(3)); + Assert.True(config.IsKeepsakeRoom(Room)); + Assert.False(config.IsKeepsakeRoom(26)); + Assert.Equal(7, config.RoomListId); + Assert.Equal(5, config.RoomLimit); + Assert.Equal(new long[] { 3, Room }, config.AdvertisedRooms(Room)); + } + + [Fact] + public void ABareArrayIsAListOfRooms() + { + var config = KeepsakeRoomConfig.Parse("[3, 25]"); + + Assert.True(config.IsKeepsakeRoom(3)); + Assert.True(config.IsKeepsakeRoom(Room)); + } + + /// + /// A half-written file is "nothing configured", not a crash and not an enabled server: this + /// switch decides whether rooms may hold keepsakes, so a typo must not turn it on. + /// + [Fact] + public void AMalformedFileConfiguresNothing() + { + var config = KeepsakeRoomConfig.Parse("{ \"RoomIds\": [3,"); + + Assert.False(config.Any); + Assert.False(config.IsKeepsakeRoom(3)); + } + + /// + /// The environment names the rooms INSTEAD of the file: an operator reaching for + /// RADICADE_KEEPSAKE_ROOMS=* means "all of them", not "all of them plus this list". + /// + [Fact] + public void TheEnvironmentReplacesTheFilesRooms() + { + var replaced = KeepsakeRoomConfig.Parse("""{ "RoomIds": [3] }""", rooms: "25;26"); + Assert.False(replaced.IsKeepsakeRoom(3)); + Assert.True(replaced.IsKeepsakeRoom(25)); + Assert.True(replaced.IsKeepsakeRoom(26)); + + var all = KeepsakeRoomConfig.Parse("""{ "RoomIds": [3] }""", rooms: "*"); + Assert.True(all.AllRooms); + Assert.True(all.IsKeepsakeRoom(9999)); + } + + [Fact] + public void TheEnvironmentCarriesTheListIdAndLimit() + { + var config = KeepsakeRoomConfig.Parse(null, rooms: "25", roomListId: "9", roomLimit: "12"); + + Assert.Equal(9, config.RoomListId); + Assert.Equal(12, config.RoomLimit); + } + + /// + /// A keepsake room answers with the room fields even while it holds nothing, and the answer + /// still carries the three instance lists the client parses. + /// + [Fact] + public async Task AConfiguredRoomAnswersTheRoomFields() + { + await using var db = NewDb(); + var api = CreateApi(db); + + var result = await WithConfigAsync( + KeepsakeRoomConfig.Parse("""{ "RoomIds": [25], "RoomListId": 7, "RoomLimit": 5 }"""), + () => api.Room(Room)); + + var ok = Assert.IsType(result); + var json = JsonSerializer.SerializeToElement(ok.Value); + + Assert.Equal(Room, json.GetProperty("KeepsakeRoomId").GetInt64()); + Assert.Equal(7, json.GetProperty("KeepsakeRoomListId").GetInt64()); + Assert.Equal(5, json.GetProperty("KeepsakeRoomLimit").GetInt32()); + Assert.Equal(new long[] { Room }, json.GetProperty("KeepsakeRooms").EnumerateArray().Select(e => e.GetInt64()).ToArray()); + + // The list the client resolves has to contain the room it is loading. + var list = Assert.Single(json.GetProperty("KeepsakeRoomLists").EnumerateArray()); + Assert.Equal(7, list.GetProperty("KeepsakeRoomListId").GetInt64()); + Assert.Equal(new long[] { Room }, list.GetProperty("KeepsakeRooms").EnumerateArray().Select(e => e.GetProperty("RoomId").GetInt64()).ToArray()); + + // Nothing is placed, so there is nothing for the keepsake loader to resolve - which is + // what keeps this answer from being the body the client null-references over. + Assert.Empty(json.GetProperty("KeepsakeInstances").EnumerateArray()); + Assert.Empty(json.GetProperty("KeepsakeCollectionRecords").EnumerateArray()); + // The room names the progression event its keepsakes belong to: that id is what the client + // looks up among the active events before it offers a keepsake to place at all. + Assert.Equal( + new[] { KeepsakeRoomConfig.DefaultEventId }, + json.GetProperty("KeepsakeProgressionEventIds").EnumerateArray().Select(e => e.GetInt64()).ToArray()); + } + + [Fact] + public async Task AnUnconfiguredRoomStillAnswersNoContent() + { + await using var db = NewDb(); + var api = CreateApi(db); + + var result = await WithConfigAsync(KeepsakeRoomConfig.Parse("""{ "RoomIds": [999] }"""), () => api.Room(Room)); + + Assert.IsType(result); + } + + /// A room that already holds a keepsake keeps its body whether or not it is listed - + /// while the feature itself is on, which is forced here because a server with no keepsake room + /// configured is a server with keepsakes disabled. + [Fact] + public async Task ARoomWithAPlacedKeepsakeKeepsItsBody() + { + await using var db = NewDb(); + await db.KeepsakeInstances.AddAsync(new KeepsakeInstance + { + KeepsakeInstanceId = Guid.Parse("a75f7547-79eb-47c6-8986-6767abcb4f92"), + RoomId = Room, + KeepsakeCategoryId = 0, + PlacedByAccountId = 2, + PlacedAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + + var api = CreateApi(db); + var previousFeature = KeepsakeFeature.Override; + KeepsakeFeature.Override = true; + try + { + var result = await WithConfigAsync(KeepsakeRoomConfig.Parse(null), () => api.Room(Room)); + + var ok = Assert.IsType(result); + var json = JsonSerializer.SerializeToElement(ok.Value); + + Assert.Single(json.GetProperty("KeepsakeInstances").EnumerateArray()); + Assert.False(json.TryGetProperty("KeepsakeRoomListId", out _)); + } + finally + { + KeepsakeFeature.Override = previousFeature; + } + } + + /// + /// ...and with the feature OFF the same room answers NO CONTENT, keepsake and all. That is the + /// point of the switch: a disabled server does not describe keepsakes to a client at all, so a + /// room holding a placed keepsake is not the exception that still talks about one. + /// + [Fact] + public async Task TheFeatureSwitchHidesEvenPlacedKeepsakes() + { + await using var db = NewDb(); + await db.KeepsakeInstances.AddAsync(new KeepsakeInstance + { + KeepsakeInstanceId = Guid.NewGuid(), + RoomId = Room, + KeepsakeCategoryId = 0, + PlacedByAccountId = 2, + PlacedAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + + var previousFeature = KeepsakeFeature.Override; + KeepsakeFeature.Override = false; + try + { + var result = await WithConfigAsync(KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""), + () => CreateApi(db).Room(Room)); + + Assert.IsType(result); + } + finally + { + KeepsakeFeature.Override = previousFeature; + } + } + + /// + /// The file this repository ships gives every room the permission. That answer is the one the + /// client reads off `GET /api/keepsakes/rooms/{id}` as "this room can hold keepsakes", so a + /// checkout that shipped it switched off would keep the MakerPen banner the feature exists to + /// clear. Narrowing it is an operator edit - `AllRooms: false` plus the ids in `RoomIds`. + /// + [Fact] + public void TheShippedConfigEnablesKeepsakesAsRequested() + { + var path = ShippedConfigPath(); + Assert.NotNull(path); + + var config = KeepsakeRoomConfig.Parse(File.ReadAllText(path!)); + + Assert.True(config.Any); + Assert.True(config.AllRooms); + Assert.True(config.IsKeepsakeRoom(25)); + Assert.True(config.RoomLimit > 0); + Assert.True(config.EventId > 0); + } + + /// + /// The feature switch follows that config: no keepsake room configured means the whole feature + /// is off, which is the state the room, the catalog, the event and the client's own feature flag + /// all read - see . RADICADE_KEEPSAKES is the operator's override of + /// exactly that inference. + /// + [Fact] + public void NoKeepsakeRoomMeansTheFeatureIsOff() + { + Assert.False(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse(null), environment: null)); + Assert.False(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse(null), environment: "off")); + Assert.True(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""), environment: null)); + Assert.True(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse("""{ "RoomIds": [25] }"""), environment: null)); + Assert.False(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""), environment: "off")); + Assert.True(KeepsakeFeature.EnabledFor(KeepsakeRoomConfig.Parse(null), environment: "on")); + } + + /// + /// The checked-in config, found from the test binaries rather than from the working directory: + /// the test host's cwd is a bin folder, so the file the repository ships would otherwise go + /// unread and this guard would silently pass. + /// + private static string? ShippedConfigPath() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + for (var depth = 0; depth < 6 && directory != null; depth++, directory = directory.Parent) + { + var candidate = Path.Join(directory.FullName, "Server", "RadicadeServer-Config", KeepsakeRoomConfig.FileName); + if (File.Exists(candidate)) return candidate; + } + + return null; + } +} diff --git a/ServerTests/KeepsakesStoreTests.cs b/ServerTests/KeepsakesStoreTests.cs new file mode 100644 index 0000000..d1b930b --- /dev/null +++ b/ServerTests/KeepsakesStoreTests.cs @@ -0,0 +1,451 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Keepsakes; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Keepsakes; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The keepsake store: a placed keepsake is the room save's GUID, seeing a room without one is +/// still NO CONTENT, and collecting is per player and idempotent. +/// +public class KeepsakesStoreTests : IDisposable +{ + private const long Room = 25; + + private readonly KeepsakeRoomConfig? _previousRooms; + private readonly bool? _previousFeature; + + /// + /// These tests exercise the feature, so they configure it rather than inheriting whatever the + /// shipped config says: keepsakes are OFF unless a keepsake room is configured + /// (), and the shipped file lists no room. Pinning "every room is a + /// keepsake room" here is also what makes a test's own override - the "no keepsake rooms" cases + /// below - a deliberate change rather than a fallback. + /// + public KeepsakesStoreTests() + { + _previousRooms = KeepsakeRoomConfig.Override; + _previousFeature = KeepsakeFeature.Override; + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""); + KeepsakeFeature.Override = null; + } + + public void Dispose() + { + KeepsakeRoomConfig.Override = _previousRooms; + KeepsakeFeature.Override = _previousFeature; + } + + private static RecDBContext NewDb() + => new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + + private static KeepsakesAPI CreateApi(RecDBContext db, long accountId = 2, string? body = null) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + var context = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }; + if (body != null) + { + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + context.Request.ContentType = "application/json"; + } + return new KeepsakesAPI(db) { ControllerContext = new ControllerContext { HttpContext = context } }; + } + + private static Guid PlacedGuid(IActionResult result) + { + var ok = Assert.IsType(result); + var json = JsonSerializer.SerializeToElement(ok.Value); + return json.GetProperty("KeepsakeInstanceId").GetGuid(); + } + + /// + /// The room's keepsake carries the id the ROOM SAVE already has. Minting a different one here + /// is how a room ends up with a keepsake the server knows under another name, so the id in the + /// placement body is the id that has to come back. + /// + [Fact] + public async Task PlacementKeepsTheRoomSavesInstanceId() + { + var savedId = Guid.Parse("a75f7547-79eb-47c6-8986-6767abcb4f92"); + await using var db = NewDb(); + var api = CreateApi(db, body: $$""" + {"RoomId":{{Room}},"SubRoomId":null,"KeepsakeCategoryConfigId":0, + "KeepsakeInstanceId":"{{savedId}}"} + """); + + var placed = await api.Place(Room); + Assert.Equal(savedId, PlacedGuid(placed)); + + var stored = await db.KeepsakeInstances.SingleAsync(); + Assert.Equal(Room, stored.RoomId); + Assert.Equal(0, stored.KeepsakeCategoryId); + Assert.Equal(2, stored.PlacedByAccountId); + } + + [Fact] + public async Task UnknownCategoryIsRejectedRatherThanStored() + { + await using var db = NewDb(); + var api = CreateApi(db, body: """{"KeepsakeCategoryConfigId":99}"""); + + Assert.IsType(await api.Place(Room)); + Assert.Empty(db.KeepsakeInstances); + } + + /// + /// One placed keepsake, seen by the player who placed it: it is an instance to collect and + /// there is no collection record yet. This is the body the room-load phase reads - an id here + /// that has no row behind it is what faults the load, so the two lists move together. + /// + [Fact] + public async Task RoomServesTheInstancesAndTheCallersRecords() + { + await using var db = NewDb(); + var id = Guid.NewGuid(); + await CreateApi(db, body: $$"""{"KeepsakeCategoryConfigId":5,"KeepsakeInstanceId":"{{id}}"}""").Place(Room); + + var result = Assert.IsType(await CreateApi(db).Room(Room)); + var json = JsonSerializer.SerializeToElement(result.Value); + + var instance = Assert.Single(json.GetProperty("KeepsakeInstances").EnumerateArray()); + Assert.Equal(id, instance.GetProperty("KeepsakeInstanceId").GetGuid()); + Assert.Equal(5, instance.GetProperty("KeepsakeCategoryConfigId").GetInt32()); + Assert.Equal(Room, instance.GetProperty("KeepsakeRoomId").GetInt64()); + + Assert.Empty(json.GetProperty("KeepsakeCollectionRecords").EnumerateArray()); + + // A permitted room names the keepsake event, which is what the client checks for before it + // offers a keepsake at all - the list of events is never empty for one of these rooms. + Assert.NotEmpty(json.GetProperty("KeepsakeProgressionEventIds").EnumerateArray()); + } + + [Fact] + public async Task CollectingMovesTheInstanceIntoTheCallersRecords() + { + await using var db = NewDb(); + var id = Guid.NewGuid(); + await CreateApi(db, body: $$"""{"KeepsakeCategoryConfigId":6,"KeepsakeInstanceId":"{{id}}"}""").Place(Room); + + var collected = Assert.IsType(await CreateApi(db, body: $$"""{"KeepsakeInstanceIds":["{{id}}"]}""").Collect(Room)); + var collectedJson = JsonSerializer.SerializeToElement(collected.Value); + Assert.Equal(Room, collectedJson.GetProperty("KeepsakeRoomId").GetInt64()); + + var result = Assert.IsType(await CreateApi(db).Room(Room)); + var json = JsonSerializer.SerializeToElement(result.Value); + + var record = Assert.Single(json.GetProperty("KeepsakeCollectionRecords").EnumerateArray()); + Assert.Equal(id, record.GetProperty("KeepsakeInstanceId").GetGuid()); + Assert.Equal(6, record.GetProperty("KeepsakeCategoryConfigId").GetInt32()); + + // Collected keepsakes are no longer instances to hand out - a second body for the same + // keepsake would put the player back in front of something they already have. + Assert.Empty(json.GetProperty("KeepsakeInstances").EnumerateArray()); + } + + [Fact] + public async Task CollectingTwiceKeepsTheFirstRecord() + { + await using var db = NewDb(); + var id = Guid.NewGuid(); + await CreateApi(db, body: $$"""{"KeepsakeCategoryConfigId":0,"KeepsakeInstanceId":"{{id}}"}""").Place(Room); + + await CreateApi(db, body: $$"""{"KeepsakeInstanceIds":["{{id}}"]}""").Collect(Room); + var first = await db.KeepsakeCollectionRecords.SingleAsync(); + + await CreateApi(db, body: $$"""{"KeepsakeInstanceIds":["{{id}}"]}""").Collect(Room); + var records = db.KeepsakeCollectionRecords.ToList(); + + Assert.Single(records); + Assert.Equal(first.CollectedAt, records[0].CollectedAt); + } + + /// + /// Another player's collection is not mine: their records do not hide an instance from me. + /// + [Fact] + public async Task CollectionIsPerPlayer() + { + await using var db = NewDb(); + var id = Guid.NewGuid(); + await CreateApi(db, accountId: 2, body: $$"""{"KeepsakeCategoryConfigId":0,"KeepsakeInstanceId":"{{id}}"}""").Place(Room); + await CreateApi(db, accountId: 2, body: $$"""{"KeepsakeInstanceIds":["{{id}}"]}""").Collect(Room); + + var result = Assert.IsType(await CreateApi(db, accountId: 3).Room(Room)); + var json = JsonSerializer.SerializeToElement(result.Value); + + Assert.Single(json.GetProperty("KeepsakeInstances").EnumerateArray()); + Assert.Empty(json.GetProperty("KeepsakeCollectionRecords").EnumerateArray()); + } + + /// + /// An unnamed keepsake call is named in the log and answered 404, never 204: a write that looks + /// successful while the room save holds an id nothing backs is the failure this all exists to + /// avoid, and the log line is how the spelling gets found. + /// + [Fact] + public async Task UnclaimedRoutesAreReportedAndRefused() + { + await using var db = NewDb(); + var api = CreateApi(db, body: """{"KeepsakeInstanceIds":[]}"""); + + Assert.IsType(await api.Unclaimed("rooms/25/whatever")); + } + + /// + /// Deleted keepsakes stay in the table but stop being served. The override pins "no keepsake + /// rooms" so this reads the removed instance and not the shipped config, which gives every + /// room the permission and would otherwise answer this room with an empty list instead. + /// + [Fact] + public async Task RemovedInstancesAreNotServed() + { + await using var db = NewDb(); + var id = Guid.NewGuid(); + await CreateApi(db, body: $$"""{"KeepsakeCategoryConfigId":4,"KeepsakeInstanceId":"{{id}}"}""").Place(Room); + + var instance = await db.KeepsakeInstances.SingleAsync(); + instance.Removed = true; + await db.SaveChangesAsync(); + + var original = KeepsakeRoomConfig.Override; + try + { + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse(null); + Assert.IsType(await CreateApi(db).Room(Room)); + } + finally + { + KeepsakeRoomConfig.Override = original; + } + } + + /// + /// The same removed instance in a room the server DOES advertise as a keepsake room: the room + /// fields come back - the permission the shipped config grants - and the deleted instance is + /// still absent from them. + /// + [Fact] + public async Task ARemovedInstanceIsNotServedEvenInAKeepsakeRoom() + { + await using var db = NewDb(); + var id = Guid.NewGuid(); + await CreateApi(db, body: $$"""{"KeepsakeCategoryConfigId":4,"KeepsakeInstanceId":"{{id}}"}""").Place(Room); + + var instance = await db.KeepsakeInstances.SingleAsync(); + instance.Removed = true; + await db.SaveChangesAsync(); + + var original = KeepsakeRoomConfig.Override; + try + { + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""); + var ok = Assert.IsType(await CreateApi(db).Room(Room)); + var json = JsonSerializer.SerializeToElement(ok.Value); + + Assert.Empty(json.GetProperty("KeepsakeInstances").EnumerateArray()); + Assert.Equal(Room, json.GetProperty("KeepsakeRoomId").GetInt64()); + } + finally + { + KeepsakeRoomConfig.Override = original; + } + } + + [Fact] + public void CatalogCarriesTheReferenceCategories() + { + var results = KeepsakeCatalog.Results(); + Assert.Equal(5, results.Length); + Assert.Equal("explore", KeepsakeCatalog.Find(0)!.VisualId); + Assert.Equal(3, KeepsakeCatalog.Find(0)!.LimitPerRoom); + Assert.Equal(40, KeepsakeCatalog.Find(6)!.XpValue); + Assert.Null(KeepsakeCatalog.Find(99)); + } + + [Fact] + public async Task CollectionAcknowledgesOnlyExistingActiveInstancesInThisRoom() + { + await using var db = NewDb(); + var id = PlacedGuid(await CreateApi(db, body: """{"CategoryId":0}""").Place(Room)); + var other = PlacedGuid(await CreateApi(db, body: """{"CategoryId":0}""").Place(Room + 1)); + var result = Assert.IsType(await CreateApi(db, body: + $$"""{"InstanceIds":["{{id}}","{{id}}","{{other}}","{{Guid.NewGuid()}}"]}""").Collect(Room)); + Assert.Equal(id, Assert.Single(JsonSerializer.SerializeToElement(result.Value) + .GetProperty("KeepsakeInstanceIds").EnumerateArray()).GetGuid()); + Assert.Single(db.KeepsakeCollectionRecords); + } + + [Fact] + public async Task RemovalIsRepeatablePreservesHistoryAndPreventsFurtherCollection() + { + await using var db = NewDb(); + var id = PlacedGuid(await CreateApi(db, body: """{"CategoryId":0}""").Place(Room)); + var body = $$"""{"InstanceIds":["{{id}}"]}"""; + await CreateApi(db, body: body).Collect(Room); + Assert.IsType(await CreateApi(db, accountId: 3).Remove(Room, id)); + Assert.IsType(await CreateApi(db).Remove(Room + 1, id)); + Assert.IsType(await CreateApi(db).Remove(Room, id)); + Assert.IsType(await CreateApi(db).Remove(Room, id)); + await CreateApi(db, accountId: 3, body: body).Collect(Room); + Assert.Single(db.KeepsakeCollectionRecords); + Assert.True((await db.KeepsakeInstances.SingleAsync()).Removed); + } + + [Fact] + public async Task PlacementCannotTakeAnotherPlayersInstanceOrMoveItToAnotherRoom() + { + await using var db = NewDb(); + var id = PlacedGuid(await CreateApi(db, body: """{"CategoryId":0}""").Place(Room)); + var body = $$"""{"CategoryId":0,"KeepsakeInstanceId":"{{id}}"}"""; + Assert.IsType(await CreateApi(db, accountId: 3, body: body).Place(Room)); + Assert.IsType(await CreateApi(db, body: body).Place(Room + 1)); + Assert.Equal(Room, (await db.KeepsakeInstances.SingleAsync()).RoomId); + } + + [Fact] + public async Task RoomAndCategoryLimitsAllowRetriesButRejectAdditionalPlacements() + { + await using var db = NewDb(); + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse("""{"AllRooms":true,"RoomLimit":1}"""); + var id = PlacedGuid(await CreateApi(db, body: """{"CategoryId":0}""").Place(Room)); + Assert.IsType(await CreateApi(db, body: """{"CategoryId":4}""").Place(Room)); + Assert.Equal(id, PlacedGuid(await CreateApi(db, body: + $$"""{"CategoryId":0,"KeepsakeInstanceId":"{{id}}"}""").Place(Room))); + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse("""{"AllRooms":true,"RoomLimit":10}"""); + await CreateApi(db, body: """{"CategoryId":0}""").Place(Room); + await CreateApi(db, body: """{"CategoryId":0}""").Place(Room); + Assert.IsType(await CreateApi(db, body: """{"CategoryId":0}""").Place(Room)); + } + + [Fact] + public async Task ValidBodyLargerThanLogPreviewIsParsedInFull() + { + await using var db = NewDb(); + var body = JsonSerializer.Serialize(new { Padding = new string('x', 2100), CategoryId = 0 }); + PlacedGuid(await CreateApi(db, body: body).Place(Room)); + Assert.Single(db.KeepsakeInstances); + } + + [Fact] + public async Task PlacementRequiresAnAccountAndAPermittedRoom() + { + await using var db = NewDb(); + var api = CreateApi(db, body: """{"CategoryId":0}"""); + api.HttpContext.User = new ClaimsPrincipal(); + Assert.IsType(await api.Place(Room)); + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse("""{"RoomIds":[26]}"""); + Assert.IsType(await CreateApi(db, body: """{"CategoryId":0}""").Place(Room)); + Assert.Empty(db.KeepsakeInstances); + } + + [Fact] + public async Task SeptemberClientCanPlaceReadCollectAndDeleteUsingItsActualRoutes() + { + await using var db = NewDb(); + var placed = Assert.IsType(await CreateApi(db, + body: $$"""{"RoomId":{{Room}},"SubRoomId":null,"KeepsakeCategory":4}""").PlaceFromClient()); + var id = Assert.IsType(placed.Value); + var room = JsonSerializer.SerializeToElement(Assert.IsType(await CreateApi(db).Room(Room)).Value); + Assert.Equal(id, Assert.Single(room.GetProperty("Instances").EnumerateArray()).GetProperty("KeepsakeInstanceId").GetGuid()); + + var eventId = KeepsakeRoomConfig.Current.EventId; + var before = JsonSerializer.SerializeToElement(Assert.IsType(await CreateApi(db).EventInstances(eventId)).Value); + Assert.Equal(Room, Assert.Single(before.GetProperty("Instances").EnumerateArray()).GetProperty("RoomId").GetInt64()); + Assert.Empty(before.GetProperty("CollectionRecords").EnumerateArray()); + + var reward = JsonSerializer.SerializeToElement(Assert.IsType(await CreateApi(db).CollectFromClient(id)).Value); + Assert.Equal(20, reward.GetProperty("TotalXp").GetInt32()); + reward = JsonSerializer.SerializeToElement(Assert.IsType(await CreateApi(db).CollectFromClient(id)).Value); + Assert.Equal(0, reward.GetProperty("TotalXp").GetInt32()); + Assert.Single(db.KeepsakeCollectionRecords); + room = JsonSerializer.SerializeToElement(Assert.IsType(await CreateApi(db).Room(Room)).Value); + Assert.Single(room.GetProperty("Instances").EnumerateArray()); + Assert.Equal(2, Assert.Single(room.GetProperty("CollectionRecords").EnumerateArray()).GetProperty("AccountId").GetInt64()); + var after = JsonSerializer.SerializeToElement(Assert.IsType(await CreateApi(db).EventInstances(eventId)).Value); + Assert.Equal(id, Assert.Single(Assert.Single(after.GetProperty("CollectionRecords").EnumerateArray()) + .GetProperty("KeepsakeInstanceIds").EnumerateArray()).GetGuid()); + var other = JsonSerializer.SerializeToElement(Assert.IsType(await CreateApi(db, accountId: 3).EventInstances(eventId)).Value); + Assert.Empty(other.GetProperty("CollectionRecords").EnumerateArray()); + Assert.IsType(await CreateApi(db).RemoveFromClient(id)); + var removed = Assert.IsType(await CreateApi(db).CollectFromClient(id)); + Assert.Equal(404, removed.StatusCode); + Assert.Equal("The keepsake instance has been removed.", Assert.IsType(removed.Value).Detail); + } + + [Fact] + public async Task ClientCollectionExplainsMissingAndDisabledInstancesWithoutWritingRecords() + { + await using var db = NewDb(); + var id = Guid.NewGuid(); + var missing = Assert.IsType(await CreateApi(db).CollectFromClient(id)); + Assert.Equal(404, missing.StatusCode); + Assert.Equal("The keepsake instance is not registered on this server.", Assert.IsType(missing.Value).Detail); + + KeepsakeFeature.Override = false; + var disabled = Assert.IsType(await CreateApi(db).CollectFromClient(id)); + Assert.Equal(404, disabled.StatusCode); + Assert.Equal("Keepsakes are disabled on this server.", Assert.IsType(disabled.Value).Detail); + Assert.Empty(db.KeepsakeCollectionRecords); + } + + [Fact] + public async Task EmptyRoomAndEventHaveNonNullClientLists() + { + await using var db = NewDb(); + foreach (var response in new[] { await CreateApi(db).Room(Room), await CreateApi(db).EventInstances(KeepsakeRoomConfig.Current.EventId) }) + { + var json = JsonSerializer.SerializeToElement(Assert.IsType(response).Value); + Assert.Empty(json.GetProperty("Instances").EnumerateArray()); + Assert.Empty(json.GetProperty("CollectionRecords").EnumerateArray()); + } + Assert.IsType(await CreateApi(db).EventInstances(9999)); + } + + [Fact] + public async Task EventCanBeDisabledWhileKeepsakesRemainEnabled() + { + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse("""{"AllRooms":true,"EventEnabled":false}"""); + await using var db = NewDb(); + Assert.True(KeepsakeFeature.Enabled); + var api = new RadicadeServer.API.Compatibility.CompatibilityAPI(db); + Assert.Equal("null", Assert.IsType(api.ActiveProgressionEvents()).Content); + Assert.IsType(api.ProgressionEventDefinition(1)); + Assert.IsType(await CreateApi(db).EventInstances(1)); + PlacedGuid(await CreateApi(db, body: """{"CategoryId":0}""").Place(Room)); + var room = JsonSerializer.SerializeToElement(Assert.IsType(await CreateApi(db).Room(Room)).Value); + Assert.Empty(room.GetProperty("KeepsakeProgressionEventIds").EnumerateArray()); + Assert.Single(room.GetProperty("Instances").EnumerateArray()); + } + + [Fact] + public async Task AllRoomsEventAdvertisesRealRoomObjectsAndPlayerRecordTracksCollectionXp() + { + await using var db = NewDb(); + db.Rooms.Add(new RadicadeServer.RecNet.Rooms.RoomData { RoomId = Room, Name = "Keepsake test" }); + await db.SaveChangesAsync(); + var keepsakes = CreateApi(db, body: """{"KeepsakeCategory":6}"""); + var id = PlacedGuid(await keepsakes.Place(Room)); + await CreateApi(db).CollectFromClient(id); + var api = new RadicadeServer.API.Compatibility.CompatibilityAPI(db) + { ControllerContext = CreateApi(db).ControllerContext }; + var definition = JsonSerializer.SerializeToElement(Assert.IsType(api.ProgressionEventDefinition(1)).Value); + var list = Assert.Single(definition.GetProperty("KeepsakeRoomLists").EnumerateArray()); + var room = Assert.Single(list.GetProperty("KeepsakeRooms").EnumerateArray()); + Assert.Equal(Room, room.GetProperty("RoomId").GetInt64()); + Assert.Equal(1, room.GetProperty("KeepsakeRoomListId").GetInt64()); + Assert.Equal(1, list.GetProperty("ProgressionEventId").GetInt64()); + Assert.True(list.GetProperty("RoomUnlockIntervalTicks").GetInt64() > 0); + var record = JsonSerializer.SerializeToElement(Assert.IsType(api.ProgressionEventRecord(1)).Value); + Assert.Equal(2, record.GetProperty("AccountId").GetInt64()); + Assert.Equal(40, record.GetProperty("Xp").GetInt32()); + } +} diff --git a/ServerTests/LeaderboardTests.cs b/ServerTests/LeaderboardTests.cs new file mode 100644 index 0000000..47dfba2 --- /dev/null +++ b/ServerTests/LeaderboardTests.cs @@ -0,0 +1,231 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using RadicadeServer.DB; +using RadicadeServer.Leaderboard; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Leaderboards; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The room scoreboard / watch leaderboard path: the client posts a stat with +/// CheckAndSetStat (SetStatRequestDTO: StatChannel + RoomId + StatValue, with identity +/// coming from the auth token rather than the body), then reads it back with GetRanks, +/// GetNearbyScores or GetPlayerRank. These tests drive the store end to end, because the +/// conformance suite only ever exercised an empty board. +/// +public class LeaderboardTests +{ + private const long RoomA = 25; + private const long RoomB = 32; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + var root = new InMemoryDatabaseRoot(); + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString(), root) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + private static async Task SeedProfilesAsync(IDbContextFactory factory, params long[] ids) + { + await using var db = factory.CreateDbContext(); + foreach (var id in ids) + db.Profiles.Add(new Profile + { + accountId = id, + profileImage = "default.png", + username = $"user{id}", + displayName = $"User {id}", + }); + await db.SaveChangesAsync(); + } + + private static string Stat(long roomId, int channel, int value) => + $"{{\"StatChannel\":{channel},\"RoomId\":{roomId},\"StatValue\":{value}}}"; + + [Fact] + public async Task SubmitScore_RanksPlayersAndKeepsRoomsSeparate() + { + var factory = new TestDbContextFactory(); + await SeedProfilesAsync(factory, 2, 3); + + await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 10), 2, 0); + await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 40), 3, 0); + // A different room's board on the same channel number must not leak into RoomA. + await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomB, 1, 999), 2, 0); + + var board = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest + { + RoomId = RoomA, + StatChannel = 1, + RankStart = 1, + RankEnd = 10, + }); + + Assert.Equal(2, board.Total); + Assert.Equal(2, board.Rows.Count); + Assert.Equal(3, board.Rows[0].PlayerId); + Assert.Equal(40, board.Rows[0].Score); + Assert.Equal(1, board.Rows[0].Rank); + Assert.Equal(2, board.Rows[1].PlayerId); + Assert.Equal(2, board.Rows[1].Rank); + Assert.Equal("User 3", board.Rows[0].PlayerName); + } + + [Fact] + public async Task SubmitScore_KeepsEachPlayersBestRatherThanTheirLast() + { + var factory = new TestDbContextFactory(); + await SeedProfilesAsync(factory, 2); + + await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 50), 2, 0); + await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 10), 2, 0); + + var board = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest + { + RoomId = RoomA, + StatChannel = 1, + RankStart = 1, + RankEnd = 10, + }); + + Assert.Equal(1, board.Total); + Assert.Equal(50, board.Rows[0].Score); + } + + [Fact] + public async Task SubmitScore_AscendingBoardRanksTheLowerTimeFirst() + { + var factory = new TestDbContextFactory(); + await SeedProfilesAsync(factory, 2, 3); + + // A race board: lower is better, and the client marks it with SortAscending. + await LeaderboardStore.SubmitScoreAsync(factory, "{\"StatChannel\":2,\"RoomId\":25,\"Time\":9000}", 2, 0); + await LeaderboardStore.SubmitScoreAsync(factory, "{\"StatChannel\":2,\"RoomId\":25,\"Time\":4000}", 3, 0); + + var board = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest + { + RoomId = RoomA, + StatChannel = 2, + SortAscending = true, + RankStart = 1, + RankEnd = 10, + }); + + Assert.Equal(2, board.Total); + Assert.Equal(3, board.Rows[0].PlayerId); + Assert.Equal(1, board.Rows[0].Rank); + } + + [Fact] + public async Task SubmitScore_FallsBackToTheAuthenticatedPlayerWhenTheBodyHasNoPlayerId() + { + // The client's SetStatRequestDTO has no player field at all; identity is the token. + var factory = new TestDbContextFactory(); + await SeedProfilesAsync(factory, 2); + + var result = await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 3, 7), 2, 0); + Assert.NotNull(result); + + await using var db = factory.CreateDbContext(); + var stored = await db.LeaderboardScores.SingleAsync(); + Assert.Equal(2, stored.PlayerId); + Assert.Equal(7, stored.Score); + } + + [Fact] + public async Task GetNearbyScores_CentresTheWindowOnTheAskingPlayer() + { + var factory = new TestDbContextFactory(); + await SeedProfilesAsync(factory, 2, 3, 4, 5, 6); + + for (var i = 0; i < 5; i++) + await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 10 * (i + 1)), 2 + i, 0); + + // Player 4 scored 30, so they sit third of five. + var nearby = await LeaderboardStore.GetNearbyScoresAsync(factory, new GetNearbyScoresRequest + { + RoomId = RoomA, + StatChannel = 1, + PlayerId = 4, + WindowSize = 3, + }); + + Assert.Equal(5, nearby.Total); + Assert.Equal(3, nearby.MyRank); + Assert.Contains(nearby.Rows, r => r.PlayerId == 4 && r.Rank == 3); + } + + [Fact] + public async Task EmptyBoard_StillNamesTheAskingPlayer() + { + // The scoreboard renders the row the server hands back, so the synthetic row for + // a player who has not scored yet must still carry their name rather than an + // empty string. Names were loaded only for players already on the board, which on + // an empty board is nobody. + var factory = new TestDbContextFactory(); + await SeedProfilesAsync(factory, 2); + + var ranks = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest + { + RoomId = RoomA, + StatChannel = 1, + PlayerId = 2, + RankStart = 1, + RankEnd = 10, + }); + Assert.Equal("User 2", ranks.Rows[0].PlayerName); + + var nearby = await LeaderboardStore.GetNearbyScoresAsync(factory, new GetNearbyScoresRequest + { + RoomId = RoomA, + StatChannel = 1, + PlayerId = 2, + WindowSize = 10, + }); + Assert.Equal("User 2", nearby.Rows[0].PlayerName); + } + + [Fact] + public async Task GetPlayerRank_NamesTheAskingPlayerWhenTheyHaveNotScored() + { + var factory = new TestDbContextFactory(); + await SeedProfilesAsync(factory, 2); + + var rank = await LeaderboardStore.GetPlayerRankAsync(factory, new GetRankRequest + { + RoomId = RoomA, + StatChannel = 1, + PlayerId = 2, + }); + + var name = rank.GetType().GetProperty("PlayerName")?.GetValue(rank) as string; + Assert.Equal("User 2", name); + } + + [Fact] + public async Task GetPlayerRank_ReportsZeroForAPlayerWhoHasNotScored() + { + var factory = new TestDbContextFactory(); + await SeedProfilesAsync(factory, 2, 3); + await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 10), 3, 0); + + var rank = await LeaderboardStore.GetPlayerRankAsync(factory, new GetRankRequest + { + RoomId = RoomA, + StatChannel = 1, + PlayerId = 2, + }); + + Assert.NotNull(rank); + } +} diff --git a/ServerTests/LevelGiftBoxTests.cs b/ServerTests/LevelGiftBoxTests.cs new file mode 100644 index 0000000..94947b4 --- /dev/null +++ b/ServerTests/LevelGiftBoxTests.cs @@ -0,0 +1,341 @@ +using System.Text.Json; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The level gift boxes for levels 1-50: a consumable at 1/3/5/7/9, and clothing of a stated star +/// rating everywhere else, ending on 5-star at 50. What goes in the box comes from the curated +/// pool, and never repeats something the player already has. +/// +public class LevelGiftBoxTests +{ + // The specification, written out independently of the implementation. + private static readonly int[] ConsumableLevels = [1, 3, 5, 7, 9]; + + private static readonly int[] TwoStarLevels = + [2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 25]; + + private static readonly int[] ThreeStarLevels = + [22, 24, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 37, 38, 39]; + + private static readonly int[] FourStarLevels = + [31, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]; + + private const int FiveStarLevel = 50; + + [Fact] + public void TheTableCoversEveryLevelFromOneToFiftyExactlyOnce() + { + var covered = ConsumableLevels + .Concat(TwoStarLevels) + .Concat(ThreeStarLevels) + .Concat(FourStarLevels) + .Append(FiveStarLevel) + .OrderBy(x => x) + .ToList(); + + Assert.Equal(Enumerable.Range(1, 50), covered); + Assert.Equal(50, covered.Distinct().Count()); + Assert.True(LevelRewardTable.IsItemLevel(1)); + Assert.True(LevelRewardTable.IsItemLevel(50)); + Assert.False(LevelRewardTable.IsItemLevel(0)); + Assert.False(LevelRewardTable.IsItemLevel(51)); + } + + [Fact] + public void ConsumableLevelsAskForAConsumableAndEverythingElseAsksForClothing() + { + for (var level = 1; level <= 50; level++) + { + var expected = ConsumableLevels.Contains(level) + ? LevelRewardTable.RewardKind.Consumable + : LevelRewardTable.RewardKind.Clothing; + Assert.Equal(expected, LevelRewardTable.KindForLevel(level)); + } + + // Above the table (and below it) nothing is granted. The progression map's token drop used + // to fill these, which is the "Level Up Reward" box a level set past 50 produced. + Assert.Equal(LevelRewardTable.RewardKind.None, LevelRewardTable.KindForLevel(51)); + Assert.Equal(LevelRewardTable.RewardKind.None, LevelRewardTable.KindForLevel(0)); + } + + [Fact] + public void TheStarRatingOfEachClothingLevelMatchesTheSpecification() + { + foreach (var level in TwoStarLevels) + Assert.Equal(Rarity.Uncommon, LevelRewardTable.RarityForLevel(level)); + + foreach (var level in ThreeStarLevels) + Assert.Equal(Rarity.Rare, LevelRewardTable.RarityForLevel(level)); + + foreach (var level in FourStarLevels) + Assert.Equal(Rarity.Epic, LevelRewardTable.RarityForLevel(level)); + + Assert.Equal(Rarity.Legendary, LevelRewardTable.RarityForLevel(FiveStarLevel)); + } + + // ------------------------------------------------------------------------------------- + // The boxes themselves + // ------------------------------------------------------------------------------------- + + private static string AvatarCatalogueJson() + { + var items = new List(); + var id = 1; + foreach (var rarity in new[] { Rarity.Uncommon, Rarity.Rare, Rarity.Epic, Rarity.Legendary }) + { + for (var i = 0; i < 4; i++) + { + items.Add(new AvatarItem + { + AvatarItemId = id++, + AvatarItemDesc = $"{rarity}-item-{i}", + FriendlyName = $"{rarity} Item {i}", + Rarity = (int)rarity, + AvatarItemType = 0, + IsBaseAvatarItem = false + }); + } + } + + return JsonSerializer.Serialize(items); + } + + private static string ConsumableCatalogueJson() => JsonSerializer.Serialize(new List + { + new() { ConsumableItemDesc = "consumable-a", FriendlyName = "Consumable A", Rarity = Rarity.Common }, + new() { ConsumableItemDesc = "consumable-b", FriendlyName = "Consumable B", Rarity = Rarity.Common }, + }); + + /// + /// A pool that names everything in the synthetic catalogues, so these tests exercise the reward + /// table rather than the shipped allow-list (which the pool tests cover separately). + /// + private static LevelUpRewardPool.Pool PoolFor(string avatarsJson, string consumablesJson) + { + var avatars = JsonSerializer.Deserialize>(avatarsJson) ?? new List(); + var consumables = JsonSerializer.Deserialize>(consumablesJson) ?? new List(); + return new LevelUpRewardPool.Pool + { + AvatarItems = avatars.Select(x => x.AvatarItemDesc).ToList(), + Consumables = consumables.Select(x => x.ConsumableItemDesc).ToList() + }; + } + + private static Gift? Build(int level, string avatars, string consumables, ISet? owned = null, int seed = 1) + => LevelUpGiftService.BuildItemReward( + level, avatars, consumables, + owned: owned, random: new Random(seed), pool: PoolFor(avatars, consumables)); + + [Fact] + public void EveryLevelOneToFiftyBuildsABoxWithTheSpecifiedItem() + { + var avatars = AvatarCatalogueJson(); + var consumables = ConsumableCatalogueJson(); + + for (var level = 1; level <= 50; level++) + { + var gift = Build(level, avatars, consumables); + + Assert.NotNull(gift); + Assert.Contains($"Level {level} Reward", gift!.FriendlyName); + + if (ConsumableLevels.Contains(level)) + { + Assert.False(string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)); + Assert.True(string.IsNullOrWhiteSpace(gift.AvatarItemDesc), + $"level {level} should hold only a consumable"); + } + else + { + Assert.False(string.IsNullOrWhiteSpace(gift.AvatarItemDesc)); + Assert.True(string.IsNullOrWhiteSpace(gift.ConsumableItemDesc), + $"level {level} should hold only clothing"); + Assert.Equal(LevelRewardTable.RarityForLevel(level), gift.Rarity); + } + + // A level-up box names its own item; it is not a storefront drop and has no price. + Assert.Equal(0, gift.Currency); + Assert.Equal(0, gift.Xp); + } + } + + [Fact] + public void EveryBoxCarriesTheCongratsMessageForItsLevel() + { + var avatars = AvatarCatalogueJson(); + var consumables = ConsumableCatalogueJson(); + + for (var level = 1; level <= 50; level++) + { + var gift = Build(level, avatars, consumables)!; + + // Message is the box's own text (Gift.Message maps onto Tooltip), and it should name + // the level the player just reached - not describe the item inside. + Assert.Equal($"Congrats on getting to Level {level}!", gift.Message); + } + } + + [Fact] + public void PlayersAtTheSameLevelDoNotAllReceiveTheSameItem() + { + var avatars = AvatarCatalogueJson(); + var consumables = ConsumableCatalogueJson(); + + // Four 2-star items exist. The old selection was "candidate[(level - 1) % count]", which + // handed every player the same item for a given level; the pool must spread them out. + var picks = new HashSet(StringComparer.OrdinalIgnoreCase); + for (var seed = 0; seed < 32; seed++) + picks.Add(Build(2, avatars, consumables, seed: seed)!.AvatarItemDesc); + + Assert.True(picks.Count > 1, "every player was handed the same level-2 item"); + Assert.All(picks, desc => Assert.StartsWith("Uncommon-item-", desc)); + } + + [Fact] + public void AnItemThePlayerAlreadyOwnsIsNotHandedOutAgain() + { + var avatars = AvatarCatalogueJson(); + var consumables = ConsumableCatalogueJson(); + + // Own every 2-star item but one. + var owned = new HashSet(StringComparer.OrdinalIgnoreCase) + { "Uncommon-item-0", "Uncommon-item-1", "Uncommon-item-2" }; + + for (var seed = 0; seed < 8; seed++) + { + var gift = Build(2, avatars, consumables, owned, seed)!; + Assert.Equal("Uncommon-item-3", gift.AvatarItemDesc); + } + } + + [Fact] + public void AnExhaustedTierFallsBackToAConsumableRatherThanToAForbiddenItem() + { + var avatars = AvatarCatalogueJson(); + var consumables = ConsumableCatalogueJson(); + + var owned = new HashSet(StringComparer.OrdinalIgnoreCase) + { "Uncommon-item-0", "Uncommon-item-1", "Uncommon-item-2", "Uncommon-item-3" }; + + var gift = Build(2, avatars, consumables, owned)!; + + Assert.True(string.IsNullOrWhiteSpace(gift.AvatarItemDesc)); + Assert.False(string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)); + Assert.Equal("Congrats on getting to Level 2!", gift.Message); + } + + [Fact] + public void AConsumableLevelWithNothingLeftGrantsNoBox() + { + var avatars = AvatarCatalogueJson(); + var consumables = ConsumableCatalogueJson(); + + var owned = new HashSet(StringComparer.OrdinalIgnoreCase) { "consumable-a", "consumable-b" }; + + Assert.Null(Build(1, avatars, consumables, owned)); + } + + [Fact] + public void AnEmptyCatalogueYieldsNoEmptyBox() + { + // If the catalogue cannot supply the item, the box must not be silently empty; when even the + // consumable fallback has nothing, the level simply gets no box - and it is never replaced + // with the old token drop. + Assert.Null(Build(4, "[]", "[]")); + Assert.Null(Build(1, AvatarCatalogueJson(), "[]")); + + // Outside the table there is no item box at all. + Assert.Null(Build(50 + 1, AvatarCatalogueJson(), ConsumableCatalogueJson())); + + // A clothing level with no clothing at all is served by the consumable fallback. + var fallback = Build(2, "[]", ConsumableCatalogueJson())!; + Assert.True(string.IsNullOrWhiteSpace(fallback.AvatarItemDesc)); + Assert.False(string.IsNullOrWhiteSpace(fallback.ConsumableItemDesc)); + } + + [Fact] + public void ALevelUpBoxIsNeverFilledWithAShirtItShouldNotHandOut() + { + var avatars = JsonSerializer.Serialize(new List + { + new() { AvatarItemId = 1, AvatarItemDesc = "exclusive-item", FriendlyName = "Top Hat (Gold)", Rarity = (int)Rarity.Uncommon, IsBaseAvatarItem = false }, + new() { AvatarItemId = 2, AvatarItemDesc = "ordinary-item", FriendlyName = "Plain Tee", Rarity = (int)Rarity.Uncommon, IsBaseAvatarItem = false }, + }); + var consumables = ConsumableCatalogueJson(); + + // The allow-list names both, so only the deny-list stands between the player and the + // exclusive top hat - which is exactly the case it has to cover. + var pool = new LevelUpRewardPool.Pool + { + AvatarItems = new List { "exclusive-item", "ordinary-item" }, + Consumables = new List { "consumable-a", "consumable-b" } + }; + + for (var seed = 0; seed < 16; seed++) + { + var gift = LevelUpGiftService.BuildItemReward( + 2, avatars, consumables, random: new Random(seed), pool: pool)!; + Assert.Equal("ordinary-item", gift.AvatarItemDesc); + } + } + + [Fact] + public void AnItemThePoolDoesNotNameIsNeverAwarded() + { + var avatars = JsonSerializer.Serialize(new List + { + new() { AvatarItemId = 1, AvatarItemDesc = "listed-item", FriendlyName = "Listed Hat", Rarity = (int)Rarity.Uncommon, IsBaseAvatarItem = false }, + new() { AvatarItemId = 2, AvatarItemDesc = "unlisted-item", FriendlyName = "Unlisted Hat", Rarity = (int)Rarity.Uncommon, IsBaseAvatarItem = false }, + }); + var consumables = ConsumableCatalogueJson(); + + var pool = new LevelUpRewardPool.Pool + { + AvatarItems = new List { "listed-item" }, + Consumables = new List { "consumable-a" } + }; + + // The unlisted item is never picked even though it is a perfectly ordinary 2-star hat. + for (var seed = 0; seed < 8; seed++) + { + var gift = LevelUpGiftService.BuildItemReward( + 2, avatars, consumables, random: new Random(seed), pool: pool)!; + Assert.Equal("listed-item", gift.AvatarItemDesc); + } + + // And once the only listed item is owned, the level falls back to a consumable rather than + // reaching for the unlisted one. + var owned = new HashSet(StringComparer.OrdinalIgnoreCase) { "listed-item" }; + var fallback = LevelUpGiftService.BuildItemReward( + 2, avatars, consumables, owned, new Random(1), pool)!; + Assert.True(string.IsNullOrWhiteSpace(fallback.AvatarItemDesc)); + Assert.False(string.IsNullOrWhiteSpace(fallback.ConsumableItemDesc)); + } + + [Fact] + public void TwoLevelsInOnePassNeverHandOutTheSameItem() + { + var avatars = AvatarCatalogueJson(); + var consumables = ConsumableCatalogueJson(); + + // Levels 2 and 4 are both 2-star. Delivered together they still have to differ, which the + // delivered-so-far set inside BuildItemReward/pass wiring is responsible for. + for (var seed = 0; seed < 16; seed++) + { + var pool = PoolFor(avatars, consumables); + var random = new Random(seed); + var owned = new HashSet(StringComparer.OrdinalIgnoreCase); + + var two = LevelUpGiftService.BuildItemReward(2, avatars, consumables, owned, random, pool)!; + owned.Add(LevelUpRewardPool.OwnershipKey(two.AvatarItemDesc)); + var four = LevelUpGiftService.BuildItemReward(4, avatars, consumables, owned, random, pool)!; + + Assert.NotEqual(two.AvatarItemDesc, four.AvatarItemDesc); + } + } +} diff --git a/ServerTests/LevelUpRewardPoolTests.cs b/ServerTests/LevelUpRewardPoolTests.cs new file mode 100644 index 0000000..d34a5a0 --- /dev/null +++ b/ServerTests/LevelUpRewardPoolTests.cs @@ -0,0 +1,174 @@ +using System.Text.Json; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The curated level-up pool: the deny-list that must never be overridden, and the shipped +/// allow-list that decides what the box may contain. +/// +public class LevelUpRewardPoolTests +{ + [Theory] + // The brief's named exclusions, as they appear in the 2023 catalogue. + [InlineData("White Hat", "A hat given to the ethical hackers of Rec Room", null)] // bug bounty / security + [InlineData("Top Hat (Gold)", "", null)] // payment milestone + [InlineData("Rec Con Hoodie (2021)", "", null)] // convention reward + [InlineData("Winged Headphones (Purple)", "RecCon 2021 exclusive", null)] + [InlineData("Early Access Tee", "", null)] // launch exclusive + [InlineData("Laser Tag Alpha Shirt", "", null)] // alpha reward + [InlineData("MrBeast T-Shirt", "", null)] // licensed collaboration + [InlineData("Space Visor (Orion)", "Unlocked during Xbox launch", null)] + [InlineData("Royal Cape (Creators Contest 2)", "", null)] // contest reward + [InlineData("SFVRCC Promo Shirt", "", null)] // promotional + [InlineData("Notes Headphones (Gold)", "", null)] // payment milestone + [InlineData("Class of 2020 Shirt", "", null)] // educational programme + [InlineData("New Years Hat Hat", "", null)] // event dressing + [InlineData("Plain Tee", "", "halloween")] // event tag alone + [InlineData("Plain Tee", "", null, true)] // control: this one is fine + public void TheDenyListRefusesTheItemsTheBriefNames(string name, string tooltip, string? tag, bool allowed = false) + { + var denied = LevelUpRewardPool.IsAlwaysDenied(name, tooltip, tag); + Assert.Equal(!allowed, denied); + } + + [Fact] + public void TheDenyListIsCaseInsensitiveAndChecksTheTooltipAsWellAsTheName() + { + Assert.True(LevelUpRewardPool.IsAlwaysDenied("Halloween Sweater", "")); + Assert.True(LevelUpRewardPool.IsAlwaysDenied("Sweater", "HALLOWEEN SWEATER")); + Assert.False(LevelUpRewardPool.IsAlwaysDenied("Stripey Sweater", "A cosy sweater")); + } + + [Fact] + public void OwnershipKeysMatchRegardlessOfSpacingOrCase() + { + Assert.Equal( + LevelUpRewardPool.OwnershipKey("AAAA-BBBB,CCCC,"), + LevelUpRewardPool.OwnershipKey("aaaa-bbbb,cccc,")); + } + + [Fact] + public void AnEmptyPoolAwardsNothingButDoesNotThrow() + { + var pool = new LevelUpRewardPool.Pool(); + var catalogue = new List + { + new() { AvatarItemDesc = "x", FriendlyName = "Hat", Rarity = (int)Rarity.Uncommon } + }; + + Assert.Null(LevelUpRewardPool.PickAvatarItem(catalogue, Rarity.Uncommon, new HashSet(), new Random(1), pool)); + } + + // ------------------------------------------------------------------------------------- + // The shipped artefacts + // ------------------------------------------------------------------------------------- + + /// + /// Walks up from the test's own output directory to the repository, so this can check the real + /// files rather than a copy. Returns null when the tree is not there (a published test run). + /// + private static string? RepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + if (File.Exists(Path.Join(directory.FullName, "Server", "RadicadeServer-Config", LevelUpRewardPool.ConfigFileName))) + return directory.FullName; + directory = directory.Parent; + } + return null; + } + + private sealed class PoolFile + { + public List AvatarItems { get; set; } = new(); + public List Consumables { get; set; } = new(); + } + + [Fact] + public void TheShippedPoolNamesOnlyOrdinaryCatalogueItemsFromThe2023Client() + { + var root = RepositoryRoot(); + if (root == null) return; + + var config = Path.Join(root, "Server", "RadicadeServer-Config"); + var pool = JsonSerializer.Deserialize( + File.ReadAllText(Path.Join(config, LevelUpRewardPool.ConfigFileName)), + new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + var catalogue = JsonSerializer.Deserialize>(File.ReadAllText(Path.Join(config, "2022-avatar.json")))!; + var resolvable = new HashSet(StringComparer.OrdinalIgnoreCase); + using (var lookup = JsonDocument.Parse(File.ReadAllText(Path.Join(config, "avatar-item-lookup.json")))) + foreach (var description in lookup.RootElement.GetProperty("descriptions").EnumerateArray()) + resolvable.Add(description.GetString() ?? ""); + + var byDesc = catalogue + .Where(item => !string.IsNullOrWhiteSpace(item.AvatarItemDesc)) + .GroupBy(item => item.AvatarItemDesc, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); + + Assert.NotEmpty(pool.AvatarItems); + Assert.NotEmpty(pool.Consumables); + + var denied = new List(); + var missing = new List(); + var notResolvable = new List(); + var wrongTier = new List(); + var baseItems = new List(); + + foreach (var desc in pool.AvatarItems) + { + if (!byDesc.TryGetValue(desc, out var item)) { missing.Add(desc); continue; } + if (LevelUpRewardPool.IsAlwaysDenied(item)) denied.Add($"{item.FriendlyName} ({desc})"); + if (!resolvable.Contains(desc)) notResolvable.Add($"{item.FriendlyName} ({desc})"); + if (item.Rarity is not ((int)Rarity.Uncommon or (int)Rarity.Rare or (int)Rarity.Epic or (int)Rarity.Legendary)) + wrongTier.Add($"{item.FriendlyName} rarity={item.Rarity}"); + if (item.IsBaseAvatarItem) baseItems.Add(item.FriendlyName); + } + + // Each of these is a way the generated list could have gone wrong, and each is a hard + // failure rather than a warning: the pool decides what a real player receives. + Assert.Empty(denied); + Assert.Empty(missing); + Assert.Empty(notResolvable); + Assert.Empty(wrongTier); + Assert.Empty(baseItems); + } + + [Fact] + public void EveryStarTierHasSomethingToGive() + { + var root = RepositoryRoot(); + if (root == null) return; + + var config = Path.Join(root, "Server", "RadicadeServer-Config"); + var file = JsonSerializer.Deserialize( + File.ReadAllText(Path.Join(config, LevelUpRewardPool.ConfigFileName)), + new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + var catalogue = JsonSerializer.Deserialize>(File.ReadAllText(Path.Join(config, "2022-avatar.json")))!; + + // Selection is driven by the pool as an object here rather than through the file loader, + // which would need the process's own working directory pointed at the repository. + var pool = new LevelUpRewardPool.Pool + { + AvatarItems = file.AvatarItems, + Consumables = file.Consumables + }; + + foreach (var rarity in new[] { Rarity.Uncommon, Rarity.Rare, Rarity.Epic, Rarity.Legendary }) + { + var pick = LevelUpRewardPool.PickAvatarItem(catalogue, rarity, new HashSet(), new Random(1), pool); + Assert.NotNull(pick); + Assert.Equal((int)rarity, pick!.Rarity); + } + + var consumable = LevelUpRewardPool.PickConsumable( + JsonSerializer.Deserialize>( + File.ReadAllText(Path.Join(config, "consumables.json")))!, + new HashSet(), new Random(1), pool); + Assert.NotNull(consumable); + } +} diff --git a/ServerTests/LoadingScreenTipTests.cs b/ServerTests/LoadingScreenTipTests.cs new file mode 100644 index 0000000..82be514 --- /dev/null +++ b/ServerTests/LoadingScreenTipTests.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.CDN.Config; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +public class LoadingScreenTipTests +{ + [Theory] + [InlineData("radicade", "yum", "Welcome to Radicade")] + [InlineData("Custom welcome", "Custom message", "Custom welcome")] + public void ReplacesOnlyLegacyPlaceholder(string title, string message, string expectedTitle) + { + using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + db.ServerConfig.Add(new ServerConfigVal("LoadingScreenTipData", + System.Text.Json.JsonSerializer.Serialize(new[] { new { Title = title, Message = message } }))); + db.SaveChanges(); + + // The old single, image-less tip's wording is carried onto the first official image; the + // "radicade"/"yum" placeholder is not worth carrying, so that one keeps the default text. + // The default is read from the catalog rather than repeated here, so rewording a tip + // (which is a copy decision, not a behaviour one) does not fail this test. + var tips = new CDNConfigAPI(db).GetLoadingScreenTipData()!; + var tip = tips[0]; + Assert.Equal(expectedTitle, tip.Title); + Assert.Equal(title == "radicade" ? LoadingScreenImages.Tips[0].Message : message, tip.Message); + } + + [Fact] + public void CyclesThroughTheOfficialImagesOnly() + { + using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + db.ServerConfig.Add(new ServerConfigVal("LoadingScreenTipData", + System.Text.Json.JsonSerializer.Serialize(new[] { new { Title = "Custom welcome", Message = "Custom message", ImageName = "avatar-upload.png" } }))); + db.SaveChanges(); + + var tips = new CDNConfigAPI(db).GetLoadingScreenTipData()!; + + Assert.Equal(LoadingScreenImages.Names, tips.Select(t => t.ImageName)); + Assert.All(tips, t => Assert.False(string.IsNullOrWhiteSpace(t.Title))); + Assert.All(tips, t => Assert.False(string.IsNullOrWhiteSpace(t.Message))); + } + + [Fact] + public void ReusesAnAdministratorsTextForItsImage() + { + using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + db.ServerConfig.Add(new ServerConfigVal("LoadingScreenTipData", + System.Text.Json.JsonSerializer.Serialize(new[] { new { Title = "Our rules", Message = "Be nice.", ImageName = "Star.png" } }))); + db.SaveChanges(); + + var star = new CDNConfigAPI(db).GetLoadingScreenTipData()!.First(t => t.ImageName == "Star.png"); + + Assert.Equal("Our rules", star.Title); + Assert.Equal("Be nice.", star.Message); + } + + [Fact] + public void NewDatabaseSeedHasWelcomeText() + { + using var json = System.Text.Json.JsonDocument.Parse(FirstTimeUtils.LoadingScreen); + Assert.Equal("Welcome to Radicade", json.RootElement[0].GetProperty("Title").GetString()); + Assert.DoesNotContain("yum", json.RootElement[0].GetProperty("Message").GetString()!); + } +} diff --git a/ServerTests/LoggedRouteCompatibilityTests.cs b/ServerTests/LoggedRouteCompatibilityTests.cs new file mode 100644 index 0000000..93bc390 --- /dev/null +++ b/ServerTests/LoggedRouteCompatibilityTests.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Matchmaking; +using RadicadeServer.API.Moderation; +using RadicadeServer.DB; +using Xunit; + +namespace RadicadeServer.Tests; + +public class LoggedRouteCompatibilityTests +{ + [Theory] + [InlineData("rooms/requiring/developer")] + [InlineData("rooms/requiring/rrplus")] + public void RestrictedRoomsHasStartupRouteAndReturnsEmptyArray(string route) + { + Assert.Equal("matchmaking", typeof(MatchmakingAPI).GetCustomAttribute()!.Template); + var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.GetRestrictedRooms))!; + Assert.Contains(method.GetCustomAttributes(), attribute => attribute.Template == route); + + using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + var result = Assert.IsType(new MatchmakingAPI(db).GetRestrictedRooms()); + var json = JsonSerializer.SerializeToElement(result.Value); + Assert.Equal(JsonValueKind.Array, json.ValueKind); + Assert.Equal(0, json.GetArrayLength()); + } + + [Fact] + public void SelectedCheerAcceptsLoggedClientRouteWithoutDuplicateCaseAliases() + { + var routes = typeof(ReputationAPI).GetMethod(nameof(ReputationAPI.SetSelectedCheer))! + .GetCustomAttributes().Select(attribute => attribute.Template!).ToArray(); + Assert.Contains("~/api/PlayerCheer/v1/SetSelectedCheer", routes); + Assert.Equal(routes.Length, routes.Distinct(StringComparer.OrdinalIgnoreCase).Count()); + } +} diff --git a/ServerTests/MatchmakeEmptyInstanceTests.cs b/ServerTests/MatchmakeEmptyInstanceTests.cs new file mode 100644 index 0000000..3a04d69 --- /dev/null +++ b/ServerTests/MatchmakeEmptyInstanceTests.cs @@ -0,0 +1,254 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A public room instance has no life of its own: Photon destroys the room the moment its +/// last client leaves, and Radicade only keeps the row. So an instance with nobody in it is +/// a photonRoomId pointing at a room that is gone. +/// +/// Handing that id to a client is how a room becomes permanently unjoinable: every +/// matchmake re-selects the same empty row (it is the only non-full, non-private instance +/// of the room), the client connects to a room name that no longer exists, the peer sits +/// there until Photon times it out - "Failed to connect to room Radicade44-" / +/// ClientTimeout - and the room load aborts. That is what the RecCenter did twice in the +/// 2026-09-20 log: once on the realtime peer, once on the voice peer, ~15s of silence each +/// time, while the dorm (a private instance, minted fresh per player) joined in under a +/// second. +/// +/// These pin the sweep that now runs before public instance selection. +/// +[Collection(DbFactoryCollection.Name)] +public class MatchmakeEmptyInstanceTests +{ + private const long RoomId = 3; + private const long SubRoomId = 3; + private const long OtherRoomId = 25; + private const long PlayerId = 2; + + /// The empty row this bug was seen on: instance 20965 of the RecCenter. + private const string DeadPhotonRoomId = "Radicade44-1fe4247ce8794b7c897ef1987d2a593d"; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + return (factory, factory.CreateDbContext()); + } + + private static void SeedRoom(RecDBContext db, long roomId = RoomId, long subRoomId = SubRoomId, string name = "RecCenter") + { + db.Rooms.Add(new RoomData + { + RoomId = roomId, + Name = name, + CreatorAccountId = 1, + Accessibility = Accessibility.Public, + MaxPlayers = 100, + CreatedAt = DateTime.UtcNow, + }); + + db.SubRooms.Add(new SubRoom + { + SubRoomId = subRoomId, + RoomId = roomId, + Name = "Home", + UnitySceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6", + MaxPlayers = 100, + Accessibility = Accessibility.Public, + }); + } + + private static RoomInstance SeedInstance( + RecDBContext db, + bool isPrivate = false, + RoomInstanceType type = RoomInstanceType.Public, + long roomId = RoomId, + long subRoomId = SubRoomId, + string photonRoomId = DeadPhotonRoomId) + { + var instance = new RoomInstance + { + roomId = roomId, + subRoomId = subRoomId, + roomInstanceType = type, + isPrivate = isPrivate, + maxCapacity = 100, + name = "^RecCenter", + location = "cbad71af-0831-44d8-b8ef-69edafa841f6", + photonRoomId = photonRoomId, + }; + + db.RoomInstances.Add(instance); + return instance; + } + + private static RealPresence AddPresence(RecDBContext db, long playerId, long? roomInstance, bool isOnline = true) + { + var presence = new RealPresence + { + playerId = playerId, + isOnline = isOnline, + roomInstance = roomInstance, + errorCode = JoinStatus.Success, + }; + + db.Presences.Add(presence); + return presence; + } + + private static async Task MatchmakeRecCenterAsync() + { + return await MatchmakingUtils.Matchmake( + JoinMode.PublicMatchmaking, + "RecCenter", + PlayerId); + } + + [Fact] + public async Task AnEmptyPublicInstanceIsNeverHandedOut() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + SeedRoom(db); + var dead = SeedInstance(db); + AddPresence(db, PlayerId, null); + await db.SaveChangesAsync(); + + var deadId = dead.roomInstanceId; + + var match = await MatchmakeRecCenterAsync(); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.NotNull(match.roomInstance); + + // The join must land on a fresh instance, not on the room nobody can enter. + Assert.NotEqual(deadId, match.roomInstance!.roomInstanceId); + Assert.NotEqual(DeadPhotonRoomId, match.roomInstance.photonRoomId); + + // And the dead row must be gone, so the next attempt cannot pick it up either. + await using var verify = factory.CreateDbContext(); + Assert.Null(await verify.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == deadId)); + } + + [Fact] + public async Task AnInstanceWithSomebodyOnlineInItIsStillReused() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + SeedRoom(db); + var live = SeedInstance(db); + AddPresence(db, PlayerId, null); + AddPresence(db, 7, live.roomInstanceId); + await db.SaveChangesAsync(); + + var liveId = live.roomInstanceId; + + var match = await MatchmakeRecCenterAsync(); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.Equal(liveId, match.roomInstance!.roomInstanceId); + + await using var verify = factory.CreateDbContext(); + Assert.NotNull(await verify.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == liveId)); + } + + [Fact] + public async Task AnInstanceHeldOnlyByAnOfflinePresenceIsReaped() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + SeedRoom(db); + + // A client that dropped after a failed join leaves its presence parked on the + // instance. It counts as nobody in the room, so the room is gone. + var parked = SeedInstance(db); + AddPresence(db, PlayerId, null); + AddPresence(db, 7, parked.roomInstanceId, isOnline: false); + await db.SaveChangesAsync(); + + var parkedId = parked.roomInstanceId; + + var match = await MatchmakeRecCenterAsync(); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.NotEqual(parkedId, match.roomInstance!.roomInstanceId); + + await using var verify = factory.CreateDbContext(); + Assert.Null(await verify.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == parkedId)); + } + + [Fact] + public async Task APrivateInstanceIsLeftAlone() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + SeedRoom(db); + + // An owner's private session is empty too, but its owner has to be able to walk + // back in through matchmake/instance/{id}, so the sweep must not touch it. + var private_ = SeedInstance(db, isPrivate: true, type: RoomInstanceType.Private); + AddPresence(db, PlayerId, null); + await db.SaveChangesAsync(); + + var privateId = private_.roomInstanceId; + + var match = await MatchmakeRecCenterAsync(); + + // Public matchmaking also must not drop the player into somebody's private room. + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.NotEqual(privateId, match.roomInstance!.roomInstanceId); + + await using var verify = factory.CreateDbContext(); + Assert.NotNull(await verify.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == privateId)); + } + + [Fact] + public async Task TheSweepOnlyTouchesTheRoomBeingJoined() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + SeedRoom(db); + SeedRoom(db, OtherRoomId, subRoomId: 32, name: "DormRoom"); + var other = SeedInstance(db, roomId: OtherRoomId, subRoomId: 32, photonRoomId: "Radicade39-other"); + AddPresence(db, PlayerId, null); + await db.SaveChangesAsync(); + + var otherId = other.roomInstanceId; + + var match = await MatchmakeRecCenterAsync(); + + Assert.Equal(JoinStatus.Success, match.errorCode); + + await using var verify = factory.CreateDbContext(); + Assert.NotNull(await verify.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == otherId)); + } +} diff --git a/ServerTests/MatchmakeInstanceRouteTests.cs b/ServerTests/MatchmakeInstanceRouteTests.cs new file mode 100644 index 0000000..5f967d9 --- /dev/null +++ b/ServerTests/MatchmakeInstanceRouteTests.cs @@ -0,0 +1,331 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Matchmaking; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// `POST matchmaking/matchmake/instance/{instanceId}` is how the 2023 client re-enters one +/// specific live instance - the private session its owner just left, picked out of +/// `GET matchmaking/room/{roomId}/instances`. Radicade had no route for it at all, so the +/// call 404'd and "get back into my private instance" had no path except public +/// matchmaking, which hands out a different instance. +/// +/// The route assertion exists because a controller-level test cannot see a missing route - +/// that is the failure being fixed. The rest pin the owner gate: entering one's own +/// instance (and *only* one's own) has to work, and a private instance must not be refused +/// on this path the way `goto/instance/{id}` refuses it for everyone else. +/// +[Collection(DbFactoryCollection.Name)] +public class MatchmakeInstanceRouteTests +{ + private const long RoomId = 3; + private const long CreatorId = 1; + private const long CoOwnerId = 2; + private const long StrangerId = 9; + private const long SubRoomId = 3; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + private static void SetUser(Controller controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + return (factory, factory.CreateDbContext()); + } + + /// + /// A room with one live private instance - the shape an owner's own RecCenter session + /// has - plus a presence row for every account the test needs to move. + /// + private static async Task SeedRoomWithPrivateInstanceAsync( + RecDBContext db, + bool isPrivate = true, + long instanceRoomId = RoomId, + params long[] presences) + { + db.Rooms.Add(new RoomData + { + RoomId = RoomId, + Name = "RecCenter", + CreatorAccountId = CreatorId, + Accessibility = Accessibility.Public, + MaxPlayers = 100, + CreatedAt = DateTime.UtcNow, + }); + + db.SubRooms.Add(new SubRoom + { + SubRoomId = SubRoomId, + RoomId = RoomId, + Name = "Home", + UnitySceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6", + MaxPlayers = 100, + Accessibility = Accessibility.Public, + }); + + var instance = new RoomInstance + { + roomId = instanceRoomId, + subRoomId = SubRoomId, + roomInstanceType = RoomInstanceType.Private, + isPrivate = isPrivate, + maxCapacity = 100, + name = "^RecCenter", + location = "cbad71af-0831-44d8-b8ef-69edafa841f6", + }; + + db.RoomInstances.Add(instance); + + foreach (var accountId in presences) + { + db.Presences.Add(new RealPresence + { + playerId = accountId, + isOnline = true, + roomInstance = null, + errorCode = JoinStatus.Success, + }); + } + + await db.SaveChangesAsync(); + return instance.roomInstanceId; + } + + [Fact] + public void SupportsTheSpecificInstanceJoinRoute() + { + var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.MatchmakeInstance))!; + + var attributes = method + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), false) + .Cast() + .ToList(); + + Assert.Contains(attributes, x => x.Template == "matchmake/instance/{instanceId}"); + Assert.Contains(attributes, x => x is HttpPostAttribute); + } + + [Fact] + public async Task TheRoomCreatorCanEnterTheirOwnPrivateInstance() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var instanceId = await SeedRoomWithPrivateInstanceAsync(db, presences: CreatorId); + + var api = new MatchmakingAPI(db); + SetUser(api, CreatorId); + + MatchResponse? match = await api.MatchmakeInstance(instanceId, null); + + Assert.NotNull(match); + Assert.Equal(JoinStatus.Success, match!.errorCode); + Assert.NotNull(match.roomInstance); + Assert.Equal(instanceId, match.roomInstance!.roomInstanceId); + + // A private instance is the whole point of this route: it must not come back + // RoomInstanceIsPrivate the way goto/instance/{id} answers. + Assert.True(match.roomInstance.isPrivate); + + await using var verify = factory.CreateDbContext(); + var presence = await verify.Presences.FirstAsync(x => x.playerId == CreatorId); + Assert.Equal(instanceId, presence.roomInstance); + Assert.Equal(JoinStatus.Success, presence.errorCode); + } + + [Fact] + public async Task ACoOwnerCanEnterTheRoomsPrivateInstance() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var instanceId = await SeedRoomWithPrivateInstanceAsync(db, presences: CoOwnerId); + + db.RoomRoles.Add(new RoomRole + { + RoomId = RoomId, + AccountId = CoOwnerId, + Role = RoleType.CoOwner, + InvitedRole = RoleType.None, + }); + await db.SaveChangesAsync(); + + var api = new MatchmakingAPI(db); + SetUser(api, CoOwnerId); + + MatchResponse? match = await api.MatchmakeInstance(instanceId, null); + + Assert.NotNull(match); + Assert.Equal(JoinStatus.Success, match!.errorCode); + + await using var verify = factory.CreateDbContext(); + Assert.Equal(instanceId, (await verify.Presences.FirstAsync(x => x.playerId == CoOwnerId)).roomInstance); + } + + [Fact] + public async Task APlayerWhoDoesNotManageTheRoomGetsNoSuchRoomAndStaysPut() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var instanceId = await SeedRoomWithPrivateInstanceAsync(db, presences: StrangerId); + + // A Moderator is not enough: the Photon room id is the join coordinate, so only + // the room's creator and its co-owners may be handed it. + db.RoomRoles.Add(new RoomRole + { + RoomId = RoomId, + AccountId = StrangerId, + Role = RoleType.Moderator, + InvitedRole = RoleType.None, + }); + await db.SaveChangesAsync(); + + var api = new MatchmakingAPI(db); + SetUser(api, StrangerId); + + MatchResponse? match = await api.MatchmakeInstance(instanceId, null); + + Assert.NotNull(match); + Assert.Equal(JoinStatus.NoSuchRoom, match!.errorCode); + Assert.Null(match.roomInstance); + + await using var verify = factory.CreateDbContext(); + Assert.Null((await verify.Presences.FirstAsync(x => x.playerId == StrangerId)).roomInstance); + } + + [Fact] + public async Task AnUnknownInstanceIsNoSuchRoom() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomWithPrivateInstanceAsync(db, presences: CreatorId); + + var api = new MatchmakingAPI(db); + SetUser(api, CreatorId); + + MatchResponse? match = await api.MatchmakeInstance(9876543, null); + + Assert.NotNull(match); + Assert.Equal(JoinStatus.NoSuchRoom, match!.errorCode); + Assert.Null(match.roomInstance); + + await using var verify = factory.CreateDbContext(); + Assert.Null((await verify.Presences.FirstAsync(x => x.playerId == CreatorId)).roomInstance); + } + + [Fact] + public void SupportsTheRoomInstanceBrowserRoute() + { + var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.GetRoomInstancesInRoom))!; + + var attributes = method + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), false) + .Cast() + .ToList(); + + Assert.Contains(attributes, x => x.Template == "room/{roomId}/instances"); + Assert.Contains(attributes, x => x is HttpGetAttribute); + } + + [Fact] + public async Task TheInstanceBrowserAnswersAnArrayToAPlayerWhoDoesNotModerateTheRoom() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var instanceId = await SeedRoomWithPrivateInstanceAsync(db, isPrivate: false, presences: StrangerId); + var presence = await db.Presences.FirstAsync(x => x.playerId == StrangerId); + presence.roomInstance = instanceId; + await db.SaveChangesAsync(); + + var api = new MatchmakingAPI(db); + SetUser(api, StrangerId); + + // The browser is the watch menu every player opens for the room they are in. It used to + // require RoleType.Moderator and answer null to everyone else, which serialises to 204 + // No Content - the client's "Failed to get room instance browser: Response was empty". + var instances = await api.GetRoomInstancesInRoom(RoomId); + + Assert.NotNull(instances); + var summary = Assert.IsType>(Assert.Single(instances!)); + Assert.Equal(instanceId, summary["roomInstanceId"]); + Assert.Equal(RoomId, summary["roomId"]); + Assert.Equal(new[] { StrangerId }, + Assert.IsAssignableFrom>(summary["playerIds"])); + } + + [Fact] + public async Task TheInstanceBrowserAnswersAnEmptyArrayToACallerWithNoIdentity() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomWithPrivateInstanceAsync(db, presences: CreatorId); + + var api = new MatchmakingAPI(db); + api.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; + + var instances = await api.GetRoomInstancesInRoom(RoomId); + + Assert.NotNull(instances); + Assert.Empty(instances!); + } + + [Fact] + public async Task AnInstanceWhoseRoomIsGoneIsNoSuchRoom() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + // The instance survives a deleted room: it must not be entered through a room + // row that no longer exists (its subroom blob is unreachable anyway). + var instanceId = await SeedRoomWithPrivateInstanceAsync( + db, + instanceRoomId: 4242, + presences: CreatorId); + + var api = new MatchmakingAPI(db); + SetUser(api, CreatorId); + + MatchResponse? match = await api.MatchmakeInstance(instanceId, null); + + Assert.NotNull(match); + Assert.Equal(JoinStatus.NoSuchRoom, match!.errorCode); + } +} diff --git a/ServerTests/MatchmakingRuntimeStateTests.cs b/ServerTests/MatchmakingRuntimeStateTests.cs new file mode 100644 index 0000000..4449a2b --- /dev/null +++ b/ServerTests/MatchmakingRuntimeStateTests.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +public class MatchmakingRuntimeStateTests +{ + [Fact] + public async Task RestartClearsBroadcastSourcesIncludingPreviouslyDeletedInstances() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + await using var db = new RecDBContext(options); + db.RoomInstances.Add(new RoomInstance { roomInstanceId = 30895, roomId = 102, eventId = 9102 }); + db.Presences.Add(new RealPresence { playerId = 2, isOnline = true, roomInstance = 30895 }); + foreach (var (id, source) in new (long, long?)[] { (9102, 30895), (9103, 99999), (9104, null) }) + db.PlayerEvents.Add(new PlayerEventDB + { + PlayerEventId = id, + CreatorPlayerId = 2, + RoomId = 102, + Name = "Broadcast test", + Description = "Restart recovery", + IsMultiInstance = true, + DefaultBroadcastPermissions = 3, + BroadcastingRoomInstanceId = source, + StartTime = DateTime.UtcNow.AddMinutes(-5), + EndTime = DateTime.UtcNow.AddHours(1) + }); + await db.SaveChangesAsync(); + + await MatchmakingRuntimeState.ResetAsync(db); + // A second startup must also be safe when there is no remaining runtime state. + await MatchmakingRuntimeState.ResetAsync(db); + + await using var verify = new RecDBContext(options); + Assert.Empty(await verify.RoomInstances.ToListAsync()); + var presence = await verify.Presences.SingleAsync(); + Assert.False(presence.isOnline); + Assert.Null(presence.roomInstance); + var events = await verify.PlayerEvents.ToListAsync(); + Assert.Equal(3, events.Count); + Assert.All(events, ev => + { + Assert.Null(ev.BroadcastingRoomInstanceId); + Assert.True(ev.IsMultiInstance); + Assert.Equal(3, ev.DefaultBroadcastPermissions); + Assert.Equal("Broadcast test", ev.Name); + }); + } +} diff --git a/ServerTests/MessagesFanoutTests.cs b/ServerTests/MessagesFanoutTests.cs new file mode 100644 index 0000000..e76709c --- /dev/null +++ b/ServerTests/MessagesFanoutTests.cs @@ -0,0 +1,123 @@ +using System.Reflection; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Messages; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Responses; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The message fan-out the September client posts as `POST /api/messages/v1/sendMultiple` when +/// one message goes to several people. The 2026-10-03 client run showed the other account's +/// client reading a 404 here while an invite was being sorted out - which is what a missing +/// route looks like from the client side - so the route is pinned by shape and behaviour now. +/// +public class MessagesFanoutTests +{ + private const long SenderId = 2; + private const long FriendId = 5; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private static void SetUser(Controller controller, long userId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + [Fact] + public void SendMultipleIsServedUnderItsClientSpelling() + { + var routes = typeof(MessagesAPI).GetMethod(nameof(MessagesAPI.SendMultipleMessages))! + .GetCustomAttributes() + .Select(attribute => attribute.Template!) + .ToArray(); + + Assert.Contains("v1/sendMultiple", routes); + } + + /// + /// One stored row per unique recipient - the same MessageReceived push and v2/get inbox row + /// the single send produces - with the sender from the token, the type passed through + /// unmapped and the data carried as sent. + /// + [Fact] + public async Task SendMultipleStoresOneRowPerUniqueRecipient() + { + var factory = new TestDbContextFactory(); + var previous = Notify.DbFactory; + Notify.DbFactory = factory; + try + { + using var db = factory.CreateDbContext(); + var api = new MessagesAPI(db); + SetUser(api, SenderId); + + var result = await api.SendMultipleMessages(new SendMultipleMessagesRequestDTO + { + ToPlayerIds = new List { FriendId, FriendId, 0, -3 }, + Type = MessageType.FriendStatusOnline, + Data = "hello" + }); + + Assert.True(result.success); + var stored = Assert.Single(await db.Messages.ToListAsync()); + Assert.Equal(FriendId, stored.ToPlayerId); + Assert.Equal(SenderId, stored.FromPlayerId); + Assert.Equal(MessageType.FriendStatusOnline, stored.Type); + Assert.Equal("hello", stored.Data); + } + finally + { + Notify.DbFactory = previous; + } + } + + /// + /// A body with no usable id is refused by the name the reference uses for that case, and + /// nothing is stored or pushed. + /// + [Fact] + public async Task SendMultipleWithoutAUsableRecipientStoresNothing() + { + var factory = new TestDbContextFactory(); + var previous = Notify.DbFactory; + Notify.DbFactory = factory; + try + { + using var db = factory.CreateDbContext(); + var api = new MessagesAPI(db); + SetUser(api, SenderId); + + var result = await api.SendMultipleMessages(new SendMultipleMessagesRequestDTO + { + ToPlayerIds = new List { 0, -1 } + }); + + Assert.False(result.success); + Assert.Empty(await db.Messages.ToListAsync()); + } + finally + { + Notify.DbFactory = previous; + } + } +} diff --git a/ServerTests/ModerationBlockDetailsTests.cs b/ServerTests/ModerationBlockDetailsTests.cs new file mode 100644 index 0000000..fcbc793 --- /dev/null +++ b/ServerTests/ModerationBlockDetailsTests.cs @@ -0,0 +1,269 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Moderation; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class ModerationBlockDetailsTests +{ + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static PlayerReportingAPI NewApi(RecDBContext db, long? accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + if (accountId.HasValue) + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.Value.ToString())); + return new PlayerReportingAPI( + db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + } + + private static void AssertReportSucceeded(ActionResult result) + { + var ok = Assert.IsType(result.Result); + Assert.True(Assert.IsType(ok.Value).Success); + } + + private static async Task AddReporterAsync(RecDBContext db, long accountId, string? roles = null) + { + db.Profiles.Add(new Profile + { + accountId = accountId, + username = $"reporter{accountId}", + displayName = $"Reporter {accountId}", + profileImage = DBConfig.DefaultProfileImage, + role = roles + }); + await db.SaveChangesAsync(); + } + + [Theory] + [InlineData(0, false)] + [InlineData(5, false)] + [InlineData(6, true)] + [InlineData(7, true)] + public void AutomaticTimeoutRequiresMoreThanFiveReportsFromAPlayer(int reportCount, bool expected) + { + Assert.Equal(expected, PlayerReportingAPI.ShouldApplyAutomaticTimeout(reportCount, reporterIsModerator: false)); + } + + [Fact] + public async Task TheSixthPublicReportCreatesAnAutomaticTimeout() + { + using var db = NewDb(); + await AddReporterAsync(db, 6); + db.Reputations.Add(new Reputation { AccountId = 42, ReportCount = 5 }); + await db.SaveChangesAsync(); + + AssertReportSucceeded(await NewApi(db, 6).CreateReport(new CreateReportRequest + { + PlayerIdReported = 42, + ReportCategory = ReportCategory.CoC_Trolling, + Details = "report details", + RoomId = 0 + })); + + var moderation = await db.Moderation.SingleAsync(); + Assert.Equal("Automatic Timeout", moderation.BanMethod); + Assert.Equal("You have received a timeout due to a high number of reports for Trolling.", moderation.Message); + Assert.True(moderation.CreatedAt <= DateTime.UtcNow); + Assert.Equal(6, (await db.Reputations.SingleAsync(x => x.AccountId == 42)).ReportCount); + } + + [Fact] + public async Task SelfAndUnauthenticatedReportsDoNotCountTowardAutomaticTimeouts() + { + using var db = NewDb(); + db.Reputations.Add(new Reputation { AccountId = 42, ReportCount = 5 }); + await db.SaveChangesAsync(); + + AssertReportSucceeded(await NewApi(db, 42).CreateReport(new CreateReportRequest + { + PlayerIdReported = 42, + ReportCategory = ReportCategory.CoC_Trolling + })); + Assert.Empty(await db.Moderation.ToListAsync()); + Assert.Equal(5, (await db.Reputations.SingleAsync(x => x.AccountId == 42)).ReportCount); + + AssertReportSucceeded(await NewApi(db, null).CreateReport(new CreateReportRequest + { + PlayerIdReported = 42, + ReportCategory = ReportCategory.CoC_Trolling + })); + Assert.Empty(await db.Moderation.ToListAsync()); + Assert.Equal(5, (await db.Reputations.SingleAsync(x => x.AccountId == 42)).ReportCount); + } + + [Fact] + public async Task ModeratorReportCreatesDirectTimeoutWithoutAlsoUsingPublicReportEscalation() + { + using var db = NewDb(); + await AddReporterAsync(db, 6, "moderator"); + db.Reputations.Add(new Reputation { AccountId = 42, ReportCount = 0 }); + await db.SaveChangesAsync(); + + AssertReportSucceeded(await NewApi(db, 6).CreateReport(new CreateReportRequest + { + PlayerIdReported = 42, + ReportCategory = ReportCategory.CoC_Trolling, + Details = "Moderator observed trolling" + })); + + var moderation = await db.Moderation.SingleAsync(); + Assert.Equal("Moderator Report", moderation.BanMethod); + Assert.Equal("Moderator observed trolling", moderation.Message); + Assert.Equal(0, (await db.Reputations.SingleAsync(x => x.AccountId == 42)).ReportCount); + } + + [Fact] + public async Task InvalidReportCategoryIsRejectedBeforeItCounts() + { + using var db = NewDb(); + await AddReporterAsync(db, 6); + + var result = await NewApi(db, 6).CreateReport(new CreateReportRequest + { + PlayerIdReported = 42, + ReportCategory = (ReportCategory)9999 + }); + + Assert.IsType(result.Result); + Assert.Empty(await db.Reputations.ToListAsync()); + } + + [Theory] + [InlineData(ReportCategory.CoC_Trolling, "Trolling")] + [InlineData(ReportCategory.CoC_Sexual, "Sexual behavior")] + public void AutomaticTimeoutReasonUsesTheReportCategory(ReportCategory category, string expected) + { + Assert.Equal(expected, PlayerReportingAPI.GetAutomaticTimeoutReason(category)); + } + + [Fact] + public async Task VoiceModerationBlockDetailsCarryStartTimeButNotModeratorOnlyNote() + { + using var db = NewDb(); + var startedAt = DateTime.UtcNow; + db.Moderation.Add(new ModerationDB + { + PlayerId = 42, + IsBan = true, + Message = "Sexually explicit behavior or photos", + ModeratorNote = "Sexual harassment in the Rec Center", + BanMethod = "Voice Moderation", + Category = ReportCategory.CoC_Sexual, + CreatedAt = startedAt, + ExpireDuration = startedAt.AddDays(1), + AppealCode = "ABC12345" + }); + await db.SaveChangesAsync(); + + var api = NewApi(db, 42); + api.ControllerContext.HttpContext.Request.Headers["X-Forwarded-For"] = "192.0.2.42"; + var result = Assert.IsType(await api.GetBlockDetails()); + var details = Assert.IsType(result.Value); + + Assert.Equal(startedAt, details.TimeoutStartedAt); + Assert.Equal("VoiceModeration", details.BanType); + Assert.Equal("Sexually explicit behavior or photos", details.Message); + } + + [Theory] + [InlineData("Automatic Timeout", "You have received a timeout due to a high number of reports for Trolling", "You have received a timeout due to a high number of reports for Trolling.")] + [InlineData("Moderator Report", "You were reported by a moderator", "You have received a timeout. You were reported by a moderator.")] + [InlineData("Moderator Report", "You were reported by a moderator.", "You have received a timeout. a moderator report.")] + [InlineData("Timeout", "Trolling", "You have received a timeout due to a high number of reports for Trolling.")] + [InlineData("Automatic Timeout", "You have received a timeout due to a high number of reports for Trolling.", "You have received a timeout due to a high number of reports for Trolling.")] + public async Task PersistedTimeoutBanHasClientFacingTimeoutCopy(string banMethod, string storedReason, string expectedMessage) + { + using var db = NewDb(); + var startedAt = DateTime.UtcNow; + db.Moderation.Add(new ModerationDB + { + PlayerId = 42, + IsBan = true, + Message = storedReason, + BanMethod = banMethod, + Category = ReportCategory.CoC_Trolling, + CreatedAt = startedAt, + ExpireDuration = startedAt.AddHours(1), + AppealCode = "ABC12345" + }); + await db.SaveChangesAsync(); + + var api = NewApi(db, 42); + api.ControllerContext.HttpContext.Request.Headers["X-Forwarded-For"] = "192.0.2.42"; + var result = Assert.IsType(await api.GetBlockDetails()); + var details = Assert.IsType(result.Value); + + Assert.Equal("Timeout", details.BanType); + Assert.Equal(startedAt, details.TimeoutStartedAt); + Assert.Equal(expectedMessage, details.DisplayReason); + Assert.Equal(expectedMessage, details.Message); + } + + [Fact] + public async Task HardwareBanShowsTheOriginalRelatedAccountOnItsOwnLine() + { + using var db = NewDb(); + var startedAt = DateTime.UtcNow; + db.Players.Add(new PlayerData { AccountId = 42, DeviceId = "shared-device" }); + db.Profiles.Add(new Profile + { + accountId = 12, + username = "HealthyMoose640", + displayName = "HealthyMoose640", + profileImage = DBConfig.DefaultProfileImage + }); + db.HardwareBans.Add(new HardwareBan + { + DeviceId = "shared-device", + BannedAccountId = 12, + Reason = "Moderator. Account ban", + CreatedAt = startedAt, + ExpiresAt = startedAt.AddDays(1) + }); + await db.SaveChangesAsync(); + + var api = NewApi(db, 42); + api.ControllerContext.HttpContext.Request.Headers["X-Forwarded-For"] = "192.0.2.42"; + var result = Assert.IsType(await api.GetBlockDetails()); + var details = Assert.IsType(result.Value); + + Assert.Contains("\nRelated account: @HealthyMoose640", details.Message); + Assert.Equal(startedAt, details.TimeoutStartedAt); + } + + /* Every ban the client is shown has to carry the moment it started, because + that is the "Banned since" line. Kicks must not: they are not bans and the + screen has nothing to date. */ + [Fact] + public void BanBlockDetailsCarryBannedSinceWhileKicksDoNot() + { + var startedAt = DateTime.UtcNow.AddDays(-3); + + var ban = PlayerUtils.BuildBlockDetails(true, ReportCategory.Misc, "reason", startedAt.AddDays(5), startedAtUtc: startedAt); + Assert.Equal(startedAt, ban.TimeoutStartedAt); + + var kick = PlayerUtils.BuildBlockDetails(true, ReportCategory.Misc, "kicked", DateTime.UtcNow.AddMinutes(15)); + Assert.Null(kick.TimeoutStartedAt); + } +} diff --git a/ServerTests/ModerationTimeoutVoiceTests.cs b/ServerTests/ModerationTimeoutVoiceTests.cs new file mode 100644 index 0000000..d11d683 --- /dev/null +++ b/ServerTests/ModerationTimeoutVoiceTests.cs @@ -0,0 +1,662 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using RadicadeServer.API.Moderation; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The three things this server can do to an account, and the rules that keep them apart: +/// +/// * a timeout is never appealable - no code is issued, so the client draws neither the appeal +/// button nor the support text under it; +/// * a voice moderation ban keeps the appeal button and refuses the appeal behind it; +/// * a voice restriction is not a ban at all: it cuts the microphone and leaves the account +/// status screen, the ban list and the reputation alone. +/// +public class ModerationTimeoutVoiceTests +{ + private const long Player = 42; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static async Task WithProfile(long accountId = Player, string? role = null) + { + var db = NewDb(); + db.Profiles.Add(new Profile + { + accountId = accountId, + username = $"player{accountId}", + displayName = $"Player {accountId}", + profileImage = "", + role = role ?? "" + }); + await db.SaveChangesAsync(); + return db; + } + + private static DefaultHttpContext SignedInAs(long accountId) => new() + { + User = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity( + new[] { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, accountId.ToString()) }, + "test")) + }; + + private static ModerationDB TimeoutBan(long playerId, string method, DateTime? expiry = null) => new() + { + PlayerId = playerId, + IsBan = true, + Message = "You have received a timeout due to a high number of reports for Trolling.", + Category = ReportCategory.CoC_Trolling, + BanMethod = method, + CreatedAt = DateTime.UtcNow, + ExpireDuration = expiry ?? DateTime.UtcNow.AddHours(1) + }; + + // ---------------------------------------------------------------- policy + + [Fact] + public void ATimeoutIsBetweenAnHourAndADay() + { + Assert.Equal(TimeSpan.FromHours(1), ModerationPolicy.ClampTimeout(TimeSpan.FromMinutes(1))); + Assert.Equal(TimeSpan.FromHours(24), ModerationPolicy.ClampTimeout(TimeSpan.FromDays(7))); + Assert.Equal(TimeSpan.FromHours(6), ModerationPolicy.ClampTimeout(TimeSpan.FromHours(6))); + } + + [Fact] + public void TimeoutsAreNotAppealableAndThatIsWhatSeparatesThemFromABan() + { + Assert.False(ModerationPolicy.IssuesAppealCode("Automatic Timeout")); + Assert.False(ModerationPolicy.IssuesAppealCode("Moderator Report")); + Assert.False(ModerationPolicy.IssuesAppealCode("Timeout")); + Assert.True(ModerationPolicy.IssuesAppealCode("Admin Panel")); + Assert.True(ModerationPolicy.IssuesAppealCode("Voice Moderation")); + } + + [Fact] + public void AVoiceBanDrawsTheButtonAndThenRefusesTheAppeal() + { + Assert.True(ModerationPolicy.IssuesAppealCode("Voice Moderation")); + Assert.True(ModerationPolicy.RefusesAppeal("Voice Moderation")); + Assert.False(ModerationPolicy.RefusesAppeal("Admin Panel")); + Assert.False(ModerationPolicy.IssuesAppealCode("Voice Moderation") && !ModerationPolicy.RefusesAppeal("Voice Moderation"), + "a voice ban must never be a ban that can actually be appealed"); + } + + [Fact] + public void ATimeoutStartsAtAnHourUnlessGivenADuration() + { + var now = new DateTime(2026, 10, 12, 12, 0, 0, DateTimeKind.Utc); + Assert.Equal(now.AddHours(1), ModerationPolicy.TimeoutExpiry(now)); + Assert.Equal(now.AddHours(8), ModerationPolicy.TimeoutExpiry(now, TimeSpan.FromHours(8))); + Assert.Equal(now.AddHours(24), ModerationPolicy.TimeoutExpiry(now, TimeSpan.FromDays(30))); + } + + [Fact] + public void ExtendingATimeoutMeasuresFromWhenItStartedAndStopsAtADay() + { + var started = new DateTime(2026, 10, 12, 12, 0, 0, DateTimeKind.Utc); + var current = started.AddHours(1); + + // More reports raise the duration but do not restart the clock. + Assert.Equal(started.AddHours(6), ModerationPolicy.ExtendedTimeoutExpiry(started, current, TimeSpan.FromHours(6))); + // A shorter ask never shortens a timeout that is already running. + Assert.Equal(current, ModerationPolicy.ExtendedTimeoutExpiry(started, current, TimeSpan.FromMinutes(5))); + // And the whole thing stops at a day from when it started. + Assert.Equal(started.AddHours(24), ModerationPolicy.ExtendedTimeoutExpiry(started, current, TimeSpan.FromDays(2))); + } + + // ---------------------------------------------------------------- timeouts + + [Fact] + public async Task ATimeoutCannotBeAppealedAndCarriesNoCode() + { + await using var db = await WithProfile(); + db.Moderation.Add(TimeoutBan(Player, ModerationPolicy.AutomaticTimeoutBanMethod)); + await db.SaveChangesAsync(); + + var appeal = new BanAppealAPI(db) { ControllerContext = new ControllerContext { HttpContext = SignedInAs(Player) } }; + var generated = await appeal.GenerateCode(); + + var payload = Assert.IsAssignableFrom(generated.Result).Value!; + var type = payload.GetType(); + Assert.False((bool)type.GetProperty("success")!.GetValue(payload)!); + Assert.Equal(AccountLock.NotAppealableMessage, type.GetProperty("error")!.GetValue(payload) as string); + + // The poll the account status screen makes must not mint a code for one either: an empty + // code is what keeps the appeal button off the screen. + var reporting = new PlayerReportingAPI(db) { ControllerContext = new ControllerContext { HttpContext = SignedInAs(Player) } }; + var details = await reporting.GetBlockDetails(); + var detailsObject = Assert.IsType(Assert.IsAssignableFrom(details).Value); + Assert.Equal("Timeout", detailsObject.BanType); + Assert.Equal(string.Empty, detailsObject.BanAppealCode); + Assert.Equal(string.Empty, detailsObject.BanAppealText); + Assert.Null((await db.Moderation.SingleAsync(x => x.PlayerId == Player)).AppealCode); + } + + [Fact] + public async Task AnAlreadyIssuedCodeCannotBeTurnedIntoATimeoutAppeal() + { + await using var db = await WithProfile(); + var ban = TimeoutBan(Player, ModerationPolicy.ModeratorReportBanMethod); + ban.AppealCode = "OLDTIMEOUT"; + db.Moderation.Add(ban); + await db.SaveChangesAsync(); + + var submitApi = new AppealAPI(db) { ControllerContext = new ControllerContext { HttpContext = SignedInAs(Player) } }; + var submitted = await submitApi.Submit(new SubmitAppealRequest + { + Code = "OLDTIMEOUT", + Text = "please lift this timeout, it was not me" + }); + + var refused = Assert.IsType(submitted.Result); + Assert.Equal(AccountLock.NotAppealableMessage, Assert.IsType(refused.Value).Message); + Assert.Null((await db.Moderation.SingleAsync(x => x.PlayerId == Player)).AppealText); + } + + [Fact] + public async Task AVoiceModerationBanKeepsTheButtonAndRefusesTheAppeal() + { + await using var db = await WithProfile(); + var ban = TimeoutBan(Player, ModerationPolicy.VoiceModerationBanMethod); + ban.AppealCode = "VOICECODE"; + db.Moderation.Add(ban); + await db.SaveChangesAsync(); + + var appeal = new BanAppealAPI(db) { ControllerContext = new ControllerContext { HttpContext = SignedInAs(Player) } }; + var generated = await appeal.GenerateCode(); + var payload = Assert.IsAssignableFrom(generated.Result).Value!; + var type = payload.GetType(); + Assert.False((bool)type.GetProperty("success")!.GetValue(payload)!); + Assert.Equal(AccountLock.NotAppealableMessage, type.GetProperty("error")!.GetValue(payload) as string); + + // The ban is still a ban, so the row keeps the code the screen draws its button from. + Assert.Equal("VOICECODE", (await db.Moderation.SingleAsync(x => x.PlayerId == Player)).AppealCode); + + var submitApi = new AppealAPI(db) { ControllerContext = new ControllerContext { HttpContext = SignedInAs(Player) } }; + var submitted = await submitApi.Submit(new SubmitAppealRequest + { + Code = "VOICECODE", + Text = "this was a joke with friends, please reconsider" + }); + var refused = Assert.IsType(submitted.Result); + Assert.Equal(AccountLock.NotAppealableMessage, Assert.IsType(refused.Value).Message); + } + + [Fact] + public async Task AModeratorReportIsAnHourLongTimeoutWithNoAppeal() + { + await using var db = await WithProfile(); + db.Profiles.Add(new Profile + { + accountId = 7, + username = "moderator", + displayName = "Moderator", + profileImage = "", + role = "moderator" + }); + await db.SaveChangesAsync(); + + var before = DateTime.UtcNow; + var api = new PlayerReportingAPI(db) { ControllerContext = new ControllerContext { HttpContext = SignedInAs(7) } }; + var result = await api.CreateReport(new CreateReportRequest + { + PlayerIdReported = Player, + ReportCategory = ReportCategory.CoC_Trolling, + Details = "griefing the lobby", + RoomId = 1 + }); + + Assert.IsType(result.Result); + + var ban = await db.Moderation.SingleAsync(x => x.PlayerId == Player); + Assert.True(ban.IsBan); + Assert.Equal(ModerationPolicy.ModeratorReportBanMethod, ban.BanMethod); + Assert.Null(ban.AppealCode); + Assert.True(ban.ExpireDuration >= before.AddMinutes(59) && ban.ExpireDuration <= before.AddMinutes(61), + $"a moderator report should be about an hour, was {ban.ExpireDuration - before}"); + } + + [Fact] + public async Task AFlipOfReportsTimesOutTheReportedPlayerWithoutAnAppeal() + { + await using var db = await WithProfile(); + db.Profiles.Add(new Profile + { + accountId = 9, + username = "reporter", + displayName = "Reporter", + profileImage = "", + role = "" + }); + await db.SaveChangesAsync(); + + var api = new PlayerReportingAPI(db) { ControllerContext = new ControllerContext { HttpContext = SignedInAs(9) } }; + var before = DateTime.UtcNow; + + // The threshold is crossed somewhere in here; reporting the same player repeatedly is what + // the automatic timeout exists to catch, so the last report must leave a timeout behind. + for (var i = 0; i < 8; i++) + { + await api.CreateReport(new CreateReportRequest + { + PlayerIdReported = Player, + ReportCategory = ReportCategory.CoC_Trolling, + Details = $"report {i}", + RoomId = 1 + }); + } + + var ban = await db.Moderation.SingleAsync(x => x.PlayerId == Player); + Assert.True(ban.IsBan); + Assert.Equal(ModerationPolicy.AutomaticTimeoutBanMethod, ban.BanMethod); + Assert.Null(ban.AppealCode); + Assert.Contains("timeout", ban.Message, StringComparison.OrdinalIgnoreCase); + Assert.True(ban.ExpireDuration <= before.AddHours(24).AddMinutes(1), + "an automatic timeout must not exceed a day"); + } + + // ---------------------------------------------------------------- the admin panel's IsTimeout + + private const string AdminSecret = "test-bot-secret"; + + private static async Task AdminPatch(RecDBContext db, string json) + { + var payload = System.Text.Json.JsonDocument.Parse(json).RootElement.Clone(); + // The panel reads its own session cookie for the action log, so the request has to exist. + var api = new RadicadeServer.Admin.AdminPanelAPI(db) + { + ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } + }; + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = AdminSecret; + try { return await api.Update("players", Player, AdminSecret, payload); } + finally { DBConfig.AdminApiKey = original; } + } + + [Fact] + public async Task TheAdminIsTimeoutCheckboxMakesTheBanATimeoutWithNoAppeal() + { + await using var db = await WithProfile(); + var before = DateTime.UtcNow; + + var result = await AdminPatch(db, """ + { + "banned": true, + "ban_reason": "Trolling", + "ban_type": "Temporary", + "ban_category": 7, + "ban_method": "Admin Panel", + "ban_is_timeout": true + } + """); + + Assert.IsType(result); + + var ban = await db.Moderation.SingleAsync(x => x.PlayerId == Player); + Assert.True(ban.IsBan); + Assert.Equal(ModerationPolicy.AutomaticTimeoutBanMethod, ban.BanMethod); + Assert.Null(ban.AppealCode); + Assert.Contains("timeout", ban.Message, StringComparison.OrdinalIgnoreCase); + // IsTimeout keeps the duration inside the timeout window. + Assert.True(ban.ExpireDuration >= before.AddMinutes(59) && ban.ExpireDuration <= before.AddHours(24).AddMinutes(1), + $"a timeout must land between an hour and a day from now, was {ban.ExpireDuration - before}"); + } + + [Fact] + public async Task AnAdminTimeoutLongerThanADayIsClampedAndAnExistingAppealCodeIsDropped() + { + await using var db = await WithProfile(); + db.Moderation.Add(new ModerationDB + { + PlayerId = Player, + IsBan = true, + Message = "old ban", + Category = ReportCategory.Misc, + BanMethod = "Admin Panel", + AppealCode = "OLDCODE", + ExpireDuration = DateTime.UtcNow.AddDays(30) + }); + await db.SaveChangesAsync(); + + var before = DateTime.UtcNow; + var until = DateTime.UtcNow.AddDays(7).ToString("O"); + var result = await AdminPatch(db, $$""" + { + "banned": true, + "ban_reason": "Voice spam", + "ban_type": "Temporary", + "ban_category": 7, + "ban_method": "Admin Panel", + "ban_until": "{{until}}", + "ban_is_timeout": true + } + """); + + Assert.IsType(result); + + var ban = await db.Moderation.SingleAsync(x => x.PlayerId == Player); + Assert.Equal(ModerationPolicy.AutomaticTimeoutBanMethod, ban.BanMethod); + Assert.Null(ban.AppealCode); + Assert.True(ban.ExpireDuration <= before.AddHours(24).AddMinutes(1), + $"a seven day timeout ask must be clamped to a day, was {ban.ExpireDuration - before}"); + } + + [Fact] + public async Task AnAdminVoiceModerationBanKeepsACodeSoTheButtonIsDrawn() + { + await using var db = await WithProfile(); + + var result = await AdminPatch(db, """ + { + "banned": true, + "ban_reason": "Sexually explicit behavior or photos", + "ban_type": "Temporary", + "ban_category": 2, + "ban_method": "Voice Moderation", + "ban_until": "2027-01-01T00:00:00.000Z" + } + """); + + Assert.IsType(result); + + var ban = await db.Moderation.SingleAsync(x => x.PlayerId == Player); + Assert.Equal(ModerationPolicy.VoiceModerationBanMethod, ban.BanMethod); + // The code is issued so the screen still draws the appeal button, and the appeal it opens + // is then refused - the player is told the ban cannot be appealed instead of being left + // staring at a screen with no way to ask. + Assert.True(ModerationPolicy.IssuesAppealCode(ban.BanMethod)); + Assert.True(ModerationPolicy.RefusesAppeal(ban.BanMethod)); + // The code is what puts the button on the screen; the refusal happens when it is pressed. + Assert.False(string.IsNullOrWhiteSpace(ban.AppealCode)); + } + + // ---------------------------------------------------------------- what the client is told + + private static async Task Poll(RecDBContext db) + { + var reporting = new PlayerReportingAPI(db) + { + ControllerContext = new ControllerContext { HttpContext = SignedInAs(Player) } + }; + var result = await reporting.GetBlockDetails(); + return Assert.IsType(Assert.IsAssignableFrom(result).Value); + } + + [Fact] + public async Task AVoiceBanTellsTheClientItIsOneSoItPrintsItsOwnSentence() + { + await using var db = await WithProfile(); + db.Moderation.Add(new ModerationDB + { + PlayerId = Player, + IsBan = true, + Message = "Sexually explicit behavior or photos", + Category = ReportCategory.CoC_Sexual, + BanMethod = ModerationPolicy.VoiceModerationBanMethod, + AppealCode = "VOICECODE", + CreatedAt = DateTime.UtcNow, + ExpireDuration = DateTime.UtcNow.AddDays(7) + }); + await db.SaveChangesAsync(); + + var details = await Poll(db); + + // The sentence the player reads for one of these is the client's own, chosen off this flag: + // a voice ban that does not set it is drawn with the ordinary "After a review of reports, + // you have received a ban for" headline instead, which is the bug this pins down. + Assert.True(details.IsVoiceModAutoban); + Assert.Equal("VoiceModeration", details.BanType); + + // The reason the moderator gave shows underneath that sentence, and the sentence itself is + // not repeated into it. + Assert.Equal("Sexually explicit behavior or photos", details.Message); + Assert.DoesNotContain("voice moderation system", details.Message, StringComparison.OrdinalIgnoreCase); + + // The button comes from the code, and the text beside it is the refusal. + Assert.Equal("VOICECODE", details.BanAppealCode); + Assert.Equal(AccountLock.NotAppealableMessage, details.BanAppealText); + } + + [Fact] + public async Task AnOrdinaryBanClaimsNeitherAVoiceBanNorATimeout() + { + await using var db = await WithProfile(); + db.Moderation.Add(new ModerationDB + { + PlayerId = Player, + IsBan = true, + Message = "Harassment", + Category = ReportCategory.Harassment, + BanMethod = "Admin Panel", + CreatedAt = DateTime.UtcNow, + ExpireDuration = DateTime.UtcNow.AddDays(3) + }); + await db.SaveChangesAsync(); + + var details = await Poll(db); + + Assert.False(details.IsVoiceModAutoban); + Assert.Equal("Moderation", details.BanType); + // An ordinary ban is shown with the reason as it was written. + Assert.Equal("Harassment", details.Message); + } + + [Fact] + public async Task ATimeoutIsNotAVoiceBan() + { + await using var db = await WithProfile(); + db.Moderation.Add(TimeoutBan(Player, ModerationPolicy.AutomaticTimeoutBanMethod)); + await db.SaveChangesAsync(); + + var details = await Poll(db); + + Assert.False(details.IsVoiceModAutoban); + Assert.Equal("Timeout", details.BanType); + Assert.Equal("You have received a timeout due to a high number of reports for Trolling.", details.Message); + // No code and no text: the appeal button and the support line under it are not drawn. + Assert.Equal(string.Empty, details.BanAppealCode); + Assert.Equal(string.Empty, details.BanAppealText); + } + + [Fact] + public async Task TheBodyPushedToABannedClientCarriesTheVoiceFlag() + { + await using var db = await WithProfile(); + var ban = new PlayerUtils.PlayerBanInfo("Moderation", "Sexually explicit behavior or photos", + DateTime.UtcNow.AddDays(7), "mod:9", ReportCategory.CoC_Sexual, + AppealCode: "VOICECODE", StartedAtUtc: DateTime.UtcNow, + BanMethod: ModerationPolicy.VoiceModerationBanMethod); + + var details = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, accountId: Player); + + Assert.True(details.IsVoiceModAutoban); + Assert.Equal("VoiceModeration", details.BanType); + Assert.Equal(AccountLock.NotAppealableMessage, details.BanAppealText); + Assert.Equal("Sexually explicit behavior or photos", details.DisplayReason); + } + + /* The client finds these by name, so the name is part of the contract. The API serializes with + no naming policy - Program.cs sets PropertyNamingPolicy = null - which is why the field the + client looks for is spelled the same in C# and on the wire. */ + [Fact] + public async Task TheVoiceFlagTravelsUnderTheNameTheClientLooksFor() + { + await using var db = await WithProfile(); + db.Moderation.Add(new ModerationDB + { + PlayerId = Player, + IsBan = true, + Message = "Sexually explicit behavior or photos", + Category = ReportCategory.CoC_Sexual, + BanMethod = ModerationPolicy.VoiceModerationBanMethod, + CreatedAt = DateTime.UtcNow, + ExpireDuration = DateTime.UtcNow.AddDays(7) + }); + await db.SaveChangesAsync(); + + var details = await Poll(db); + var json = JsonSerializer.Serialize(details, new JsonSerializerOptions { PropertyNamingPolicy = null }); + + Assert.Contains("\"IsVoiceModAutoban\":true", json); + // The fields the client already reads out of this body are spelled the same way. + Assert.Contains("\"IsBan\":true", json); + Assert.Contains("\"BanAppealText\":", json); + } + + [Fact] + public async Task AnAdminVoiceModerationBanReachesTheClientAsAVoiceBan() + { + await using var db = await WithProfile(); + + var result = await AdminPatch(db, """ + { + "banned": true, + "ban_reason": "Sexually explicit behavior or photos", + "ban_type": "Temporary", + "ban_category": 2, + "ban_method": "Voice Moderation", + "ban_until": "2027-01-01T00:00:00.000Z" + } + """); + + Assert.IsType(result); + + var details = await Poll(db); + Assert.True(details.IsVoiceModAutoban); + Assert.Equal("VoiceModeration", details.BanType); + } + + // ---------------------------------------------------------------- voice restrictions + + [Fact] + public async Task AVoiceRestrictionCutsVoiceChatAndNothingElse() + { + await using var db = await WithProfile(); + + Assert.Null(await VoiceChatRestrictions.ActiveAsync(db, Player)); + + var restriction = await VoiceChatRestrictions.ApplyAsync(db, Player, "slurs in voice chat", + ReportCategory.CoC_Trolling, TimeSpan.FromHours(6), VoiceChatRestrictions.Method, "StaffOne", "warning given first"); + + Assert.Equal(Player, restriction.PlayerId); + Assert.Equal(VoiceChatRestrictions.Method, restriction.IssuedBy); + Assert.Equal("StaffOne", restriction.IssuedByUsername); + + var active = await VoiceChatRestrictions.ActiveAsync(db, Player); + Assert.NotNull(active); + + // A restriction is not a ban: nothing may resolve as one, or the player is ejected and the + // account status screen appears. + Assert.Empty(await db.Moderation.Where(x => x.PlayerId == Player).ToListAsync()); + Assert.Null(await db.ResolveBanAsync(Player)); + + var payload = VoiceChatRestrictions.Payload(active); + var payloadType = payload.GetType(); + Assert.True((bool)payloadType.GetProperty("restricted")!.GetValue(payload)!); + Assert.Equal(restriction.ExpiresAt, (DateTime)payloadType.GetProperty("expiresAtUtc")!.GetValue(payload)!); + + Assert.True(await VoiceChatRestrictions.ClearAsync(db, Player, "StaffOne")); + Assert.Null(await VoiceChatRestrictions.ActiveAsync(db, Player)); + Assert.False(await VoiceChatRestrictions.ClearAsync(db, Player, "StaffOne")); + + // Lifted, not deleted: the history stays. + var row = await db.VoiceChatRestrictions.SingleAsync(x => x.PlayerId == Player); + Assert.NotNull(row.ClearedAt); + Assert.Equal("StaffOne", row.ClearedBy); + } + + [Fact] + public async Task ARestrictionTellsThePlayerWhyInACoachMessage() + { + await using var db = await WithProfile(); + + var restriction = await VoiceChatRestrictions.ApplyAsync(db, Player, "slurs in voice chat", + ReportCategory.CoC_Trolling, TimeSpan.FromHours(6), VoiceChatRestrictions.Method); + + Assert.NotNull(restriction); + + // What the player is told when the restriction lands, and who it comes from. The send + // itself is best effort - it goes out through the notify hub, which a unit test has no + // factory for - so the sentence and the sender are what can be pinned here. + Assert.Equal( + "You have had your voice privileges temporarily restricted due to speech which violates the Code of Conduct.", + VoiceChatRestrictions.CoachMessage); + Assert.Equal(1, VoiceChatRestrictions.CoachAccountId); + } + + [Fact] + public async Task ASecondRestrictionExtendsTheFirstAndNeverShortensIt() + { + await using var db = await WithProfile(); + + var first = await VoiceChatRestrictions.ApplyAsync(db, Player, "first", ReportCategory.Misc, + TimeSpan.FromHours(12), VoiceChatRestrictions.Method); + var longExpiry = first.ExpiresAt; + + var shortened = await VoiceChatRestrictions.ApplyAsync(db, Player, "second", ReportCategory.Misc, + TimeSpan.FromHours(1), VoiceChatRestrictions.Method); + Assert.Equal(longExpiry, shortened.ExpiresAt); + Assert.Equal("second", shortened.Reason); + + var extended = await VoiceChatRestrictions.ApplyAsync(db, Player, "third", ReportCategory.Misc, + TimeSpan.FromDays(30), VoiceChatRestrictions.Method); + Assert.True(extended.ExpiresAt > longExpiry); + Assert.True(extended.ExpiresAt <= DateTime.UtcNow.AddDays(30).AddMinutes(1)); + Assert.Single(await db.VoiceChatRestrictions.Where(x => x.PlayerId == Player).ToListAsync()); + } + + [Fact] + public async Task TheClientEndpointAnswersWithTheStateTheClientBuildReads() + { + await using var db = await WithProfile(); + var api = new RadicadeServer.API.Config.VoiceChatRestrictionAPI(db) + { + ControllerContext = new ControllerContext { HttpContext = SignedInAs(Player) } + }; + + var clear = Assert.IsAssignableFrom(await api.GetRestriction()).Value!; + Assert.False((bool)clear.GetType().GetProperty("restricted")!.GetValue(clear)!); + + await VoiceChatRestrictions.ApplyAsync(db, Player, "voice spam", ReportCategory.CoC_Trolling, + TimeSpan.FromHours(6), VoiceChatRestrictions.Method); + + var restricted = Assert.IsAssignableFrom(await api.GetRestriction()).Value!; + var type = restricted.GetType(); + Assert.True((bool)type.GetProperty("restricted")!.GetValue(restricted)!); + Assert.Equal("voice spam", type.GetProperty("reason")!.GetValue(restricted) as string); + Assert.True((int)type.GetProperty("secondsRemaining")!.GetValue(restricted)! > 0); + } + + [Fact] + public async Task AnExpiredRestrictionIsNoRestriction() + { + await using var db = await WithProfile(); + db.VoiceChatRestrictions.Add(new VoiceChatRestriction + { + PlayerId = Player, + Reason = "expired", + Category = ReportCategory.Misc, + StartedAt = DateTime.UtcNow.AddHours(-5), + ExpiresAt = DateTime.UtcNow.AddHours(-1), + IssuedBy = VoiceChatRestrictions.Method + }); + await db.SaveChangesAsync(); + + Assert.Null(await VoiceChatRestrictions.ActiveAsync(db, Player)); + var payload = VoiceChatRestrictions.Payload(null); + Assert.False((bool)payload.GetType().GetProperty("restricted")!.GetValue(payload)!); + } +} diff --git a/ServerTests/NameserverRedirectTests.cs b/ServerTests/NameserverRedirectTests.cs new file mode 100644 index 0000000..748d61f --- /dev/null +++ b/ServerTests/NameserverRedirectTests.cs @@ -0,0 +1,124 @@ +using Xunit; + +namespace RadicadeServer.Tests; + +public class NameserverRedirectTests +{ + [Theory] + [InlineData("wss://notifications.rec.net/hub/v1?id=a", "http://127.0.0.1:3031", "ws://127.0.0.1:3031/notify/hub/v1?id=a")] + [InlineData("ws://notify.rec.net/hub/v1", "https://private.example", "wss://private.example/notify/hub/v1")] + [InlineData("wss://notifications.radicade.internal/notify/hub/v1", "https://private.example", "wss://private.example/notify/hub/v1")] + // The socket the client actually opens now: the SignalR transport submits it through the same + // funnel as the REST calls, and a socket URL the funnel leaves alone is a host with no DNS + // record - the hub never connects and the boot dies on it. The nameserver advertises the + // service under its full name, so this host is the one that has to come back to /notify. + [InlineData("wss://notifications.radicade.app/hub/v1?&id=q041FbQsdxYHA8PQ2oC8dg", "https://ns.radicade.app", "wss://ns.radicade.app/notify/hub/v1?&id=q041FbQsdxYHA8PQ2oC8dg")] + [InlineData("wss://other.example/hub/v1", "https://private.example", "wss://other.example/hub/v1")] + public void SocketRoutingUsesConfiguredServerTransport(string input, string server, string expected) + => Assert.Equal(expected, NameserverRedirect.RewriteWebSocket(input, server)); + + [Theory] + [InlineData("https://ns.rec.net", "https://ns.radicade.app/ns")] + [InlineData("https://ns.rec.net/nameserver?build=20230301", "https://ns.radicade.app/nameserver?build=20230301")] + [InlineData("http://NS.REC.NET/ns", "https://ns.radicade.app/ns")] + [InlineData("https://ns.radicade.internal/ns", "https://ns.radicade.app/ns")] + public void StockClientBootstrapsWithoutPatchedMetadata(string input, string expected) + => Assert.Equal(expected, NameserverRedirect.Rewrite(input, "https://ns.radicade.app/")); + + // The stock client resolves the image host itself, so a patch that only rewrote the + // bootstrap host left every avatar, photo and thumbnail pointed at the real RecNet. + // img.rec.net serves bare image names; this server serves them under /images. + [Theory] + [InlineData("https://img.rec.net/abc123.png", "https://ns.radicade.app/images/abc123.png")] + [InlineData("https://img.rec.net/abc123.png?sig=p1", "https://ns.radicade.app/images/abc123.png?sig=p1")] + [InlineData("https://IMG.REC.NET/DefaultProfileImage.jpg", "https://ns.radicade.app/images/DefaultProfileImage.jpg")] + [InlineData("https://images.rec.net/abc123.png", "https://ns.radicade.app/images/abc123.png")] + // A loader that already adds the /images or /img segment must not have it doubled. + [InlineData("https://img.rec.net/images/abc123.png", "https://ns.radicade.app/images/abc123.png")] + [InlineData("https://img.rec.net/img/abc123.png", "https://ns.radicade.app/images/abc123.png")] + public void ImageHostRequestsReachTheServersImageRoute(string input, string expected) + => Assert.Equal(expected, NameserverRedirect.Rewrite(input, "https://ns.radicade.app")); + + // Room saves and their Unity asset bundles download from the CDN host by blob name; + // the same traffic is served here at the root, so the path passes through unchanged. + [Theory] + [InlineData("https://cdn.rec.net/room/9c1f.bin", "https://ns.radicade.app/room/9c1f.bin")] + [InlineData("https://cdn.rec.net/unity/12345/0/1", "https://ns.radicade.app/unity/12345/0/1")] + [InlineData("https://data.rec.net/9c1f.bin", "https://ns.radicade.app/data/9c1f.bin")] + [InlineData("https://storage.rec.net/upload", "https://ns.radicade.app/storage/upload")] + [InlineData("https://notify.rec.net/hub/v1/negotiate", "https://ns.radicade.app/notify/hub/v1/negotiate")] + [InlineData("https://api.rec.net/api/config/v2", "https://ns.radicade.app/api/config/v2")] + [InlineData("https://accounts.rec.net/account/me", "https://ns.radicade.app/accounts/account/me")] + [InlineData("https://rooms.rec.net/rooms/2?include=all", "https://ns.radicade.app/rooms/rooms/2?include=all")] + [InlineData("https://matchmaking.rec.net/player/login", "https://ns.radicade.app/matchmaking/player/login")] + [InlineData("https://auth.rec.net/connect/token", "https://ns.radicade.app/auth/connect/token")] + [InlineData("https://chat.rec.net/thread?maxCount=30", "https://ns.radicade.app/chat/thread?maxCount=30")] + public void ServiceHostRequestsKeepTheirPathOnTheConfiguredServer(string input, string expected) + => Assert.Equal(expected, NameserverRedirect.Rewrite(input, "https://ns.radicade.app")); + + // The nameserver names each service on its own host now, and the client builds every call + // as that base plus its own path literal - so the host has to come back to exactly the + // route the path-prefixed base used to produce. api is the one bare base, and cdn keeps + // the /cdn segment its advertised base has always carried. + [Theory] + [InlineData("https://auth.radicade.app/connect/token", "https://ns.radicade.app/auth/connect/token")] + [InlineData("https://api.radicade.app/api/config/v2", "https://ns.radicade.app/api/config/v2")] + [InlineData("https://accounts.radicade.app/account/me", "https://ns.radicade.app/accounts/account/me")] + [InlineData("https://rooms.radicade.app/rooms/2?include=all", "https://ns.radicade.app/rooms/rooms/2?include=all")] + [InlineData("https://matchmaking.radicade.app/player/login", "https://ns.radicade.app/matchmaking/player/login")] + [InlineData("https://chat.radicade.app/thread?maxCount=30", "https://ns.radicade.app/chat/thread?maxCount=30")] + [InlineData("https://images.radicade.app/abc123.png", "https://ns.radicade.app/images/abc123.png")] + // A loader that already adds the /images or /img segment must not have it doubled. + [InlineData("https://images.radicade.app/img/abc123.png", "https://ns.radicade.app/images/abc123.png")] + [InlineData("https://cdn.radicade.app/room/9c1f.bin", "https://ns.radicade.app/cdn/room/9c1f.bin")] + [InlineData("https://data.radicade.app/9c1f.bin", "https://ns.radicade.app/data/9c1f.bin")] + // The notify service is the one advertised name that is not its own route: the host is + // notifications.radicade.app, the route it has to land on is /notify. + [InlineData("https://notifications.radicade.app/hub/v1/negotiate", "https://ns.radicade.app/notify/hub/v1/negotiate")] + [InlineData("https://www.radicade.app/creator", "https://ns.radicade.app/www/creator")] + [InlineData("https://stringscdn.radicade.app/strings.json", "https://ns.radicade.app/stringscdn/strings.json")] + [InlineData("https://studiocdn.radicade.app/roomunityasset/x", "https://ns.radicade.app/studiocdn/roomunityasset/x")] + [InlineData("https://thorn.radicade.app/endpoint", "https://ns.radicade.app/thorn/endpoint")] + [InlineData("https://videos.radicade.app/blob.mp4", "https://ns.radicade.app/videos/blob.mp4")] + public void NameserverServiceHostsRedirectOntoTheirOwnRoute(string input, string expected) + => Assert.Equal(expected, NameserverRedirect.Rewrite(input, "https://ns.radicade.app")); + + [Theory] + [InlineData("https://ns.radicade.app/ns")] + [InlineData("https://example.com/?url=https://ns.rec.net")] + [InlineData("https://ns.rec.net.example.com/ns")] + [InlineData("https://api.steampowered.com/")] + // Only the listed hosts are rewritten: a lookalike domain is someone else's traffic. + [InlineData("https://img.rec.net.evil.example/abc.png")] + [InlineData("https://myimg.rec.net/abc.png")] + // The service domain is not a wildcard either - the apex is the site, not a service, and + // a host that merely ends in it is someone else's. + [InlineData("https://auth.radicade.app.evil.example/connect/token")] + [InlineData("https://myauth.radicade.app/connect/token")] + [InlineData("https://radicade.app/ns")] + // The notify service's old short name was retired in favour of notifications.radicade.app, + // and the rename was clean: nothing maps this host any more, so a request still naming it + // is left exactly as it is - there is no second spelling of the service to drift from. + [InlineData("https://notify.radicade.app/hub/v1/negotiate")] + public void LeavesOtherRequestsAndAlreadyPatchedClientsUntouched(string input) + => Assert.Equal(input, NameserverRedirect.Rewrite(input, "https://ns.radicade.app")); + + [Fact] + public void PatchedPublicServerRequestsFollowLocalConfiguration() + { + Assert.Equal("http://127.0.0.1:3031/ns", NameserverRedirect.Rewrite("https://ns.radicade.app/ns", "http://127.0.0.1:3031")); + Assert.Equal("http://127.0.0.1:3031/api/config/v2?build=20230301", NameserverRedirect.Rewrite("https://ns.radicade.app/api/config/v2?build=20230301", "http://127.0.0.1:3031")); + Assert.Equal("https://ns.radicade.app.example.com/ns", NameserverRedirect.Rewrite("https://ns.radicade.app.example.com/ns", "http://127.0.0.1:3031")); + } + + [Fact] + public void RedirectedServiceHostsFollowLocalConfigurationToo() + { + Assert.Equal("http://127.0.0.1:3031/images/abc.png", NameserverRedirect.Rewrite("https://img.rec.net/abc.png", "http://127.0.0.1:3031")); + Assert.Equal("http://127.0.0.1:3031/room/9c1f.bin", NameserverRedirect.Rewrite("https://cdn.rec.net/room/9c1f.bin", "http://127.0.0.1:3031")); + } + + [Fact] + public void RejectsMalformedServerConfiguration() + => Assert.Throws(() => NameserverRedirect.Rewrite("https://ns.rec.net", "localhost:3030")); +} diff --git a/ServerTests/NotificationSubscriptionTests.cs b/ServerTests/NotificationSubscriptionTests.cs new file mode 100644 index 0000000..5b63d6a --- /dev/null +++ b/ServerTests/NotificationSubscriptionTests.cs @@ -0,0 +1,299 @@ +using System.Reflection; +using System.Security.Claims; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class NotificationSubscriptionTests +{ + private const long TargetId = 200; + private const long SubscriberId = 100; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + public sealed class RecordingClient : ISingleClientProxy + { + public List Notifications { get; } = new(); + public Func? BeforeSend { get; set; } + + public Task InvokeCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public async Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + if (BeforeSend != null) await BeforeSend(); + Assert.Equal("Notification", method); + Notifications.Add(Assert.IsType(args[0])); + } + } + + public class RecordingHubClients : DispatchProxy + { + public Dictionary ClientsById { get; } = new(StringComparer.Ordinal); + + protected override object? Invoke(MethodInfo? method, object?[]? args) + { + if (method?.Name == "Client" && args is [string connectionId]) + return ClientsById[connectionId]; + throw new NotSupportedException($"Unexpected hub-client member: {method?.Name}"); + } + } + + private sealed class TestHubContext : IHubContext + { + public IHubClients Clients { get; } + public IGroupManager Groups => null!; + + public TestHubContext(IHubClients clients) => Clients = clients; + } + + private sealed class TestHubCallerContext(long accountId, string connectionId) : HubCallerContext + { + public override string ConnectionId => connectionId; + public override string? UserIdentifier => accountId.ToString(); + public override ClaimsPrincipal User => new(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, accountId.ToString())], "test")); + public override IDictionary Items { get; } = new Dictionary(); + public override IFeatureCollection Features { get; } = new FeatureCollection(); + public override CancellationToken ConnectionAborted => CancellationToken.None; + public override void Abort() { } + } + + [Fact] + public async Task SlowOrFailedSocketDoesNotDelayOtherSubscribersAndLogoutStillPushesOffline() + { + var factory = new TestDbContextFactory(); + await using var db = factory.CreateDbContext(); + db.Presences.Add(new RealPresence { playerId = TargetId, isOnline = true }); + await db.SaveChangesAsync(); + var oldFactory = Notify.DbFactory; + var oldContext = Notify.Context; + var clients = DispatchProxy.Create(); + var recording = (RecordingHubClients)(object)clients; + var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var delivered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + recording.ClientsById["presence-slow"] = new RecordingClient { BeforeSend = () => release.Task }; + recording.ClientsById["presence-fast"] = new RecordingClient + { + BeforeSend = () => { delivered.TrySetResult(); return Task.CompletedTask; } + }; + recording.ClientsById["presence-owner"] = new RecordingClient + { + BeforeSend = () => Task.FromException(new IOException("Socket closed")) + }; + Task? pending = null; + try + { + Notify.DbFactory = factory; + Notify.Context = new TestHubContext(clients); + lock (Notify.ClientsLock) + Notify.Clients.AddRange(new[] + { + new HubClient { AccountId = SubscriberId, ConnectionId = "presence-slow", HasSubscriptions = true, Subscriptions = [TargetId] }, + new HubClient { AccountId = 101, ConnectionId = "presence-fast", HasSubscriptions = true, Subscriptions = [TargetId] }, + new HubClient { AccountId = TargetId, ConnectionId = "presence-owner" } + }); + pending = Notify.UpdateSubscriptions(TargetId); + await delivered.Task.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.False(pending.IsCompleted); + release.SetException(new IOException("Subscriber disconnected")); + await pending; + Assert.Single(recording.ClientsById["presence-fast"].Notifications); + + var api = new RadicadeServer.API.Matchmaking.MatchmakingPlayerAPI(db) + { + ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext + { + HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, TargetId.ToString())], "test")) + } + } + }; + await api.Logout(); + using var offline = System.Text.Json.JsonDocument.Parse(recording.ClientsById["presence-fast"].Notifications.Last()); + Assert.False(offline.RootElement.GetProperty("Msg").GetProperty("isOnline").GetBoolean()); + } + finally + { + release.TrySetResult(); + if (pending != null) await pending; + lock (Notify.ClientsLock) + Notify.Clients.RemoveAll(client => client.ConnectionId.StartsWith("presence-")); + Notify.DbFactory = oldFactory; + Notify.Context = oldContext; + } + } + + [Fact] + public async Task UpdatesReachOnlySocketsSubscribedToTheChangedPlayer() + { + var factory = new TestDbContextFactory(); + var db = factory.CreateDbContext(); + await using var _db = db; + var oldFactory = Notify.DbFactory; + var oldContext = Notify.Context; + var oldRoomFactory = RoomUtils.DbFactory; + var hubClients = DispatchProxy.Create(); + var connections = (RecordingHubClients)(object)hubClients; + foreach (var id in new[] { "target", "subscriber-one", "subscriber-two", "other-subscriber" }) + connections.ClientsById[id] = new RecordingClient(); + var hubContext = new TestHubContext(hubClients); + + db.Profiles.Add(new Profile + { + accountId = TargetId, + profileImage = "profile", + username = "target", + displayName = "Target", + createdAt = DateTime.UtcNow + }); + db.Rooms.Add(new RoomData + { + RoomId = 50, + Name = "Public room", + CreatorAccountId = 1, + Accessibility = Accessibility.Public + }); + db.RoomInstances.Add(new RoomInstance + { + roomInstanceId = 500, + roomId = 50, + subRoomId = 51, + photonRegion = "us", + photonRoomId = "secret-photon-room", + dataBlob = "secret-room-save", + roomCode = "secret-room-code" + }); + db.Players.Add(new PlayerData { AccountId = TargetId, AppVersion = "20230825" }); + db.Presences.Add(new RealPresence + { + playerId = TargetId, + isOnline = true, + roomInstance = 500 + }); + await db.SaveChangesAsync(); + + lock (Notify.ClientsLock) + { + Notify.Clients.RemoveAll(client => client.AccountId is TargetId or SubscriberId or 101); + Notify.Clients.AddRange(new[] + { + new HubClient { AccountId = TargetId, ConnectionId = "target" }, + new HubClient { AccountId = SubscriberId, ConnectionId = "subscriber-one" }, + new HubClient { AccountId = SubscriberId, ConnectionId = "subscriber-two" }, + new HubClient { AccountId = 101, ConnectionId = "other-subscriber" } + }); + } + + try + { + Notify.Context = hubContext; + Notify.DbFactory = factory; + RoomUtils.DbFactory = factory; + + var firstSocket = new NotifyHub(hubContext, db) + { + Context = new TestHubCallerContext(SubscriberId, "subscriber-one") + }; + await firstSocket.Subscribe(new SubscriptionUpdate { PlayerIds = [TargetId] }); + + // A second socket for the same account replaces only its own list; it does not + // inherit the first socket's subscription. + var secondSocket = new NotifyHub(hubContext, db) + { + Context = new TestHubCallerContext(SubscriberId, "subscriber-two") + }; + await secondSocket.Subscribe(new SubscriptionUpdate { PlayerIds = [] }); + + var otherSocket = new NotifyHub(hubContext, db) + { + Context = new TestHubCallerContext(101, "other-subscriber") + }; + await otherSocket.Subscribe(new SubscriptionUpdate { PlayerIds = [TargetId] }); + + await Notify.UpdateSubscriptions(TargetId); + await Notify.UpdateSubscriptions(TargetId, Notifs.SubscriptionUpdateProfile, new { accountId = TargetId }); + + Assert.Empty(connections.ClientsById["target"].Notifications); + Assert.Empty(connections.ClientsById["subscriber-two"].Notifications); + Assert.Equal(4, connections.ClientsById["subscriber-one"].Notifications.Count); + Assert.Equal(4, connections.ClientsById["other-subscriber"].Notifications.Count); + + using var presence = System.Text.Json.JsonDocument.Parse(connections.ClientsById["subscriber-one"].Notifications[0]); + Assert.Equal("PresenceUpdate", presence.RootElement.GetProperty("Id").GetString()); + var roomInstance = presence.RootElement.GetProperty("Msg").GetProperty("roomInstance"); + Assert.Equal(string.Empty, roomInstance.GetProperty("photonRoomId").GetString()); + Assert.Equal(string.Empty, roomInstance.GetProperty("dataBlob").GetString()); + Assert.Equal(string.Empty, roomInstance.GetProperty("roomCode").GetString()); + + using var profile = System.Text.Json.JsonDocument.Parse(connections.ClientsById["subscriber-one"].Notifications[3]); + Assert.Equal("AccountUpdate", profile.RootElement.GetProperty("Id").GetString()); + + // Appearing offline hides the room in pushes, without ending the real session. + var statusApi = new RadicadeServer.API.Matchmaking.MatchmakingPlayerAPI(db) + { + ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext + { + HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, TargetId.ToString())], "test")) + } + } + }; + await statusApi.SetStatusVisibility(RadicadeServer.RecNet.Enums.Players.StatusVisibility.Offline); + using var hidden = System.Text.Json.JsonDocument.Parse(connections.ClientsById["subscriber-one"].Notifications.Last()); + Assert.False(hidden.RootElement.GetProperty("Msg").GetProperty("isOnline").GetBoolean()); + Assert.Equal(System.Text.Json.JsonValueKind.Null, hidden.RootElement.GetProperty("Msg").GetProperty("roomInstance").ValueKind); + Assert.True((await db.Presences.SingleAsync()).isOnline); + + await statusApi.Heartbeat(); + Assert.Equal(RadicadeServer.RecNet.Enums.Players.StatusVisibility.Offline, + (await db.Presences.SingleAsync()).statusVisibility); + + // A second live connection keeps the account online; closing the last one does not. + lock (Notify.ClientsLock) + Notify.Clients.Add(new HubClient { AccountId = TargetId, ConnectionId = "target-second" }); + var targetHub = new NotifyHub(hubContext, db) + { + Context = new TestHubCallerContext(TargetId, "target") + }; + await targetHub.OnDisconnectedAsync(null!); + Assert.True((await db.Presences.SingleAsync()).isOnline); + targetHub.Context = new TestHubCallerContext(TargetId, "target-second"); + await targetHub.OnDisconnectedAsync(null!); + Assert.False((await db.Presences.SingleAsync()).isOnline); + Assert.Equal(500, (await db.Presences.SingleAsync()).roomInstance); + } + finally + { + lock (Notify.ClientsLock) + Notify.Clients.RemoveAll(client => client.AccountId is TargetId or SubscriberId or 101); + Notify.Context = oldContext; + Notify.DbFactory = oldFactory; + RoomUtils.DbFactory = oldRoomFactory; + } + } +} diff --git a/ServerTests/OrientationHandoffTests.cs b/ServerTests/OrientationHandoffTests.cs new file mode 100644 index 0000000..35de98a --- /dev/null +++ b/ServerTests/OrientationHandoffTests.cs @@ -0,0 +1,246 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Matchmaking; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The account-creation flow ends on the launch-game step, and the client asks for its dorm by +/// route (matchmake/dorm) rather than by room name. That route resolves the dorm directly, +/// so it never ran through the Orientation redirect that lives on the room-name path in +/// - a brand-new player was dropped in their dorm with +/// the Orientation marker still unspent. Orientation is a solo room (MaxPlayers 1), so the +/// hand-off mints one instance per player, the same shape the dorm uses. +/// +/// These pin both dorm entry points, that the hand-off happens exactly once, and that a server +/// without an Orientation room still lands its players somewhere. +/// +[Collection(DbFactoryCollection.Name)] +public class OrientationHandoffTests +{ + private const long PlayerId = 812; + private const long OrientationRoomId = 13; + private const long OrientationSubRoomId = 113; + private const long DormRoomId = 8121; + private const long DormSubRoomId = 8122; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private static async Task<(TestDbContextFactory Factory, RecDBContext Db)> CreateContextAsync( + bool seedOrientationRoom = true) + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + + var db = factory.CreateDbContext(); + + if (seedOrientationRoom) + { + db.Rooms.Add(new RoomData + { + RoomId = OrientationRoomId, + Name = "Orientation", + Description = "An introductory tour of Rec Room!", + Accessibility = Accessibility.Public, + MaxPlayers = 1, + CreatedAt = DateTime.UtcNow + }); + db.SubRooms.Add(new SubRoom + { + SubRoomId = OrientationSubRoomId, + RoomId = OrientationRoomId, + Name = "Home", + UnitySceneId = "c79709d8-a31b-48aa-9eb8-cc31ba9505e8", + MaxPlayers = 1, + Accessibility = Accessibility.Public + }); + } + + // The player's own dorm, which GetOrCreateDorm recognises by owner rather than by name. + db.Rooms.Add(new RoomData + { + RoomId = DormRoomId, + Name = "DormRoom", + IsDorm = true, + CreatorAccountId = PlayerId, + Accessibility = Accessibility.Private, + CreatedAt = DateTime.UtcNow + }); + db.SubRooms.Add(new SubRoom + { + SubRoomId = DormSubRoomId, + RoomId = DormRoomId, + Name = "Home", + UnitySceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6", + MaxPlayers = 4, + Accessibility = Accessibility.Private + }); + + await db.SaveChangesAsync(); + return (factory, db); + } + + /// + /// An account the account-creation flow just created, with the settings that flow writes: + /// the Orientation marker, and no HAS_COMPLETED_ORIENTATION. + /// + private static async Task SeedOnboardingPlayerAsync( + RecDBContext db, + long playerId = PlayerId, + bool needsOrientation = true) + { + var settings = new List + { + new() { Key = "Recroom.AccountCreation.HasStarted", Value = "True" }, + new() { Key = "Recroom.AccountCreation.HasFinished", Value = "False" }, + }; + + if (needsOrientation) + settings.Add(new Setting { Key = "Radicade.NeedsOrientation", Value = "1" }); + + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = playerId, + Settings = settings.ToJson() + }); + db.Presences.Add(new RealPresence { playerId = playerId }); + await db.SaveChangesAsync(); + } + + private static MatchmakingAPI CreateMatchmakingApi(RecDBContext db, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + + var api = new MatchmakingAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + + return api; + } + + private static async Task MatchmakeDormAsync(MatchmakingAPI api) + { + var result = Assert.IsType(await api.MatchmakeDorm()); + return Assert.IsType(result.Value); + } + + private static async Task StillOwesOrientationAsync(TestDbContextFactory factory) + { + await using var verify = factory.CreateDbContext(); + var settings = await verify.PlayerSettings.SingleAsync(x => x.AccountId == PlayerId); + return settings.DeserializeSettings() + .Any(setting => setting.Key == "Radicade.NeedsOrientation"); + } + + [Fact] + public async Task TheLaunchStepTheOnboardingFlowEndsOnOpensOrientation() + { + var (factory, db) = await CreateContextAsync(); + await using var _db = db; + await SeedOnboardingPlayerAsync(db); + + var match = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId)); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.NotNull(match.roomInstance); + Assert.Equal(OrientationRoomId, match.roomInstance!.roomId); + + await using var verify = factory.CreateDbContext(); + var presence = await verify.Presences.SingleAsync(x => x.playerId == PlayerId); + Assert.Equal(match.roomInstance.roomInstanceId, presence.roomInstance); + Assert.False(await StillOwesOrientationAsync(factory)); + } + + [Fact] + public async Task TheRoomNameDormPathOpensOrientationToo() + { + var (factory, db) = await CreateContextAsync(); + await using var _db = db; + await SeedOnboardingPlayerAsync(db); + + var match = await MatchmakingUtils.Matchmake(JoinMode.PublicMatchmaking, "DormRoom", PlayerId); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.NotNull(match.roomInstance); + Assert.Equal(OrientationRoomId, match.roomInstance!.roomId); + Assert.False(await StillOwesOrientationAsync(factory)); + } + + [Fact] + public async Task OrientationIsHandedOutOnceAndThenTheDormIsWhereTheyLand() + { + var (factory, db) = await CreateContextAsync(); + await using var _db = db; + await SeedOnboardingPlayerAsync(db); + + var first = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId)); + Assert.Equal(OrientationRoomId, first.roomInstance!.roomId); + + // The marker was spent, so the second launch is an ordinary dorm launch: the player is not + // sent back to Orientation every time they go home. + var second = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId)); + await using var verify = factory.CreateDbContext(); + var room = await verify.Rooms.AsNoTracking().SingleAsync(x => x.RoomId == second.roomInstance!.roomId); + Assert.True(room.IsDorm); + Assert.Equal(DormRoomId, room.RoomId); + Assert.NotEqual(first.roomInstance.roomInstanceId, second.roomInstance!.roomInstanceId); + } + + [Fact] + public async Task APlayerWhoOwesNoOrientationGoesStraightToTheirDorm() + { + var (factory, db) = await CreateContextAsync(); + await using var _db = db; + await SeedOnboardingPlayerAsync(db, needsOrientation: false); + + var match = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId)); + + await using var verify = factory.CreateDbContext(); + var room = await verify.Rooms.AsNoTracking().SingleAsync(x => x.RoomId == match.roomInstance!.roomId); + Assert.Equal(DormRoomId, room.RoomId); + Assert.True(room.IsDorm); + } + + [Fact] + public async Task AServerWithNoOrientationRoomStillLandsThePlayerInTheirDorm() + { + var (factory, db) = await CreateContextAsync(seedOrientationRoom: false); + await using var _db = db; + await SeedOnboardingPlayerAsync(db); + + var match = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId)); + + await using var verify = factory.CreateDbContext(); + var room = await verify.Rooms.AsNoTracking().SingleAsync(x => x.RoomId == match.roomInstance!.roomId); + Assert.Equal(DormRoomId, room.RoomId); + + // Nothing was handed out, so the marker stays: the player still gets Orientation if the room + // is added later. + Assert.True(await StillOwesOrientationAsync(factory)); + } +} diff --git a/ServerTests/PlatformNotificationsTests.cs b/ServerTests/PlatformNotificationsTests.cs new file mode 100644 index 0000000..0ba2b95 --- /dev/null +++ b/ServerTests/PlatformNotificationsTests.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer.API.Notifications; +using Xunit; + +namespace RadicadeServer.Tests; + +public class PlatformNotificationsTests +{ + [Fact] + public void CategoriesReturnsCountedObjectForClientDeserializer() + { + var response = Assert.IsType(new PlatformNotificationsAPI().GetCategories()); + var json = JsonSerializer.SerializeToElement(response.Value); + + Assert.Equal(JsonValueKind.Object, json.ValueKind); + var results = json.GetProperty("Results"); + Assert.Equal(JsonValueKind.Array, results.ValueKind); + Assert.Equal(results.GetArrayLength(), json.GetProperty("TotalResults").GetInt64()); + } +} diff --git a/ServerTests/PlayerCheerTests.cs b/ServerTests/PlayerCheerTests.cs new file mode 100644 index 0000000..c1acfdc --- /dev/null +++ b/ServerTests/PlayerCheerTests.cs @@ -0,0 +1,247 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Moderation; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Responses; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Cheering another player (`api/PlayerCheer/v1/create`) and the cheer badges a profile reads +/// back out of `api/playerReputation/v1/{id}`. +/// +/// Two things were wrong with the loop. A cheer only ever moved a column: the target was never +/// sent a PlayerCheer message, so nothing appeared on their client at all. And the reputation +/// payload hardcoded IsCheerful = true while flooring every category count at 1, so the client +/// drew all five cheer badges as already earned - on accounts that had never been cheered. +/// +/// These tests pin the ledger end to end: the cheer is stored, the target is told, the count is +/// reported as it really is, and the badge appears at Reputation.CheerBadgeThreshold cheers. +/// +[Collection(DbFactoryCollection.Name)] +public class PlayerCheerTests +{ + private const long CheererId = 41; + private const long TargetId = 42; + + private static readonly JsonSerializerOptions ServerJson = new() + { + PropertyNamingPolicy = null + }; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private static async Task<(RecDBContext Db, IDbContextFactory Factory, IDbContextFactory Previous)> + CreateContextAsync() + { + var factory = new TestDbContextFactory(); + var previous = Notify.DbFactory; + Notify.DbFactory = factory; + + var db = factory.CreateDbContext(); + db.Profiles.AddRange(ProfileFor(CheererId), ProfileFor(TargetId)); + db.Players.AddRange(new PlayerData { AccountId = CheererId }, new PlayerData { AccountId = TargetId }); + await db.SaveChangesAsync(); + + return (db, factory, previous); + } + + private static Profile ProfileFor(long id) => new() + { + accountId = id, + username = $"player{id}", + displayName = $"Player {id}", + profileImage = string.Empty, + createdAt = DateTime.UtcNow + }; + + private static void SetUser(Controller controller, long userId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetFormBody(Controller controller, string body) + { + var request = controller.HttpContext.Request; + request.ContentType = "application/x-www-form-urlencoded"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + request.ContentLength = request.Body.Length; + } + + private static async Task> CheerAsync(ReputationAPI api, long to, int category, long from = CheererId) + { + SetUser(api, from); + SetFormBody(api, $"PlayerIdTo={to}&CheerCategory={category}"); + return await api.CheerPlayer(); + } + + private static JsonElement ReputationJson(RecDBContext db, long accountId) + { + var result = Assert.IsType(new ReputationAPI(db).GetReputation(accountId)); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(result.Value, ServerJson)); + return doc.RootElement.Clone(); + } + + [Fact] + public async Task Cheer_ReachesTheTargetAsAPlayerCheerMessage() + { + var (db, factory, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var result = await CheerAsync(new ReputationAPI(db), TargetId, (int)CheerCategory.Helpful); + Assert.IsType(result.Result); + + await using var verify = factory.CreateDbContext(); + var message = Assert.Single(verify.Messages); + Assert.Equal(TargetId, message.ToPlayerId); + Assert.Equal(CheererId, message.FromPlayerId); + Assert.Equal(MessageType.PlayerCheer, message.Type); + Assert.Equal(((int)CheerCategory.Helpful).ToString(), message.Data); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task Cheer_CountsOnlyTheCategoryThatWasCheered() + { + var (db, _, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new ReputationAPI(db); + Assert.IsType((await CheerAsync(api, TargetId, (int)CheerCategory.Helpful)).Result); + Assert.IsType((await CheerAsync(api, TargetId, (int)CheerCategory.Helpful)).Result); + + var json = ReputationJson(db, TargetId); + // Reported as stored: a count of 2 stays 2, and the categories nobody cheered + // stay at 0 instead of being floored up to a badge-looking 1. + Assert.Equal(2, json.GetProperty("CheerHelpful").GetInt32()); + Assert.Equal(0, json.GetProperty("CheerGeneral").GetInt32()); + Assert.Equal(0, json.GetProperty("CheerCreative").GetInt32()); + Assert.Equal(0, json.GetProperty("CheerGreatHost").GetInt32()); + Assert.Equal(0, json.GetProperty("CheerSportsman").GetInt32()); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task Cheer_BadgeIsEarnedAtTheFifthCheer() + { + var (db, _, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new ReputationAPI(db); + + for (var i = 0; i < Reputation.CheerBadgeThreshold - 1; i++) + Assert.IsType((await CheerAsync(api, TargetId, (int)CheerCategory.General)).Result); + + var before = await db.Reputations.AsNoTracking().FirstAsync(x => x.AccountId == TargetId); + Assert.False(before.CheerBadgeEarned); + + Assert.IsType((await CheerAsync(api, TargetId, (int)CheerCategory.General)).Result); + + var after = await db.Reputations.AsNoTracking().FirstAsync(x => x.AccountId == TargetId); + Assert.True(after.CheerBadgeEarned); + Assert.Equal(Reputation.CheerBadgeThreshold, after.CheerGeneral); + + // The badge is what the stored flag tracks; the wire gate the client reads to + // decide whether the target may hand cheers out stays open either way. + Assert.True(ReputationJson(db, TargetId).GetProperty("IsCheerful").GetBoolean()); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public void Reputation_ReportsTheGateOpenAndTheCountersExactlyAsStored() + { + // IsCheerful on the wire is the "may hand out cheers" gate, which the reference + // server answers true for every account - it is not the earned badge. What has to + // stay honest is the counters: a stale row must not carry a badge the player never + // earned, and nobody has cheered this account so every counter reads 0. + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + using var db = new RecDBContext(options); + db.Reputations.Add(new Reputation { AccountId = TargetId, IsCheerful = true, CheerGeneral = 1, CheerCreative = 1 }); + db.SaveChanges(); + + var json = ReputationJson(db, TargetId); + Assert.True(json.GetProperty("IsCheerful").GetBoolean()); + Assert.Equal(1, json.GetProperty("CheerGeneral").GetInt32()); + Assert.Equal(0, json.GetProperty("CheerHelpful").GetInt32()); + } + + [Fact] + public async Task Cheer_RejectsYourselfAndUnknownCategories() + { + var (db, _, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + var api = new ReputationAPI(db); + + Assert.IsType( + (await CheerAsync(api, CheererId, (int)CheerCategory.General)).Result); + Assert.IsType( + (await CheerAsync(api, TargetId, 999)).Result); + + Assert.Empty(db.Messages); + Assert.Empty(db.Reputations); + } + finally + { + Notify.DbFactory = previous; + } + } + + [Fact] + public async Task Cheer_CreditStaysADailyAllowanceAndNotABadge() + { + var (db, _, previous) = await CreateContextAsync(); + await using var disposeDb = db; + try + { + // CheerCredit is how many cheers the player may hand out. An account with no credit + // recorded keeps the client's 20-cheer default, which is why it is still floored - + // unlike the five badge counts, which are reported exactly as stored. + Assert.Equal(20, ReputationJson(db, CheererId).GetProperty("CheerCredit").GetInt32()); + } + finally + { + Notify.DbFactory = previous; + } + } +} diff --git a/ServerTests/PlayerEventNotificationTests.cs b/ServerTests/PlayerEventNotificationTests.cs new file mode 100644 index 0000000..047bcd9 --- /dev/null +++ b/ServerTests/PlayerEventNotificationTests.cs @@ -0,0 +1,427 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Events; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A created event has to reach the creator's own open screen, not just the database. +/// +/// The client fills its event list once at login from `api/playerevents/v1/all` and never +/// refetches after a write - so an event created without a `PlayerEventCreated` (80) frame is +/// stored, acknowledged with a toast, and still absent from Watch > Events until the player +/// relogs. These tests pin the frames, their numeric ids (the client subscribes the decimal +/// string, not the enum name) and the payload shape the frame decoder reads. +/// +[Collection(DbFactoryCollection.Name)] +public class PlayerEventNotificationTests +{ + private const long CreatorId = 42; + + [Fact] + public void CreatedAndUpdatedFramesUseTheNumericIdsTheClientSubscribesTo() + { + var ev = Event(); + + var created = PlayerEventNotifications.Created(ev); + var updated = PlayerEventNotifications.Updated(ev); + + Assert.Equal(80, Convert.ToInt64(created.Id, System.Globalization.CultureInfo.InvariantCulture)); + Assert.Equal(81, Convert.ToInt64(updated.Id, System.Globalization.CultureInfo.InvariantCulture)); + + // The id goes out as the decimal string: the client's dispatcher drops any frame whose + // Id is not the exact key its handler registered under, and "PlayerEventCreated" has no + // subscriber of its own. + Assert.Equal("\"80\"", Notify.WireId(created.Id)); + } + + [Fact] + public void TheCreatedFrameCarriesTheCamelCaseEventTheFrameDecoderReads() + { + var created = PlayerEventNotifications.Created(Event()); + var msg = JsonSerializer.SerializeToElement(created.Msg); + + // Every key the frame decoder looks for, in the casing the notification channel uses. + foreach (var key in new[] + { + "tags", "playerEventId", "creatorPlayerId", "roomId", "subRoomId", "clubId", + "name", "description", "imageName", "startTime", "endTime", "attendeeCount", + "accessibility", "isMultiInstance", "supportMultiInstanceRoomChat", + "defaultBroadcastPermissions", "canRequestBroadcastPermissions", + "broadcastingRoomInstanceId" + }) + Assert.True(msg.TryGetProperty(key, out _), $"the frame is missing '{key}'"); + + // The stored-only fields the read shapes serve are not part of the frame. + Assert.False(msg.TryGetProperty("State", out _)); + Assert.False(msg.TryGetProperty("PlayerEventId", out _)); + + Assert.Equal(7, msg.GetProperty("playerEventId").GetInt64()); + Assert.Equal(CreatorId, msg.GetProperty("creatorPlayerId").GetInt64()); + Assert.Equal(102, msg.GetProperty("roomId").GetInt64()); + Assert.Equal(3, msg.GetProperty("attendeeCount").GetInt32()); + Assert.Equal(JsonValueKind.Null, msg.GetProperty("broadcastingRoomInstanceId").ValueKind); + + var tag = Assert.Single(msg.GetProperty("tags").EnumerateArray()); + Assert.Equal("workshops", tag.GetProperty("tag").GetString()); + Assert.Equal(0, tag.GetProperty("type").GetInt32()); + } + + [Fact] + public void TheFramesTimestampsSerializeTheSameWayAnEventReadDoes() + { + // The event the frame adds is displayed with the time it carries, so a frame timestamp + // must look like the one the same event shows when it arrives from v1/all - a local + // wall-clock pick time round-trips through this server without a zone. + var read = JsonSerializer.SerializeToElement(new PlayerEvent + { + StartTime = new DateTime(2026, 10, 8, 19, 10, 0, DateTimeKind.Unspecified), + EndTime = new DateTime(2026, 10, 8, 19, 40, 0, DateTimeKind.Unspecified) + }); + var frame = JsonSerializer.SerializeToElement(PlayerEventNotifications.Created(Event()).Msg); + + Assert.Equal(read.GetProperty("StartTime").GetString(), frame.GetProperty("startTime").GetString()); + Assert.Equal(read.GetProperty("EndTime").GetString(), frame.GetProperty("endTime").GetString()); + } + + [Fact] + public void BroadcastStateRidesAlongOnTheUpdatedFrame() + { + var ev = Event(); + ev.BroadcastingRoomInstanceId = 30848; + + var msg = JsonSerializer.SerializeToElement(PlayerEventNotifications.Updated(ev).Msg); + + Assert.Equal(30848, msg.GetProperty("broadcastingRoomInstanceId").GetInt64()); + } + + [Fact] + public void DeletedFrameCarriesTheBareIdUnderTheClientsOwnCasing() + { + var deleted = PlayerEventNotifications.Deleted(7); + var msg = JsonSerializer.SerializeToElement(deleted.Msg); + + Assert.Equal(82, Convert.ToInt64(deleted.Id, System.Globalization.CultureInfo.InvariantCulture)); + + // The client's deleted-event model names the field camelCase (`playerEventId`), the same + // spelling every other frame payload uses. A frame that carries only PascalCase binds + // nothing there, so the card survives the delete; the PascalCase key stays as an alias. + Assert.Equal(7, msg.GetProperty("playerEventId").GetInt64()); + Assert.Equal(7, msg.GetProperty("PlayerEventId").GetInt64()); + + // Nothing else rides along: the client has no use for the rest of the row it deleted. + Assert.All(msg.EnumerateObject(), + property => Assert.Contains(property.Name, new[] { "playerEventId", "PlayerEventId" })); + } + + [Fact] + public void ResponseChangedFrameCarriesBothNestedObjects() + { + // The client's handlers dereference one level down with no null guard, so a frame with + // only half the payload is worse than no frame at all. + var frame = PlayerEventNotifications.ResponseChanged(Event(), new PlayerEventResponse + { + PlayerEventResponseId = 11, + PlayerEventId = 7, + PlayerId = 55, + Type = EventResponses.Pending, + CreatedAt = new DateTime(2026, 10, 8, 19, 0, 0, DateTimeKind.Utc) + }); + + var msg = JsonSerializer.SerializeToElement(frame.Msg); + + Assert.Equal(83, Convert.ToInt64(frame.Id, System.Globalization.CultureInfo.InvariantCulture)); + Assert.Equal(7, msg.GetProperty("PlayerEvent").GetProperty("playerEventId").GetInt64()); + var response = msg.GetProperty("PlayerEventResponse"); + Assert.Equal(11, response.GetProperty("PlayerEventResponseId").GetInt64()); + Assert.Equal(55, response.GetProperty("PlayerId").GetInt64()); + Assert.Equal((int)EventResponses.Pending, response.GetProperty("Type").GetInt32()); + } + + [Fact] + public async Task CreatingAnEventPushesTheCreatedFrameToTheCreatorAndNoOneElse() + { + var factory = new TestDbContextFactory(); + await using var db = factory.CreateDbContext(); + var room = new RoomData { Name = "Broadcast Room" }; + db.Rooms.Add(room); + await db.SaveChangesAsync(); + + var oldContext = Notify.Context; + var hubClients = DispatchProxy.Create(); + var recording = (RecordingHubClients)(object)hubClients; + recording.ClientsById["creator-socket"] = new RecordingClient(); + recording.ClientsById["bystander-socket"] = new RecordingClient(); + + lock (Notify.ClientsLock) + { + Notify.Clients.RemoveAll(client => + client.ConnectionId is "creator-socket" or "bystander-socket"); + Notify.Clients.AddRange(new[] + { + new HubClient { AccountId = CreatorId, ConnectionId = "creator-socket" }, + new HubClient { AccountId = 99, ConnectionId = "bystander-socket" } + }); + } + + try + { + Notify.Context = new TestHubContext(hubClients); + + var controller = new EventsAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, CreatorId.ToString())], "test")) + } + } + }; + var body = $$"""{ "Name": "Watch me", "RoomId": {{room.RoomId}}, "Tags": [{ "Tag": "workshops", "Type": 0 }] }"""; + var bytes = System.Text.Encoding.UTF8.GetBytes(body); + controller.HttpContext.Request.Body = new MemoryStream(bytes); + controller.HttpContext.Request.ContentType = "application/json"; + controller.HttpContext.Request.ContentLength = bytes.Length; + + var result = await controller.CreateEventV2(); + + Assert.IsType(result); + + var sent = Assert.Single(recording.ClientsById["creator-socket"].Notifications); + Assert.Empty(recording.ClientsById["bystander-socket"].Notifications); + + using var frame = JsonDocument.Parse(sent); + Assert.Equal("80", frame.RootElement.GetProperty("Id").GetString()); + + var playerEvent = frame.RootElement.GetProperty("Msg"); + var stored = await db.PlayerEvents.SingleAsync(); + Assert.Equal(stored.PlayerEventId, playerEvent.GetProperty("playerEventId").GetInt64()); + Assert.Equal("Watch me", playerEvent.GetProperty("name").GetString()); + Assert.Equal("workshops", Assert.Single(playerEvent.GetProperty("tags").EnumerateArray()) + .GetProperty("tag").GetString()); + } + finally + { + lock (Notify.ClientsLock) + Notify.Clients.RemoveAll(client => + client.ConnectionId is "creator-socket" or "bystander-socket"); + Notify.Context = oldContext; + } + } + + [Fact] + public async Task DeletingAnEventPushesTheDeletedFrameToTheCreator() + { + var factory = new TestDbContextFactory(); + await using var db = factory.CreateDbContext(); + var stored = Event(); + db.PlayerEvents.Add(stored); + await db.SaveChangesAsync(); + + var oldContext = Notify.Context; + var hubClients = DispatchProxy.Create(); + var recording = (RecordingHubClients)(object)hubClients; + recording.ClientsById["creator-socket"] = new RecordingClient(); + + lock (Notify.ClientsLock) + { + Notify.Clients.RemoveAll(client => client.ConnectionId == "creator-socket"); + Notify.Clients.Add(new HubClient { AccountId = CreatorId, ConnectionId = "creator-socket" }); + } + + try + { + Notify.Context = new TestHubContext(hubClients); + + var controller = new EventsAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, CreatorId.ToString())], "test")) + } + } + }; + + var result = await controller.DeleteEventV2(stored.PlayerEventId); + + Assert.IsType(result); + + var sent = Assert.Single(recording.ClientsById["creator-socket"].Notifications); + using var frame = JsonDocument.Parse(sent); + Assert.Equal("82", frame.RootElement.GetProperty("Id").GetString()); + Assert.Equal(stored.PlayerEventId, + frame.RootElement.GetProperty("Msg").GetProperty("playerEventId").GetInt64()); + } + finally + { + lock (Notify.ClientsLock) + Notify.Clients.RemoveAll(client => client.ConnectionId == "creator-socket"); + Notify.Context = oldContext; + } + } + + [Fact] + public async Task BroadcastChangesReachEveryEchoInstanceButNotOtherEventsOrOfflinePlayers() + { + var factory = new TestDbContextFactory(); + await using var db = factory.CreateDbContext(); + var ev = Event(); + ev.IsMultiInstance = true; + db.PlayerEvents.Add(ev); + db.RoomInstances.AddRange( + new RoomInstance { roomInstanceId = 100, roomId = ev.RoomId, eventId = ev.PlayerEventId }, + new RoomInstance { roomInstanceId = 101, roomId = ev.RoomId, eventId = ev.PlayerEventId }, + new RoomInstance { roomInstanceId = 102, roomId = ev.RoomId, eventId = ev.PlayerEventId }, + new RoomInstance { roomInstanceId = 103, roomId = ev.RoomId, eventId = 999 }); + db.Presences.AddRange( + new RadicadeServer.RecNet.Accounts.RealPresence { playerId = CreatorId, roomInstance = 100, isOnline = true }, + new RadicadeServer.RecNet.Accounts.RealPresence { playerId = 51, roomInstance = 101, isOnline = true }, + new RadicadeServer.RecNet.Accounts.RealPresence { playerId = 52, roomInstance = 102, isOnline = true }, + new RadicadeServer.RecNet.Accounts.RealPresence { playerId = 53, roomInstance = 103, isOnline = true }, + new RadicadeServer.RecNet.Accounts.RealPresence { playerId = 54, roomInstance = 101, isOnline = false }); + await db.SaveChangesAsync(); + + var oldContext = Notify.Context; + var hubClients = DispatchProxy.Create(); + var recording = (RecordingHubClients)(object)hubClients; + var clients = new[] { CreatorId, 51L, 52L, 53L, 54L }.Select(id => new HubClient + { + AccountId = id, ConnectionId = $"broadcast-test-{id}" + }).ToArray(); + foreach (var client in clients) + recording.ClientsById[client.ConnectionId] = new RecordingClient(); + lock (Notify.ClientsLock) + Notify.Clients.AddRange(clients); + + try + { + Notify.Context = new TestHubContext(hubClients); + // Start, change the source instance, then stop while viewers stay in their rooms. + foreach (long? source in new long?[] { 100, 102, null }) + { + var controller = new EventsAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, CreatorId.ToString())], "test")) + } + } + }; + var body = JsonSerializer.SerializeToUtf8Bytes(new + { + PlayerEventId = ev.PlayerEventId, RoomInstanceId = source + }); + controller.Request.Body = new MemoryStream(body); + controller.Request.ContentType = "application/json"; + controller.Request.ContentLength = body.Length; + Assert.IsType(await controller.BroadcastRoomInstance()); + Assert.Equal(source, (await db.PlayerEvents.SingleAsync()).BroadcastingRoomInstanceId); + + foreach (var client in clients) + { + var notifications = recording.ClientsById[client.ConnectionId].Notifications; + if (client.AccountId is 53 or 54) + Assert.Empty(notifications); + else + { + using var frame = JsonDocument.Parse(Assert.Single(notifications)); + Assert.Equal("81", frame.RootElement.GetProperty("Id").GetString()); + var payload = frame.RootElement.GetProperty("Msg"); + Assert.Equal(ev.PlayerEventId, payload.GetProperty("playerEventId").GetInt64()); + var broadcast = payload.GetProperty("broadcastingRoomInstanceId"); + Assert.Equal(source, broadcast.ValueKind == JsonValueKind.Null ? (long?)null : broadcast.GetInt64()); + } + notifications.Clear(); + } + } + } + finally + { + lock (Notify.ClientsLock) + foreach (var client in clients) + Notify.Clients.Remove(client); + Notify.Context = oldContext; + } + } + + private static PlayerEventDB Event() => new() + { + // The id is assigned by the store; a test that reads it back after a save uses the + // generated one. + PlayerEventId = 7, + Name = "Launch Party", + Description = "Big launch", + RoomId = 102, + CreatorPlayerId = CreatorId, + AttendeeCount = 3, + StartTime = new DateTime(2026, 10, 8, 19, 10, 0, DateTimeKind.Unspecified), + EndTime = new DateTime(2026, 10, 8, 19, 40, 0, DateTimeKind.Unspecified), + Accessibility = Accessibility.Public, + TagsJson = """[{"Tag":"workshops","Type":0}]""" + }; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + } + + private sealed class RecordingClient : ISingleClientProxy + { + public List Notifications { get; } = new(); + + public Task InvokeCoreAsync(string method, object?[] args, + CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + Assert.Equal("Notification", method); + Notifications.Add(Assert.IsType(args[0])); + return Task.CompletedTask; + } + } + + private class RecordingHubClients : DispatchProxy + { + public Dictionary ClientsById { get; } = new(StringComparer.Ordinal); + + protected override object? Invoke(MethodInfo? method, object?[]? args) + { + if (method?.Name == "Client" && args is [string connectionId]) + return ClientsById[connectionId]; + throw new NotSupportedException($"Unexpected hub-client member: {method?.Name}"); + } + } + + private sealed class TestHubContext : IHubContext + { + public IHubClients Clients { get; } + public IGroupManager Groups => null!; + + public TestHubContext(IHubClients clients) => Clients = clients; + } +} diff --git a/ServerTests/PlayerEventQueriesTests.cs b/ServerTests/PlayerEventQueriesTests.cs new file mode 100644 index 0000000..0b4dd37 --- /dev/null +++ b/ServerTests/PlayerEventQueriesTests.cs @@ -0,0 +1,126 @@ +using RadicadeServer.API.Events; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Events; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Tests the filtering rules behind the game's event endpoints: +/// - /api/playerevents/v1/search (public, not-ended events; name/description match) +/// - /api/playerevents/v1/room/{roomId} (events hosted in that room) +/// +public class PlayerEventQueriesTests +{ + private static PlayerEventDB MakeEvent( + long id, + long roomId, + string name = "Launch Party", + DateTime? start = null, + DateTime? end = null, + Accessibility accessibility = Accessibility.Public) + { + var startTime = start ?? DateTime.UtcNow.AddHours(1); + return new PlayerEventDB + { + PlayerEventId = id, + Name = name, + Description = "A test event", + ImageName = "", + StartTime = startTime, + EndTime = end ?? startTime.AddHours(2), + CreatorPlayerId = 42, + AttendeeCount = 1, + RoomId = roomId, + Accessibility = accessibility + }; + } + + [Fact] + public void CreatedPublicEvent_AppearsInSearchResults() + { + var ev = MakeEvent(1, 100, name: "Launch Party"); + + var results = PlayerEventQueries.Search(new[] { ev }, null, DateTime.UtcNow); + + Assert.Single(results); + Assert.Equal(1, results[0].PlayerEventId); + } + + [Fact] + public void CreatedEvent_AppearsInRoomResults() + { + var ev = MakeEvent(1, 100); + + var results = PlayerEventQueries.InRoom(new[] { ev }, 100); + + Assert.Single(results); + Assert.Equal(1, results[0].PlayerEventId); + } + + [Fact] + public void Search_MatchesNameCaseInsensitively() + { + var ev = MakeEvent(1, 100, name: "Launch Party"); + + var results = PlayerEventQueries.Search(new[] { ev }, "launch", DateTime.UtcNow); + + Assert.Single(results); + } + + [Fact] + public void Search_MatchesDescription() + { + var ev = MakeEvent(1, 100, name: "Other"); + ev.Description = "A test event"; + + var results = PlayerEventQueries.Search(new[] { ev }, "TEST", DateTime.UtcNow); + + Assert.Single(results); + } + + [Fact] + public void Search_ExcludesEndedEvents() + { + var ev = MakeEvent(1, 100, end: DateTime.UtcNow.AddHours(-1)); + + var results = PlayerEventQueries.Search(new[] { ev }, null, DateTime.UtcNow); + + Assert.Empty(results); + } + + [Fact] + public void Search_ExcludesPrivateEvents() + { + var ev = MakeEvent(1, 100, accessibility: Accessibility.Private); + + var results = PlayerEventQueries.Search(new[] { ev }, null, DateTime.UtcNow); + + Assert.Empty(results); + } + + [Fact] + public void RoomEvents_OnlyReturnEventsForThatRoom() + { + var inRoom = MakeEvent(1, 100); + var otherRoom = MakeEvent(2, 200); + + var results = PlayerEventQueries.InRoom(new[] { inRoom, otherRoom }, 100); + + Assert.Single(results); + Assert.Equal(1, results[0].PlayerEventId); + } + + [Fact] + public void Search_ReturnsNewestEventsFirst() + { + var later = MakeEvent(1, 100, start: DateTime.UtcNow.AddHours(3)); + var sooner = MakeEvent(2, 100, start: DateTime.UtcNow.AddHours(1)); + + var results = PlayerEventQueries.Search(new[] { sooner, later }, null, DateTime.UtcNow); + + Assert.Equal(2, results.Count); + Assert.Equal(1, results[0].PlayerEventId); + Assert.Equal(2, results[1].PlayerEventId); + } +} diff --git a/ServerTests/PlayerReportedRouteTests.cs b/ServerTests/PlayerReportedRouteTests.cs new file mode 100644 index 0000000..3f96736 --- /dev/null +++ b/ServerTests/PlayerReportedRouteTests.cs @@ -0,0 +1,298 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Accounts; +using RadicadeServer.API.Events; +using RadicadeServer.API.Relationships.V1; +using RadicadeServer.API; +using RadicadeServer.Clubs; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Relationships; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The routes a single player report named, each of which failed in a different way: +/// a missing route (404), a route registered under another verb (405), and a handler +/// that refused a request whose id arrived somewhere it did not look (400). +/// +/// The tests below pin the behaviour rather than the implementation: a request of the +/// shape the client sends has to reach a handler, find its record, and act on it. +/// +public class PlayerReportedRouteTests +{ + private const long CallerId = 42; + private const long OtherId = 77; + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new RecDBContext(options); + } + + private static T CreateController(RecDBContext db, long? userId = CallerId) where T : Controller + { + var identity = new ClaimsIdentity("TestAuth"); + if (userId.HasValue) + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.Value.ToString())); + + var controller = (T)Activator.CreateInstance(typeof(T), db)!; + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + return controller; + } + + private static void SetJsonBody(Controller controller, string jsonBody) + { + var bytes = Encoding.UTF8.GetBytes(jsonBody); + controller.HttpContext.Request.Body = new MemoryStream(bytes); + controller.HttpContext.Request.ContentType = "application/json"; + controller.HttpContext.Request.ContentLength = bytes.Length; + } + + private static ChatThread ThreadWith(params long[] playerIds) => new() + { + chatThreadName = "", + messages = "[]", + playerIds = JsonSerializer.Serialize(playerIds) + }; + + // ---- POST /chat/thread/party -> 405 ---------------------------------------------------- + + [Fact] + public void PartyThreadIsRegisteredForBothGetAndPost() + { + var attributes = typeof(ThreadAPI).GetMethod(nameof(ThreadAPI.GetPartyThread))! + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), true) + .Cast() + .ToList(); + + // The client's party lookup POSTs; with only the GET registered that is a 405, + // which reads in the log as the party simply never loading. + Assert.Contains(attributes, a => a.HttpMethods.Contains("GET")); + Assert.Contains(attributes, a => a.HttpMethods.Contains("POST")); + + // And both verbs answer the same empty list, which is the "no party" spelling. + var controller = CreateController(CreateDb()); + var result = Assert.IsType(controller.GetPartyThread()); + Assert.Empty(Assert.IsAssignableFrom(result.Value).Cast()); + } + + // ---- PUT /chat/thread/1/favorite -> 404 ------------------------------------------------ + + [Fact] + public async Task FavoriteThreadRouteExistsAndIsReportedOnTheThread() + { + // The route the star uses. It had no attribute at all, so the call 404ed. + var methods = typeof(ThreadAPI).GetMethod(nameof(ThreadAPI.FavoriteThread))! + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), true) + .Cast() + .SelectMany(a => a.HttpMethods) + .ToList(); + Assert.Contains("PUT", methods); + + using var db = CreateDb(); + var thread = ThreadWith(CallerId, OtherId); + db.Chats.Add(thread); + await db.SaveChangesAsync(); + + var controller = CreateController(db); + SetJsonBody(controller, """{ "isFavorited": true }"""); + Assert.IsType(await controller.FavoriteThread(thread.chatThreadId)); + + var stored = await db.Chats.SingleAsync(x => x.chatThreadId == thread.chatThreadId); + Assert.Contains(CallerId, stored.GetFavoritedBy()); + + // The star has to show up on the read that renders the conversation, or the button + // never lights even though the write worked. + var read = await controller.GetThreadById(thread.chatThreadId); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize( + Assert.IsType(read).Value)); + Assert.True(doc.RootElement.GetProperty("isFavorited").GetBoolean()); + } + + [Fact] + public async Task UnfavoritingClearsTheStarAndLeavesTheOtherMemberAlone() + { + using var db = CreateDb(); + var thread = ThreadWith(CallerId, OtherId); + thread.SetFavoritedBy([CallerId, OtherId]); + db.Chats.Add(thread); + await db.SaveChangesAsync(); + + var controller = CreateController(db); + controller.HttpContext.Request.Method = "DELETE"; + Assert.IsType(await controller.FavoriteThread(thread.chatThreadId)); + + var stored = await db.Chats.SingleAsync(x => x.chatThreadId == thread.chatThreadId); + Assert.DoesNotContain(CallerId, stored.GetFavoritedBy()); + Assert.Contains(OtherId, stored.GetFavoritedBy()); + } + + // ---- POST /api/relationships/v1/ignore and /mute -> 400 -------------------------------- + + [Fact] + public async Task IgnoreAndMuteAcceptThePlayerIdFromTheBody() + { + using var db = CreateDb(); + var controller = CreateController(db); + // The client posts the target in the body; the handler bound `id` from the route + // only, so this resolved to 0 and answered "invalid player id" (400). + SetJsonBody(controller, $$"""{ "playerId": {{OtherId}} }"""); + + Assert.IsType(await controller.Ignore()); + Assert.IsType(await controller.Mute()); + + var mine = await db.Relationships.SingleAsync(x => x.AccountId == CallerId && x.PlayerId == OtherId); + Assert.NotEqual(ReciprocalStatus.None, mine.Ignored); + Assert.NotEqual(ReciprocalStatus.None, mine.Muted); + } + + [Fact] + public async Task RelationshipFlagsStillReadTheIdFromTheRoute() + { + using var db = CreateDb(); + var controller = CreateController(db); + // The `ignore/{id}` spelling binds the id into the action's own route data. + controller.ControllerContext.RouteData = new Microsoft.AspNetCore.Routing.RouteData(); + controller.ControllerContext.RouteData.Values["id"] = OtherId.ToString(); + + Assert.IsType(await controller.Ignore()); + + var mine = await db.Relationships.SingleAsync(x => x.AccountId == CallerId && x.PlayerId == OtherId); + Assert.NotEqual(ReciprocalStatus.None, mine.Ignored); + } + + [Fact] + public async Task RelationshipFlagWithoutAnyIdIsStillRefused() + { + using var db = CreateDb(); + var controller = CreateController(db); + Assert.IsType(await controller.Ignore()); + } + + // ---- PUT /clubs/club/1783155111/members/invite -> 400 ---------------------------------- + + [Fact] + public void NamedMembershipTypeIsAcceptedByTheInvite() + { + // The invite screen sends the tier by NAME ("Member"/"Moderator"/"Coowner"). + // Reading only the number made the whole invite 400 with "Invalid membershipType" + // even though the account id was fine. + var parse = typeof(ClubsAPI).GetMethod("ParseInviteMembershipType", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; + + Assert.Equal(ClubMembershipTypes.Member, (int)parse.Invoke(null, ["Member"])!); + Assert.Equal(ClubMembershipTypes.Moderator, (int)parse.Invoke(null, ["moderator"])!); + Assert.Equal(ClubMembershipTypes.Coowner, (int)parse.Invoke(null, ["CoOwner"])!); + + // The numeric spelling still works, and anything unknown is still refused. + Assert.Equal(ClubMembershipTypes.Moderator, (int)parse.Invoke(null, ["20"])!); + Assert.Equal(-1, (int)parse.Invoke(null, ["Banned"])!); + } + + // ---- POST /api/playerevents/v2/delete/1 -> 404 ----------------------------------------- + + [Fact] + public async Task EventDeleteReadsTheIdFromTheBodyWhenItIsNotInTheRoute() + { + using var db = CreateDb(); + db.PlayerEvents.Add(new PlayerEventDB { PlayerEventId = 1, Name = "Doomed", CreatorPlayerId = CallerId }); + await db.SaveChangesAsync(); + + var controller = CreateController(db); + // The delete screen posts the id in the body. With the route-only binding this + // resolved to 0 and 404ed for an event that existed. + SetJsonBody(controller, """{ "PlayerEventId": 1 }"""); + + Assert.IsType(await controller.DeleteEventV2()); + Assert.Empty(await db.PlayerEvents.ToListAsync()); + } + + [Fact] + public async Task EventDeleteOfAMissingEventAnswersSuccessSoAStaleCardCanBeCleared() + { + // The client drops an event card only when the PlayerEventDeleted (82) frame reaches it, + // so a card left behind by a frame it could not read is retried as another delete. + // Answering that retry with the result envelope - rather than a 404 it can only render as + // an error - is what lets the card finally go away. An event that is somebody else's is + // still refused; see the test below. + using var db = CreateDb(); + var controller = CreateController(db); + SetJsonBody(controller, """{ "PlayerEventId": 999 }"""); + + var result = Assert.IsType(await controller.DeleteEventV2()); + using var document = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)); + Assert.Equal(999, document.RootElement.GetProperty("PlayerEvent") + .GetProperty("PlayerEventId").GetInt64()); + Assert.Equal(0, document.RootElement.GetProperty("Result").GetInt32()); + Assert.Empty(await db.PlayerEvents.ToListAsync()); + } + + [Fact] + public async Task EventDeleteStillRefusesSomeoneElsesEvent() + { + using var db = CreateDb(); + db.PlayerEvents.Add(new PlayerEventDB { PlayerEventId = 1, Name = "Not yours", CreatorPlayerId = OtherId }); + await db.SaveChangesAsync(); + + var controller = CreateController(db, CallerId); + Assert.IsType(await controller.DeleteEventV2(1)); + } + + // ---- PUT /accounts/account/me/emoji -> 404 -------------------------------------------- + + [Fact] + public async Task AccountEmojiRouteExistsAndPersists() + { + var methods = typeof(AccountsAPI).GetMethod(nameof(AccountsAPI.SetDisplayEmoji))! + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), true) + .Cast() + .SelectMany(a => a.HttpMethods) + .ToList(); + Assert.Contains("PUT", methods); + + using var db = CreateDb(); + db.Profiles.Add(new Profile { accountId = CallerId, username = "tester", displayName = "Tester", profileImage = "" }); + db.Players.Add(new PlayerData { AccountId = CallerId, Hashword = "x" }); + await db.SaveChangesAsync(); + + var controller = CreateController(db); + SetJsonBody(controller, """{ "displayEmoji": "😀" }"""); + var answer = await controller.SetDisplayEmoji(); + + Assert.NotNull(answer); + Assert.Equal("😀", answer!.displayEmoji); + Assert.Equal("😀", (await db.Profiles.SingleAsync(x => x.accountId == CallerId)).displayEmoji); + + // An empty value clears it rather than storing an empty string. + SetJsonBody(controller, """{ "displayEmoji": "" }"""); + var cleared = await controller.SetDisplayEmoji(); + Assert.Null(cleared!.displayEmoji); + Assert.Null((await db.Profiles.SingleAsync(x => x.accountId == CallerId)).displayEmoji); + } + + [Fact] + public async Task AccountEmojiIsReportedOnTheAccountTheClientReads() + { + using var db = CreateDb(); + db.Profiles.Add(new Profile { accountId = CallerId, username = "tester", displayName = "Tester", profileImage = "", displayEmoji = "🔥" }); + db.Players.Add(new PlayerData { AccountId = CallerId, Hashword = "x" }); + await db.SaveChangesAsync(); + + var controller = CreateController(db); + var account = await controller.GetProfile(); + Assert.Equal("🔥", account!.displayEmoji); + } +} diff --git a/ServerTests/PlayerSettingsPersistenceTests.cs b/ServerTests/PlayerSettingsPersistenceTests.cs new file mode 100644 index 0000000..6c8c12c --- /dev/null +++ b/ServerTests/PlayerSettingsPersistenceTests.cs @@ -0,0 +1,101 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Config; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts.Local; +using Xunit; + +namespace RadicadeServer.Tests; + +public class PlayerSettingsPersistenceTests +{ + [Theory] + [InlineData("text/plain", "{\"Key\":\"MusicVolume\",\"Value\":0.25}", "MusicVolume", "0.25")] + [InlineData("application/json", "{\"Key\":\"AutoSprint\",\"Value\":false}", "AutoSprint", "False")] + [InlineData("application/x-www-form-urlencoded", "Key=MusicVolume&Value=0.75", "MusicVolume", "0.75")] + public async Task PostRequestSurvivesFreshDatabaseContext(string contentType, string body, string key, string expected) + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + await using (var db = new RecDBContext(options)) + { + var api = CreateApi(db); + api.Request.ContentType = contentType; + api.Request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(body)); + var result = await api.SetSettingRequest(); + Assert.IsType(result); + } + await using var reloaded = new RecDBContext(options); + Assert.Equal(expected, CreateApi(reloaded).GetSettings().Single(x => x.Key == key).Value); + } + + [Fact] + public async Task InvalidBulkBodyReturnsFailureAndPreservesSavedValues() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + await using var db = new RecDBContext(options); + var api = CreateApi(db); + await api.SetSetting(new Setting { Key = "MusicVolume", Value = "0.2" }); + api.Request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("not-json")); + Assert.IsType(await api.PutSettings()); + Assert.Equal("0.2", api.GetSettings().Single(x => x.Key == "MusicVolume").Value); + } + + private static PlayerSettingsAPI CreateApi(RecDBContext db) + { + var context = new Microsoft.AspNetCore.Http.DefaultHttpContext(); + context.User = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity( + [new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "2")], "test")); + return new PlayerSettingsAPI(db) + { + ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = context } + }; + } + + [Fact] + public async Task GetDoesNotReplaceSavedToggleWithDefault() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + await using var db = new RecDBContext(options); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = 2, + Settings = "[{\"Key\":\"ShowNames\",\"Value\":\"0\"}]" + }); + await db.SaveChangesAsync(); + + var api = new PlayerSettingsAPI(db); + var context = new Microsoft.AspNetCore.Http.DefaultHttpContext(); + context.User = new System.Security.Claims.ClaimsPrincipal( + new System.Security.Claims.ClaimsIdentity([ + new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "2") + ], "test")); + api.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext + { + HttpContext = context + }; + + var settings = api.GetSettings(); + Assert.Equal("0", settings.Single(x => x.Key == "ShowNames").Value); + } + + [Fact] + public async Task PutSettingsSurvivesASecondRead() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; + await using var db = new RecDBContext(options); + var api = new PlayerSettingsAPI(db); + var context = new Microsoft.AspNetCore.Http.DefaultHttpContext(); + context.User = new System.Security.Claims.ClaimsPrincipal( + new System.Security.Claims.ClaimsIdentity([ + new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "2") + ], "test")); + api.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = context }; + + await api.SetSetting(new Setting { Key = "AutoSprint", Value = "False" }); + + Assert.Equal("False", api.GetSettings().Single(x => x.Key == "AutoSprint").Value); + } +} diff --git a/ServerTests/PlayerWarningTests.cs b/ServerTests/PlayerWarningTests.cs new file mode 100644 index 0000000..4282377 --- /dev/null +++ b/ServerTests/PlayerWarningTests.cs @@ -0,0 +1,977 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using RadicadeServer.API.Moderation; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Messages; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The moderator warning endpoints (POST api/playerwarnings and +/// POST api/playerwarnings/acknowledge). +/// +/// The 2023 client has the whole feature - the "Warn Player" action, a warn screen with an +/// "Acknowledge warning" button, and both route literals in its RecNet layer - but this server +/// mapped neither route, so every warn 404'd. A 404 there is silent: nothing logs, the button +/// just does nothing, which is why it went unnoticed. +/// +/// The route assertions are the point of this file, for the same reason they are in +/// RoleLookupTests: a correct handler behind a missing prefix is the failure mode this project +/// keeps hitting. These two templates are absolute (they start with "/"), so ASP.NET ignores the +/// controller's own api/PlayerReporting/ prefix - the client's literal is the whole path. +/// +[Collection(DbFactoryCollection.Name)] +public class PlayerWarningTests +{ + // Mirrors the server's MVC JSON options, including PropertyNameCaseInsensitive - which is + // what makes System.Text.Json reject a type whose members differ only in case. Without it a + // case-colliding response DTO serializes fine in a test and 500s in production. + private static readonly JsonSerializerOptions ServerJson = new() + { + PropertyNamingPolicy = null, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + }; + + private static string Serialize(object? value) => JsonSerializer.Serialize(value, ServerJson); + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static PlayerReportingAPI NewApi(RecDBContext db, long? callerId = 6) + { + var identity = new ClaimsIdentity("TestAuth"); + if (callerId != null) + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, callerId.Value.ToString())); + + return new PlayerReportingAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + } + + /// Give the controller a request body the way the client sends it. + private static void SetBody(Controller controller, string body, string contentType) + { + var request = controller.ControllerContext.HttpContext.Request; + request.ContentType = contentType; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + } + + private static async Task WithProfileAsync(long accountId, string? roles) + { + var db = NewDb(); + db.Profiles.Add(new Profile + { + accountId = accountId, + role = roles, + username = $"player{accountId}", + displayName = $"Player {accountId}", + profileImage = DBConfig.DefaultProfileImage + }); + await db.SaveChangesAsync(); + return db; + } + + // ----------------------------------------------------------------------------------- + // Routes + // ----------------------------------------------------------------------------------- + + private static string[] PostTemplates(Type controller, string methodName) => + controller.GetMethod(methodName)!.GetCustomAttributes() + .Select(a => a.Template) + .Where(t => t != null) + .Select(t => t!) + .ToArray(); + + [Fact] + public void TheWarningRoutesAreTheAbsolutePathsTheClientBuilds() + { + var create = PostTemplates(typeof(PlayerReportingAPI), nameof(PlayerReportingAPI.CreateWarning)); + var acknowledge = PostTemplates(typeof(PlayerReportingAPI), nameof(PlayerReportingAPI.AcknowledgeWarning)); + + // The client's own literals, verbatim from its metadata string table. + Assert.Contains("/api/playerwarnings", create); + Assert.Contains("/api/playerwarnings/acknowledge", acknowledge); + + // Every one of them absolute: a template that started with "api/" would be concatenated + // onto the controller's "api/PlayerReporting/" prefix and answer a path no client asks for. + Assert.All(create.Concat(acknowledge), template => Assert.StartsWith("/", template)); + } + + // ----------------------------------------------------------------------------------- + // Issuing a warning + // ----------------------------------------------------------------------------------- + + [Fact] + public async Task AModeratorWarnsAPlayerAndTheModeratorComesFromTheToken() + { + using var db = await WithProfileAsync(6, "developer,moderator"); + var api = NewApi(db, 6); + SetBody(api, "WarnedPlayerId=205&ReportCategory=101&DisplayReason=Sexual gestures&ModeratorNote=dfg", + "application/x-www-form-urlencoded"); + + var result = Assert.IsType(await api.CreateWarning()); + using var json = JsonDocument.Parse(Serialize(result.Value)); + Assert.True(json.RootElement.GetProperty("Success").GetBoolean()); + + var warning = await db.PlayerWarnings.SingleAsync(); + Assert.Equal(6, warning.ModeratorPlayerId); // the caller, never a body field + Assert.Equal(205, warning.WarnedPlayerId); + Assert.Equal(ReportCategory.CoC_Sexual, warning.ReportCategory); + Assert.Equal("Sexual gestures", warning.DisplayReason); + Assert.Equal("dfg", warning.ModeratorNote); + Assert.Null(warning.AcknowledgedAt); + Assert.NotEqual(default, warning.CreatedAt); + } + + [Fact] + public async Task AbsentFieldsAreStoredRatherThanInvented() + { + using var db = await WithProfileAsync(6, "moderator"); + var api = NewApi(db, 6); + SetBody(api, "WarnedPlayerId=206", "application/x-www-form-urlencoded"); + + Assert.IsType(await api.CreateWarning()); + + var warning = await db.PlayerWarnings.SingleAsync(); + Assert.Equal(0, (int)warning.ReportCategory); + Assert.Null(warning.DisplayReason); + Assert.Null(warning.ModeratorNote); + } + + [Fact] + public async Task WarningsAreAppendOnly() + { + using var db = await WithProfileAsync(6, "moderator"); + + foreach (var note in new[] { "first", "second" }) + { + var api = NewApi(db, 6); + SetBody(api, $"WarnedPlayerId=207&ModeratorNote={note}", "application/x-www-form-urlencoded"); + Assert.IsType(await api.CreateWarning()); + } + + var rows = await db.PlayerWarnings.Where(x => x.WarnedPlayerId == 207).OrderBy(x => x.Id).ToListAsync(); + Assert.Equal(2, rows.Count); + Assert.Equal(new[] { "first", "second" }, rows.Select(r => r.ModeratorNote)); + } + + [Fact] + public async Task TheJSONBodyShapeBindsTheSameWayAsTheFormBody() + { + using var db = await WithProfileAsync(6, "moderator"); + var api = NewApi(db, 6); + SetBody(api, + """{"WarnedPlayerId":"208","ReportCategory":"103","DisplayReason":"griefing","ModeratorNote":"n"}""", + "application/json"); + + Assert.IsType(await api.CreateWarning()); + + var warning = await db.PlayerWarnings.SingleAsync(); + Assert.Equal(208, warning.WarnedPlayerId); + Assert.Equal(ReportCategory.CoC_Trolling, warning.ReportCategory); + Assert.Equal("griefing", warning.DisplayReason); + } + + [Fact] + public async Task AWarningWithoutAWarnedPlayerIsRefused() + { + using var db = await WithProfileAsync(6, "moderator"); + var api = NewApi(db, 6); + SetBody(api, "ModeratorNote=dfg", "application/x-www-form-urlencoded"); + + Assert.IsType(await api.CreateWarning()); + Assert.Empty(db.PlayerWarnings); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("gameClient")] + public async Task AValidTokenWithoutAStaffRoleIsRefusedAndNothingIsWritten(string? roles) + { + using var db = await WithProfileAsync(9, roles); + var api = NewApi(db, 9); + SetBody(api, "WarnedPlayerId=205", "application/x-www-form-urlencoded"); + + var result = Assert.IsType(await api.CreateWarning()); + Assert.Equal(403, result.StatusCode); + Assert.Empty(db.PlayerWarnings); + } + + [Fact] + public async Task APlayerWithNoProfileRowCannotWarnAnyone() + { + using var db = NewDb(); + var api = NewApi(db, 6); + SetBody(api, "WarnedPlayerId=205", "application/x-www-form-urlencoded"); + + var result = Assert.IsType(await api.CreateWarning()); + Assert.Equal(403, result.StatusCode); + } + + // ----------------------------------------------------------------------------------- + // Delivering a warning, and acknowledging it + // ----------------------------------------------------------------------------------- + + [Fact] + public async Task APendingWarningRidesTheNoBanBlockDetailsResponseAsTheWarnScreen() + { + using var db = await WithProfileAsync(6, "moderator"); + var api = NewApi(db, 6); + SetBody(api, "WarnedPlayerId=205&ReportCategory=101&DisplayReason=Sexual gestures", + "application/x-www-form-urlencoded"); + Assert.IsType(await api.CreateWarning()); + + var warned = NewApi(db, 205); + var ok = Assert.IsType(await warned.GetBlockDetails()); + using var json = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.True(json.RootElement.GetProperty("IsWarning").GetBoolean()); + Assert.False(json.RootElement.GetProperty("IsBan").GetBoolean()); + // Duration drives the BAN screen whenever it is > 0, so a warning must leave it at 0 or + // the player is shown a ban countdown for a warning. + Assert.Equal(0, json.RootElement.GetProperty("Duration").GetInt32()); + Assert.Equal("Sexual gestures", json.RootElement.GetProperty("Message").GetString()); + Assert.Equal(6, json.RootElement.GetProperty("PlayerIdReporter").GetInt64()); + } + + [Fact] + public async Task AcknowledgingClearsOnlyTheCallersPendingWarnings() + { + using var db = await WithProfileAsync(6, "moderator"); + foreach (var warned in new[] { 205L, 206L }) + { + var api = NewApi(db, 6); + SetBody(api, $"WarnedPlayerId={warned}", "application/x-www-form-urlencoded"); + Assert.IsType(await api.CreateWarning()); + } + + // The acknowledge posts no body at all on some builds, so this call sends none. + var result = Assert.IsType(await NewApi(db, 205).AcknowledgeWarning()); + using var json = JsonDocument.Parse(Serialize(result.Value)); + Assert.True(json.RootElement.GetProperty("Success").GetBoolean()); + + Assert.NotNull((await db.PlayerWarnings.SingleAsync(x => x.WarnedPlayerId == 205)).AcknowledgedAt); + Assert.Null((await db.PlayerWarnings.SingleAsync(x => x.WarnedPlayerId == 206)).AcknowledgedAt); + } + + [Fact] + public async Task AnAcknowledgedWarningLeavesThePlainEmptyBlockDetails() + { + using var db = await WithProfileAsync(6, "moderator"); + var api = NewApi(db, 6); + SetBody(api, "WarnedPlayerId=205", "application/x-www-form-urlencoded"); + Assert.IsType(await api.CreateWarning()); + + Assert.IsType(await NewApi(db, 205).AcknowledgeWarning()); + + var ok = Assert.IsType(await NewApi(db, 205).GetBlockDetails()); + using var json = JsonDocument.Parse(Serialize(ok.Value)); + + // Back to the four-key empty object the no-ban response has always been. + Assert.Equal(4, json.RootElement.EnumerateObject().Count()); + Assert.Equal(0, json.RootElement.GetProperty("Duration").GetInt32()); + } + + [Fact] + public async Task ABanOutranksAPendingWarning() + { + using var db = await WithProfileAsync(6, "moderator"); + var api = NewApi(db, 6); + SetBody(api, "WarnedPlayerId=205", "application/x-www-form-urlencoded"); + Assert.IsType(await api.CreateWarning()); + + db.Moderation.Add(new ModerationDB + { + PlayerId = 205, + IsBan = true, + Message = "banned", + Category = ReportCategory.Misc, + ExpireDuration = DateTime.UtcNow.AddHours(1) + }); + await db.SaveChangesAsync(); + + var ok = Assert.IsType(await NewApi(db, 205).GetBlockDetails()); + using var json = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.False(json.RootElement.GetProperty("IsWarning").GetBoolean()); + Assert.True(json.RootElement.GetProperty("IsBan").GetBoolean()); + Assert.True(json.RootElement.GetProperty("Duration").GetInt32() > 0); + } + + // ----------------------------------------------------------------------------------- + // Delivering it: out of the room, into the dorm, and NOT as a chat message + // ----------------------------------------------------------------------------------- + + /// + /// The static factories RoomUtils and MatchmakingUtils reach for when they are + /// called from a request that only has its own context - the same pattern the other + /// factory-dependent suites use. + /// + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + /// + /// Room 1 (the dorm template, cloned per player) plus a public room the player is standing + /// in - the only state that makes an ejection observable. + /// + private static async Task<(TestDbContextFactory Factory, RecDBContext Db, RoomInstance Room)> + APlayerInARoomAsync(long playerId, bool alreadyInTheirOwnDorm = false) + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + + var db = factory.CreateDbContext(); + + db.Rooms.Add(new RoomData + { + RoomId = 1, + Name = "DormRoom", + IsDorm = true, + CreatorAccountId = 1, + Accessibility = Accessibility.Private, + MaxPlayers = 4, + CreatedAt = DateTime.UtcNow, + }); + db.SubRooms.Add(new SubRoom + { + RoomId = 1, + Name = "DormRoom", + UnitySceneId = "DormRoomScene", + MaxPlayers = 4, + Accessibility = Accessibility.Private, + DataBlob = "", + }); + + db.Rooms.Add(new RoomData + { + RoomId = 25, + Name = "RecCenter", + CreatorAccountId = 1, + Accessibility = Accessibility.Public, + MaxPlayers = 20, + CreatedAt = DateTime.UtcNow, + }); + db.SubRooms.Add(new SubRoom + { + RoomId = 25, + Name = "Home", + UnitySceneId = "RecCenterScene", + MaxPlayers = 20, + Accessibility = Accessibility.Public, + }); + + // The player's own dorm, for the "already in it" case: a private room they created. + if (alreadyInTheirOwnDorm) + { + db.Rooms.Add(new RoomData + { + RoomId = 77, + Name = "DormRoom", + IsDorm = true, + CreatorAccountId = playerId, + Accessibility = Accessibility.Private, + MaxPlayers = 4, + CreatedAt = DateTime.UtcNow, + }); + db.SubRooms.Add(new SubRoom + { + RoomId = 77, + Name = "DormRoom", + UnitySceneId = "DormRoomScene", + MaxPlayers = 4, + Accessibility = Accessibility.Private, + DataBlob = "", + }); + } + + var instance = new RoomInstance + { + roomId = alreadyInTheirOwnDorm ? 77 : 25, + subRoomId = 0, + roomInstanceType = RoomInstanceType.Public, + isPrivate = alreadyInTheirOwnDorm, + maxCapacity = 20, + name = alreadyInTheirOwnDorm ? "^DormRoom" : "^RecCenter", + location = alreadyInTheirOwnDorm ? "DormRoomScene" : "RecCenterScene", + }; + db.RoomInstances.Add(instance); + await db.SaveChangesAsync(); + + db.Presences.Add(new RealPresence + { + playerId = playerId, + isOnline = true, + roomInstance = instance.roomInstanceId, + errorCode = JoinStatus.Success, + }); + await db.SaveChangesAsync(); + + return (factory, db, instance); + } + + /// The room a player's presence currently points at, with the room row behind it. + private static async Task<(RoomInstance Instance, RoomData Room)> WhereIsAsync(RecDBContext db, long playerId) + { + var presence = await db.Presences.AsNoTracking().FirstAsync(x => x.playerId == playerId); + var instance = await db.RoomInstances.AsNoTracking().FirstAsync(x => x.roomInstanceId == presence.roomInstance); + var room = await db.Rooms.AsNoTracking().FirstAsync(x => x.RoomId == instance.roomId); + return (instance, room); + } + + [Fact] + public async Task AWarningIsNotDeliveredAsACoachMessage() + { + var (factory, db, _) = await APlayerInARoomAsync(205); + + // Notify.DbFactory is wired to the SAME store on purpose. With it null, SendMessage returns + // immediately and the assertion below would pass even if a warning still tried to deliver + // itself as a coach message; with it pointed somewhere else, the row would land in the other + // database. Pointing it at this store is what makes "no message was written" mean it. + var previousFactory = Notify.DbFactory; + try + { + Notify.DbFactory = factory; + + await WithModeratorAsync(db, warnedPlayerId: 205); + + Assert.Empty(await db.Messages.ToListAsync()); + + // Positive control: this exact wiring does see a message when one is sent, so the empty + // result above is the warning's doing rather than a harness that cannot see writes. + await Notify.SendMessage(205, new Message + { + Type = MessageType.CoachMessage, + Data = "control", + FromPlayerId = 1, + }); + Assert.Single(await db.Messages.ToListAsync()); + } + finally + { + Notify.DbFactory = previousFactory; + } + } + + [Fact] + public async Task AWarningTakesThePlayerOutOfTheRoomTheyAreInAndIntoTheirDorm() + { + const long playerId = 205; + var (_, db, wasIn) = await APlayerInARoomAsync(playerId); + + await WithModeratorAsync(db, warnedPlayerId: playerId); + + var (nowIn, room) = await WhereIsAsync(db, playerId); + + // Out of the room they were warned in... + Assert.NotEqual(wasIn.roomInstanceId, nowIn.roomInstanceId); + + // ...and standing in their own dorm, which is what makes them face the warn screen on the + // room load that follows. + Assert.True(room.IsDorm); + Assert.Equal(playerId, room.CreatorAccountId); + } + + [Fact] + public async Task AWarningDoesNotBounceAPlayerOutOfTheirOwnDorm() + { + const long playerId = 206; + var (_, db, wasIn) = await APlayerInARoomAsync(playerId, alreadyInTheirOwnDorm: true); + + await WithModeratorAsync(db, warnedPlayerId: playerId); + + var (nowIn, room) = await WhereIsAsync(db, playerId); + + Assert.Equal(wasIn.roomInstanceId, nowIn.roomInstanceId); + Assert.True(room.IsDorm); + } + + /// A moderator issues a warning to through the route. + private static async Task WithModeratorAsync(RecDBContext db, long warnedPlayerId) + { + // Only once: the moderator row has accountId as its key, so a second call for a second + // warning would be a duplicate insert rather than a second warn. + if (!await db.Profiles.AnyAsync(x => x.accountId == 6)) + { + db.Profiles.Add(new Profile + { + accountId = 6, + role = "moderator", + username = "mod", + displayName = "Mod", + profileImage = DBConfig.DefaultProfileImage + }); + await db.SaveChangesAsync(); + } + + var api = NewApi(db, 6); + SetBody(api, $"WarnedPlayerId={warnedPlayerId}&ReportCategory=101", "application/x-www-form-urlencoded"); + Assert.IsType(await api.CreateWarning()); + } + + /// Adds the developer role a target needs to be asked before anything is applied. + private static async Task WithDeveloperProfileAsync(RecDBContext db, long accountId) + { + db.Profiles.Add(new Profile + { + accountId = accountId, + role = "developer", + username = $"dev{accountId}", + displayName = $"Dev {accountId}", + profileImage = DBConfig.DefaultProfileImage + }); + await db.SaveChangesAsync(); + } + + /// Posts one voteToKick body as and requires it to be accepted. + private static async Task AnswerVoteKickAsync(RecDBContext db, long callerId, string body) + { + var api = NewApi(db, callerId); + SetBody(api, body, "application/x-www-form-urlencoded"); + + // The route is declared ActionResult, so the object result is on .Result. + var result = await api.VoteToKick(); + var ok = Assert.IsType(result.Result); + Assert.True(((OkResponse)ok.Value!).Success); + } + + // ----------------------------------------------------------------------------------- + // Cannot be bypassed: a pending warning holds the player in their dorm until they + // acknowledge it. This is the whole point of the feature - the row alone is not enough, + // because leaving the dorm used to clear nothing and block nothing. + // ----------------------------------------------------------------------------------- + + [Fact] + public async Task APendingWarningAnswersTheRoomJoinWithTheirDormInsteadOfTheRoomTheyAskedFor() + { + const long playerId = 220; + var (_, db, wasIn) = await APlayerInARoomAsync(playerId); + + await WithModeratorAsync(db, warnedPlayerId: playerId); + + // The play-menu join, which is also what goto/room/{name} and matchmake/room/{id} run. + var match = await MatchmakingUtils.Matchmake(JoinMode.PublicMatchmaking, "RecCenter", playerId); + + // Answered as a successful matchmake into the dorm rather than as an error, so the client + // loads the dorm it is already in with the warning screen still up. + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.NotNull(match.roomInstance); + + var (nowIn, room) = await WhereIsAsync(db, playerId); + Assert.True(room.IsDorm); + Assert.Equal(playerId, room.CreatorAccountId); + Assert.Equal(match.roomInstance!.roomInstanceId, nowIn.roomInstanceId); + Assert.NotEqual(wasIn.roomInstanceId, nowIn.roomInstanceId); + } + + [Fact] + public async Task APendingWarningAlsoHoldsTheInstanceIdJoinsShut() + { + const long playerId = 221; + var (_, db, wasIn) = await APlayerInARoomAsync(playerId); + + await WithModeratorAsync(db, warnedPlayerId: playerId); + + // JoinRoom is the funnel behind a friend's presence, an invite, a deep link, a clubhouse and + // an event - the doors that used to let a warned player walk straight back out. + var match = await MatchmakingUtils.JoinRoom(playerId, wasIn.roomInstanceId); + + var (nowIn, room) = await WhereIsAsync(db, playerId); + Assert.True(room.IsDorm); + Assert.NotEqual(wasIn.roomInstanceId, nowIn.roomInstanceId); + Assert.Equal(match.roomInstance!.roomInstanceId, nowIn.roomInstanceId); + } + + [Fact] + public async Task AcknowledgingIsWhatLiftsTheHoldNotLeavingTheDorm() + { + const long playerId = 222; + var (_, db, _) = await APlayerInARoomAsync(playerId); + + await WithModeratorAsync(db, warnedPlayerId: playerId); + + // Still pending: the warning was delivered, not answered. + Assert.True(await db.HasPendingWarningAsync(playerId)); + var held = await MatchmakingUtils.Matchmake(JoinMode.PublicMatchmaking, "RecCenter", playerId); + Assert.True((await db.Rooms.AsNoTracking().FirstAsync(x => x.RoomId == held.roomInstance!.roomId)).IsDorm); + + Assert.IsType(await NewApi(db, playerId).AcknowledgeWarning()); + + var released = await MatchmakingUtils.Matchmake(JoinMode.PublicMatchmaking, "RecCenter", playerId); + var room = await db.Rooms.AsNoTracking().FirstAsync(x => x.RoomId == released.roomInstance!.roomId); + Assert.Equal("RecCenter", room.Name); + } + + [Fact] + public async Task APendingWarningStillLetsThePlayerIntoTheirOwnDorm() + { + const long playerId = 223; + var (_, db, _) = await APlayerInARoomAsync(playerId, alreadyInTheirOwnDorm: true); + + await WithModeratorAsync(db, warnedPlayerId: playerId); + + // Their own dorm is where the warning is answered, so it is not held shut - a warned player + // has to be able to get back into the room the warning screen lives in. + var match = await MatchmakingUtils.Matchmake(JoinMode.PublicMatchmaking, "DormRoom", playerId); + + Assert.Equal(JoinStatus.Success, match.errorCode); + Assert.NotNull(match.roomInstance); + + var room = await db.Rooms.AsNoTracking().FirstAsync(x => x.RoomId == match.roomInstance!.roomId); + Assert.True(room.IsDorm); + Assert.Equal(playerId, room.CreatorAccountId); + } + + [Fact] + public void TheServedWarningKeepsDurationAtZeroAndOnlyTheDeliveryCarriesTheScreenHold() + { + var warning = new PlayerWarning + { + Id = 1, + ModeratorPlayerId = 6, + WarnedPlayerId = 205, + DisplayReason = "Sexual gestures", + }; + + var served = PlayerUtils.BuildWarningBlockDetails(warning); + var delivered = PlayerUtils.BuildWarningBlockDetails(warning, (int)PlayerUtils.WarningScreenHold.TotalSeconds); + + // What moderationBlockDetails answers with: the warn screen's text, and no duration - the + // pending state is not a timeout. + Assert.True(served.IsWarning); + Assert.False(served.IsBan); + Assert.Equal(0, served.Duration); + Assert.Equal("Sexual gestures", served.Message); + + // What is pushed at the client: the client only holds the dorm's screen up (and so only ever + // asks its warn screen for text) when the delivered frame carries Duration > 0. IsBan stays + // false, which is what keeps it the warn screen and not the ban screen. + Assert.True(delivered.IsWarning); + Assert.False(delivered.IsBan); + Assert.True(delivered.Duration > 0); + Assert.Equal("Sexual gestures", delivered.Message); + } + + [Fact] + public async Task AWarningIssuedWhileThePlayerIsOfflineStaysPending() + { + using var db = await WithProfileAsync(6, "moderator"); + + // Player 205 has no presence row at all, and no notify connection is registered, so both the + // presence push and the two frames are dropped. The warning is written BEFORE either, which + // is why a player who is offline when they are warned is still held when they come back. + await WithModeratorAsync(db, 205); + + Assert.True(await db.HasPendingWarningAsync(205)); + Assert.Empty(await db.Messages.ToListAsync()); + } + + [Fact] + public async Task WarningsStackAndOneAcknowledgementClearsThemAll() + { + using var db = await WithProfileAsync(6, "moderator"); + + await WithModeratorAsync(db, 224); + await WithModeratorAsync(db, 224); + + var rows = await db.PlayerWarnings.Where(x => x.WarnedPlayerId == 224).ToListAsync(); + Assert.Equal(2, rows.Count); + Assert.All(rows, row => Assert.Null(row.AcknowledgedAt)); + + Assert.IsType(await NewApi(db, 224).AcknowledgeWarning()); + + Assert.False(await db.HasPendingWarningAsync(224)); + } + + [Fact] + public async Task ASecondAcknowledgementIsANoOpRatherThanAnError() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithModeratorAsync(db, 225); + + Assert.IsType(await NewApi(db, 225).AcknowledgeWarning()); + + // The button is already pressed, or the screen was re-opened after it closed: the second + // post has nothing left to clear and must still succeed, or the client hangs on its screen. + Assert.IsType(await NewApi(db, 225).AcknowledgeWarning()); + Assert.NotNull((await db.PlayerWarnings.SingleAsync()).AcknowledgedAt); + } + + // ----------------------------------------------------------------------------------- + // Developers: the server asks, waits, and only then applies. + // ----------------------------------------------------------------------------------- + + [Fact] + public async Task AWarningForADeveloperIsParkedAndNothingIsWritten() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithDeveloperProfileAsync(db, 230); + + var api = NewApi(db, 6); + SetBody(api, "WarnedPlayerId=230&ReportCategory=101&DisplayReason=Sexual gestures&ModeratorNote=dfg", + "application/x-www-form-urlencoded"); + Assert.IsType(await api.CreateWarning()); + + // Parked, not issued: a warning that exists is a warning that pins its player to the dorm, + // and the developer has not decided whether to accept it. + Assert.Empty(await db.PlayerWarnings.ToListAsync()); + + var decision = PlayerUtils.TakeDeveloperDecision(230); + Assert.NotNull(decision); + Assert.Equal(PlayerUtils.DeveloperDecisionKind.Warning, decision!.Kind); + Assert.Equal("Sexual gestures", decision.DisplayReason); + Assert.Equal("dfg", decision.ModeratorNote); + Assert.Equal(6, decision.IssuedBy); + } + + [Fact] + public async Task ADeveloperAcceptingTheWarningGetsItWrittenAsTheModeratorsWarning() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithDeveloperProfileAsync(db, 231); + await WithModeratorAsync(db, 231); + + await AnswerVoteKickAsync(db, 231, "PlayerId=231&Response=true&GameSessionId=9301"); + + var warning = await db.PlayerWarnings.SingleAsync(); + Assert.Equal(231, warning.WarnedPlayerId); + Assert.Equal(6, warning.ModeratorPlayerId); // the moderator who issued it, not the developer who accepted + Assert.Equal(ReportCategory.CoC_Sexual, warning.ReportCategory); + + // Accepting is not acknowledging: the warning is delivered now and stays pending until the + // developer presses acknowledge on the warn screen. + Assert.Null(warning.AcknowledgedAt); + Assert.True(await db.HasPendingWarningAsync(231)); + } + + [Fact] + public async Task ADeveloperDenyingTheWarningLeavesNothingBehind() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithDeveloperProfileAsync(db, 232); + await WithModeratorAsync(db, 232); + + await AnswerVoteKickAsync(db, 232, "PlayerId=232&Response=false&GameSessionId=9302"); + + // Deny means deny: no warning to hold them, no moderation row, nothing pending to clear. + Assert.Empty(await db.PlayerWarnings.ToListAsync()); + Assert.Empty(await db.Moderation.ToListAsync()); + Assert.False(await db.HasPendingWarningAsync(232)); + Assert.Null(PlayerUtils.TakeDeveloperDecision(232)); + } + + [Fact] + public async Task ARepeatedAnswerCannotApplyTheWarningTwice() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithDeveloperProfileAsync(db, 233); + await WithModeratorAsync(db, 233); + + await AnswerVoteKickAsync(db, 233, "PlayerId=233&Response=true&GameSessionId=9303"); + await AnswerVoteKickAsync(db, 233, "PlayerId=233&Response=true&GameSessionId=9303"); + + Assert.Single(await db.PlayerWarnings.ToListAsync()); + } + + /// + /// A parked prompt whose developer is not connected can no longer be answered, so it must not + /// keep the slot. It used to: the client that would have shown the dialog was gone, nothing was + /// written when the action was parked, and every later warning aimed at that developer was + /// dropped with only a console line - the panel reported a prompt as sent and the developer's + /// client was never asked anything again for the life of the process. + /// + [Fact] + public async Task ADeveloperPromptNothingCouldReceiveIsReplacedInsteadOfWedging() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithDeveloperProfileAsync(db, 234); + + var warning = new PlayerWarning { WarnedPlayerId = 234, DisplayReason = "Trolling" }; + Assert.True(await PlayerUtils.DeferForDeveloperAsync( + db, 234, PlayerUtils.DeveloperDecisionKind.Warning, PlayerUtils.BuildWarningBlockDetails(warning, 60))); + Assert.Null(Notify.GetClient(234)); // no live notify connection anywhere in this test + + var kick = PlayerUtils.BuildBlockDetails(true, ReportCategory.VoteKick, "kicked", DateTime.UtcNow.AddMinutes(15)); + Assert.True(await PlayerUtils.DeferForDeveloperAsync(db, 234, PlayerUtils.DeveloperDecisionKind.Kick, kick)); + + // The replacement is what the developer is asked about, so their answer applies the action + // the moderator just issued instead of the one nobody could have read. + var decision = PlayerUtils.TakeDeveloperDecision(234); + Assert.NotNull(decision); + Assert.Equal(PlayerUtils.DeveloperDecisionKind.Kick, decision!.Kind); + } + + /// + /// The other half of the same rule: a developer who IS connected keeps the prompt they were + /// already shown. Replacing a readable prompt would mean the developer reads one action and + /// their answer applies a different one - the client answers with only the player's id. + /// + [Fact] + public async Task AConnectedDeveloperKeepsThePromptTheyWereAlreadyShown() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithDeveloperProfileAsync(db, 235); + + var client = new HubClient { AccountId = 235, ConnectionId = "warning-test-235" }; + Notify.Clients.Add(client); + try + { + var warning = new PlayerWarning { WarnedPlayerId = 235, DisplayReason = "Trolling" }; + Assert.True(await PlayerUtils.DeferForDeveloperAsync( + db, 235, PlayerUtils.DeveloperDecisionKind.Warning, PlayerUtils.BuildWarningBlockDetails(warning, 60))); + + var kick = PlayerUtils.BuildBlockDetails(true, ReportCategory.VoteKick, "kicked", DateTime.UtcNow.AddMinutes(15)); + Assert.True(await PlayerUtils.DeferForDeveloperAsync(db, 235, PlayerUtils.DeveloperDecisionKind.Kick, kick)); + + Assert.Equal(PlayerUtils.DeveloperDecisionKind.Warning, PlayerUtils.TakeDeveloperDecision(235)!.Kind); + } + finally + { + Notify.Clients.Remove(client); + } + } + + [Fact] + public async Task AnAnswerWithNothingParkedIsStable() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithDeveloperProfileAsync(db, 234); + + // A late answer (the decision expired), a duplicate, or a response that arrives after the + // player reconnected: nothing is parked, so there is nothing to apply and nothing to crash. + await AnswerVoteKickAsync(db, 234, "PlayerId=234&Response=true&GameSessionId=9304"); + + Assert.Empty(await db.PlayerWarnings.ToListAsync()); + Assert.Empty(await db.Moderation.ToListAsync()); + } + + [Fact] + public async Task AVoteKickThatReachesADeveloperIsParkedUntilTheyAcceptIt() + { + using var db = await WithProfileAsync(6, "moderator"); + await WithDeveloperProfileAsync(db, 235); + + const string vote = "PlayerId=235&Response=true&GameSessionId=9305&Reason=Griefing"; + foreach (var voter in new[] { 301L, 302L, 303L }) + await AnswerVoteKickAsync(db, voter, vote); + + // The threshold was reached against a developer and nothing happened to them yet. + Assert.Empty(await db.Moderation.ToListAsync()); + + await AnswerVoteKickAsync(db, 235, "PlayerId=235&Response=true&GameSessionId=9305"); + + // Accepted: the kick is applied for real, which is the delivery the park skipped. + Assert.Single(await db.Moderation.ToListAsync()); + } + + // ----------------------------------------------------------------------------------- + // The write envelope + // ----------------------------------------------------------------------------------- + + [Fact] + public async Task TheWriteEnvelopeAnswersBothKeyCasingsWithAnEmptyError() + { + using var db = await WithProfileAsync(6, "moderator"); + var api = NewApi(db, 6); + SetBody(api, "WarnedPlayerId=205", "application/x-www-form-urlencoded"); + + var ok = Assert.IsType(await api.CreateWarning()); + using var json = JsonDocument.Parse(Serialize(ok.Value)); + + // This server answers its report write in PascalCase; the reference implementation + // documents lowercase for this one. Newtonsoft ignores what it has no member for, so both + // are served rather than betting on one. + Assert.True(json.RootElement.GetProperty("Success").GetBoolean()); + Assert.True(json.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal("", json.RootElement.GetProperty("Error").GetString()); + Assert.Equal("", json.RootElement.GetProperty("error").GetString()); + } + + // ----------------------------------------------------------------------------------- + // Releasing the hold, and the reason the warn screen is filed under + // ----------------------------------------------------------------------------------- + + /// + /// The frame an acknowledgement pushes is the "nothing is pending against you" state, not a + /// second warning. The client's dorm hold is a coroutine started by the duration on the kick + /// frame, and the state it is handed on acknowledgement is what it renders: a cleared block + /// must therefore raise neither the warn screen nor the ban screen - IsWarning and IsBan both + /// false, Duration 0 - or acknowledging a warning would re-raise a moderation screen instead + /// of closing one. + /// + [Fact] + public void TheAcknowledgedStateRaisesNeitherScreen() + { + var cleared = PlayerUtils.BuildClearedBlockDetails(); + + Assert.False(cleared.IsWarning); + Assert.False(cleared.IsBan); + Assert.False(cleared.IsHostKick); + Assert.Equal(0, cleared.Duration); + Assert.Equal(string.Empty, cleared.Message); + Assert.Equal(ReportCategory.Unknown, cleared.ReportCategory); + } + + /// + /// The sixth Code of Conduct reason has to be 200, because that is the number the 2023 + /// client's category enum carries for it (KOFEKCMPOEK.DHGJGCOMKOM.InappropriateClothing = 200) + /// and the warn screen picks the reason it renders from that number. A different value would + /// file the warning correctly on the server and show the player the wrong reason - and the + /// admin panel's warn route validates on the same enum, so it has to be defined on both. + /// + [Fact] + public void TheCustomClothingReasonIsTheNumberOfTheClientEnum() + { + Assert.Equal(200, (int)ReportCategory.InappropriateClothing); + Assert.Equal(200, (int)KickReason.InappropriateClothing); + + // What the panel's category validation (Enum.IsDefined) and the warn write both read. + Assert.True(Enum.IsDefined(typeof(ReportCategory), 200)); + Assert.True(Enum.IsDefined(typeof(KickReason), 200)); + + // The five original reasons keep the numbers they already had: the panel and the client + // both address them by number. + Assert.Equal(100, (int)ReportCategory.CoC_Underage); + Assert.Equal(101, (int)ReportCategory.CoC_Sexual); + Assert.Equal(102, (int)ReportCategory.CoC_Discrimination); + Assert.Equal(103, (int)ReportCategory.CoC_Trolling); + Assert.Equal(104, (int)ReportCategory.CoC_NameOrProfile); + } +} diff --git a/ServerTests/ProfileSettingsTests.cs b/ServerTests/ProfileSettingsTests.cs new file mode 100644 index 0000000..c622b6f --- /dev/null +++ b/ServerTests/ProfileSettingsTests.cs @@ -0,0 +1,687 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Accounts; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Auth; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The four requests the 2023 client makes about its own profile that Radicade had no +/// route for, so each failed on the wire in a different way: +/// +/// PUT accounts/account/me/bannerimage -> 404 +/// PUT accounts/accountprivacysettings/recenthistoryvisibility -> 405 +/// GET platformnotifications/accounts/{id}/receives/GameplayInvites -> 404 +/// GET econ/api/avatar/v2/{accountId} -> 404 +/// +/// The route assertions below exist because a controller-level test cannot see a missing +/// or mis-shaped route - that is exactly the failure being fixed. +/// +public class ProfileSettingsTests +{ + private const long AccountId = 6; + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new RecDBContext(options); + } + + private static AccountsAPI CreateAccountsController(RecDBContext db) => new(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = CreateUser() } + } + }; + + private static ClaimsPrincipal CreateUser() + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, AccountId.ToString())); + return new ClaimsPrincipal(identity); + } + + private static void SetBody(Controller controller, string? body, string contentType, string query = "") + { + var context = controller.ControllerContext.HttpContext; + context.Request.ContentType = contentType; + context.Request.QueryString = new QueryString(query); + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body ?? string.Empty)); + } + + private static async Task SeedProfileAsync(RecDBContext db, bool recentHistoryVisible = true, string? bannerImage = null) + { + var profile = new Profile + { + accountId = AccountId, + username = "bob", + displayName = "Bob", + profileImage = "img.png", + isRecentHistoryVisible = recentHistoryVisible, + bannerImage = bannerImage + }; + db.Profiles.Add(profile); + await db.SaveChangesAsync(); + return profile; + } + + private static JsonDocument Serialize(object? value) => + JsonDocument.Parse(JsonSerializer.Serialize(value)); + + [Theory] + [InlineData(true, "{\"pronounFlags\":3}", "application/json", "")] + [InlineData(true, "pronounFlags=3", "application/x-www-form-urlencoded", "")] + [InlineData(true, "", "application/json", "?pronounFlags=3")] + [InlineData(true, "{\"pronouns\":3}", "application/json", "")] + [InlineData(true, "{\"personalPronouns\":3}", "application/json", "")] + [InlineData(true, "pronouns=3", "application/x-www-form-urlencoded", "")] + [InlineData(true, "value=3", "application/x-www-form-urlencoded", "")] + [InlineData(true, "", "application/json", "?pronouns=3")] + [InlineData(false, "{\"identityFlags\":3}", "application/json", "")] + [InlineData(false, "identityFlags=3", "application/x-www-form-urlencoded", "")] + [InlineData(false, "value=3", "application/x-www-form-urlencoded", "")] + [InlineData(false, "", "application/json", "?identityFlags=3")] + public async Task IdentitySettings_SaveAndSurviveAccountReload(bool pronouns, string body, string contentType, string query) + { + using var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, body, contentType, query); + + var result = pronouns ? await api.SetPersonalPronouns() : await api.SetIdentityFlags(); + + Assert.True(result.success); + db.ChangeTracker.Clear(); + var stored = await db.Profiles.AsNoTracking().SingleAsync(); + Assert.Equal(3, pronouns ? stored.personalPronouns : stored.identityFlags); + var account = await CreateAccountsController(db).GetProfile(); + Assert.NotNull(account); + Assert.Equal(3, pronouns ? account.personalPronouns : account.identityFlags); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(4)] + [InlineData(8)] + [InlineData(16)] + [InlineData(32)] + [InlineData(6)] + public async Task PronounFlags_ClientRequestSetsClearsAndCombinesPronouns(int flags) + { + using var db = CreateDb(); + var profile = await SeedProfileAsync(db); + await SeedPlayerAsync(db); + profile.personalPronouns = 1; + profile.identityFlags = 5; + await db.SaveChangesAsync(); + var api = CreateAccountsController(db); + SetBody(api, $"pronounFlags={flags}", "application/x-www-form-urlencoded"); + + Assert.True((await api.SetPersonalPronouns()).success); + + db.ChangeTracker.Clear(); + var account = await CreateAccountsController(db).GetProfile(); + Assert.Equal(flags, account!.personalPronouns); + Assert.Equal(5, account.identityFlags); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task IdentitySettings_CanBeClearedExplicitly(bool pronouns) + { + using var db = CreateDb(); + var profile = await SeedProfileAsync(db); + profile.personalPronouns = 3; + profile.identityFlags = 5; + await db.SaveChangesAsync(); + var api = CreateAccountsController(db); + SetBody(api, pronouns ? "{\"pronouns\":0}" : "{\"identityFlags\":0}", "application/json"); + + var result = pronouns ? await api.SetPersonalPronouns() : await api.SetIdentityFlags(); + + Assert.True(result.success); + var stored = await db.Profiles.AsNoTracking().SingleAsync(); + Assert.Equal(pronouns ? 0 : 3, stored.personalPronouns); + Assert.Equal(pronouns ? 5 : 0, stored.identityFlags); + } + + [Theory] + [InlineData("{}")] + [InlineData("{\"value\":null}")] + [InlineData("{\"value\":\"invalid\"}")] + [InlineData("{\"value\":-1}")] + [InlineData("{\"value\":2147483648}")] + [InlineData("{\"unrelated\":0}")] + public async Task IdentitySettings_InvalidWritesDoNotClearExistingSelections(string body) + { + using var db = CreateDb(); + var profile = await SeedProfileAsync(db); + profile.personalPronouns = 3; + profile.identityFlags = 5; + await db.SaveChangesAsync(); + var api = CreateAccountsController(db); + SetBody(api, body, "application/json"); + Assert.False((await api.SetPersonalPronouns()).success); + SetBody(api, body, "application/json"); + Assert.False((await api.SetIdentityFlags()).success); + var stored = await db.Profiles.AsNoTracking().SingleAsync(); + Assert.Equal(3, stored.personalPronouns); + Assert.Equal(5, stored.identityFlags); + } + + [Fact] + public async Task IdentitySettings_MissingProfileDoesNotReportSuccess() + { + using var db = CreateDb(); + var api = CreateAccountsController(db); + SetBody(api, "{\"value\":3}", "application/json"); + Assert.False((await api.SetPersonalPronouns()).success); + Assert.False((await api.SetIdentityFlags()).success); + } + + [Fact] + public async Task EmojiPicker_ReturnsSelectableStringsThatSaveAndReload() + { + using var db = CreateDb(); + var catalog = new RadicadeServer.API.Compatibility.CompatibilityAPI(db); + var emojis = Assert.IsType(Assert.IsType(catalog.WhitelistedEmojis()).Value); + Assert.Contains("😀", emojis); + Assert.Contains("🏳️‍🌈", emojis); + Assert.Equal(emojis.Length, emojis.Distinct().Count()); + Assert.All(emojis, emoji => Assert.False(string.IsNullOrWhiteSpace(emoji))); + + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, JsonSerializer.Serialize(new { emoji = emojis[0] }), "application/json"); + Assert.Equal(emojis[0], (await api.SetDisplayEmoji())!.displayEmoji); + db.ChangeTracker.Clear(); + Assert.Equal(emojis[0], (await CreateAccountsController(db).GetProfile())!.displayEmoji); + } + + [Fact] + public async Task PrivacySettings_ReportsTheStoredRecentHistoryVisibility() + { + var db = CreateDb(); + await SeedProfileAsync(db, recentHistoryVisible: false); + var api = CreateAccountsController(db); + + var ok = Assert.IsType(await api.GetAccountPrivacySettings(AccountId)); + using var json = Serialize(ok.Value); + + // Both spellings the client's string table carries, because its two-field DTO for + // this object could bind either one. + Assert.False(json.RootElement.GetProperty("isRecentHistoryVisible").GetBoolean()); + Assert.False(json.RootElement.GetProperty("isrecenthistoryvisible").GetBoolean()); + Assert.Equal(AccountId, json.RootElement.GetProperty("accountId").GetInt64()); + } + + [Theory] + [InlineData("{\"isRecentHistoryVisible\":false}", "application/json")] + [InlineData("{\"IsRecentHistoryVisible\":false}", "application/json")] + [InlineData("{\"isrecenthistoryvisible\":\"false\"}", "application/json")] + [InlineData("{\"isrecenthistoryvisible\":0}", "application/json")] + [InlineData("{\"recentHistoryVisibility\":false}", "application/json")] + [InlineData("{\"isVisible\":false}", "application/json")] + [InlineData("{\"whatever\":false}", "application/json")] + [InlineData("isRecentHistoryVisible=false", "application/x-www-form-urlencoded")] + [InlineData("isrecenthistoryvisible=False", "application/x-www-form-urlencoded")] + [InlineData("false", "text/plain")] + [InlineData("0", "text/plain")] + [InlineData("", "application/json", "?isRecentHistoryVisible=false")] + [InlineData("", "application/x-www-form-urlencoded", "?value=false")] + public async Task RecentHistoryVisibility_AcceptsEveryShapeTheClientMightSend( + string body, string contentType, string query = "") + { + var db = CreateDb(); + await SeedProfileAsync(db); + var api = CreateAccountsController(db); + SetBody(api, body, contentType, query); + + var result = await api.SetRecentHistoryVisibility(); + + Assert.IsType(result); + Assert.False((await db.Profiles.SingleAsync()).isRecentHistoryVisible); + } + + [Fact] + public async Task RecentHistoryVisibility_EnablesAsWellAsDisables() + { + var db = CreateDb(); + await SeedProfileAsync(db, recentHistoryVisible: false); + var api = CreateAccountsController(db); + SetBody(api, "{\"isRecentHistoryVisible\":true}", "application/json"); + + Assert.IsType(await api.SetRecentHistoryVisibility()); + Assert.True((await db.Profiles.SingleAsync()).isRecentHistoryVisible); + } + + [Fact] + public async Task RecentHistoryVisibility_RejectsAnUnreadableBodyInsteadOfStoringFalse() + { + var db = CreateDb(); + await SeedProfileAsync(db); + var api = CreateAccountsController(db); + SetBody(api, "{\"unrelated\":\"text\"}", "application/json"); + + Assert.IsType(await api.SetRecentHistoryVisibility()); + + // A guess (false) here would have looked like a working toggle that resets itself. + Assert.True((await db.Profiles.SingleAsync()).isRecentHistoryVisible); + } + + [Fact] + public async Task BannerImage_StoresTheUploadedKey() + { + var db = CreateDb(); + await SeedProfileAsync(db); + var api = CreateAccountsController(db); + const string key = "sharecamera/2026-09-19/9f2c.jpg"; + + var result = await api.SetBannerImage(key); + + Assert.IsType(result); + Assert.Equal(key, (await db.Profiles.AsNoTracking().SingleAsync()).bannerImage); + } + + [Fact] + public async Task BannerImage_RejectsAnEmptyKeyWithoutWipingTheStoredBanner() + { + var db = CreateDb(); + await SeedProfileAsync(db, bannerImage: "sharecamera/2026-01-01/old.jpg"); + var api = CreateAccountsController(db); + + Assert.IsType(await api.SetBannerImage(string.Empty)); + + Assert.Equal("sharecamera/2026-01-01/old.jpg", (await db.Profiles.AsNoTracking().SingleAsync()).bannerImage); + } + + [Fact] + public async Task AvatarV2_ReturnsAnotherPlayersRenderSubset() + { + var db = CreateDb(); + db.Avatars.Add(new PlayerAvatar + { + AccountId = AccountId, + Avatar = new RadicadeServer.RecNet.Avatar.Avatar + { + OutfitSelections = "selections", + FaceFeatures = "{\"ver\":3}", + SkinColor = "skin", + HairColor = "hair" + }.ToJson(), + Outfits = "[]" + }); + await db.SaveChangesAsync(); + + var avatar = await new AvatarV2(db).GetAvatarForAccount(AccountId); + + Assert.Equal("selections", avatar.OutfitSelections); + Assert.Equal("skin", avatar.SkinColor); + Assert.Equal("hair", avatar.HairColor); + } + + [Fact] + public async Task AvatarV2_FallsBackToTheDefaultOutfitForAPlayerWithoutOne() + { + var avatar = await new AvatarV2(CreateDb()).GetAvatarForAccount(AccountId); + + // A null body is what left other players undrawable, so the fallback has to be a + // real avatar rather than an empty object. + var expected = DBConfig.DefaultAvatar.FromJson(); + Assert.NotNull(expected); + Assert.Equal(expected!.SkinColor, avatar.SkinColor); + Assert.Equal(expected.HairColor, avatar.HairColor); + } + + [Fact] + public void ReceivesGameplayInvites_AnswersBareTrue() + { + var result = Assert.IsType( + new PlatformNotificationsAPI().Receives(AccountId, "GameplayInvites")); + + // The client's parser reads a bare boolean; an envelope object fails it. + Assert.Equal(true, result.Value); + } + + private static async Task SeedPlayerAsync( + RecDBContext db, DateTime? birthday = null) + { + var player = new RadicadeServer.RecNet.Auth.PlayerData + { + AccountId = AccountId, + Hashword = "hashed", + Birthday = birthday + }; + db.Players.Add(player); + await db.SaveChangesAsync(); + return player; + } + + /// + /// The account write the new-user flow makes. Its age page hands the answer to + /// accounts/account/me as birthdayUTC - the name this build's own account object + /// carries - and until this route existed that write met the GET-only mapping, so the birthday + /// was dropped and the player was asked for it again on the next boot. + /// + [Fact] + public async Task AccountMeWrite_StoresTheBirthdayTheAgePagePosts() + { + var db = CreateDb(); + var profile = await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, "{\"birthdayUTC\":\"2018-05-04T00:00:00Z\"}", "application/json"); + + var account = await api.UpdateAccount(); + + Assert.NotNull(account); + Assert.Equal(new DateTime(2018, 5, 4), (await db.Players.AsNoTracking().SingleAsync()).Birthday); + + // Eight years old is the junior split, and the profile the session reads carries it. + Assert.True((await db.Players.AsNoTracking().SingleAsync()).IsJunior); + Assert.True((await db.Profiles.AsNoTracking().SingleAsync()).isJunior); + Assert.True(profile.isJunior); + + // Answering with the account is what lets the flow read back what it just set. + Assert.Equal(new DateTime(2018, 5, 4), account!.birthdayUTC!.Value.Date); + Assert.True(account.hasBirthday); + Assert.True(account.isJunior); + } + + [Fact] + public async Task AccountMeWrite_ReadsTheBirthdayOutOfAWholeAccountObject() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody( + api, + "{\"accountId\":6,\"username\":\"bob\",\"displayName\":\"Bob\",\"bio\":\"hi\"," + + "\"email\":\"bob@example.com\",\"identityFlags\":3,\"birthdayUTC\":\"1999-05-04T00:00:00Z\"}", + "application/json"); + + Assert.NotNull(await api.UpdateAccount()); + + Assert.Equal(new DateTime(1999, 5, 4), (await db.Players.AsNoTracking().SingleAsync()).Birthday); + Assert.False((await db.Players.AsNoTracking().SingleAsync()).IsJunior); + } + + [Fact] + public async Task AccountMeWrite_AcceptsTheBirthdaySpeltWithoutUtc() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, "{\"birthday\":\"1999-05-04T00:00:00Z\"}", "application/json"); + + Assert.NotNull(await api.UpdateAccount()); + + Assert.Equal(new DateTime(1999, 5, 4), (await db.Players.AsNoTracking().SingleAsync()).Birthday); + } + + [Fact] + public async Task AccountMeWrite_LeavesAnAccountUntouchedWhenTheBodyCarriesNoBirthday() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db, birthday: new DateTime(1999, 5, 4)); + var api = CreateAccountsController(db); + SetBody(api, "{\"bio\":\"a bio the flow never sends\"}", "application/json"); + + // The body is the whole account, so a write that carries no age still has to answer with + // the account rather than fail: the client posts this object for more than the birthday. + var account = await api.UpdateAccount(); + + Assert.NotNull(account); + Assert.Equal(new DateTime(1999, 5, 4), (await db.Players.AsNoTracking().SingleAsync()).Birthday); + Assert.True(account!.hasBirthday); + } + + [Fact] + public async Task AccountMeWrite_DoesNotStoreAnAgeTheDatePickerCouldNotHaveGiven() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, "{\"birthdayUTC\":\"1800-01-01T00:00:00Z\"}", "application/json"); + + Assert.NotNull(await api.UpdateAccount()); + + // Storing it would make the account 226 years old, and junior state would never come back. + Assert.Null((await db.Players.AsNoTracking().SingleAsync()).Birthday); + } + + /// + /// The flow's name page writes account/me/displayname, while the title screen greets a new player + /// with @username - so a write that only moved the display name left the player looking at the + /// generated name the create had to fall back to, and the account looked auto-made. While the + /// flow is unfinished, the name it collects is the account's name. + /// + [Fact] + public async Task DisplayNameWrite_NamesTheAccountWhileTheFlowIsStillOpen() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = AccountId, + Settings = "[{\"Key\":\"Recroom.AccountCreation.HasStarted\",\"Value\":\"True\"}," + + "{\"Key\":\"Recroom.AccountCreation.HasFinished\",\"Value\":\"False\"}]" + }); + await db.SaveChangesAsync(); + var api = CreateAccountsController(db); + + await api.SetDisplayName("ChosenByThePlayer"); + + var profile = await db.Profiles.AsNoTracking().SingleAsync(); + Assert.Equal("ChosenByThePlayer", profile.username); + Assert.Equal("ChosenByThePlayer", profile.displayName); + } + + [Fact] + public async Task DisplayNameWrite_LeavesTheUsernameAloneOnceTheFlowIsOver() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = AccountId, + Settings = "[{\"Key\":\"Recroom.AccountCreation.HasStarted\",\"Value\":\"True\"}," + + "{\"Key\":\"Recroom.AccountCreation.HasFinished\",\"Value\":\"True\"}]" + }); + await db.SaveChangesAsync(); + var api = CreateAccountsController(db); + + await api.SetDisplayName("ARenamedDisplayName"); + + var profile = await db.Profiles.AsNoTracking().SingleAsync(); + Assert.Equal("bob", profile.username); + Assert.Equal("ARenamedDisplayName", profile.displayName); + } + + /// + /// The flow's name page writes the account object rather than a field of its own, and the create + /// has already minted the account by the time that page is reached. So the chosen name lands + /// here or not at all - without this the player kept the generated name the create fell back to + /// while the flow's own settings said they had chosen one. + /// + [Fact] + public async Task AccountMeWrite_KeepsTheNameTheUsernamePageChose() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, "{\"username\":\"BobbyTwoShoes\",\"birthdayUTC\":\"1999-05-04T00:00:00Z\"}", "application/json"); + + Assert.NotNull(await api.UpdateAccount()); + + var profile = await db.Profiles.AsNoTracking().SingleAsync(); + Assert.Equal("BobbyTwoShoes", profile.username); + Assert.Equal("BobbyTwoShoes", profile.displayName); + } + + [Fact] + public async Task AccountMeWrite_RefusesANameAnotherAccountAlreadyHas() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + db.Profiles.Add(new Profile + { + accountId = 7, + username = "BobbyTwoShoes", + displayName = "BobbyTwoShoes", + profileImage = "default" + }); + await db.SaveChangesAsync(); + var api = CreateAccountsController(db); + SetBody(api, "{\"username\":\"BobbyTwoShoes\"}", "application/json"); + + // The write still answers with the account: the client posts this object for more than the + // name, and a refusal of one field must not look like a failed write of all of them. + Assert.NotNull(await api.UpdateAccount()); + + Assert.Equal("bob", (await db.Profiles.AsNoTracking().SingleAsync(x => x.accountId == AccountId)).username); + } + + /// + /// The age page in the flow is three steppers (Month, Day, Year), not a date string. A body + /// carrying them under names of their own used to parse as nothing at all and leave the account + /// unaged with no error the player could see. + /// + [Fact] + public async Task AccountMeWrite_ReadsTheBirthdayOutOfTheThreeDatePickerSteppers() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, "{\"month\":8,\"day\":30,\"year\":2011}", "application/json"); + + Assert.NotNull(await api.UpdateAccount()); + + Assert.Equal(new DateTime(2011, 8, 30), (await db.Players.AsNoTracking().SingleAsync()).Birthday); + + // Read as a real date, not as three numbers that happened to be in the body. + Assert.False((await db.Players.AsNoTracking().SingleAsync()).IsJunior); + } + + [Fact] + public async Task AccountMeWrite_ReadsTheThreeSteppersOutOfAWholeAccountObjectToo() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, "{\"account\":{\"month\":5,\"day\":4,\"year\":1999}}", "application/json"); + + Assert.NotNull(await api.UpdateAccount()); + + Assert.Equal(new DateTime(1999, 5, 4), (await db.Players.AsNoTracking().SingleAsync()).Birthday); + } + + [Fact] + public async Task AccountMeWrite_ReadsTheBirthdayOutOfAFormBodyToo() + { + var db = CreateDb(); + await SeedProfileAsync(db); + await SeedPlayerAsync(db); + var api = CreateAccountsController(db); + SetBody(api, "birthdayUTC=1999-05-04T00:00:00Z", "application/x-www-form-urlencoded"); + + Assert.NotNull(await api.UpdateAccount()); + + Assert.Equal(new DateTime(1999, 5, 4), (await db.Players.AsNoTracking().SingleAsync()).Birthday); + } + + [Fact] + public void SelfAccount_ReportsTheBirthdayUnderTheNameTheClientReads() + { + var profile = new Profile { accountId = AccountId, username = "bob", displayName = "Bob" }; + var player = new RadicadeServer.RecNet.Auth.PlayerData { AccountId = AccountId, Hashword = "x" }; + + var withoutBirthday = RadicadeServer.Utils.PlayerUtils.BuildSelfAccount(profile, player); + using var beforeJson = Serialize(withoutBirthday); + + // No age is reported as no age. This member is what the title screen reads to decide the + // age page has been answered, so the 0001-01-01 placeholder this used to carry - against a + // hasBirthday that said the opposite - is why the page was skipped for every account on + // this server. The key itself stays, because these members bind by position. + Assert.Null(withoutBirthday.birthdayUTC); + Assert.False(withoutBirthday.hasBirthday); + Assert.Equal(JsonValueKind.Null, beforeJson.RootElement.GetProperty("birthdayUTC").ValueKind); + + // The one exception is a stored placeholder: the column some half-created accounts carry + // is still not an age, and it must not be reported as one either. + player.Birthday = DateTime.MinValue; + Assert.Null(RadicadeServer.Utils.PlayerUtils.BuildSelfAccount(profile, player).birthdayUTC); + + player.Birthday = new DateTime(1999, 5, 4); + var withBirthday = RadicadeServer.Utils.PlayerUtils.BuildSelfAccount(profile, player); + using var afterJson = Serialize(withBirthday); + + Assert.Equal(new DateTime(1999, 5, 4), withBirthday.birthdayUTC!.Value.Date); + Assert.True(withBirthday.hasBirthday); + Assert.Equal(1999, afterJson.RootElement.GetProperty("birthdayUTC").GetDateTime().Year); + } + + [Fact] + public void NewProfileRoutesAreMappedWhereTheClientRequestsThem() + { + AssertRoute(nameof(AccountsAPI.SetBannerImage), "account/me/bannerimage", "HttpPutAttribute"); + AssertRoute(nameof(AccountsAPI.UpdateAccount), "account/me", "HttpPostAttribute"); + AssertRoute(nameof(AccountsAPI.UpdateAccount), "account/me", "HttpPutAttribute"); + AssertRoute(nameof(AccountsAPI.SetRecentHistoryVisibility), "accountprivacysettings/recenthistoryvisibility", "HttpPutAttribute"); + AssertRoute(nameof(AccountsAPI.GetAccountPrivacySettings), "accountprivacysettings/{accountId:long}", "HttpGetAttribute"); + AssertRoute(nameof(PlatformNotificationsAPI.Receives), "accounts/{accountId:long}/receives/{notificationType}", "HttpGetAttribute"); + } + + [Fact] + public void AvatarV2IsAlsoMappedByAccountId() + { + var route = typeof(AvatarV2).GetCustomAttributes() + .Select(attribute => attribute.Template) + .ToList(); + + Assert.Contains("api/avatar/v2", route); + Assert.Contains("econ/api/avatar/v2", route); + + var method = typeof(AvatarV2).GetMethod(nameof(AvatarV2.GetAvatarForAccount))!; + Assert.Equal("{accountId:long}", method.GetCustomAttribute()!.Template); + } + + private static void AssertRoute(string action, string template, string verbAttributeName) + { + var method = typeof(TController).GetMethod(action, BindingFlags.Public | BindingFlags.Instance)!; + var attribute = method.GetCustomAttributes(inherit: false) + .FirstOrDefault(candidate => candidate.GetType().Name == verbAttributeName); + + Assert.NotNull(attribute); + Assert.Equal(template, attribute!.GetType().GetProperty("Template")!.GetValue(attribute) as string); + } +} diff --git a/ServerTests/ProgressionAndReportingRouteTests.cs b/ServerTests/ProgressionAndReportingRouteTests.cs new file mode 100644 index 0000000..97c1f09 --- /dev/null +++ b/ServerTests/ProgressionAndReportingRouteTests.cs @@ -0,0 +1,435 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using RadicadeServer.API.Moderation; +using RadicadeServer.API.Relationships.V1; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Moderation; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Relationships; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Two routes the client calls that never reached a handler (a 404 and a 400), plus the +/// time-in-room XP rules: public rooms only, 10 XP every 5 minutes, 60 per room, 150 per day. +/// +[Collection(DbFactoryCollection.Name)] +public class ProgressionAndReportingRouteTests +{ + private const long CallerId = 42; + private const long OtherId = 77; + + private static RecDBContext CreateDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static T CreateController(RecDBContext db, long? userId = CallerId) where T : Controller + { + var identity = new ClaimsIdentity("TestAuth"); + if (userId.HasValue) + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.Value.ToString())); + + var controller = typeof(T) == typeof(PlayerReportingAPI) + ? (T)(object)new PlayerReportingAPI(db) + : (T)Activator.CreateInstance(typeof(T), db)!; + + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + return controller; + } + + private static void SetJsonBody(Controller controller, string jsonBody) + { + var bytes = Encoding.UTF8.GetBytes(jsonBody); + controller.HttpContext.Request.Body = new MemoryStream(bytes); + controller.HttpContext.Request.ContentType = "application/json"; + controller.HttpContext.Request.ContentLength = bytes.Length; + } + + // ---- POST api/relationships/sendfriendintroductions -> 404 ------------------------------ + + [Fact] + public void SendFriendIntroductionsIsRegisteredOnTheUnversionedPath() + { + var attributes = typeof(RelationshipsV1).GetMethod(nameof(RelationshipsV1.SendFriendIntroductions))! + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), true) + .Cast() + .ToList(); + + Assert.Contains(attributes, a => a.HttpMethods.Contains("POST")); + // The client asks for `api/relationships/sendfriendintroductions`, with no version + // segment - that is the spelling that 404ed. + Assert.Contains(attributes, a => (a.Template ?? "").EndsWith("api/relationships/sendfriendintroductions")); + } + + [Fact] + public async Task SendFriendIntroductionsAnswersTheSharedSuccessShape() + { + using var db = CreateDb(); + var controller = CreateController(db); + SetJsonBody(controller, JsonSerializer.Serialize(new { toPlayerIds = new[] { OtherId }, aboutPlayerId = CallerId })); + + var result = Assert.IsType(await controller.SendFriendIntroductions()); + + // The client's wrapper returns ONE object of the same success type the PlayerReporting + // routes answer with; an array of relationships is a Malformed Response to it (which is + // exactly what the caller logged before this route was corrected). + var ok = Assert.IsType(result.Value); + Assert.True(ok.Success); + } + + [Fact] + public async Task SendFriendIntroductionsReadsTheIdsFromTheQueryToo() + { + using var db = CreateDb(); + var controller = CreateController(db); + // The ids may arrive in the query rather than the body; the route must still answer its + // success object instead of throwing on an empty body. + controller.HttpContext.Request.QueryString = new QueryString($"?toPlayerIds={OtherId}&aboutPlayerId={CallerId}"); + + var result = Assert.IsType(await controller.SendFriendIntroductions()); + Assert.True(Assert.IsType(result.Value).Success); + } + + [Fact] + public async Task SendFriendIntroductionsToleratesJunkIds() + { + using var db = CreateDb(); + var controller = CreateController(db); + // A caller, a non-numeric entry and a real target must not throw: the route answers its + // success object whatever the body contains. + SetJsonBody(controller, + JsonSerializer.Serialize(new { toPlayerIds = new object[] { CallerId, "not-a-number", OtherId } })); + + var result = Assert.IsType(await controller.SendFriendIntroductions()); + Assert.True(Assert.IsType(result.Value).Success); + } + + // ---- POST api/PlayerReporting/v1/instantKick -> 400 ------------------------------------ + + private static async Task WithTargetAsync(long targetId) + { + var db = CreateDb(); + db.Profiles.Add(new Profile + { + accountId = targetId, + username = $"player{targetId}", + displayName = $"Player {targetId}", + profileImage = DBConfig.DefaultProfileImage + }); + await db.SaveChangesAsync(); + return db; + } + + /// Unwraps the OkResponse action result and asserts it is a 200. + private static async Task AssertOkAsync(Task> call) + { + var result = await call; + Assert.IsType(result.Result); + } + + private static async Task AssertKickedAsync(RecDBContext db, long targetId) + { + var moderation = await db.Moderation.SingleAsync(x => x.PlayerId == targetId); + Assert.True(moderation.IsBan); + } + + [Fact] + public async Task InstantKickAcceptsTheTargetFromTheBody() + { + using var db = await WithTargetAsync(OtherId); + var controller = CreateController(db); + SetJsonBody(controller, + JsonSerializer.Serialize(new { PlayerId = OtherId, GameSessionId = 7, Reason = "Cheating" })); + + await AssertOkAsync(controller.InstantKick()); + await AssertKickedAsync(db, OtherId); + } + + [Fact] + public async Task InstantKickAcceptsTheTargetFromTheQueryString() + { + using var db = await WithTargetAsync(OtherId); + var controller = CreateController(db); + controller.HttpContext.Request.QueryString = new QueryString($"?playerId={OtherId}"); + + // No body at all: the id is only in the query. Binding used to look at the body alone, + // which made this a 400 ("Missing player id") for a request that named its target. + await AssertOkAsync(controller.InstantKick()); + await AssertKickedAsync(db, OtherId); + } + + [Fact] + public async Task InstantKickAcceptsABareIdBody() + { + using var db = await WithTargetAsync(OtherId); + var controller = CreateController(db); + SetJsonBody(controller, OtherId.ToString()); + + await AssertOkAsync(controller.InstantKick()); + await AssertKickedAsync(db, OtherId); + } + + [Fact] + public async Task InstantKickFindsAPlayerNamedFieldUnderAnyName() + { + using var db = await WithTargetAsync(OtherId); + var controller = CreateController(db); + // The client's own DTO has changed its field name between builds; anything that names a + // player still resolves. + SetJsonBody(controller, JsonSerializer.Serialize(new { request = new { kickedPlayerId = OtherId } })); + + await AssertOkAsync(controller.InstantKick()); + await AssertKickedAsync(db, OtherId); + } + + [Fact] + public async Task InstantKickStillRefusesARequestThatNamesNobody() + { + using var db = await WithTargetAsync(OtherId); + var controller = CreateController(db); + SetJsonBody(controller, """{"Reason":"no target here"}"""); + + var result = await controller.InstantKick(); + Assert.IsType(result.Result); + Assert.Empty(await db.Moderation.ToListAsync()); + } + + // ---- room XP rules ---------------------------------------------------------------------- + + private const long PublicRoomId = 101; + private const long OtherPublicRoomId = 102; + + private static async Task SeedProgressAsync(RecDBContext db, long playerId) + { + var progression = new ProgressionData { PlayerId = playerId }; + db.Progression.Add(progression); + await db.SaveChangesAsync(); + return progression; + } + + /// Moves the room's last payout back in time so the next call sees a full interval. + private static async Task RewindRoomClockAsync(RecDBContext db, long playerId, TimeSpan by) + { + var progression = await db.Progression.SingleAsync(x => x.PlayerId == playerId); + var json = progression.GetProgressionJson()!; + json.RoomXpLastGrantAt = (json.RoomXpLastGrantAt ?? DateTime.UtcNow).Subtract(by); + progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + } + + private static async Task AwardAsync(RecDBContext db, long playerId, long roomId, bool isPublic = true) + => await RoomProgressionService.AwardRoomXpAsync(db, playerId, roomId, isPublic); + + [Fact] + public async Task EnteringARoomPaysNothingUntilAnIntervalHasPassed() + { + using var db = CreateDb(); + var progression = await SeedProgressAsync(db, CallerId); + + // Arrival anchors the session and starts the clock. + Assert.Equal(0, await AwardAsync(db, CallerId, PublicRoomId)); + Assert.Equal(0, progression.XP); + + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + Assert.Equal(10, await AwardAsync(db, CallerId, PublicRoomId)); + + var json = progression.GetProgressionJson()!; + Assert.Equal(10, json.RoomXpEarned); + Assert.Equal(10, json.XpEarnedToday); + Assert.Equal(10, progression.XP); + } + + [Fact] + public async Task APrivateRoomNeverPays() + { + using var db = CreateDb(); + var progression = await SeedProgressAsync(db, CallerId); + + Assert.Equal(0, await AwardAsync(db, CallerId, PublicRoomId, isPublic: false)); + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + Assert.Equal(0, await AwardAsync(db, CallerId, PublicRoomId, isPublic: false)); + + Assert.Equal(0, progression.XP); + } + + [Fact] + public async Task OneRoomStopsPayingOnceItHasGivenSixtyXp() + { + using var db = CreateDb(); + var progression = await SeedProgressAsync(db, CallerId); + + Assert.Equal(0, await AwardAsync(db, CallerId, PublicRoomId)); + for (var i = 0; i < 6; i++) + { + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + Assert.Equal(10, await AwardAsync(db, CallerId, PublicRoomId)); + } + + // 60 XP is the room's ceiling: further intervals in the same room pay nothing... + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(10)); + Assert.Equal(0, await AwardAsync(db, CallerId, PublicRoomId)); + Assert.Equal(60, progression.XP); + + // ...but moving to another room starts a fresh session. + Assert.Equal(0, await AwardAsync(db, CallerId, OtherPublicRoomId)); + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + Assert.Equal(10, await AwardAsync(db, CallerId, OtherPublicRoomId)); + Assert.Equal(70, progression.XP); + + var json = progression.GetProgressionJson()!; + Assert.Equal(OtherPublicRoomId, json.RoomXpRoomId); + Assert.Equal(10, json.RoomXpEarned); + } + + [Fact] + public async Task TheDailyCeilingStopsPayoutForTwentyFourHours() + { + using var db = CreateDb(); + var progression = await SeedProgressAsync(db, CallerId); + + // Start the player two full rooms away from the daily ceiling: 60 + 60 = 120, then the + // third room can only pay the remaining 30 - three instalments of 10. + Assert.Equal(0, await AwardAsync(db, CallerId, PublicRoomId)); + for (var i = 0; i < 6; i++) + { + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + await AwardAsync(db, CallerId, PublicRoomId); + } + + Assert.Equal(0, await AwardAsync(db, CallerId, OtherPublicRoomId)); + for (var i = 0; i < 6; i++) + { + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + await AwardAsync(db, CallerId, OtherPublicRoomId); + } + Assert.Equal(120, progression.XP); + + // A third room can pay only 30 more before the daily ceiling: 120 + 30 = 150. + const long thirdRoom = 103; + Assert.Equal(0, await AwardAsync(db, CallerId, thirdRoom)); + for (var i = 0; i < 3; i++) + { + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + Assert.Equal(10, await AwardAsync(db, CallerId, thirdRoom)); + } + + // The ceiling lands on exactly 150, which is what starts the 24 hour wait. + Assert.Equal(150, progression.XP); + + // The next interval would take the player past 150, so the ceiling holds at 150. + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + Assert.Equal(0, await AwardAsync(db, CallerId, thirdRoom)); + Assert.Equal(150, progression.XP); + + var json = progression.GetProgressionJson()!; + Assert.Equal(150, json.XpEarnedToday); + Assert.NotNull(json.XpDailyCapReachedAt); + } + + [Fact] + public async Task ReachingTheDailyCeilingLocksPayoutForTheNextTwentyFourHours() + { + using var db = CreateDb(); + var progression = await SeedProgressAsync(db, CallerId); + + // Put the player one instalment from the ceiling, mid-session. Only 5 XP is left under + // it, so that instalment pays the remainder rather than a whole 10. + var json = progression.GetProgressionJson()!; + json.XpEarnedToday = 145; + json.XpEarnedTodayDate = DateTime.UtcNow.Date; + json.RoomXpRoomId = PublicRoomId; + json.RoomXpEarned = 20; + json.RoomXpLastGrantAt = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(5)); + progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + + Assert.Equal(5, await AwardAsync(db, CallerId, PublicRoomId)); + json = progression.GetProgressionJson()!; + Assert.Equal(150, json.XpEarnedToday); + Assert.NotNull(json.XpDailyCapReachedAt); + + // Within the 24 hour wait the room cannot pay, however long the player stays. + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(30)); + Assert.Equal(0, await AwardAsync(db, CallerId, PublicRoomId)); + Assert.Equal(5, progression.XP); + + // Moving to another room does not help - the ceiling is per player, not per room. + Assert.Equal(0, await AwardAsync(db, CallerId, OtherPublicRoomId)); + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + Assert.Equal(0, await AwardAsync(db, CallerId, OtherPublicRoomId)); + + // Once the 24 hours are up, earnings resume. + json = progression.GetProgressionJson()!; + json.XpEarnedTodayDate = DateTime.UtcNow.Date; + json.XpDailyCapReachedAt = DateTime.UtcNow.Subtract(TimeSpan.FromHours(25)); + json.RoomXpRoomId = OtherPublicRoomId; + json.RoomXpEarned = 0; + json.RoomXpLastGrantAt = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(5)); + progression.Progress = json.ToJson(); + await db.SaveChangesAsync(); + + Assert.Equal(10, await AwardAsync(db, CallerId, OtherPublicRoomId)); + Assert.Equal(15, progression.XP); + } + + [Fact] + public async Task LevellingUpRaisesTheStoredLevelWithoutGiftRows() + { + using var db = CreateDb(); + var progression = await SeedProgressAsync(db, CallerId); + + // Six intervals of 10 fill the room to its 60 XP ceiling, which the shipped map puts at + // level 4 (thresholds 10/20/30/50/70). There are no GiftItems rows in this database, and + // the level must still move - a missing reward row cannot freeze progression. + Assert.Equal(0, await AwardAsync(db, CallerId, PublicRoomId)); + for (var i = 0; i < 6; i++) + { + await RewindRoomClockAsync(db, CallerId, TimeSpan.FromMinutes(5)); + Assert.Equal(10, await AwardAsync(db, CallerId, PublicRoomId)); + } + + Assert.Equal(60, progression.XP); + Assert.Equal(4, progression.Level); + } + + [Fact] + public async Task TheLeveledUpPlayerIsPushedToTheClientWithoutRelogging() + { + using var db = CreateDb(); + await SeedProgressAsync(db, CallerId); + db.Players.Add(new PlayerData { AccountId = CallerId, Hashword = "x" }); + db.Profiles.Add(new Profile + { + accountId = CallerId, + username = "tester", + displayName = "Tester", + profileImage = DBConfig.DefaultProfileImage + }); + await db.SaveChangesAsync(); + + // The push is best effort and must not throw when there is no connected client - what it + // must not do is leave the level change to be noticed at the next login. + await RoomProgressionService.PushProgressionUpdateAsync(db, CallerId); + } + + [Fact] + public async Task RoomXpIsRejectedForAnUnknownPlayerAndRoom() + { + using var db = CreateDb(); + Assert.Equal(0, await RoomProgressionService.AwardRoomXpAsync(db, 0, PublicRoomId, true)); + Assert.Equal(0, await RoomProgressionService.AwardRoomXpAsync(db, CallerId, 0, true)); + } +} diff --git a/ServerTests/ProgressionLevelUpdateTests.cs b/ServerTests/ProgressionLevelUpdateTests.cs new file mode 100644 index 0000000..cc7811c --- /dev/null +++ b/ServerTests/ProgressionLevelUpdateTests.cs @@ -0,0 +1,421 @@ +using System.Reflection; +using System.Text.Json; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Notifications; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Store; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A level the server has just raised has to be visible without a relogin, on every screen that +/// shows that player - not only on the player's own - and the only lever the client offers for that +/// is one notification. +/// +/// The client keeps its notification handlers in a string-keyed table and delivers a frame only +/// when its Id EQUALS a registered key. A dump of that table (the patch's dispatcher probe, run at +/// login) shows exactly one progression entry, under the literal name +/// "PlayerProgressionLevelUpdate" - there is no numeric id for it. The server used to announce a +/// level change with an account update instead, which is why the new level and the new XP both +/// only appeared after the game was restarted. +/// +public class ProgressionLevelUpdateTests +{ + private const long PlayerId = 3; + private const long RoomId = 7; + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + return new RecDBContext(options); + } + + /// + /// A hub context that records every frame it pushes, per connection. Which connection a frame + /// went to is the whole point of the broadcast test, so a single shared list is not enough. + /// + private sealed class RecordingHub : IDisposable + { + private readonly IHubContext _context; + private readonly List _previousClients; + private readonly IHubContext? _previousContext; + + public RecordingHub() + { + var hubClients = DispatchProxy.Create(); + ((HubClientsProxy)(object)hubClients).CreateClient = connectionId => + { + var recorder = new RecordingSingleClient(connectionId, this); + ByConnection[connectionId] = recorder.Sent; + return recorder; + }; + + _context = DispatchProxy.Create, HubContextProxy>(); + ((HubContextProxy)(object)_context).HubClients = hubClients; + + _previousContext = Notify.Context; + _previousClients = Notify.Clients; + + Notify.Context = _context; + Notify.Clients = new List(); + } + + /// connection id -> the frames pushed down it, in order. + public Dictionary> ByConnection { get; } = new(); + + /// Every frame pushed anywhere, in order. + public List Sent { get; } = new(); + + public void AddConnection(long accountId, string connectionId, params long[] subscriptions) + { + Notify.Clients.Add(new HubClient + { + AccountId = accountId, + ConnectionId = connectionId, + Subscriptions = subscriptions.ToList(), + HasSubscriptions = subscriptions.Length > 0, + }); + } + + public List For(string connectionId) => + ByConnection.TryGetValue(connectionId, out var frames) ? frames : new List(); + + public void Dispose() + { + Notify.Context = _previousContext; + Notify.Clients = _previousClients; + } + } + + private static ProgressionData Progression(long playerId, int level, int xp) => new() + { + PlayerId = playerId, + Level = level, + XP = xp, + Progress = new ProgressionJson().ToJson(), + }; + + [Fact] + public async Task ALevelChangeIsPushedUnderTheNameTheClientRegistersItsHandlerFor() + { + using var db = CreateDb(); + db.Progression.Add(Progression(PlayerId, 6, 420)); + await db.SaveChangesAsync(); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + + await RoomProgressionService.SendProgressionLevelUpdateAsync(db, PlayerId); + + var frame = Assert.Single(hub.Sent); + + Assert.Contains("\"Id\":\"PlayerProgressionLevelUpdate\"", frame); + Assert.DoesNotContain("\"Id\":6", frame); + } + + [Fact] + public async Task TheFrameCarriesTheLevelAndTheXpIntoThatLevel() + { + using var db = CreateDb(); + db.Progression.Add(Progression(PlayerId, 6, 420)); + await db.SaveChangesAsync(); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + + await RoomProgressionService.SendProgressionLevelUpdateAsync(db, PlayerId); + + var frame = Assert.Single(hub.Sent); + + using var doc = JsonDocument.Parse(frame); + var msg = doc.RootElement.GetProperty("Msg"); + + // The bulk progression route is the one place the client shows a level from, and it serves + // exactly these names - so the frame repeats them. The XP rides beside the level as + // progress through it, not as the lifetime total the row holds: this row is level 6, which + // begins at 90, so the 420 it has earned reads as 330. Handing over the total is what made + // a finished level's XP carry into the next one. + Assert.Equal(6, msg.GetProperty("Level").GetInt32()); + Assert.Equal(330, msg.GetProperty("XP").GetInt32()); + Assert.Equal(PlayerId, msg.GetProperty("PlayerId").GetInt64()); + + // ...and the lower-cased spellings too, because the client's own type for this frame is not + // visible from here and its parser ignores a member it does not declare. + Assert.Equal(6, msg.GetProperty("level").GetInt32()); + Assert.Equal(330, msg.GetProperty("xp").GetInt32()); + Assert.Equal(PlayerId, msg.GetProperty("playerId").GetInt64()); + } + + [Fact] + public async Task AnotherPlayerWatchingThemGetsTheFrameToo() + { + using var db = CreateDb(); + db.Progression.Add(Progression(PlayerId, 2, 40)); + await db.SaveChangesAsync(); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + // A second, different account that is watching this player - a friend, or anyone in the same + // room. Each of them holds this player's level from what it fetched, and only a frame moves + // it, so without this they keep showing the old level until they relaunch. + hub.AddConnection(9001, "watcher", PlayerId); + + await RoomProgressionService.SendProgressionLevelUpdateAsync(db, PlayerId); + + Assert.Contains(hub.For("owner"), frame => frame.Contains("\"Id\":\"PlayerProgressionLevelUpdate\"", StringComparison.Ordinal)); + Assert.Contains(hub.For("watcher"), frame => frame.Contains("\"Id\":\"PlayerProgressionLevelUpdate\"", StringComparison.Ordinal)); + Assert.Contains(hub.For("watcher"), frame => frame.Contains("\"Level\":2", StringComparison.Ordinal)); + } + + [Fact] + public async Task AConnectionThatIsNotWatchingThemGetsNothing() + { + using var db = CreateDb(); + db.Progression.Add(Progression(PlayerId, 2, 40)); + await db.SaveChangesAsync(); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + hub.AddConnection(9001, "stranger", 42); + + await RoomProgressionService.SendProgressionLevelUpdateAsync(db, PlayerId); + + Assert.Empty(hub.For("stranger")); + } + + [Fact] + public async Task TheSharedPushEmitsTheProgressionFrameEvenWhenNothingElseCanBePushed() + { + // No profile row at all: every other push in the shared path bails out here, and a level the + // panel just set would be invisible again if the progression frame were behind them. + using var db = CreateDb(); + db.Progression.Add(Progression(PlayerId, 9, 640)); + await db.SaveChangesAsync(); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + await RoomProgressionService.PushProgressionUpdateAsync(db, PlayerId); + + Assert.Contains(hub.Sent, frame => frame.Contains("\"Id\":\"PlayerProgressionLevelUpdate\"", StringComparison.Ordinal)); + } + + [Fact] + public async Task ARoomXpGrantAnnouncesTheLevelUpAndZeroesTheClientsXp() + { + using var db = CreateDb(); + + // A session already anchored to this room whose last instalment is two intervals old: the + // grant is due, and with it the announcement. + var json = new ProgressionJson + { + RoomXpRoomId = RoomId, + RoomXpEarned = 0, + RoomXpLastGrantAt = DateTime.UtcNow.AddMinutes(-11), + XpEarnedToday = 0, + XpEarnedTodayDate = DateTime.UtcNow.Date, + }; + + db.Progression.Add(new ProgressionData + { + PlayerId = PlayerId, + Level = 1, + XP = 0, + Progress = json.ToJson(), + }); + await db.SaveChangesAsync(); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + + var granted = await RoomProgressionService.AwardRoomXpAsync(db, PlayerId, RoomId, isPublicRoom: true); + + // Eleven elapsed minutes is two intervals, so 20 XP - which is exactly the map's level 2. + // The grant that levels the player up announces the new level with the XP reset to the + // start of it, rather than the 20 they finished level 1 on. + Assert.Equal(20, granted); + Assert.Contains(hub.Sent, frame => frame.Contains("\"Id\":\"PlayerProgressionLevelUpdate\"", StringComparison.Ordinal)); + Assert.Contains(hub.Sent, frame => frame.Contains("\"Level\":2", StringComparison.Ordinal)); + Assert.Contains(hub.Sent, frame => frame.Contains("\"xp\":0", StringComparison.Ordinal)); + } + + [Fact] + public async Task TheMapsTokenDropPastTheTopLevelIsNeverHandedOut() + { + using var db = CreateDb(); + + // The progression map still names a token gift drop for level 51 (33363), so put that row in + // the database: it is exactly what the old fallback handed out as a "Level Up Reward" box. + db.GiftItems.Add(new GiftDropDB + { + GiftDropId = 33363, + Name = "Level Up Reward", + Description = "Level up reward", + CurrencyAmount = 750, + }); + + // Enough XP to clear level 50's cumulative requirement many times over; the level it lands + // on is the top of the reward table, because the map's steps past 50 are never reached. + db.Progression.Add(new ProgressionData + { + PlayerId = PlayerId, + Level = 1, + XP = 40000, + Progress = new ProgressionJson().ToJson(), + }); + await db.SaveChangesAsync(); + + // The XP has to reach the table's own top level, or this asserts nothing. + Assert.True( + LevelUpGiftService.ComputeLevel(LevelUpGiftService.ParseMap(), 40000) == LevelRewardTable.LastItemLevel, + "the XP in this test must push the level to the top of the reward table"); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + + var delivered = await LevelUpGiftService.DeliverLevelUpGiftsAsync( + db, PlayerId, recomputeFromXp: true, fromLevel: 50, notify: false); + + // There is no level above the table to grant anything for: the XP computes to the table's + // own top level, so the map's token drop for level 51 is unreachable - it is the fallback + // for nothing, and it never becomes a "Level Up Reward" box. + Assert.Empty(delivered); + Assert.False(LevelRewardTable.IsItemLevel(LevelRewardTable.LastItemLevel + 1)); + Assert.False(await db.Gifts.AnyAsync(x => x.AccountId == PlayerId)); + Assert.DoesNotContain(hub.Sent, frame => frame.Contains("Level Up Reward", StringComparison.Ordinal)); + } + + // ------------------------------------------------------------------------------------- + // What each XP source pays + // ------------------------------------------------------------------------------------- + + [Fact] + public void TheThreeXpSourcesPayTheSpecifiedAmounts() + { + // 10 XP every 5 minutes in a public room, 1 for opening a gift box, 2 for finishing a game. + Assert.Equal(10, RoomProgressionService.XpPerInterval); + Assert.Equal(TimeSpan.FromMinutes(5), RoomProgressionService.Interval); + Assert.Equal(1, ProgressionAwardService.XpPerGiftBoxOpened); + Assert.Equal(2, ProgressionAwardService.XpPerGameCompleted); + + // The room's caps are XP amounts and were left where they were. + Assert.Equal(60, RoomProgressionService.RoomXpCap); + Assert.Equal(150, RoomProgressionService.DailyXpCap); + } + + [Fact] + public async Task XpFromGiftBoxesAndGamesAccumulatesAndRaisesTheLevelAtTheMapsThreshold() + { + using var db = CreateDb(); + + // The map is what decides the level, and its first step is 20 total XP for level 2. + var map = LevelUpGiftService.ParseMap(); + Assert.Equal(1, LevelUpGiftService.ComputeLevel(map, 19)); + Assert.Equal(2, LevelUpGiftService.ComputeLevel(map, 20)); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + hub.AddConnection(9001, "watcher", PlayerId); + + // Twenty XP earned a point or two at a time - the two new sources, not room time: ten gift + // boxes at 1 XP, then five completed games at 2 XP. + for (var i = 0; i < 10; i++) + await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box"); + + for (var i = 0; i < 5; i++) + await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGameCompleted, "game completed"); + + // Persisted, not held in memory... + var stored = await db.Progression.FirstAsync(x => x.PlayerId == PlayerId); + Assert.Equal(20, stored.XP); + + // ...and every point of it counted toward the level. + Assert.Equal(2, stored.Level); + + // Each grant also told the client, including the player watching them. + Assert.Contains(hub.For("watcher"), frame => frame.Contains("\"Level\":2", StringComparison.Ordinal)); + } + + [Fact] + public async Task AGiftBoxAwardIsNotCountedAgainstTheRoomAndDailyCaps() + { + // The caps bound time-in-room earning; the other two sources are not time-in-room. + using var db = CreateDb(); + + var json = new ProgressionJson + { + RoomXpRoomId = RoomId, + RoomXpEarned = 0, + RoomXpLastGrantAt = DateTime.UtcNow.AddMinutes(-6), + XpEarnedToday = 0, + XpEarnedTodayDate = DateTime.UtcNow.Date, + }; + + db.Progression.Add(new ProgressionData + { + PlayerId = PlayerId, + Level = 1, + XP = 0, + Progress = json.ToJson(), + }); + await db.SaveChangesAsync(); + + using var hub = new RecordingHub(); + hub.AddConnection(PlayerId, "owner"); + + await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box"); + + // A room instalment still pays in full, and the gift's point did not consume any of it. + var granted = await RoomProgressionService.AwardRoomXpAsync(db, PlayerId, RoomId, isPublicRoom: true); + + Assert.Equal(10, granted); + Assert.Equal(11, (await db.Progression.FirstAsync(x => x.PlayerId == PlayerId)).XP); + } + + public class HubContextProxy : DispatchProxy + { + public IHubClients HubClients = null!; + protected override object? Invoke(MethodInfo? method, object?[]? args) + => method?.Name == "get_Clients" ? HubClients : throw new NotSupportedException(); + } + + public class HubClientsProxy : DispatchProxy + { + public Func CreateClient = null!; + protected override object? Invoke(MethodInfo? method, object?[]? args) + => method?.Name == "Client" ? CreateClient((string)args![0]!) : throw new NotSupportedException(); + } + + private sealed class RecordingSingleClient : ISingleClientProxy + { + private readonly string _connectionId; + private readonly RecordingHub _hub; + + public RecordingSingleClient(string connectionId, RecordingHub hub) + { + _connectionId = connectionId; + _hub = hub; + } + + public List Sent { get; } = new(); + + public Task InvokeCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + var frame = Assert.IsType(args[0]); + Sent.Add(frame); + _hub.Sent.Add(frame); + return Task.CompletedTask; + } + } +} diff --git a/ServerTests/ProgressionXpResetTests.cs b/ServerTests/ProgressionXpResetTests.cs new file mode 100644 index 0000000..0c68784 --- /dev/null +++ b/ServerTests/ProgressionXpResetTests.cs @@ -0,0 +1,404 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Players.V2; +using RadicadeServer.API.Storefront.V4; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A client paints one bare XP number beside a player's level, and handing it the lifetime total +/// made a finished level's XP carry into the next one: a player who leveled up at 50 XP still read +/// 50 on the new level. The client is now told their progress THROUGH the current level instead, +/// which restarts at zero on a level-up. +/// +/// The lifetime total is never reduced. It is what decides the level and what the game's own +/// award path keeps adding to - this is a change to the +/// number the client is shown, not to the XP a player has earned. +/// +public class ProgressionXpResetTests +{ + private const long PlayerId = 11; + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + return new RecDBContext(options); + } + + private static ProgressionData Progression(int level, int xp) => new() + { + PlayerId = PlayerId, + Level = level, + XP = xp, + Progress = new ProgressionJson().ToJson(), + }; + + [Fact] + public void TheThresholdsAreTheCumulativeStepsTheLevelIsComputedFrom() + { + var map = LevelUpGiftService.ParseMap(); + + // The shipped map: ten XP for the first three levels, twenty through level ten, then the + // wider tiers - each level's threshold being every step up to and including it. + Assert.Equal(10, LevelUpGiftService.ThresholdXpForLevel(map, 1)); + Assert.Equal(20, LevelUpGiftService.ThresholdXpForLevel(map, 2)); + Assert.Equal(90, LevelUpGiftService.ThresholdXpForLevel(map, 6)); + Assert.Equal(170, LevelUpGiftService.ThresholdXpForLevel(map, 10)); + Assert.Equal(620, LevelUpGiftService.ThresholdXpForLevel(map, 20)); + Assert.Equal(1770, LevelUpGiftService.ThresholdXpForLevel(map, 30)); + Assert.Equal(5370, LevelUpGiftService.ThresholdXpForLevel(map, 40)); + Assert.Equal(16170, LevelUpGiftService.ThresholdXpForLevel(map, 50)); + } + + [Fact] + public void EveryThresholdIsExactlyTheXpThatComputesToThatLevel() + { + // The two halves have to be the same walk of the same map, or the number the client is + // shown would be measured against a level it is not on. + var map = LevelUpGiftService.ParseMap(); + + // Only up to the top level: past it the map's steps are never reached, because the + // level itself stops at 50. + foreach (var entry in map.Where(entry => entry.Level > 1 && entry.Level <= LevelUpGiftService.MaxLevel)) + { + var threshold = LevelUpGiftService.ThresholdXpForLevel(map, entry.Level); + + Assert.Equal(entry.Level, LevelUpGiftService.ComputeLevel(map, threshold)); + + // ...and a single XP short of it is still the level below. + Assert.Equal(entry.Level - 1, LevelUpGiftService.ComputeLevel(map, threshold - 1)); + } + } + + [Fact] + public void LandingOnEveryLevelLeavesTheClientFacingXpAtZero() + { + // This is the fix, asserted at every level the map names: arriving on a level shows zero, + // rather than whatever the previous level finished on. + var map = LevelUpGiftService.ParseMap(); + + // The top level is the exception, asserted on its own below: its XP is filled, not zero. + foreach (var entry in map.Where(entry => entry.Level >= 1 && entry.Level < LevelUpGiftService.MaxLevel)) + { + var threshold = LevelUpGiftService.ThresholdXpForLevel(map, entry.Level); + + Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(map, entry.Level, threshold)); + } + } + + [Fact] + public void TheNumberBesideTheLevelIsProgressThroughIt_NotTheLifetimeTotal() + { + var map = LevelUpGiftService.ParseMap(); + + // The reported case, with the level computed the way the server computes it: 420 lifetime + // XP reaches level 15, and level 15 begins at 395 - so the number beside the level reads + // 25, not the 420 the player has ever earned. + Assert.Equal(15, LevelUpGiftService.ComputeLevel(map, 420)); + Assert.Equal(25, LevelUpGiftService.XpIntoCurrentLevel(map, 15, 420)); + + // Part-way through a level, it is simply how far in they are. + Assert.Equal(5, LevelUpGiftService.XpIntoCurrentLevel(map, 2, 25)); + } + + [Fact] + public void AFreshPlayersXpIsNeverShownAsNegative() + { + // The map's first step is ten XP, so a brand-new level 1 player sits below their own + // level's threshold until they have earned it. The floor keeps that off the screen. + var map = LevelUpGiftService.ParseMap(); + + Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(map, 1, 0)); + Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(map, 1, 9)); + } + + [Fact] + public void TheProgressionRouteServesTheLevelRelativeXpAndTheSameLevel() + { + // The route the client fetches levels from. The level is unchanged - it was computed from + // the total - but the XP it ships alongside is progress through that level: 420 is level + // 15, which begins at 395. + var response = new ProgressionResponse(Progression(15, 420)); + + Assert.Equal(15, response.Level); + Assert.Equal(25, response.XP); + } + + [Fact] + public void TheRealtimeFrameCarriesTheLevelRelativeXpUnderBothCasings() + { + var notification = new PlayerProgressionLevelUpdateNotification(Progression(2, 25)); + + Assert.Equal(2, notification.Level); + Assert.Equal(5, notification.XP); + + using var json = JsonDocument.Parse(JsonSerializer.Serialize(notification)); + + Assert.Equal(5, json.RootElement.GetProperty("XP").GetInt32()); + Assert.Equal(5, json.RootElement.GetProperty("xp").GetInt32()); + } + + [Fact] + public async Task ALevelUpThroughTheAwardPathZeroesTheClientsXpButKeepsTheTotal() + { + using var db = CreateDb(); + + // One XP short of level 2 (the map's level 2 lands at 20 total), so a single award crosses. + db.Progression.Add(Progression(1, 19)); + await db.SaveChangesAsync(); + + await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box"); + + var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + + // Nothing is deleted: every point still counts toward the total, and the total is what + // decided the new level. + Assert.Equal(20, stored.XP); + Assert.Equal(2, stored.Level); + + // ...but the earlier level's XP is not carried into the new one. + Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(stored)); + Assert.Equal(0, new ProgressionResponse(stored).XP); + + // The next point is progress into level 2, not another reset total. + await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box"); + + stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + + Assert.Equal(21, stored.XP); + Assert.Equal(1, new ProgressionResponse(stored).XP); + } + + [Fact] + public async Task TheWatchsXpBalanceIsTheLevelRelativeNumber() + { + // XP is carried as the RecRoyale_Season1 storefront balance, and the balance endpoint is + // where the client reads the number it paints as the player's XP. This row has just + // reached level 2, so that number is zero rather than the 20 they finished level 1 on. + using var db = CreateDb(); + db.Progression.Add(Progression(2, 20)); + await db.SaveChangesAsync(); + + var api = new StorefrontsV4(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + new[] { new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString()) }, "TestAuth")) + } + } + }; + + var balances = await api.GetBalance(CurrencyType.RecRoyale_Season1); + + var balance = Assert.Single(Assert.IsAssignableFrom>(balances)); + Assert.Equal(CurrencyType.RecRoyale_Season1, balance.CurrencyType); + Assert.Equal(0, balance.Balance); + } + + [Fact] + public void TheLevelStopsAtTheGamesTopLevel() + { + var map = LevelUpGiftService.ParseMap(); + + // 50 is the cap. The map goes on naming steps all the way to level 87, but the reward table + // ends at 50 and so does progression: XP past level 50's threshold holds the level where it + // is instead of walking into levels the game has no content for. + Assert.Equal(50, LevelUpGiftService.MaxLevel); + Assert.Equal(49, LevelUpGiftService.ComputeLevel(map, 15090)); + Assert.Equal(50, LevelUpGiftService.ComputeLevel(map, 16170)); + Assert.Equal(50, LevelUpGiftService.ComputeLevel(map, 16440)); + Assert.Equal(50, LevelUpGiftService.ComputeLevel(map, 40000)); + Assert.Equal(50, LevelUpGiftService.ComputeLevel(map, int.MaxValue)); + } + + [Fact] + public void TheTopLevelsXpIsFilledToItsOwnRequirement() + { + var map = LevelUpGiftService.ParseMap(); + + // Level 50 is a 1080-XP level like the rest of its tier, so a player who has reached it + // reads a full bar - not the zero a level-up resets the levels below it to. + Assert.Equal(1080, LevelUpGiftService.RequiredXpForLevel(map, 50)); + Assert.Equal(1080, LevelUpGiftService.XpIntoCurrentLevel(map, 50, 16170)); + Assert.Equal(1080, LevelUpGiftService.XpIntoCurrentLevel(map, 50, 16440)); + Assert.Equal(1080, LevelUpGiftService.XpIntoCurrentLevel(map, 50, 40000)); + + // A row the map itself would have called level 57 reads the same. + Assert.Equal(1080, LevelUpGiftService.XpIntoCurrentLevel(map, 57, 40000)); + } + + [Fact] + public void TheLevelsBelowTheTopStillRestartAtZero() + { + var map = LevelUpGiftService.ParseMap(); + + // The filled bar is the top level's alone: landing on level 49 still starts at zero. + Assert.Equal(1080, LevelUpGiftService.RequiredXpForLevel(map, 49)); + Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(map, 49, 15090)); + Assert.Equal(1, LevelUpGiftService.XpIntoCurrentLevel(map, 49, 15091)); + } + + [Fact] + public void AStoredLevelPastTheTopIsReportedAsTheTopLevel() + { + // Both surfaces the client reads progression from hold the level at the cap, so a row + // written while the map still ran past 50 cannot show a level the game has no content for. + var response = new ProgressionResponse(Progression(57, 40000)); + + Assert.Equal(50, response.Level); + Assert.Equal(1080, response.XP); + + var notification = new PlayerProgressionLevelUpdateNotification(Progression(99, 40000)); + + Assert.Equal(50, notification.Level); + Assert.Equal(1080, notification.XP); + } + + [Fact] + public void TheRewardTableRunsToTheTopLevel() + { + // Boxes and the cap are the same level, so raising one cannot leave the other behind. + Assert.Equal(LevelUpGiftService.MaxLevel, LevelRewardTable.LastItemLevel); + Assert.True(LevelRewardTable.IsItemLevel(LevelUpGiftService.MaxLevel)); + Assert.False(LevelRewardTable.IsItemLevel(LevelUpGiftService.MaxLevel + 1)); + } + + [Fact] + public async Task ReachingTheTopLevelLeavesTheClientsXpFilledRatherThanZeroed() + { + using var db = CreateDb(); + + // One XP short of level 50 (the map's level 50 lands at 16170 total). + db.Progression.Add(Progression(49, 16169)); + await db.SaveChangesAsync(); + + await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box"); + + var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + + // The total still grew by the point earned, and that is what set the level... + Assert.Equal(16170, stored.XP); + Assert.Equal(50, stored.Level); + + // ...but the number beside it is the level filled to its requirement, not zero. + Assert.Equal(1080, new ProgressionResponse(stored).XP); + + // And it stays filled as more XP lands, rather than overflowing or emptying again. + await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box"); + + stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + + Assert.Equal(16171, stored.XP); + Assert.Equal(50, stored.Level); + Assert.Equal(1080, new ProgressionResponse(stored).XP); + } + + [Fact] + public async Task TheWatchsXpBalanceIsFilledAtTheTopLevel() + { + // The balance endpoint is where the client reads the number it paints as XP, and at the top + // level that number is the level's full requirement rather than the lifetime total. + using var db = CreateDb(); + db.Progression.Add(Progression(50, 17000)); + await db.SaveChangesAsync(); + + var api = new StorefrontsV4(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + new[] { new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString()) }, "TestAuth")) + } + } + }; + + var balances = await api.GetBalance(CurrencyType.RecRoyale_Season1); + + var balance = Assert.Single(Assert.IsAssignableFrom>(balances)); + Assert.Equal(1080, balance.Balance); + } + + [Fact] + public void ALevelTheXpDoesNotPayForStillShowsXpMoving() + { + var map = LevelUpGiftService.ParseMap(); + + // A level set ahead of the XP - what an admin's level edit leaves behind - used to pin the + // number at zero for good: 25 XP is nowhere near level 49's 15090, so subtracting it never + // came out positive and nothing the player earned moved it. + Assert.Equal(2, LevelUpGiftService.ComputeLevel(map, 25)); + Assert.Equal(5, LevelUpGiftService.XpIntoCurrentLevel(map, 49, 25)); + Assert.Equal(6, LevelUpGiftService.XpIntoCurrentLevel(map, 49, 26)); + + // The same for a row claiming level 5 (70 XP) on 25: the number tracks the XP earned. + Assert.Equal(5, LevelUpGiftService.XpIntoCurrentLevel(map, 5, 25)); + Assert.Equal(7, LevelUpGiftService.XpIntoCurrentLevel(map, 5, 27)); + } + + [Fact] + public async Task XpEarnedOnALevelAheadOfTheXpIsStillVisibleToTheClient() + { + using var db = CreateDb(); + + // The row an admin's level edit leaves behind: level 49 holding 20 XP. + db.Progression.Add(Progression(49, 20)); + await db.SaveChangesAsync(); + + await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGameCompleted, "game completed"); + + var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + + // The level is left where the edit put it, and the XP the game paid still shows: 22 total is + // two points into level 2, not the frozen zero the level-49 threshold produced. + Assert.Equal(22, stored.XP); + Assert.Equal(49, stored.Level); + Assert.Equal(2, new ProgressionResponse(stored).XP); + } + + [Fact] + public async Task TheObjectivesRouteServesTheSameLevelRelativeXpAsEveryOtherSurface() + { + using var db = CreateDb(); + db.Progression.Add(Progression(2, 25)); + await db.SaveChangesAsync(); + + var api = new PlayersV2(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + new[] { new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString()) }, "TestAuth")) + } + } + }; + + var result = await api.CompleteObjectives(new List()); + + var body = Assert.IsAssignableFrom>( + Assert.IsType(result.Result).Value); + + var single = Assert.Single(body); + Assert.Equal(2, single.Level); + Assert.Equal(5, single.XP); + } +} diff --git a/ServerTests/QuestGameConfigTests.cs b/ServerTests/QuestGameConfigTests.cs new file mode 100644 index 0000000..41addc8 --- /dev/null +++ b/ServerTests/QuestGameConfigTests.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Config; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using Xunit; + +namespace RadicadeServer.Tests; + +public class QuestGameConfigTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void QuestProbeIsDisabledWithOrWithoutLegacySeed(bool seeded) + { + using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + if (seeded) db.GameConfig.Add(new ConfigVal("AntiHile.QD", "true")); + db.GameConfig.Add(new ConfigVal("QuestTest.Unrelated", "preserved")); + db.SaveChanges(); + + var result = Assert.IsType(new GameConfigAPI(db).GetAllGameConfigs()); + var configs = Assert.IsType>(result.Value); + Assert.Equal("false", Assert.Single(configs.Where(x => x.Key == "AntiHile.QD")).Value); + Assert.Equal("preserved", configs.Single(x => x.Key == "QuestTest.Unrelated").Value); + if (seeded) Assert.Equal("true", db.GameConfig.Single(x => x.Key == "AntiHile.QD").Value); + } +} diff --git a/ServerTests/QuestOutfitsTests.cs b/ServerTests/QuestOutfitsTests.cs new file mode 100644 index 0000000..58c4616 --- /dev/null +++ b/ServerTests/QuestOutfitsTests.cs @@ -0,0 +1,82 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Avatars; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts.Local; +using Xunit; + +namespace RadicadeServer.Tests; + +public class QuestOutfitsTests +{ + [Fact] + public async Task CurrentOutfitUsesAuthenticatedAccountsSavedAppearance() + { + await using var db = NewDatabase(); + const string stored = """ + {"OutfitSelections":"body,material","HairColor":"hair","SkinColor":"skin","FaceFeatures":"{}"} + """; + db.Avatars.AddRange( + new PlayerAvatar { AccountId = 12, Avatar = stored, Outfits = "[]" }, + new PlayerAvatar { AccountId = 99, Avatar = "{}", Outfits = "[]" }); + await db.SaveChangesAsync(); + + var api = CreateApi(db, 12); + var result = Assert.IsType(await api.GetCurrentOutfit()); + var dto = Assert.IsType(result.Value); + var json = JsonSerializer.SerializeToElement(dto); + Assert.Equal(0, json.GetProperty("DataVersion").GetInt64()); + Assert.Equal(0, json.GetProperty("Slot").GetInt32()); + Assert.Equal(0, json.GetProperty("Accessibility").GetInt32()); + Assert.Empty(json.GetProperty("Selections").EnumerateArray()); + var legacy = json.GetProperty("LegacyData"); + Assert.Equal("body,material", legacy.GetProperty("SelectionsV1").GetString()); + Assert.Equal("hair", legacy.GetProperty("HairColor").GetString()); + Assert.Equal("skin", legacy.GetProperty("SkinColor").GetString()); + Assert.Equal("{}", legacy.GetProperty("FaceFeatures").GetString()); + Assert.Equal(JsonValueKind.Null, legacy.GetProperty("SelectionsV2").ValueKind); + Assert.Equal(stored, (await db.Avatars.FindAsync(12L))!.Avatar); + Assert.Equal("{}", (await db.Avatars.FindAsync(99L))!.Avatar); + } + + [Fact] + public async Task CurrentOutfitRequiresAnAuthenticatedAccount() + { + await using var db = NewDatabase(); + Assert.IsType(await CreateApi(db, null).GetCurrentOutfit()); + Assert.False(await db.Avatars.AnyAsync()); + Assert.NotEmpty(typeof(OutfitsAPI).GetCustomAttributes(typeof(AuthorizeAttribute), true)); + } + + [Fact] + public async Task CurrentOutfitInitializesMissingAvatarThroughExistingLoader() + { + await using var db = NewDatabase(); + var result = Assert.IsType(await CreateApi(db, 12).GetCurrentOutfit()); + var dto = Assert.IsType(result.Value); + var stored = Assert.Single(await db.Avatars.ToListAsync()); + Assert.Equal(12, stored.AccountId); + Assert.Equal(stored.DeserializeAvatar()!.OutfitSelections, dto.LegacyData.SelectionsV1); + } + + private static RecDBContext NewDatabase() => new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + + private static OutfitsAPI CreateApi(RecDBContext db, long? accountId) => new(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = accountId.HasValue + ? new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, accountId.Value.ToString())], "test")) + : new ClaimsPrincipal(new ClaimsIdentity()) + } + } + }; +} diff --git a/ServerTests/QuestRewardDeliveryTests.cs b/ServerTests/QuestRewardDeliveryTests.cs new file mode 100644 index 0000000..9c72a1c --- /dev/null +++ b/ServerTests/QuestRewardDeliveryTests.cs @@ -0,0 +1,160 @@ +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.RecNet.Progression; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RecNet.Storefront; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// What happens to a quest's reward box when the player walks away from it. +/// +/// A quest's reward is not a choice and is not openable anywhere else - the server picked one unowned +/// item out of the run's rank pool when the reward was generated, and the box is only the delivery of +/// that item. Leaving the quest with the box still shut therefore has to deliver it, not strand it, +/// not re-roll it, and not pay tokens for it. These tests cover the delivery, and the three things +/// the delivery must not touch: a box for the quest the player is standing in, a box that is no +/// quest's, and the item's identity. +/// +public class QuestRewardDeliveryTests +{ + private const long PlayerId = 4411; + + private static RecDBContext NewDb() => + new(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static async Task SeedRoomAsync(RecDBContext db, string name) + { + var room = new RoomData { Name = name }; + db.Rooms.Add(room); + await db.SaveChangesAsync(); + return room; + } + + private static Gift QuestBox(GiftContext context = GiftContext.StuntRunner_TheMainEvent_C) => new() + { + GiftDropId = 8801, + FriendlyName = "Runner Harness (Green)", + Tooltip = "Stunt Runner: The Main Event", + AvatarItemDesc = "d2d3264b-fbf6-40b5-9f10-c85ec737d112,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", + CurrencyType = CurrencyType.Invalid, + Currency = 0, + Rarity = Rarity.Uncommon, + Context = context + }; + + private static async Task SeedRewardAsync(RecDBContext db, Gift box, bool consumed = false) + { + var reward = new GameReward + { + RewardSelectionId = 5150, + Message = "Stunt Runner: The Main Event", + GiftContext = box.Context, + RewardType = RewardType.PostQuestActivity, + GiftDrop1 = box, + GiftDrop2 = box, + GiftDrop3 = box, + CreatedAt = DateTime.UtcNow.AddMinutes(-2), + Consumed = consumed, + ConsumedGiftDropId = consumed ? box.GiftDropId : 0 + }; + var json = new ProgressionJson { PendingGameRewards = new List { reward } }; + db.Progression.Add(new ProgressionData { PlayerId = PlayerId, Progress = json.ToJson() }); + await db.SaveChangesAsync(); + } + + /// + /// The player leaves the quest without opening the box: they get the item that was already inside + /// it, the box is taken back, and the reward is recorded as collected against that same box. + /// + [Fact] + public async Task LeavingTheQuestWithTheBoxUnopenedDeliversTheItemItAlreadyHeld() + { + await using var db = NewDb(); + var recCenter = await SeedRoomAsync(db, "RecCenter"); + var box = QuestBox(); + await SeedRewardAsync(db, box); + await StorefrontShared.PersistGiftBoxAsync(db, PlayerId, box, notify: false); + + var delivered = await QuestRewardDelivery.DeliverOnArrivalAsync(db, PlayerId, recCenter.RoomId); + + Assert.Equal(1, delivered); + Assert.Empty(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); + + var unlocks = await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == PlayerId); + var item = Assert.Single(unlocks!.GetAvatarItems(), x => x.AvatarItemDesc == box.AvatarItemDesc); + Assert.Equal("Runner Harness (Green)", item.FriendlyName); + + var progression = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); + var saved = Assert.Single(progression.GetProgressionJson()!.PendingGameRewards); + Assert.True(saved.Consumed); + Assert.Equal(box.GiftDropId, saved.ConsumedGiftDropId); + } + + /// + /// Arriving back in the quest itself - a different instance of it, or a different rank of it - + /// leaves the box alone, because it is still openable there. + /// + [Fact] + public async Task ArrivingInTheQuest_KeepsTheBoxOpenable() + { + await using var db = NewDb(); + var stuntRunner = await SeedRoomAsync(db, "StuntRunner"); + var box = QuestBox(); + await SeedRewardAsync(db, box); + await StorefrontShared.PersistGiftBoxAsync(db, PlayerId, box, notify: false); + + var delivered = await QuestRewardDelivery.DeliverOnArrivalAsync(db, PlayerId, stuntRunner.RoomId); + + Assert.Equal(0, delivered); + Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); + Assert.Null(await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == PlayerId)); + } + + /// + /// A box that is no quest's - a storefront purchase, a level-up box, a daily-challenge box - is + /// not the quest's special box and is left where it is: it can be opened anywhere. + /// + [Theory] + [InlineData(GiftContext.LevelUp)] + [InlineData(GiftContext.All_Daily_Challenges_Complete)] + [InlineData(GiftContext.Purchased_Gift_A)] + [InlineData(GiftContext.Dodgeball)] + public async Task ABoxThatIsNoQuestsIsLeftAlone(GiftContext context) + { + await using var db = NewDb(); + var recCenter = await SeedRoomAsync(db, "RecCenter"); + var box = QuestBox(context); + await StorefrontShared.PersistGiftBoxAsync(db, PlayerId, box, notify: false); + + var delivered = await QuestRewardDelivery.DeliverOnArrivalAsync(db, PlayerId, recCenter.RoomId); + + Assert.Equal(0, delivered); + Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); + } + + /// Which contexts count as a quest's own, and which do not. + [Theory] + [InlineData(GiftContext.StuntRunner_TheMainEvent_C, true)] + [InlineData(GiftContext.StuntRunner_TheMainEvent_SS, true)] + [InlineData(GiftContext.Quest_Goblin_S, true)] + [InlineData(GiftContext.Quest_Pirate1_C, true)] + [InlineData(GiftContext.Dodgeball, false)] + [InlineData(GiftContext.Paintball_ClearCut, false)] + [InlineData(GiftContext.Lasertag, false)] + [InlineData(GiftContext.Reacquisition, false)] + [InlineData(GiftContext.LevelUp, false)] + [InlineData(GiftContext.GameRewards_Tokens, false)] + public void OnlyARankedQuestsContextIsAQuestContext(GiftContext context, bool expected) + { + Assert.Equal(expected, RroRewardPools.IsQuestContext(context)); + } +} diff --git a/ServerTests/RecNetShapeConformanceTests.cs b/ServerTests/RecNetShapeConformanceTests.cs new file mode 100644 index 0000000..5be5eb1 --- /dev/null +++ b/ServerTests/RecNetShapeConformanceTests.cs @@ -0,0 +1,2196 @@ +using System.Net; +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.IdentityModel.Tokens; +using RadicadeServer.API; +using RadicadeServer.API.Accounts; +using RadicadeServer.API.Analytics; +using RadicadeServer.API.Avatars; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.API.Avatars.V1; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.API.Avatars.V3; +using RadicadeServer.API.Avatars.V4; +using RadicadeServer.API.Config; +using RadicadeServer.API.Events; +using RadicadeServer.API.Link; +using RadicadeServer.API.Matchmaking; +using RadicadeServer.API.Progression; +using RadicadeServer.API.Progression.Objectives.V1; +using RadicadeServer.API.Players.V2; +using RadicadeServer.API.Storefront.V3; +using RadicadeServer.Auth; +using RadicadeServer.API.Moderation; +using RadicadeServer.CDN; +using RadicadeServer.Clubs.Announcements; +using RadicadeServer.DB; +using RadicadeServer.Extensions; +using RadicadeServer.Leaderboard; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Accounts.Local; +using RadicadeServer.RecNet.Config; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Events; +using RadicadeServer.RecNet.Inventions; +using RadicadeServer.API.Inventions.V1; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Leaderboards; +using RadicadeServer.RecNet.Responses; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Conformance smoke-tests for the wire shapes the game client deserializes. The +/// client's deserializer is strict — it logs "Failed to find key ..." when a DTO is +/// missing a field, and casts (e.g. Dictionary -> List) when an endpoint returns the +/// wrong root shape — so this suite pins the exact JSON each RecNet endpoint must emit. +/// +[Collection(DbFactoryCollection.Name)] +public class RecNetShapeConformanceTests +{ + private const long CreatorId = 42; + + private static readonly JsonSerializerOptions ServerJson = new() + { + PropertyNamingPolicy = null, + WriteIndented = true, + }; + + // The full key set the client's PlayerEvent model requires (verified against the + // archived FemRec-2023 server's event schema). + private static readonly string[] PlayerEventKeys = + { + "PlayerEventId", "Name", "Description", "ImageName", "StartTime", "EndTime", + "CreatorPlayerId", "AttendeeCount", "RoomId", "SubRoomId", "Accessibility", + "ClubId", "State", "Tags", "CreatedAt", + }; + + // Search/searchlive results carry these two extra fields the client reads + // (verified against the live recconned reference server). + private static readonly string[] PlayerEventSearchKeys = + { + "PlayerCount", "IsFull", + }; + + private static string Serialize(object? value) => JsonSerializer.Serialize(value, ServerJson); + + private static RecDBContext CreateDb() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new RecDBContext(options); + } + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + var root = new InMemoryDatabaseRoot(); + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString(), root) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + private static EventsAPI CreateEventsController(RecDBContext db) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, CreatorId.ToString())); + return new EventsAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + } + + // Gives the controller a request body (and content type) the way the client sends it. + private static void SetBody(Controller controller, string? body, string contentType, string query = "") + { + var context = controller.ControllerContext.HttpContext; + context.Request.ContentType = contentType; + context.Request.QueryString = new QueryString(query); + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body ?? string.Empty)); + } + + private static void SetUser(Controller controller, long accountId = CreatorId, string platformId = "0") + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + identity.AddClaim(new Claim("rn.platid", platformId)); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static async Task SeedRoomAsync(RecDBContext db) + { + var room = new RoomData { Name = "Test Room" }; + db.Rooms.Add(room); + await db.SaveChangesAsync(); + return room.RoomId; + } + + private static async Task CreateEventAsync(EventsAPI controller, long roomId) + { + var result = await controller.CreateEvent("Conformance", null, roomId, null, null, null, null); + return Assert.IsType(Assert.IsType(result.Result).Value); + } + + private static string[] TopLevelKeys(string json) + { + using var doc = JsonDocument.Parse(json); + return doc.RootElement.EnumerateObject().Select(p => p.Name).ToArray(); + } + + private static void AssertIsEmptyArray(string json) + { + using var doc = JsonDocument.Parse(json); + Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind); + Assert.Equal(0, doc.RootElement.GetArrayLength()); + } + + private static string CreateBearerToken(long accountId) + { + if (string.IsNullOrWhiteSpace(DBConfig.JwtKey)) + { + DBConfig.JwtKey = "radicade-local-development-jwt-key-change-before-production"; + } + + var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(DBConfig.JwtKey)); + var token = new JwtSecurityToken( + claims: new[] { new Claim("userId", accountId.ToString()) }, + signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)); + + return "Bearer " + new JwtSecurityTokenHandler().WriteToken(token); + } + + [Fact] + public async Task PlayerEvent_ContainsAllClientRequiredFields() + { + var db = CreateDb(); + var controller = CreateEventsController(db); + var roomId = await SeedRoomAsync(db); + var ev = await CreateEventAsync(controller, roomId); + + var keys = TopLevelKeys(Serialize(ev)); + + foreach (var key in PlayerEventKeys) + Assert.Contains(key, keys); + } + + [Fact] + public async Task EventSearch_EmptyResult_IsBareArray() + { + var db = CreateDb(); + var controller = CreateEventsController(db); + + var result = await controller.SearchEvents(null, null); + + AssertIsEmptyArray(Serialize(result)); + } + + [Fact] + public async Task EventSearch_Result_HasPlayerCountAndIsFull() + { + var db = CreateDb(); + var controller = CreateEventsController(db); + var roomId = await SeedRoomAsync(db); + await CreateEventAsync(controller, roomId); + + var result = await controller.SearchEvents(null, null); + var json = Serialize(result); + + using var doc = JsonDocument.Parse(json); + Assert.Equal(1, doc.RootElement.GetArrayLength()); + var keys = doc.RootElement[0].EnumerateObject().Select(p => p.Name).ToArray(); + foreach (var key in PlayerEventSearchKeys) + Assert.Contains(key, keys); + } + + [Fact] + public async Task RoomEvents_EmptyResult_IsBareArray() + { + var db = CreateDb(); + var controller = CreateEventsController(db); + var roomId = await SeedRoomAsync(db); + + var result = await controller.GetEventsInRoom(roomId); + + AssertIsEmptyArray(Serialize(result)); + } + + [Fact] + public async Task CreatedEvents_EmptyResult_IsBareArray() + { + var db = CreateDb(); + var controller = CreateEventsController(db); + + var result = await controller.GetCreatedEvents(); + + AssertIsEmptyArray(Serialize(result)); + } + + [Fact] + public void EventTagFilters_ReturnExpectedKeys() + { + var db = CreateDb(); + var controller = CreateEventsController(db); + + var keys = TopLevelKeys(Serialize(controller.GetEventTagFilters())); + + Assert.Contains("PinnedFilters", keys); + Assert.Contains("PopularFilters", keys); + Assert.Contains("TrendingFilters", keys); + } + + [Fact] + public async Task AllEvents_ReturnCreatedAndResponsesKeys() + { + var db = CreateDb(); + var controller = CreateEventsController(db); + + var keys = TopLevelKeys(Serialize(await controller.GetAllEvents())); + + Assert.Contains("Created", keys); + Assert.Contains("Responses", keys); + } + + [Fact] + public void ClubAnnouncementDeleteRouteIsRegistered() + { + var delete = typeof(ClubAnnouncementsAPI).GetMethod(nameof(ClubAnnouncementsAPI.DeleteClubAnnouncement))!; + var routes = delete.GetCustomAttributes(typeof(HttpDeleteAttribute), false) + .Cast() + .Select(attribute => attribute.Template) + .ToArray(); + Assert.Contains("club/{clubId:long}/{announcementId:long}", routes); + Assert.DoesNotContain("~/clubs/announcements/club/{clubId:long}/{announcementId:long}", routes); + } + + [Fact] + public void Success_SpellsItsAbsentErrorAsAnEmptyString() + { + // The reference serves error: "" on success and never null. The 2023 client builds its + // own failure exception straight from this field, so `"error": null` on a 200 turns into + // `ArgumentException: Error message cannot be null` in its response continuation - the + // friend's client logged exactly that after an invite it had actually sent + // (docs/2023-0831-CLIENT-SWITCH.md, the invite section). + using var document = JsonDocument.Parse(Serialize(new Success(true, null, null))); + Assert.Equal(string.Empty, document.RootElement.GetProperty("error").GetString()); + Assert.Equal(JsonValueKind.Null, document.RootElement.GetProperty("value").ValueKind); + + // A refusal still carries the reason it was refused. + using var refusal = JsonDocument.Parse(Serialize(new Success(false, "nope", null))); + Assert.False(refusal.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal("nope", refusal.RootElement.GetProperty("error").GetString()); + } + + [Fact] + public void ClubAnnouncements_ReturnsTheReferenceEnvelopeShape() + { + var api = new ClubAnnouncementsAPI(); + var ok = Assert.IsType(api.GetClubAnnouncements(0)); + + using var document = JsonDocument.Parse(Serialize(ok.Value)); + var root = document.RootElement; + Assert.Equal(JsonValueKind.Object, root.ValueKind); + Assert.Equal(string.Empty, root.GetProperty("error").GetString()); + Assert.True(root.GetProperty("success").GetBoolean()); + + var value = root.GetProperty("value"); + Assert.Equal(JsonValueKind.Object, value.ValueKind); + Assert.Equal(JsonValueKind.Array, value.GetProperty("Announcements").ValueKind); + Assert.Equal(0, value.GetProperty("ClubId").GetInt64()); + Assert.Equal(JsonValueKind.Null, value.GetProperty("LastAnnouncementId").ValueKind); + Assert.Equal(0, value.GetProperty("LastReadAnnouncementId").GetInt64()); + } + [Fact] + public async Task EventResponses_ContainAllClientRequiredFields() + { + // The client's PlayerEventResponse model reads PlayerEventResponseId, + // PlayerEventId, PlayerId, CreatedAt and Type and throws a raw + // KeyNotFoundException when any are missing (verified against the EpicQuest + // client metadata deserializer). + var db = CreateDb(); + var controller = CreateEventsController(db); + var roomId = await SeedRoomAsync(db); + var ev = await CreateEventAsync(controller, roomId); + + var result = await controller.GetResponses(ev.PlayerEventId); + var responses = Assert.IsType>(Assert.IsType(result.Result).Value); + + var response = Assert.Single(responses); + var keys = TopLevelKeys(Serialize(response)); + + foreach (var key in new[] { "PlayerEventResponseId", "PlayerEventId", "PlayerId", "CreatedAt", "Type" }) + Assert.Contains(key, keys); + } + + [Fact] + public void UnreadAnnouncements_EmptyResult_IsBareArray() + { + var api = new ClubAnnouncementsAPI(); + + AssertIsEmptyArray(Serialize(api.GetUnreadAnnouncements())); + AssertIsEmptyArray(Serialize(api.GetUnreadSubscribedAnnouncements())); + } + + [Fact] + public void EventGetByIdRoute_IsRegistered() + { + var method = typeof(EventsAPI).GetMethod(nameof(EventsAPI.GetEvent)); + + var templates = method!.GetCustomAttributes().Select(a => a.Template); + + Assert.Contains("v1/{eventId:long}", templates); + } + + [Fact] + public void GotoEventRoute_IsRegistered() + { + var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.GoToEvent)); + + var templates = method!.GetCustomAttributes().Select(a => a.Template); + + Assert.Contains("goto/event/{eventId}", templates); + } + + [Fact] + public void ClubAnnouncementsRoute_IsRegistered() + { + var method = typeof(ClubAnnouncementsAPI).GetMethod(nameof(ClubAnnouncementsAPI.PostClubAnnouncements)); + + var templates = method!.GetCustomAttributes().Select(a => a.Template); + + Assert.Contains("club/{clubId}", templates); + } + + [Fact] + public void EventResponsesAndAttendeesRoutes_AreRegistered() + { + var responses = typeof(EventsAPI).GetMethod(nameof(EventsAPI.GetResponses))!; + var responseTemplates = responses.GetCustomAttributes().Select(a => a.Template); + Assert.Contains("v1/{eventId:long}/responses", responseTemplates); + + var attendees = typeof(EventsAPI).GetMethod(nameof(EventsAPI.GetAttendees))!; + var attendeeTemplates = attendees.GetCustomAttributes().Select(a => a.Template); + Assert.Contains("v1/{eventId:long}/attendees", attendeeTemplates); + } + + // The RRUI events service fetches club-event feeds from + // /api/playerevents/v1/... (verified from the client's string literals: + // "api/playerevents/v1/club/{0}", "api/playerevents/v1/clubs"). The single-club + // awaiter parses the paged envelope { Events, ContinuationToken }; the multi-club + // awaiter parses a bare list. Both were 404s -> "Failed to get club events". + [Fact] + public void ClubEventFeeds_AreRegisteredUnderPlayerevents() + { + var single = typeof(EventsAPI).GetMethod(nameof(EventsAPI.GetEventsByClub))!; + var singleTemplates = single.GetCustomAttributes().Select(a => a.Template); + Assert.Contains("v1/club/{clubId:long}", singleTemplates); + Assert.Contains("v1/club_events/{clubId:long}", singleTemplates); + + var multi = typeof(EventsAPI).GetMethod(nameof(EventsAPI.GetEventsByClubs))!; + var multiTemplates = multi.GetCustomAttributes().Select(a => a.Template); + Assert.Contains("v1/clubs", multiTemplates); + + var upcoming = typeof(EventsAPI).GetMethod(nameof(EventsAPI.GetUpcomingEvents))!; + Assert.Contains("v1/upcoming_events", + upcoming.GetCustomAttributes().Select(a => a.Template)); + } + + [Fact] + public async Task ClubEventSingleFeed_ServesPagedEnvelope() + { + var api = CreateEventsController(CreateDb()); + var result = Assert.IsType(await api.GetEventsByClub(123)); + var json = Serialize(result.Value); + using var doc = System.Text.Json.JsonDocument.Parse(json); + Assert.Equal(System.Text.Json.JsonValueKind.Object, doc.RootElement.ValueKind); + // Ordinal codec: Events (list) first, ContinuationToken (string) second. + var props = doc.RootElement.EnumerateObject().ToList(); + Assert.Equal("Events", props[0].Name); + Assert.Equal("ContinuationToken", props[1].Name); + Assert.Equal(System.Text.Json.JsonValueKind.Array, props[0].Value.ValueKind); + } + + [Fact] + public async Task ClubEventMultiFeed_ServesBareList() + { + var api = CreateEventsController(CreateDb()); + var events = await api.GetEventsByClubs(new List { 1, 2 }); + Assert.NotNull(events); + Assert.Empty(events); + } + + [Fact] + public async Task AccountSearch_ReturnsBareArrayWithPlayerKeys() + { + // Verified against the live recconned reference server: account search is a + // bare array of player DTOs with this exact lowercase key set. Missing keys + // (personalPronouns/identityFlags/displayEmoji) made the client's parser + // return null ("ArgumentNullException: source"). + var db = CreateDb(); + db.Profiles.Add(new RadicadeServer.RecNet.Auth.Profile + { + accountId = 7, + username = "bob", + displayName = "Bob", + profileImage = "img.png", + isJunior = false, + platforms = 0, + createdAt = DateTime.UtcNow, + }); + await db.SaveChangesAsync(); + + var api = new AccountsAPI(db); + var result = await api.GetByName("bob"); + + var json = Serialize(result); + + using var doc = JsonDocument.Parse(json); + Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind); + Assert.Equal(1, doc.RootElement.GetArrayLength()); + + var playerKeys = doc.RootElement[0].EnumerateObject().Select(p => p.Name).ToArray(); + foreach (var key in new[] { "accountId", "profileImage", "isJunior", "platforms", + "username", "displayName", "createdAt", "personalPronouns", + "identityFlags", "displayEmoji" }) + Assert.Contains(key, playerKeys); + } + + [Fact] + public async Task AccountSearch_ReportsStoredPronounsAndIdentityFlags() + { + // Both are stored on the profile and settable through + // /account/me/personalpronouns and /account/me/identityflags, so search results + // (the invite / player card) must echo what the player chose. They used to be + // hardcoded to 0 here, so every searched player showed no pronouns. + var db = CreateDb(); + db.Profiles.Add(new RadicadeServer.RecNet.Auth.Profile + { + accountId = 7, + username = "bob", + displayName = "Bob", + profileImage = "img.png", + personalPronouns = 3, + identityFlags = 5, + }); + await db.SaveChangesAsync(); + + var result = await new AccountsAPI(db).GetByName("bob"); + + using var doc = JsonDocument.Parse(Serialize(result)); + var player = doc.RootElement[0]; + Assert.Equal(3, player.GetProperty("personalPronouns").GetInt32()); + Assert.Equal(5, player.GetProperty("identityFlags").GetInt32()); + } + + [Fact] + public async Task SetUsername_RefusesANameAnotherAccountAlreadyUses() + { + // Sign-in resolves an account by username OR display name, so letting two + // accounts share a name made that lookup land on whichever row came first and + // locked the other account out of the web login. + var db = CreateDb(); + db.Profiles.Add(new RadicadeServer.RecNet.Auth.Profile + { + accountId = 7, username = "bob", displayName = "Bob", profileImage = "img.png" + }); + db.Profiles.Add(new RadicadeServer.RecNet.Auth.Profile + { + accountId = 8, username = "alice", displayName = "Alice", profileImage = "img.png" + }); + await db.SaveChangesAsync(); + + var api = new AccountsAPI(db); + SetUser(api, 8); + + Assert.False((await api.SetUsername("bob")).success); + Assert.False((await api.SetUsername("Bob")).success); + Assert.False((await api.SetUsername(" ")).success); + + Assert.True((await api.SetUsername(" alice2 ")).success); + + var mine = await db.Profiles.SingleAsync(x => x.accountId == 8); + Assert.Equal("alice2", mine.username); + } + + [Fact] + public async Task ProfileGallery_ReturnsOnlyPlayersPublicPhotos() + { + var db = CreateDb(); + RadicadeServer.RecNet.CDN.ImageDB Photo(long id, long owner, + RadicadeServer.RecNet.CDN.SavedImageAccessibility access, + RadicadeServer.RecNet.CDN.SavedImageType type) => new() + { + Id = id, OwnerId = owner, Name = $"{id}.png", Accessibility = access, + ImageType = type, CreatedAt = new DateTime(2026, 1, 1).AddDays(id) + }; + var publicAccess = RadicadeServer.RecNet.CDN.SavedImageAccessibility.Public; + var photoType = RadicadeServer.RecNet.CDN.SavedImageType.ShareCamera; + db.Images.AddRange(Photo(1, 7, publicAccess, photoType), + Photo(2, 7, publicAccess, photoType), + Photo(3, 8, publicAccess, photoType), + Photo(4, 7, RadicadeServer.RecNet.CDN.SavedImageAccessibility.Private, photoType), + Photo(5, 7, publicAccess, RadicadeServer.RecNet.CDN.SavedImageType.ProfileThumbnail)); + await db.SaveChangesAsync(); + + var api = new ImagesAPI(db); + var images = await api.GetImagesFromPlayer(7); + // Reference behavior: a player's photo list serves every PUBLIC image + // they own (the profile-gallery grid), not just ShareCamera shots. + Assert.Equal(new long[] { 5, 2, 1 }, images.Select(image => + (long)image.GetType().GetProperty("Id")!.GetValue(image)!)); + Assert.All(images, image => Assert.Equal(7, + (long)image.GetType().GetProperty("PlayerId")!.GetValue(image)!)); + Assert.Empty(await api.GetImagesFromPlayer(99)); + } + + [Fact] + public void ProfileWishlist_HasEconRoute() + { + var routes = typeof(RadicadeServer.API.Wishlists.WishlistAPI) + .GetCustomAttributes().Select(route => route.Template); + Assert.Contains("econ/api/itemWishlists/v1", routes); + } + + [Fact] + public async Task ProfilePresence_UsesPlayersLoginVersion() + { + var db = CreateDb(); + db.Players.Add(new RadicadeServer.RecNet.Auth.PlayerData { AccountId = 7, AppVersion = "20230301" }); + await db.SaveChangesAsync(); + var presence = await Presence.CreateAsync(db, new RealPresence { playerId = 7 }, null); + Assert.Equal("20230301", presence.appVersion); + var missing = await Presence.CreateAsync(db, new RealPresence { playerId = 99 }, null); + Assert.Equal(DBConfig.GameVersion, missing.appVersion); + } + + // A stock client's key ID is "p1" + the stock ".rec.net" tail literal; the + // metadata patcher rewrites that tail to ".radicade.internal", which is what + // the patched build compares against. Serving either one on the wrong route + // makes the client drop the image with "Image signature malformed" before it + // ever reaches RSA verification, so the two IDs are pinned here. + [Fact] + public void ImageRoutesServeTheKeyIdTheirClientBuildCanParse() + { + Assert.Equal("KEY:RSA:p1.radicade.internal", RadicadeServer.Images.ImageAPI.PatchedKeyId); + Assert.Equal("KEY:RSA:p1.rec.net", RadicadeServer.Images.ImageAPI.StockKeyId); + Assert.StartsWith($"key-id={RadicadeServer.Images.ImageAPI.PatchedKeyId}; data=", + RadicadeServer.Images.ImageAPI.SignatureString); + Assert.StartsWith($"key-id={RadicadeServer.Images.ImageAPI.StockKeyId}; data=", + RadicadeServer.Images.ImageAPI.StockSignatureString); + // With no override in the environment the bare route serves the stock ID; the + // override exists only so a mis-mapped deploy can be corrected by a restart. + Assert.Equal(RadicadeServer.Images.ImageAPI.StockKeyId, RadicadeServer.Images.ImageAPI.BareRouteKeyId); + // Same placeholder signature bytes on both: only the key ID decides which + // build accepts the header. + Assert.Equal(RadicadeServer.Images.ImageAPI.SignatureString[ + RadicadeServer.Images.ImageAPI.SignatureString.IndexOf("data=", StringComparison.Ordinal)..], + RadicadeServer.Images.ImageAPI.StockSignatureString[ + RadicadeServer.Images.ImageAPI.StockSignatureString.IndexOf("data=", StringComparison.Ordinal)..]); + } + + [Theory] + [InlineData("img")] + [InlineData("alternate")] + [InlineData("default")] + public async Task ProfileImage_AllRoutesIncludeContentSignature(string route) + { var originalImages = DBConfig.CDNImages; + var directory = Path.Combine(Path.GetTempPath(), "radicade-image-test-" + Guid.NewGuid()); + Directory.CreateDirectory(directory); + try + { + DBConfig.CDNImages = directory; + var imageName = route == "default" ? DBConfig.DefaultRoomImage : "profile.png"; + await File.WriteAllBytesAsync(Path.Combine(directory, imageName), + Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=")); + var api = new RadicadeServer.Images.ImageAPI + { + ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } + }; + var result = route switch + { + "img" => await api.GetImageFile(imageName, null, null, "", null), + "alternate" => await api.GetImageFileAlt(imageName, null, null, "", null), + _ => await api.GetDefaultImage(null, null, "", null) + }; + + Assert.IsType(result); + + // The client compares the header's key ID against a literal its own + // global-metadata.dat carries, so each route has to answer with the ID + // its caller can parse: /images/img/ is the metadata-patched + // build's own URL, and the bare route is what the BepInEx plugin's + // in-flight rewrite of img.rec.net asks for (a stock client). + var expectedKeyId = route == "img" + ? RadicadeServer.Images.ImageAPI.PatchedKeyId + : RadicadeServer.Images.ImageAPI.StockKeyId; + // ValidateImageSignature splits on ';', trims the fields, and compares + // the complete key ID before decoding data. + var fields = api.Response.Headers["Content-Signature"].ToString() + .Split(';').Select(field => field.Trim()).ToArray(); + Assert.Equal($"key-id={expectedKeyId}", fields[0]); + Assert.Equal(256, Convert.FromBase64String(fields[1]["data=".Length..]).Length); + } + finally + { + DBConfig.CDNImages = originalImages; + Directory.Delete(directory, recursive: true); + } + } + + // A third patcher (or a player who edited their own metadata) produces a key ID + // neither route default matches, and its requests are byte-identical to everyone + // else's. A client that can state its own literal is answered with it verbatim; + // anything malformed falls back to the route default rather than being echoed. + [Theory] + [InlineData("img", "KEY:RSA:p1.rec.net", "KEY:RSA:p1.rec.net")] + [InlineData("bare", "KEY:RSA:p1.radicade.internal", "KEY:RSA:p1.radicade.internal")] + [InlineData("bare", "KEY:RSA:p1.something.internal", "KEY:RSA:p1.something.internal")] + [InlineData("bare", null, "KEY:RSA:p1.rec.net")] + [InlineData("bare", "", "KEY:RSA:p1.rec.net")] + [InlineData("img", null, "KEY:RSA:p1.radicade.internal")] + public void StatedKeyIdWinsOverRouteDefault(string route, string clientKeyId, string expected) + { + var routeDefault = route == "img" + ? RadicadeServer.Images.ImageAPI.PatchedKeyId + : RadicadeServer.Images.ImageAPI.StockKeyId; + var header = RadicadeServer.Images.ImageAPI.SignatureFor(clientKeyId, route, routeDefault); + Assert.StartsWith($"key-id={expected}; data=", header); + } + + [Theory] + [InlineData("stock")] + [InlineData("patched")] + [InlineData("KEY:RSA:p1")] + [InlineData("KEY:RSA:p1.")] + [InlineData("KEY:RSA:p1.rec.net; data=steal")] + [InlineData("KEY:RSA:p1.rec.net\\r\\nX-Injected: 1")] + [InlineData("KEY:RSA:p2.rec.net")] + [InlineData("KEY:RSA:p1.a b")] + public void MalformedKeyIdsAreRefusedRatherThanEchoed(string clientKeyId) + { + Assert.False(RadicadeServer.Images.ImageAPI.IsWellFormedKeyId(clientKeyId)); + var header = RadicadeServer.Images.ImageAPI.SignatureFor( + clientKeyId, "bare", RadicadeServer.Images.ImageAPI.StockKeyId); + Assert.StartsWith($"key-id={RadicadeServer.Images.ImageAPI.StockKeyId}; data=", header); + Assert.DoesNotContain("Injected", header); + } + + [Fact] + public void AcceptableKeyIdsAreTheShapeTheClientBuildsItself() + { + Assert.True(RadicadeServer.Images.ImageAPI.IsWellFormedKeyId("KEY:RSA:p1.rec.net")); + Assert.True(RadicadeServer.Images.ImageAPI.IsWellFormedKeyId("KEY:RSA:p1.radicade.internal")); + Assert.True(RadicadeServer.Images.ImageAPI.IsWellFormedKeyId("KEY:RSA:p1.a-b_c.1")); + } + + [Fact] + public async Task ProfileBio_NullStoredBioReturnsEmptyString() + { + var db = CreateDb(); + db.Profiles.Add(new RadicadeServer.RecNet.Auth.Profile + { + accountId = 7, username = "bob", displayName = "Bob", + profileImage = "img.png", bio = null + }); + await db.SaveChangesAsync(); + var response = await new AccountsAPI(db).GetBioFromId("7"); + Assert.NotNull(response); + Assert.Equal(string.Empty, response.bio); + } + + [Fact] + public async Task ProfilePrivacy_ReturnsSettingsObject() + { + var db = CreateDb(); + db.Profiles.Add(new RadicadeServer.RecNet.Auth.Profile + { + accountId = 7, username = "bob", displayName = "Bob", profileImage = "img.png" + }); + await db.SaveChangesAsync(); + var result = await new AccountsAPI(db).GetAccountPrivacySettings(7); + var ok = Assert.IsType(result); + Assert.Contains("accountId", Serialize(ok.Value)); + } + + [Fact] + public void Reputation_FreshAccountHasTheGateOpenAndNoCheerCounters() + { + // Two separate things share one payload. IsCheerful is the gate the client reads to + // decide whether this player may hand cheers out at all, and the reference server + // answers true for everyone. The five counters are the earned badges, and those used + // to be floored at 1, which is what made the client draw all five as already earned. + var api = new ReputationAPI(CreateDb()); + var result = Assert.IsType(api.GetReputation(7)); + var json = Serialize(result.Value); + using var doc = JsonDocument.Parse(json); + Assert.True(doc.RootElement.GetProperty("IsCheerful").GetBoolean()); + Assert.Equal(0, doc.RootElement.GetProperty("CheerGeneral").GetInt32()); + Assert.Equal(0, doc.RootElement.GetProperty("CheerHelpful").GetInt32()); + } + + [Fact] + public async Task Reputation_SelectedCheerCreatesMissingReputation() + { + var db = CreateDb(); + var api = new ReputationAPI(db); + SetUser(api, 7); + SetBody(api, "Category=40", "application/x-www-form-urlencoded"); + + var result = await api.SetSelectedCheer(); + + Assert.IsType(result.Result); + // 40 is Creative on the client's ladder (0/10/20/30/40); the badge has to store + // the value the client sent so it renders the badge it thinks it picked. + Assert.Equal(40, (await db.Reputations.SingleAsync()).SelectedCheer); + } + + [Fact] + public async Task Reputation_SelectedCheerAcceptsEveryShapeTheClientUses() + { + // The client posts this as a form, and [FromBody] rejected every non-JSON request + // with "415 Unsupported Media Type", which is why the badge never changed. All + // four shapes have to keep working. + async Task SelectedCheerFor(string? body, string contentType, string query = "") + { + var db = CreateDb(); + var api = new ReputationAPI(db); + SetUser(api, 7); + SetBody(api, body, contentType, query); + Assert.IsType((await api.SetSelectedCheer()).Result); + return (await db.Reputations.SingleAsync()).SelectedCheer; + } + + Assert.Equal(10, await SelectedCheerFor("Category=10", "application/x-www-form-urlencoded")); + Assert.Equal(20, await SelectedCheerFor("", null!, "?category=20")); + Assert.Equal(30, await SelectedCheerFor("30", "text/plain")); + Assert.Equal(40, await SelectedCheerFor("{\"Category\":40}", "application/json")); + } + + [Fact] + public async Task Reputation_CheerPlayerCountsTheCategoryTheClientSends() + { + // The client's ladder is 0/10/20/30/40. With sequential enum values none of the + // four cheers below matched a switch arm, so they only moved Noteriety. + var db = CreateDb(); + var api = new ReputationAPI(db); + SetUser(api, 7); + SetBody(api, "PlayerId=9&Category=10", "application/x-www-form-urlencoded"); + + var result = await api.CheerPlayer(); + + Assert.IsType(result.Result); + var reputation = await db.Reputations.SingleAsync(x => x.AccountId == 9); + Assert.Equal(1, reputation.CheerHelpful); + } + + [Fact] + public async Task Reputation_SelectedCheerAcceptsTheDeveloperBadge() + { + // 9000 is the client's RecRoomDeveloper badge. The developer profile screen posts + // it like any other badge, and a server that only knew 0/10/20/30/40 answered + // with an unknown-category error instead of equipping it. + var db = CreateDb(); + var api = new ReputationAPI(db); + SetUser(api, 7); + SetBody(api, "Category=9000", "application/x-www-form-urlencoded"); + + Assert.IsType((await api.SetSelectedCheer()).Result); + Assert.Equal(9000, (await db.Reputations.SingleAsync()).SelectedCheer); + } + + [Theory] + [InlineData(50)] // the spendable credit balance + [InlineData(9000)] // the staff-only badge + public async Task Reputation_CheerPlayerRefusesCategoriesThatAreNotAwardable(int category) + { + // Credit is the currency the client spends on cheers; a client that could buy it + // by cheering would never run out. RecRoomDeveloper is staff-only. Neither is + // something a player can hand out. + var db = CreateDb(); + var api = new ReputationAPI(db); + SetUser(api, 7); + SetBody(api, $"PlayerId=9&Category={category}", "application/x-www-form-urlencoded"); + + var result = await api.CheerPlayer(); + + Assert.IsType(result.Result); + Assert.Empty(await db.Reputations.ToListAsync()); + } + + [Fact] + public async Task CreateActionLink_ReturnsScalarResultCode() + { + // The client converts the response root ("Object must implement + // IConvertible" when it gets an array), so the endpoint must return a + // bare scalar, not an array/object. + var api = new LinkAPI(); + var context = new DefaultHttpContext(); + context.Request.Method = "POST"; + context.Request.ContentType = "application/json"; + context.Request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("{\"roomId\": 10}")); + api.ControllerContext = new ControllerContext { HttpContext = context }; + + var action = await api.CheckActionLink(); + var ok = Assert.IsType(action.Result); + + var json = Serialize(ok.Value); + using var doc = JsonDocument.Parse(json); + Assert.Equal(JsonValueKind.Number, doc.RootElement.ValueKind); + } + + [Fact] + public async Task VersionCheck_AllowsPlay_WhenNoGameUpdateIsPublished() + { + // Wire shape pinned to the working 2023-era reference server (FemRec-2023, + // archived at essentialsasset/RecRoom-Live-Servers): the client's strict + // key lookups abort boot ("RecNet version check failed", error code: + // student) unless ValidVersion is present, even on HTTP 200. With no Game + // Update published the endpoint never gates - the client sends varying query + // keys across boots (v=, version=, none) - and the version a client sends is + // still never what decides. The one thing that does flip this answer is a + // live Game Update announcement: see GameUpdateVersionCheckTests. + var db = CreateDb(); + db.ServerConfig.Add(new ServerConfigVal("GameVersion", DBConfig.GameVersion)); + await db.SaveChangesAsync(); + var api = new VersionCheckAPI(db); + + foreach (var version in new[] { "20230414", DBConfig.GameVersion, "19990101", null }) + { + var ok = Assert.IsType(await api.Get(version, version)); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.Equal(0, doc.RootElement.GetProperty("ValidVersion").GetInt32()); + Assert.Equal(0, doc.RootElement.GetProperty("VersionStatus").GetInt32()); + Assert.Equal(0, doc.RootElement.GetProperty("UpdateNotificationStage").GetInt32()); + Assert.False(doc.RootElement.GetProperty("IsVersionIslanded").GetBoolean()); + Assert.False(doc.RootElement.GetProperty("IsCrossPlayDisabled").GetBoolean()); + Assert.Equal(5, doc.RootElement.EnumerateObject().Count()); + } + } + + [Fact] + public void VersionCheck_IslandedVersions_IsBareEmptyArray() + { + var api = new VersionCheckAPI(CreateDb()); + + var ok = Assert.IsType(api.GetIslandedVersions()); + + AssertIsEmptyArray(Serialize(ok.Value)); + } + + [Fact] + public void StartupConfig_ReferencePlaceholderEndpoints_ReturnExpectedShapes() + { + var api = new ConfigAPI(CreateDb()); + + foreach (var result in new[] + { + api.GetAzureSpeechConfig(), + api.GetBacktraceConfig(), + api.GetStatsigUserProperties(), + api.GetVoiceConfig(), + }) + { + Assert.IsType(result); + } + + var azure = Assert.IsType(api.GetAzureSpeechConfig()); + var azureKeys = TopLevelKeys(Serialize(azure.Value)); + Assert.Contains("Key", azureKeys); + Assert.Contains("Region", azureKeys); + Assert.Contains("Enabled", azureKeys); + + var backtrace = Assert.IsType(api.GetBacktraceConfig()); + var backtraceKeys = TopLevelKeys(Serialize(backtrace.Value)); + Assert.Contains("ReportBudget", backtraceKeys); + Assert.Contains("VersionRegex", backtraceKeys); + + var amplitude = Assert.IsType(api.GetAmplitudeConfig()); + using var amplitudeDoc = JsonDocument.Parse(Serialize(amplitude.Value)); + Assert.Equal("e1693a1003671058b6abc356c8ba8d59", amplitudeDoc.RootElement.GetProperty("AmplitudeKey").GetString()); + Assert.True(amplitudeDoc.RootElement.GetProperty("UseRudderStack").GetBoolean()); + Assert.Equal("23NiJHIgu3koaGNCZIiuYvIQNCu", amplitudeDoc.RootElement.GetProperty("RudderStackKey").GetString()); + Assert.True(amplitudeDoc.RootElement.GetProperty("UseStatSig").GetBoolean()); + Assert.Equal("client-SBZkOrjD3r1Cat3f3W8K6sBd11WKlXZXIlCWj6l4Aje", amplitudeDoc.RootElement.GetProperty("StatSigKey").GetString()); + Assert.Equal(0, amplitudeDoc.RootElement.GetProperty("StatSigEnvironment").GetInt32()); + } + + [Fact] + public void NameServer_IncludesRecRoom2023ServiceKeys() + { + var nameServer = DBConfig.NameServer; + var keys = TopLevelKeys(Serialize(nameServer)); + + foreach (var key in new[] + { + "Accounts", "AI", "API", "Auth", "BugReporting", "Cards", "CDN", + "Chat", "Clubs", "CMS", "Commerce", "Data", "DataCollection", + "Discovery", "Econ", "GameLogs", "Geo", "Images", "Leaderboard", + "Link", "Lists", "Locker", "Matchmaking", "Moderation", + "Notifications", "PlatformNotifications", "PlayerSettings", + "RoomComments", "RoomieIntegrations", "Rooms", "Storage", + "Strings", "StringsCDN", "Studio", "Thorn", "Videos", "WWW", + }) + { + Assert.Contains(key, keys); + } + + // The dump's `Service` label is a logical route namespace: e.g. Econ routes share + // the econ base while API routes share the API base, and Commerce and Leaderboard + // are separate bases. Each advertised base is a host of its own with no path - the + // client appends its own path literal to it and the patch turns the host back into + // the route prefix (NameserverRedirect.cs). + Assert.Equal(DBConfig.ServiceUrl("api"), nameServer.API); + Assert.Equal(DBConfig.ServiceUrl("econ"), nameServer.Econ); + Assert.Equal(DBConfig.ServiceUrl("commerce"), nameServer.Commerce); + Assert.Equal(DBConfig.ServiceUrl("leaderboard"), nameServer.Leaderboard); + Assert.DoesNotContain("Service", keys); + } + + [Fact] + public void EndpointServiceLabelsResolveThroughTheirConfiguredBaseHosts() + { + // The 2023 endpoint dump's Service column is logical classification. The + // nameserver values are base URLs - hosts now, one per service - and endpoint + // route templates supply the rest. + var apiBase = typeof(EventsAPI).GetCustomAttributes() + .Select(attribute => attribute.Template); + Assert.Contains("api/playerevents", apiBase); + Assert.Equal(DBConfig.ServiceUrl("api"), DBConfig.NameServer.API); + + var econBase = typeof(AvatarV1).GetCustomAttributes() + .Select(attribute => attribute.Template); + Assert.Contains("econ/api/avatar/v1", econBase); + Assert.Equal(DBConfig.ServiceUrl("econ"), DBConfig.NameServer.Econ); + + var commerceBase = typeof(RadicadeServer.Commerce.CommercePlaceholderAPI) + .GetCustomAttributes() + .Select(attribute => attribute.Template); + Assert.Contains("commerce", commerceBase); + var catalog = typeof(RadicadeServer.Commerce.CommercePlaceholderAPI) + .GetMethod("GetCatalogAll")!; + Assert.Contains("api/catalog/v1/all", catalog.GetCustomAttributes() + .Select(attribute => attribute.Template)); + Assert.Equal(DBConfig.ServiceUrl("commerce"), DBConfig.NameServer.Commerce); + + var leaderboardRoutes = typeof(LeaderboardAPI) + .GetCustomAttributes() + .Select(attribute => attribute.Template); + Assert.Contains("leaderboard/leaderboard", leaderboardRoutes); + var leaderboardCompatRoutes = typeof(LeaderboardCompatAPI) + .GetCustomAttributes() + .Select(attribute => attribute.Template); + Assert.Contains("leaderboard", leaderboardCompatRoutes); + Assert.Equal(DBConfig.ServiceUrl("leaderboard"), DBConfig.NameServer.Leaderboard); + } + + [Fact] + public void AvatarV1_DefaultUnlocks_HasEconServiceRouteAlias() + { + var routes = typeof(AvatarV1).GetCustomAttributes().Select(a => a.Template); + + Assert.Contains("api/avatar/v1", routes); + Assert.Contains("econ/api/avatar/v1", routes); + } + + [Fact] + public void AvatarV1_DefaultBaseAvatarItems_ReturnsReferenceShape() + { + var api = new AvatarV1(CreateDb()); + + var ok = Assert.IsType(api.GetDefaultBaseAvatarItems()); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind); + Assert.All(doc.RootElement.EnumerateArray(), item => + Assert.True(item.GetProperty("IsBaseAvatarItem").GetBoolean())); + } + + [Fact] + public async Task AvatarV4_Items_FiltersExistingLegacyUnlockBlobs() + { + var db = CreateDb(); + db.Unlocks.Add(new RadicadeServer.RecNet.Accounts.PlayerUnlocks + { + AccountId = 23, + AvatarItems = """ +[ + { + "AvatarItemDesc": "07116e9b-b182-40b4-90c2-99738e7aedd6,", + "UnlockedLevel": 0, + "PlatformMask": -1, + "FriendlyName": "Parrot Purse", + "Tooltip": "", + "Rarity": 50 + }, + { + "AvatarItemDesc": "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,", + "UnlockedLevel": 0, + "PlatformMask": -1, + "FriendlyName": "Coach Shirt", + "Tooltip": "", + "Rarity": 0 + } +] +""" + }); + await db.SaveChangesAsync(); + + var api = new AvatarV4(db); + SetUser(api, 23); + + var ok = Assert.IsType((await api.GetLocalAvatarItems()).Result); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + var descs = doc.RootElement + .EnumerateArray() + .Select(item => item.GetProperty("AvatarItemDesc").GetString()) + .ToArray(); + + Assert.DoesNotContain("07116e9b-b182-40b4-90c2-99738e7aedd6,", descs); + Assert.Contains("d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,", descs); + Assert.Equal(descs.Length, descs.Distinct().Count()); + } + + [Fact] + public async Task AvatarV4_Items_PreservesPlayerUnlocksBeyondCoachShirt() + { + var db = CreateDb(); + const string goggles = "_qXaNbm4yEeyalo372QbvA,"; + db.Unlocks.Add(new PlayerUnlocks + { + AccountId = 24, + AvatarItems = """ +[{"AvatarItemDesc":"_qXaNbm4yEeyalo372QbvA,,,","UnlockedLevel":0,"PlatformMask":-1,"FriendlyName":"Biker Goggles","Tooltip":"","Rarity":20}] +""" + }); + await db.SaveChangesAsync(); + var api = new AvatarV4(db); + SetUser(api, 24); + + var ok = Assert.IsType((await api.GetLocalAvatarItems()).Result); + var items = Assert.IsType>(ok.Value); + Assert.Contains(items, item => item.AvatarItemDesc == goggles); + } + + [Fact] + public async Task AvatarV4_Items_ServesCatalogToUnauthenticatedWardrobeRequests() + { + var db = CreateDb(); + var api = new AvatarV4(db); + // The client asks for this list from inside a room when the customize menu opens, + // and that request arrives without a usable bearer token. Answering 401 there + // leaves the player with an empty wardrobe, so the route must stay anonymous. + api.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity()) } + }; + + var result = await api.GetLocalAvatarItems(); + + Assert.IsType(result.Result); + } + + /// + /// The 2023 client saves outfits through /econ/api/avatar/v4/saved/set and expects a + /// lean { Success, Slot } ack - not the outfit it just posted. A 404 here is what the + /// client's "save this outfit" silently does when the route is missing. + /// + [Fact] + public async Task AvatarV4_SavedOutfitSet_AnswersTheLeanSuccessAck() + { + var db = CreateDb(); + var api = new AvatarV4(db); + SetUser(api, 23); + + var result = await api.SetSavedOutfit(new Outfit + { + Slot = 1, + PreviewImageName = "outfit-one.png", + OutfitSelectionsV2 = "[\"a\",\"b\"]", + CustomAvatarItems = new object[] { "item-1" }, + }); + + var ok = Assert.IsType(result); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + Assert.True(doc.RootElement.GetProperty("Success").GetBoolean()); + Assert.Equal(1, doc.RootElement.GetProperty("Slot").GetInt32()); + } + + /// + /// v4 and v3 are one slot table: the client saves through v4 and lists through v3, and + /// "save over this outfit" is a re-save of an existing slot. Separate stores would let a + /// player save an outfit that their own outfit list cannot find. + /// + [Fact] + public async Task AvatarV4_SavedOutfitSet_IsVisibleToTheV3ListAndOverwritesItsSlot() + { + var db = CreateDb(); + var v4 = new AvatarV4(db); + SetUser(v4, 77); + + Assert.IsType(await v4.SetSavedOutfit(new Outfit { Slot = 0, PreviewImageName = "first.png" })); + Assert.IsType(await v4.SetSavedOutfit(new Outfit { Slot = 1, PreviewImageName = "second.png" })); + Assert.IsType(await v4.SetSavedOutfit(new Outfit { Slot = 0, PreviewImageName = "replaced.png" })); + + var v3 = new AvatarV3(db); + SetUser(v3, 77); + var outfits = await v3.GetSavedOutfits(); + + Assert.Equal(2, outfits.Count); + Assert.Equal("replaced.png", outfits.Single(x => x.Slot == 0).PreviewImageName); + Assert.Equal("second.png", outfits.Single(x => x.Slot == 1).PreviewImageName); + } + + /// + /// A body with no slot is refused instead of defaulted: guessing would overwrite slot 0, + /// which is exactly the outfit the player did not ask to replace. + /// + [Fact] + public async Task AvatarV4_SavedOutfitSet_RefusesABodyWithNoSlot() + { + var db = CreateDb(); + var api = new AvatarV4(db); + SetUser(api, 23); + + var result = await api.SetSavedOutfit(new Outfit { PreviewImageName = "orphan.png" }); + + Assert.IsType(result); + Assert.Empty(db.Avatars); + } + + /// + /// A player who has never saved an outfit gets an empty list, not a 404 - the client's + /// saved-outfits screen reads this at the same time as the wardrobe. + /// + [Fact] + public async Task AvatarV4_SavedOutfitSet_NeverLeavesANullSlotBehind() + { + var db = CreateDb(); + var api = new AvatarV4(db); + SetUser(api, 23); + + // Pre-existing player data can carry the leading null the old handler stored when it + // accepted an empty body. + db.Avatars.Add(new PlayerAvatar { AccountId = 23, Avatar = "{}", Outfits = "[null]" }); + await db.SaveChangesAsync(); + + Assert.IsType(await api.SetSavedOutfit(new Outfit { Slot = 0 })); + + var stored = await db.Avatars.AsNoTracking().SingleAsync(x => x.AccountId == 23); + using var doc = JsonDocument.Parse(stored.Outfits); + var entry = Assert.Single(doc.RootElement.EnumerateArray()); + Assert.NotEqual(JsonValueKind.Null, entry.ValueKind); + + var v3 = new AvatarV3(db); + SetUser(v3, 23); + Assert.Single(await v3.GetSavedOutfits()); + } + + [Fact] + public void AvatarCatalog_PreservesBrowsableMetadata() + { + var item = new RadicadeServer.RecNet.Avatar.AvatarItem + { + AvatarItemDesc = "_OWVy3z6iU-M3-zbQgSLig,,,", AvatarItemId = 835, + AvatarItemType = 0, FriendlyName = "Vampire Hunter Gloves (Blue)", + ThumbnailImage = "cimomkml6k4toyowd1voh7hqm.png", TagList = "gloves", + CreatedAt = new DateTime(2018, 11, 1, 17, 51, 50, DateTimeKind.Utc) + }; + var copy = Assert.Single(RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.KeepSupportedItems([item])); + using var doc = JsonDocument.Parse(Serialize(copy)); + Assert.Equal(835, doc.RootElement.GetProperty("AvatarItemId").GetInt32()); + Assert.Equal(item.ThumbnailImage, copy.ThumbnailImage); + Assert.Equal(item.TagList, copy.TagList); + Assert.Equal(item.CreatedAt, copy.CreatedAt); + Assert.False(doc.RootElement.GetProperty("IsBaseAvatarItem").GetBoolean()); + Assert.Equal(0, doc.RootElement.GetProperty("AvatarItemType").GetInt32()); + } + + [Fact] + public void AvatarCatalog_ReadsConfiguredCatalogInsteadOfBootFallback() + { + var originalCatalog = DBConfig.DefaultAvatarCatalog; + var originalFallback = DBConfig.DefaultAvatarUnlocks; + try + { + DBConfig.DefaultAvatarCatalog = """ + [{"AvatarItemDesc":"catalog-item,,,","UnlockedLevel":0,"PlatformMask":-1,"FriendlyName":"Catalog Item","Rarity":1}] + """; + DBConfig.DefaultAvatarUnlocks = """ + [{"AvatarItemDesc":"fallback-item,,,","UnlockedLevel":0,"PlatformMask":-1,"FriendlyName":"Fallback","Rarity":1}] + """; + var items = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.GetSafeDefaultItems(); + Assert.Contains(items, item => item.AvatarItemDesc == "catalog-item,"); + Assert.DoesNotContain(items, item => item.AvatarItemDesc == "fallback-item,"); + } + finally + { + DBConfig.DefaultAvatarCatalog = originalCatalog; + DBConfig.DefaultAvatarUnlocks = originalFallback; + } + } + + [Fact] + public async Task AvatarV2_GetLocalAvatar_RepairsShortUnsafeOutfitSelection() + { + var db = CreateDb(); + db.Avatars.Add(new RadicadeServer.RecNet.Accounts.Local.PlayerAvatar + { + AccountId = 2, + Avatar = """ +{"OutfitSelections":"d0a9262f-5504-46a7-bb10-7507503db58e,,,,1","HairColor":"fT0t-J1A-0i_tdoogFEz-g","SkinColor":"85343b16-d58a-4091-96d8-083a81fb03ae","FaceFeatures":"{}"} +""", + Outfits = "[]" + }); + await db.SaveChangesAsync(); + + var api = new AvatarV2(db); + SetUser(api, 2); + + var avatar = await api.GetLocalAvatar(); + var storedAvatar = await db.Avatars.SingleAsync(x => x.AccountId == 2); + + Assert.NotNull(avatar); + Assert.Equal( + "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,,1", + avatar.OutfitSelections); + Assert.Contains("95e4cc30-cb68-473d-a395-feadf5b51512", storedAvatar.Avatar); + Assert.Contains("0440f08f-ef1d-49d8-942b-523056e8bb45", storedAvatar.Avatar); + } + + [Fact] + public void AvatarUnlockCatalog_PreservesConfiguredItemsExceptKnownUnsafeEntries() + { + var original = DBConfig.DefaultAvatarUnlocks; + try + { + DBConfig.DefaultAvatarUnlocks = """ +[ + { + "AvatarItemDesc": "07116e9b-b182-40b4-90c2-99738e7aedd6,", + "UnlockedLevel": 0, + "PlatformMask": -1, + "FriendlyName": "Parrot Purse", + "Tooltip": "", + "Rarity": 50 + }, + { + "AvatarItemDesc": "11b3413f-2c18-41cb-8ef2-55fb825ed476,", + "UnlockedLevel": 0, + "PlatformMask": -1, + "FriendlyName": "Missing 2023 Client Prefab", + "Tooltip": "", + "Rarity": 50 + }, + { + "AvatarItemDesc": "_qXaNbm4yEeyalo372QbvA,,,", + "UnlockedLevel": 0, + "PlatformMask": -1, + "FriendlyName": "Biker Goggles", + "Tooltip": "", + "Rarity": 20 + } +] +"""; + + var items = RadicadeServer.RecNet.Avatar.AvatarUnlockCatalog.GetSafeDefaultItems(); + + Assert.DoesNotContain(items, item => + item.AvatarItemDesc == "07116e9b-b182-40b4-90c2-99738e7aedd6,"); + Assert.DoesNotContain(items, item => + item.AvatarItemDesc == "11b3413f-2c18-41cb-8ef2-55fb825ed476,"); + Assert.Contains(items, item => + item.AvatarItemDesc == "_qXaNbm4yEeyalo372QbvA,"); + Assert.Single(items); + } + finally + { + DBConfig.DefaultAvatarUnlocks = original; + } + } + + [Fact] + public void EacChallenge_ReturnsJsonStringNotRawText() + { + var api = new EACApi(); + + var json = Assert.IsType(api.GetFakeEACChallenge()); + var serialized = Serialize(json.Value); + + using var doc = JsonDocument.Parse(serialized); + Assert.Equal(JsonValueKind.String, doc.RootElement.ValueKind); + Assert.StartsWith("AQAAA", doc.RootElement.GetString()); + } + + [Fact] + public void RecRoom2023_ServiceRouteAliases_AreRegistered() + { + string[] Routes() => typeof(T).GetCustomAttributes().Select(a => a.Template ?? "").ToArray(); + + Assert.Contains("econ/api/avatar/v2", Routes()); + Assert.Contains("econ/api/avatar/v4", Routes()); + + // The client saves outfits through v4 and reads them back through v3. + Assert.Contains("saved/set", typeof(AvatarV4) + .GetMethod(nameof(AvatarV4.SetSavedOutfit))! + .GetCustomAttributes() + .Select(a => a.Template)); + Assert.Contains("saved", typeof(AvatarV3) + .GetMethod(nameof(AvatarV3.GetSavedOutfits))! + .GetCustomAttributes() + .Select(a => a.Template)); + Assert.Contains("econ/api/objectives/v1", Routes()); + Assert.Contains("econ/econ/customAvatarItems", Routes()); + Assert.Contains("econ/api/storefronts/v3", Routes()); + Assert.Contains("cdn/sigs", Routes()); + Assert.Contains("playersettings", Routes()); + + var settingsRoute = typeof(PlayerSettingsAPI) + .GetMethod(nameof(PlayerSettingsAPI.GetSettings))! + .GetCustomAttributes() + .Select(a => a.Template); + Assert.Contains("", settingsRoute); + Assert.Contains("playersettings", settingsRoute); + + var setSettingsRoute = typeof(PlayerSettingsAPI) + .GetMethod(nameof(PlayerSettingsAPI.PutSettings))! + .GetCustomAttributes() + .Select(a => a.Template); + Assert.Contains("", setSettingsRoute); + Assert.Contains("playersettings", setSettingsRoute); + + var exclusiveLoginRoute = typeof(MatchmakingPlayerAPI) + .GetMethod(nameof(MatchmakingPlayerAPI.ExclusiveLogin))! + .GetCustomAttributes() + .Select(a => a.Template); + Assert.Contains("exclusivelogin", exclusiveLoginRoute); + } + + [Fact] + public async Task MatchmakingPlayer_ExclusiveLogin_UsesRegularLoginShape() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + + await using var db = factory.CreateDbContext(); + db.Presences.Add(new RealPresence { playerId = 52 }); + db.Rooms.Add(new RoomData + { + RoomId = 1, + Name = "DormRoom", + IsDorm = true, + CreatorAccountId = 1, + Accessibility = Accessibility.Private, + MaxPlayers = 4, + CreatedAt = DateTime.UtcNow, + }); + db.SubRooms.Add(new SubRoom + { + RoomId = 1, + Name = "DormRoom", + UnitySceneId = "DormRoomScene", + MaxPlayers = 4, + Accessibility = Accessibility.Private, + DataBlob = "", + }); + await db.SaveChangesAsync(); + + var api = new MatchmakingPlayerAPI(db); + SetUser(api, 52, "steam-test"); + + var ok = Assert.IsType( + await api.ExclusiveLogin(loginLock: "lock-52")); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal(52, doc.RootElement.GetProperty("playerId").GetInt64()); + Assert.Equal("lock-52", doc.RootElement.GetProperty("loginLock").GetString()); + Assert.NotNull((await db.Presences.FirstAsync(x => x.playerId == 52)).roomInstance); + } + + [Fact] + public async Task MatchmakingPlayerPresence_DoesNotCreateDormAsALookupSideEffect() + { + const long playerId = 77; + + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + + await using var db = factory.CreateDbContext(); + db.Profiles.Add(new RadicadeServer.RecNet.Auth.Profile + { + accountId = playerId, + username = "test-player", + displayName = "TestPlayer", + profileImage = "DefaultProfileImage", + platforms = 0, + createdAt = DateTime.UtcNow, + }); + db.Presences.Add(new RealPresence + { + playerId = playerId, + isOnline = false, + deviceClass = DeviceClass.Screen, + statusVisibility = StatusVisibility.Public, + }); + db.Rooms.Add(new RoomData + { + RoomId = 1, + Name = "DormRoom", + IsDorm = true, + CreatorAccountId = 1, + Accessibility = Accessibility.Private, + MaxPlayers = 4, + CreatedAt = DateTime.UtcNow, + }); + db.SubRooms.Add(new SubRoom + { + RoomId = 1, + Name = "DormRoom", + UnitySceneId = "DormRoomScene", + MaxPlayers = 4, + Accessibility = Accessibility.Private, + DataBlob = "", + }); + await db.SaveChangesAsync(); + + var api = new MatchmakingAPI(db); + api.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + + var presences = await api.GetPlayerPresence(new List { playerId }); + var presence = Assert.Single(presences); + var storedPresence = await db.Presences.FirstAsync(x => x.playerId == playerId); + + Assert.Equal(playerId, presence.playerId); + Assert.False(presence.isOnline); + Assert.Null(presence.roomInstance); + Assert.Null(storedPresence.roomInstance); + Assert.Empty(await db.RoomInstances.ToListAsync()); + } + + [Fact] + public async Task InventionsFromCreators_ReturnsCreatorOwnedItemsInNewestFirstOrder() + { + using var db = CreateDb(); + db.Inventions.AddRange( + new InventionDB + { + CreatorPlayerId = 7, + InventionId = 1, + Name = "Older creator item", + CreatedAt = new DateTime(2023, 9, 1, 0, 0, 0, DateTimeKind.Utc), + IsPublished = false, + }, + new InventionDB + { + CreatorPlayerId = 7, + InventionId = 2, + Name = "Newer creator item", + CreatedAt = new DateTime(2023, 9, 2, 0, 0, 0, DateTimeKind.Utc), + IsPublished = true, + }, + new InventionDB + { + CreatorPlayerId = 8, + InventionId = 3, + Name = "Other creator item", + CreatedAt = new DateTime(2023, 9, 3, 0, 0, 0, DateTimeKind.Utc), + IsPublished = true, + }); + await db.SaveChangesAsync(); + + var api = new InventionsV1(db); + var route = typeof(InventionsV1).GetMethod(nameof(InventionsV1.GetInventionsFromCreators))! + .GetCustomAttributes() + .Select(attribute => attribute.Template); + Assert.Contains("fromcreators", route); + + var response = Assert.IsType( + await api.GetInventionsFromCreators([7], skip: 0, take: 1)); + var items = Assert.IsAssignableFrom>(response.Value).ToArray(); + var item = Assert.Single(items); + Assert.Equal("Newer creator item", item.Name); + Assert.True(item.IsPublished); + + var otherCreators = Assert.IsType( + await api.GetInventionsFromCreators([7, 8], skip: 0, take: 10)); + Assert.Equal(3, Assert.IsAssignableFrom>(otherCreators.Value).Count()); + + var missingCreators = Assert.IsType( + await api.GetInventionsFromCreators(null)); + Assert.NotNull(missingCreators.Value); + } + + [Fact] + public void DataCollection_Placeholders_ReturnSuccessShapes() + { + var api = new DataCollectionAPI(); + + Assert.Equal("{}", Serialize(Assert.IsType(api.Event()).Value)); + Assert.Equal("[]", Serialize(Assert.IsType(api.Events()).Value)); + Assert.Equal("{}", Serialize(Assert.IsType(api.Sampling()).Value)); + } + + [Fact] + public void CustomAvatarItems_Placeholders_ReturnReferenceShapes() + { + using var db = CreateDb(); + var api = new CustomAvatarItemsAPI(db); + SetUser(api, CreatorId); + + // The gate the drawing screen asks before it enables anything. Its answer has to be the + // client's own two-member result, not the server's PascalCase one: a "success" body is an + // unknown member to that reader, its success flag stays false, and every create/modify + // option on the screen comes up disabled. + var allowed = Assert.IsType(api.IsCreationAllowedForAccount()); + var allowedKeys = TopLevelKeys(Serialize(allowed.Value)); + Assert.Contains("ALOCDGMHFKA", allowedKeys); + Assert.Contains("FBILGKFKFJE", allowedKeys); + Assert.True(Assert.IsType(allowed.Value!.GetType().GetProperty("ALOCDGMHFKA")!.GetValue(allowed.Value))); + + Assert.Equal("true", Serialize(Assert.IsType(api.IsEnabled()).Value)); + + // Scalars, read into an int: the body has to be a bare number, since an object wrapper + // fails that read. Both were unmapped, so the item details page logged a 404 on open. + Assert.Equal("0", Serialize(Assert.IsType(api.MinPriceForPublicItem()).Value)); + Assert.True(Assert.IsType(api.ItemOwnershipLimit()).Value is int limit && limit > 0); + + // The two bare-array routes. The client reads them with a plain-list reader, so a page + // here would fail it at offset 0. + Assert.Equal("[]", Serialize(Assert.IsType(api.Featured().GetAwaiter().GetResult()).Value)); + + // The paged { Results, TotalResults } object, not a bare array: the client reads these + // routes into a paged DTO and a body it cannot bind throws inside the wardrobe's item + // download, leaving the customize screen empty. + var owned = Assert.IsType(api.Owned().GetAwaiter().GetResult()); + var ownedBody = Serialize(owned.Value); + var ownedKeys = TopLevelKeys(ownedBody); + Assert.Contains("Results", ownedKeys); + Assert.Contains("TotalResults", ownedKeys); + Assert.StartsWith("{", ownedBody); + + var page = Assert.IsType(api.FromCreator(2).GetAwaiter().GetResult()); + var pageKeys = TopLevelKeys(Serialize(page.Value)); + Assert.Contains("Results", pageKeys); + Assert.Contains("TotalResults", pageKeys); + + // The bare /v1 and /v1/me spellings the same client also asks for. Both were unmapped, + // so every boot logged a 404 for them; they must carry the paged body, not a 404 and + // not a bare array. + var routes = typeof(CustomAvatarItemsAPI) + .GetMethod(nameof(CustomAvatarItemsAPI.Items))! + .GetCustomAttributes() + .Select(a => a.Template) + .ToArray(); + Assert.Contains("v1", routes); + Assert.Contains("v1/me", routes); + + var items = Assert.IsType(api.Items().GetAwaiter().GetResult()); + var itemsBody = Serialize(items.Value); + Assert.StartsWith("{", itemsBody); + Assert.Contains("Results", TopLevelKeys(itemsBody)); + Assert.Contains("TotalResults", TopLevelKeys(itemsBody)); + + var saves = Assert.IsType(api.CurrentSavesForLegacyAvatarItems()); + Assert.Contains("customAvatarItemSavesByAvatarItemDesc", TopLevelKeys(Serialize(saves.Value))); + } + + /// + /// The create call, exactly as the client sent it on the wire: multipart form data with a + /// metadata part and a thumbnail file part. This is the request the server used to + /// answer with an empty array, so the item was never stored and never showed up in the + /// creator's lists afterwards. + /// + [Fact] + public async Task CustomAvatarItems_Create_StoresTheItemAndServesItBack() + { + using var db = CreateDb(); + var api = new CustomAvatarItemsAPI(db); + SetUser(api, CreatorId); + + const string metadata = "{\"Name\":\"Test\",\"Description\":\"Test\",\"Price\":0,\"BaseAvatarItemId\":2184,\"BaseAvatarItemColor\":\"#F55C1A\",\"Accessibility\":0}"; + var thumbnail = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 1, 2, 3, 4 }; + var design = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 5, 6, 7, 8 }; + var (body, contentType) = MultipartBody(metadata, thumbnail, design); + + api.ControllerContext.HttpContext.Request.ContentType = contentType; + api.ControllerContext.HttpContext.Request.Body = new MemoryStream(body); + + var created = Assert.IsType(await api.Create()); + var createdBody = Serialize(created.Value); + var createdKeys = TopLevelKeys(createdBody); + Assert.Contains("CustomAvatarItemId", createdKeys); + Assert.Contains("ImageName", createdKeys); + Assert.Contains("DesignFilename", createdKeys); + + // The key the client's card art is actually looked up by. Without it the item lists in + // the watch but every card draws the placeholder, because the reader has no thumbnail + // name to fetch - which is what a create response carrying only ImageName produced. + Assert.Contains("ThumbnailImageFilename", createdKeys); + Assert.Contains("IsFeatured", createdKeys); + + using var doc = JsonDocument.Parse(createdBody); + var itemId = doc.RootElement.GetProperty("CustomAvatarItemId").GetString(); + Assert.True(Guid.TryParse(itemId, out _)); + Assert.Equal("Test", doc.RootElement.GetProperty("Name").GetString()); + Assert.Equal(2184, doc.RootElement.GetProperty("BaseAvatarItemId").GetInt32()); + Assert.Equal("#F55C1A", doc.RootElement.GetProperty("BaseAvatarItemColor").GetString()); + + // Both uploads landed where the image route serves them from: the thumbnail is the card + // art, the design is the texture the shirt is drawn with. + var imageName = doc.RootElement.GetProperty("ImageName").GetString(); + var designName = doc.RootElement.GetProperty("DesignFilename").GetString(); + + // Both spellings of the thumbnail name point at the same file, so whichever one the + // client's reader binds, the card art it asks the CDN for is the file that was written. + Assert.Equal(imageName, doc.RootElement.GetProperty("ThumbnailImageFilename").GetString()); + + var imagePath = Path.Join(DBConfig.CDNImages, imageName); + var designPath = Path.Join(DBConfig.CDNImages, designName); + Assert.True(File.Exists(imagePath), $"expected the thumbnail at {imagePath}"); + Assert.True(File.Exists(designPath), $"expected the design at {designPath}"); + Assert.NotEqual(imageName, designName); + try + { + // The design lookup the item pages make: a bare list, with the item's record in it. + var designApi = new CustomAvatarItemsAPI(db); + SetUser(designApi, CreatorId); + var designs = Assert.IsType(await designApi.Design()); + var designsBody = Serialize(designs.Value); + Assert.StartsWith("[", designsBody); + Assert.Contains(designName, designsBody); + + // ... and the same lookup for another account stays empty while the item is private. + var strangerView = new CustomAvatarItemsAPI(db); + SetUser(strangerView, 999); + Assert.Equal("[]", Serialize(Assert.IsType(await strangerView.Design()).Value)); + + // ... and the item is readable again: paged lists, single-item fetch, and the + // resolve call that takes ids. + var mine = Assert.IsType(await api.Items()); + using var mineDoc = JsonDocument.Parse(Serialize(mine.Value)); + Assert.Equal(1, mineDoc.RootElement.GetProperty("TotalResults").GetInt32()); + Assert.Equal(itemId, mineDoc.RootElement.GetProperty("Results")[0].GetProperty("CustomAvatarItemId").GetString()); + + // The list is what the watch draws its cards from, so the thumbnail name has to + // survive into it and not only into the create response. + Assert.Equal(imageName, mineDoc.RootElement.GetProperty("Results")[0].GetProperty("ThumbnailImageFilename").GetString()); + + var single = Assert.IsType(await api.Item(Guid.Parse(itemId))); + using var singleDoc = JsonDocument.Parse(Serialize(single.Value)); + Assert.Equal("Test", singleDoc.RootElement.GetProperty("Name").GetString()); + + // Fresh controllers: the form is parsed once per request, and the create above + // already consumed this context's body. + var resolveApi = new CustomAvatarItemsAPI(db); + SetUser(resolveApi, CreatorId); + SetBody(resolveApi, $"customAvatarItemIds={itemId}", "application/x-www-form-urlencoded"); + var bulk = Assert.IsType(await resolveApi.Bulk()); + var bulkBody = Serialize(bulk.Value); + Assert.StartsWith("[", bulkBody); + Assert.Contains("Test", bulkBody); + + // Another player asking for the same ids gets nothing while the item is private. + var strangerApi = new CustomAvatarItemsAPI(db); + SetUser(strangerApi, 999); + SetBody(strangerApi, $"customAvatarItemIds={itemId}", "application/x-www-form-urlencoded"); + Assert.Equal("[]", Serialize(Assert.IsType(await strangerApi.Bulk()).Value)); + } + finally + { + if (File.Exists(imagePath)) + File.Delete(imagePath); + if (File.Exists(designPath)) + File.Delete(designPath); + } + } + + /// + /// The design save, which the item pages make after a create and whenever a design is edited. + /// It has to attach to the right item: the id the caller names, or - which is what the create + /// flow means - the item they made most recently. + /// + [Fact] + public async Task CustomAvatarItems_SaveDesign_AttachesToTheCallersNewestItem() + { + using var db = CreateDb(); + var older = new CustomAvatarItemDB + { + CustomAvatarItemId = Guid.NewGuid(), + CreatorAccountId = CreatorId, + Name = "Older", + BaseAvatarItemColor = "#111111", + CreatedAt = DateTime.UtcNow.AddMinutes(-5), + ModifiedAt = DateTime.UtcNow.AddMinutes(-5), + }; + var newest = new CustomAvatarItemDB + { + CustomAvatarItemId = Guid.NewGuid(), + CreatorAccountId = CreatorId, + Name = "Newest", + BaseAvatarItemColor = "#222222", + CreatedAt = DateTime.UtcNow, + ModifiedAt = DateTime.UtcNow, + }; + db.CustomAvatarItems.Add(older); + db.CustomAvatarItems.Add(newest); + await db.SaveChangesAsync(); + + var image = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 9, 9, 9 }; + var (body, contentType) = MultipartBody("{}", image, image); + + var api = new CustomAvatarItemsAPI(db); + SetUser(api, CreatorId); + api.ControllerContext.HttpContext.Request.ContentType = contentType; + api.ControllerContext.HttpContext.Request.QueryString = new QueryString("?customAvatarItemColor=%23FF0000"); + api.ControllerContext.HttpContext.Request.Body = new MemoryStream(body); + + var saved = Assert.IsType(await api.SaveDesign()); + using var doc = JsonDocument.Parse(Serialize(saved.Value)); + Assert.Equal(newest.CustomAvatarItemId.ToString(), doc.RootElement.GetProperty("CustomAvatarItemId").GetString()); + Assert.Equal("#FF0000", doc.RootElement.GetProperty("CustomAvatarItemColor").GetString()); + + // The upload is kept where the image route serves it from, and the design lookup finds it. + var designName = doc.RootElement.GetProperty("DesignFilename").GetString(); + Assert.False(string.IsNullOrEmpty(designName)); + var designPath = Path.Join(DBConfig.CDNImages, designName); + Assert.True(File.Exists(designPath), $"expected the design at {designPath}"); + try + { + var lookup = new CustomAvatarItemsAPI(db); + SetUser(lookup, CreatorId); + SetBody(lookup, null, "application/json", "?customAvatarItemId=" + newest.CustomAvatarItemId); + var found = Serialize(Assert.IsType(await lookup.Design()).Value); + Assert.Contains(newest.CustomAvatarItemId.ToString(), found); + Assert.DoesNotContain(older.CustomAvatarItemId.ToString(), found); + } + finally + { + if (File.Exists(designPath)) + File.Delete(designPath); + } + } + + /// + /// Builds the multipart body BestHTTP produces: a literal form-data envelope, headers and + /// all, so the controller's own parsing is what is under test. + /// + private static (byte[] Body, string ContentType) MultipartBody(string metadata, byte[] thumbnail, byte[] design) + { + const string boundary = "BestHTTP_HTTPMultiPartForm_Test"; + using var stream = new MemoryStream(); + + void Write(string text) => stream.Write(Encoding.UTF8.GetBytes(text)); + + void WriteFile(string name, byte[] bytes) + { + Write($"--{boundary}\r\n"); + Write("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"file.bin\"\r\n"); + Write("Content-Type: image/png\r\n\r\n"); + stream.Write(bytes); + Write("\r\n"); + } + + Write($"--{boundary}\r\n"); + Write("Content-Disposition: form-data; name=\"metadata\"\r\n"); + Write("Content-Type: text/plain; charset=utf-8\r\n\r\n"); + Write(metadata); + Write("\r\n"); + WriteFile("thumbnailImage", thumbnail); + WriteFile("design", design); + Write($"--{boundary}--\r\n"); + + return (stream.ToArray(), $"multipart/form-data; boundary={boundary}"); + } + + [Fact] + public void CdnSigs_ReturnNonEmptyFileFor2023FileCheck() + { + var api = new SigsCDN(); + + var result = Assert.IsType( + api.GetSignature("dcfe17c0-7c51-49e8-ae74-691fc5bfc0ea")); + + Assert.Equal("text/plain", result.ContentType); + Assert.NotEmpty(result.FileContents); + var text = Encoding.ASCII.GetString(result.FileContents); + Assert.Contains("RecRoom.exe:AAAAAAAAAAAAAAAAAAAAAA==:AAAAAAAAAAAAAAAAAAAAAA==", text); + } + + [Fact] + public void CdnSigs_RangeRequestsReturnPartialContentShape() + { + var context = new DefaultHttpContext(); + context.Request.Headers.Range = "bytes=2-4"; + var api = new SigsCDN + { + ControllerContext = new ControllerContext + { + HttpContext = context + } + }; + + var result = Assert.IsType( + api.GetSignature("dcfe17c0-7c51-49e8-ae74-691fc5bfc0ea")); + + Assert.Equal(206, context.Response.StatusCode); + Assert.Equal("bytes", context.Response.Headers.AcceptRanges); + Assert.StartsWith("bytes 2-4/", context.Response.Headers.ContentRange.ToString()); + Assert.Equal(Encoding.ASCII.GetBytes("cRo"), result.FileContents); + } + + [Fact] + public void Hile_ReturnsRawBooleanSuccessBody() + { + var api = new HileAPI(); + + var ok = Assert.IsType( + api.PostHile(new RadicadeServer.RecNet.Moderation.Hile())); + + Assert.True(Assert.IsType(ok.Value)); + } + + [Fact] + public void GameConfigs_DisableLocal2023HashChecks() + { + var db = CreateDb(); + db.GameConfig.Add(new ConfigVal("AndroidMobile.runHashCheck", "1")); + db.GameConfig.Add(new ConfigVal("OculusQuest.enforceHashCheck", "1")); + db.SaveChanges(); + var api = new GameConfigAPI(db); + + var ok = Assert.IsType(api.GetAllGameConfigs()); + var configs = Assert.IsAssignableFrom>(ok.Value); + var byKey = configs.ToDictionary(x => x.Key, x => x.Value); + + Assert.Equal("0", byKey["AndroidMobile.runHashCheck"]); + Assert.Equal("0", byKey["AndroidMobile.enforceHashCheck"]); + Assert.Equal("0", byKey["Desktop_Oculus.runHashCheck"]); + Assert.Equal("0", byKey["Desktop_Steam.runHashCheck"]); + Assert.Equal("0", byKey["OculusQuest.enforceHashCheck"]); + Assert.Equal("0", byKey["OculusQuest.runFileWatcher"]); + Assert.Equal("0", byKey["Steam.runHashCheck"]); + } + + [Theory] + [InlineData(null)] + [InlineData("false")] + public void GameConfigs_EnableRRUIHomeScreen(string? storedValue) + { + var db = CreateDb(); + if (storedValue != null) + { + db.GameConfig.Add(new ConfigVal("RRUI.UseRRUIHomeScreen", storedValue)); + db.SaveChanges(); + } + + var ok = Assert.IsType(new GameConfigAPI(db).GetAllGameConfigs()); + var configs = Assert.IsAssignableFrom>(ok.Value); + Assert.Equal("true", Assert.Single(configs, x => x.Key == "RRUI.UseRRUIHomeScreen").Value); + // USE_NEW_HOME_SCREEN is the watch home selector the 2023 client actually honours. + // Serving "false" here is what produced the old tile-grid watch, so the forced value + // is "true" even when the stored DB row says otherwise. + Assert.Equal("true", Assert.Single(configs, x => x.Key == "USE_NEW_HOME_SCREEN").Value); + Assert.Equal("true", Assert.Single(configs, x => x.Key == "HAS_SEEN_HOME_SCREEN_CHOICE").Value); + Assert.Equal("true", Assert.Single(configs, x => x.Key == "new_watch_architecture_enabled").Value); + Assert.Equal("true", Assert.Single(configs, x => x.Key == "watch_layer.new_watch_architecture_enabled").Value); + } + + [Fact] + public void GameConfigs_SuppressInvalid2023SplitTestConfigs() + { + var db = CreateDb(); + db.GameConfig.Add(new ConfigVal( + "splitTestHardOverrides", + "PlayMenuRRUI_2021_01_11=On")); + db.GameConfig.Add(new ConfigVal( + "splitTestsAvailable", + "PlayMenuRRUI_2021_01_11=Off,On")); + db.GameConfig.Add(new ConfigVal( + "splitTestSegmentProbabilities", + "{\"PlayMenuRRUI_2021_01_11\":[{\"SegmentName\":\"On\",\"Probability\":1.0}]}")); + db.SaveChanges(); + var api = new GameConfigAPI(db); + + var ok = Assert.IsType(api.GetAllGameConfigs()); + var configs = Assert.IsAssignableFrom>(ok.Value); + var byKey = configs.ToDictionary(x => x.Key, x => x.Value); + + // The 2023 client has no enum for PlayMenuRRUI_2021_01_11, so it is filtered + // out of the hard overrides before serving - here that empties the list + // entirely since the seeded row contains nothing else. + Assert.Equal("", byKey["splitTestHardOverrides"]); + // Available list and probabilities pass through from the DB. + Assert.Contains("PlayMenuRRUI_2021_01_11=Off,On", byKey["splitTestsAvailable"]); + Assert.Contains("PlayMenuRRUI_2021_01_11", byKey["splitTestSegmentProbabilities"]); + } + + [Fact] + public async Task Leaderboard_GetRanks_EmptyBoardReturnsSelfRow() + { + var factory = new TestDbContextFactory(); + + var response = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest + { + PlayerId = 2, + StatChannel = 1000, + RankStart = 1, + RankEnd = 10 + }); + + Assert.Equal(1, response.Total); + Assert.Equal(1, response.MyRank); + var row = Assert.Single(response.Rows); + Assert.Equal(2, row.PlayerId); + Assert.Equal(1, row.Rank); + Assert.Equal(0, row.Score); + Assert.Equal(0, row.Time); + } + + [Fact] + public async Task Leaderboard_GetNearbyScores_EmptyBoardReturnsSelfRow() + { + var factory = new TestDbContextFactory(); + + var response = await LeaderboardStore.GetNearbyScoresAsync(factory, new GetNearbyScoresRequest + { + PlayerId = 2, + StatChannel = 1000, + WindowSize = 10 + }); + + Assert.Equal(1, response.Total); + Assert.Equal(1, response.MyRank); + var row = Assert.Single(response.Rows); + Assert.Equal(2, row.PlayerId); + Assert.Equal(1, row.Rank); + } + + [Fact] + public async Task PlayersV2_ProgressionBulk_IncludesReferenceIdField() + { + var db = CreateDb(); + var api = new PlayersV2(db); + + using var json = JsonDocument.Parse(Serialize(await api.GetBulkProgressionV2(new[] { 2 }))); + var first = json.RootElement[0]; + + Assert.Equal(2, first.GetProperty("Id").GetInt64()); + Assert.Equal(2, first.GetProperty("PlayerId").GetInt64()); + Assert.Equal(1, first.GetProperty("Level").GetInt32()); + Assert.Equal(0, first.GetProperty("XP").GetInt32()); + } + + [Fact] + public void PlayerReputationV2_Bulk_ReturnsReferenceShape() + { + var db = CreateDb(); + var api = new ReputationAPI(db); + + var ok = Assert.IsType(api.GetBulkReputationV2(new long[] { 2 })); + using var json = JsonDocument.Parse(Serialize(ok.Value)); + var first = json.RootElement[0]; + + Assert.Equal(2, first.GetProperty("Id").GetInt64()); + Assert.Equal(2, first.GetProperty("accountId").GetInt64()); + // IsCheerful is the "may hand out cheers" gate the client reads, and the reference + // server (RecFlare 2023) answers it true for every account. The badges come from + // the five counters, which an uncheered account reports as 0. + Assert.True(first.GetProperty("IsCheerful").GetBoolean()); + Assert.Equal(0, first.GetProperty("Noteriety").GetSingle()); + Assert.Equal(0, first.GetProperty("SelectedCheer").GetInt32()); + Assert.Equal(20, first.GetProperty("CheerCredit").GetInt32()); + Assert.Equal(0, first.GetProperty("CheerGeneral").GetInt32()); + Assert.Equal(0, first.GetProperty("CheerHelpful").GetInt32()); + Assert.False(first.TryGetProperty("AccountId", out _)); + Assert.False(first.TryGetProperty("ReputationScore", out _)); + Assert.False(first.TryGetProperty("Trust", out _)); + } + + [Fact] + public async Task PlayerSettings_PutAlias_SavesSettings() + { + var db = CreateDb(); + db.Players.Add(new RadicadeServer.RecNet.Auth.PlayerData { AccountId = 7, Hashword = "x" }); + await db.SaveChangesAsync(); + + var api = new PlayerSettingsAPI(db); + SetUser(api, 7); + api.ControllerContext.HttpContext.Request.Body = + new MemoryStream(Encoding.UTF8.GetBytes("[{\"Key\":\"MusicVolume\",\"Value\":\"0.5\"}]")); + + var ok = Assert.IsType(await api.PutSettings()); + using var json = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.Equal(JsonValueKind.Array, json.RootElement.ValueKind); + Assert.Contains(json.RootElement.EnumerateArray(), item => + item.GetProperty("Key").GetString() == "MusicVolume" && + item.GetProperty("Value").GetString() == "0.5"); + } + + [Fact] + public void PlayerSettings_Defaults_UseRecRoom2023OnboardedState() + { + var defaults = DBConfig.DefaultSettings2023.FromJson>(); + + Assert.NotNull(defaults); + + // No AccountCreation flags here on purpose - see + // PlayerSettings_GetAlias_LeavesTheOnboardingFlagsToTheAccountSeed. An account with no settings + // row is one the server has never finished onboarding, and saying otherwise is what made the + // client skip the new-user pages. + Assert.DoesNotContain(defaults, item => + item.Key.StartsWith("Recroom.AccountCreation.", StringComparison.Ordinal)); + Assert.Contains(defaults, item => + item.Key == "TUTORIAL_COMPLETE_MASK" && + item.Value == "109"); + Assert.Contains(defaults, item => + item.Key == "TUTORIAL_COMPLETED_MASK" && + item.Value == "109"); + Assert.Contains(defaults, item => + item.Key == "HAS_COMPLETED_ORIENTATION" && + item.Value == "True"); + } + + [Fact] + public async Task PlayerSettings_SetV2_ReturnsReferenceEmptyArray() + { + var db = CreateDb(); + db.Players.Add(new RadicadeServer.RecNet.Auth.PlayerData { AccountId = 9, Hashword = "x" }); + await db.SaveChangesAsync(); + + var api = new PlayerSettingsAPI(db); + SetUser(api, 9); + + var ok = Assert.IsType( + await api.SetSetting(new Setting { Key = "MusicVolume", Value = "0.25" })); + + Assert.Equal("[]", Serialize(ok.Value)); + } + + [Fact] + public void PlayerSettings_GetAlias_LeavesPasswordAndFinishIncompleteWithoutAPassword() + { + // Missing settings must not make an account without a credential skip the password page. + var db = CreateDb(); + db.Players.Add(new RadicadeServer.RecNet.Auth.PlayerData { AccountId = 8, Hashword = "" }); + db.SaveChanges(); + + var api = new PlayerSettingsAPI(db); + SetUser(api, 8); + + var settings = api.GetSettings(); + + Assert.Contains(settings, item => item.Key == "Recroom.AccountCreation.HasCreatedPassword" && item.Value == "False"); + Assert.Contains(settings, item => item.Key == "Recroom.AccountCreation.HasFinished" && item.Value == "False"); + } + + [Fact] + public void PlayerSettings_GetAlias_IncludesReferenceMetadataFields() + { + var db = CreateDb(); + db.Players.Add(new RadicadeServer.RecNet.Auth.PlayerData { AccountId = 8, Hashword = "x" }); + db.SaveChanges(); + + var api = new PlayerSettingsAPI(db); + SetUser(api, 8); + + using var json = JsonDocument.Parse(Serialize(api.GetSettings())); + var first = json.RootElement[0]; + + Assert.Equal(1, first.GetProperty("Id").GetInt64()); + Assert.Equal(8, first.GetProperty("accountId").GetInt64()); + Assert.True(first.TryGetProperty("Key", out _)); + Assert.True(first.TryGetProperty("Value", out _)); + } + + [Fact] + public void PlayerSettings_GetAlias_ClearsPersistedSplitTestAssignments() + { + var db = CreateDb(); + db.Players.Add(new RadicadeServer.RecNet.Auth.PlayerData { AccountId = 8, Hashword = "x" }); + db.PlayerSettings.Add(new PlayerSettings + { + AccountId = 8, + Settings = """ + [{"Key":"SplitTestAssignedSegments","Value":"1|{\"SplitTesting+PlayMenuRRUI_2021_01_11\":\"On\"}"}] + """ + }); + db.SaveChanges(); + + var api = new PlayerSettingsAPI(db); + SetUser(api, 8); + + var settings = api.GetSettings(); + + Assert.Contains(settings, item => + item.Key == "SplitTestAssignedSegments" && + item.Value == "1|{}"); + } + + [Fact] + public async Task PlayerReporting_NoBan_ReturnsReferenceEmptyBlockDetails() + { + var db = CreateDb(); + db.Players.Add(new RadicadeServer.RecNet.Auth.PlayerData { AccountId = 8, Hashword = "x" }); + await db.SaveChangesAsync(); + + var api = new PlayerReportingAPI(db); + SetUser(api, 8); + + var ok = Assert.IsType(await api.GetBlockDetails()); + using var json = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.Equal(4, json.RootElement.EnumerateObject().Count()); + Assert.Equal(0, json.RootElement.GetProperty("ReportCategory").GetInt32()); + Assert.Equal(0, json.RootElement.GetProperty("Duration").GetInt32()); + Assert.Equal(0, json.RootElement.GetProperty("GameSessionId").GetInt64()); + Assert.Equal("", json.RootElement.GetProperty("Message").GetString()); + } + + [Fact] + public void Checklist_Current_ReturnsDefaultRows() + { + var api = new ChecklistAPI(); + + var ok = Assert.IsType(api.Current()); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind); + Assert.Equal(4, doc.RootElement.GetArrayLength()); + Assert.Equal(1, doc.RootElement[0].GetProperty("Order").GetInt32()); + Assert.Equal(25, doc.RootElement[0].GetProperty("CreditAmount").GetInt32()); + } +} diff --git a/ServerTests/ReputationTests.cs b/ServerTests/ReputationTests.cs new file mode 100644 index 0000000..3f61d19 --- /dev/null +++ b/ServerTests/ReputationTests.cs @@ -0,0 +1,156 @@ +using RadicadeServer.RecNet.Enums.Moderation; +using RadicadeServer.RecNet.Moderation; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Unit tests for the reputation system spec: +/// - every player starts at ReputationScore = 10 +/// - Noteriety starts at 0 and grows +0.5 per cheer / subscriber received +/// - bans drop reputation: -0.5 per ban, -1 per cheating ban, -4 per permanent ban (floored at 0) +/// - Trust label: Trusted (8-10), Neutral (6-7), Not Trusted (0-5) +/// - reports increment ReportCount, which never feeds Noteriety (so reports can't lock +/// the client's room-comment / club-creation features) +/// +public class ReputationTests +{ + [Fact] + public void CheerCategory_MatchesTheClientsLadder() + { + // Copied off the client's own enum in dump.cs (and matching the archived 2023 + // servers FemRec and Rubidium). Numbering them sequentially silently broke every + // cheer except General: none of 10/20/30/40 matched a switch arm. + Assert.Equal(0, (int)CheerCategory.General); + Assert.Equal(10, (int)CheerCategory.Helpful); + Assert.Equal(20, (int)CheerCategory.Sportsman); + Assert.Equal(30, (int)CheerCategory.GreatHost); + Assert.Equal(40, (int)CheerCategory.Creative); + // The badge only developers can equip. The client asks for it by this value, so + // leaving it out of the enum made the request look like an unknown category. + Assert.Equal(9000, (int)CheerCategory.RecRoomDeveloper); + } + + [Fact] + public void NewReputation_StartsAtTen() + { + var rep = new Reputation { AccountId = 42 }; + Assert.Equal(Reputation.DefaultReputation, rep.ReputationScore); + Assert.Equal(10.0, rep.ReputationScore); + Assert.Equal(0, rep.Noteriety); + Assert.Equal(0, rep.ReportCount); + } + + [Theory] + [InlineData(10.0, "Trusted")] + [InlineData(9.5, "Trusted")] + [InlineData(8.0, "Trusted")] + [InlineData(7.99, "Neutral")] + [InlineData(7.0, "Neutral")] + [InlineData(6.0, "Neutral")] + [InlineData(5.99, "Not Trusted")] + [InlineData(5.0, "Not Trusted")] + [InlineData(1.0, "Not Trusted")] + [InlineData(0.0, "Not Trusted")] + public void Trust_FollowsTheScoreBands(double score, string expected) + { + var rep = new Reputation { AccountId = 1, ReputationScore = score }; + Assert.Equal(expected, rep.Trust); + } + + [Fact] + public void NormalBan_DropsReputationByHalf() + { + var rep = new Reputation { AccountId = 1, ReputationScore = 10 }; + rep.ApplyBanPenalty(permanent: false, cheating: false); + Assert.Equal(9.5, rep.ReputationScore); + } + + [Fact] + public void CheatingBan_DropsReputationByOne() + { + var rep = new Reputation { AccountId = 1, ReputationScore = 10 }; + rep.ApplyBanPenalty(permanent: false, cheating: true); + Assert.Equal(9.0, rep.ReputationScore); + } + + [Fact] + public void PermanentBan_DropsReputationByFour() + { + var rep = new Reputation { AccountId = 1, ReputationScore = 10 }; + rep.ApplyBanPenalty(permanent: true, cheating: false); + Assert.Equal(6.0, rep.ReputationScore); + } + + [Fact] + public void CheatingPermanentBan_AppliesTheLargerPenalty() + { + // A cheating offense that escalates to permanent takes the permanent (-4) penalty. + var rep = new Reputation { AccountId = 1, ReputationScore = 10 }; + rep.ApplyBanPenalty(permanent: true, cheating: true); + Assert.Equal(6.0, rep.ReputationScore); + } + + [Fact] + public void RepeatedBans_NeverPushReputationNegative() + { + var rep = new Reputation { AccountId = 1, ReputationScore = 1.0 }; + rep.ApplyBanPenalty(permanent: true, cheating: true); // -4 + rep.ApplyBanPenalty(permanent: false, cheating: false); // -0.5 + Assert.Equal(0.0, rep.ReputationScore); + } + + [Fact] + public void AddCheer_GrowsNoterietyByHalfPerCheer() + { + var rep = new Reputation { AccountId = 1 }; + rep.AddCheer(CheerCategory.General); + rep.AddCheer(CheerCategory.Creative); + Assert.Equal(1.0, rep.Noteriety); // 2 x 0.5 + Assert.Equal(1, rep.CheerGeneral); + Assert.Equal(1, rep.CheerCreative); + } + + [Fact] + public void AddSubscriber_GrowsNoterietyByHalf() + { + var rep = new Reputation { AccountId = 1 }; + rep.AddSubscriber(); + rep.AddSubscriber(); + Assert.Equal(1.0, rep.Noteriety); + } + + [Fact] + public void Reports_NeverTouchNoteriety() + { + // Simulates the report flow: ReportCount goes up, Noteriety stays a fame score. + var rep = new Reputation { AccountId = 1 }; + for (var i = 0; i < 20; i++) rep.ReportCount += 1; + Assert.Equal(20, rep.ReportCount); + Assert.Equal(0.0, rep.Noteriety); + } + + [Fact] + public void ForClient_PassesThroughScoreTrustAndReportCount() + { + var rep = new Reputation { AccountId = 7, ReputationScore = 6.5, ReportCount = 3, Noteriety = 1.0 }; + var client = rep.ForClient(); + + Assert.Equal(7, client.AccountId); + Assert.Equal(6.5, client.ReputationScore); + Assert.Equal("Neutral", client.Trust); + Assert.Equal(3, client.ReportCount); + // Noteriety is normalized to a tiny 0-1 fraction so the client's moderation + // gates (room comments / club creation) never trip on it. + Assert.Equal(Math.Min(0.05, rep.Noteriety / 100.0), client.Noteriety); + Assert.True(client.Noteriety <= 0.05); + } + + [Fact] + public void ForClient_NormalizesNoterietyButNeverAboveTheCeiling() + { + var rep = new Reputation { AccountId = 1, Noteriety = 5000 }; + var client = rep.ForClient(); + Assert.Equal(0.05, client.Noteriety); + } +} diff --git a/ServerTests/RoleLookupTests.cs b/ServerTests/RoleLookupTests.cs new file mode 100644 index 0000000..e6888c6 --- /dev/null +++ b/ServerTests/RoleLookupTests.cs @@ -0,0 +1,139 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The role lookup the client uses to decide whether to show developer-only content. +/// +/// The 2023 client builds this URL from a literal of the form role/{0}/{1} and resolves +/// it against its RecNet base, and the reference implementation serves it at the host root +/// (GET /role/developer/:id). This server mounted it only under auth/role/, so on a +/// client whose base does not end in auth/ the lookup 404s - measured against the live +/// server: /role/developer/6 answered 404 while /auth/role/developer/6 answered 200. +/// A 404 here does not error visibly; it just silently reports "not a developer", which hides +/// everything the role gates. +/// +/// This is the same class of bug the equipment backpack hit - a correct handler behind a missing +/// prefix - which is why the route assertions are the point of this file. +/// +[Collection(DbFactoryCollection.Name)] +public class RoleLookupTests +{ + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static async Task WithProfile(long accountId, string? roles) + { + var db = NewDb(); + db.Profiles.Add(new Profile + { + accountId = accountId, + role = roles, + username = $"player{accountId}", + displayName = $"Player {accountId}", + profileImage = DBConfig.DefaultProfileImage + }); + await db.SaveChangesAsync(); + return db; + } + + /// Every route the client can reach this lookup on: prefix + "{roleType}/{id}". + private static string[] Routes() + { + var method = typeof(RolesAPI).GetMethod(nameof(RolesAPI.GetDeveloperRole))!; + var templates = method.GetCustomAttributes().Select(a => a.Template).ToArray(); + + return typeof(RolesAPI).GetCustomAttributes() + .SelectMany(prefix => templates.Select(suffix => $"{prefix.Template}/{suffix}")) + .ToArray(); + } + + [Fact] + public void TheRoleLookupAnswersAtTheRootPathTheClientBuilds() + { + var routes = Routes(); + + // What the client's literal resolves to when its base is the host root, and what the + // reference implementation serves. + Assert.Contains("role/{roleType}/{id}", routes); + + // The path this server already had, kept so nothing that works today starts 404ing. + Assert.Contains("auth/role/{roleType}/{id}", routes); + } + + [Theory] + [InlineData("developer")] + [InlineData("moderator")] + public void BothWalkedRolesReachTheLookup(string roleType) + { + var routes = Routes().Select(route => route.Replace("{roleType}", roleType)).ToArray(); + + Assert.Contains($"role/{roleType}/{{id}}", routes); + Assert.Contains($"auth/role/{roleType}/{{id}}", routes); + } + + [Fact] + public async Task TheLookupReportsTheProfilesRoles() + { + using var db = await WithProfile(6, "developer,moderator"); + var api = new RolesAPI(db); + + Assert.True(await api.GetDeveloperRole("developer", 6)); + Assert.True(await api.GetDeveloperRole("moderator", 6)); + } + + [Fact] + public async Task ARoleTheProfileDoesNotHoldIsFalse() + { + using var db = await WithProfile(6, "developer"); + var api = new RolesAPI(db); + + // The client reads a bare boolean, so a role that is not held has to answer false + // rather than error - it is a gate, not a lookup that can fail. + Assert.False(await api.GetDeveloperRole("moderator", 6)); + } + + [Fact] + public async Task AnUnknownOrRolelessAccountIsNotADeveloper() + { + using var roleless = await WithProfile(1, null); + Assert.False(await new RolesAPI(roleless).GetDeveloperRole("developer", 1)); + + using var db = NewDb(); + Assert.False(await new RolesAPI(db).GetDeveloperRole("developer", 99999)); + } + + [Fact] + public async Task RolesAreMatchedCaseInsensitivelyAndTrimmed() + { + // The admin panel writes whatever the operator picked, joined with commas. + using var db = await WithProfile(6, " Developer , Moderator "); + var api = new RolesAPI(db); + + Assert.True(await api.GetDeveloperRole("developer", 6)); + Assert.True(await api.GetDeveloperRole("moderator", 6)); + } + + [Fact] + public async Task GrantingTheDeveloperRoleFlipsTheLookup() + { + using var db = await WithProfile(6, null); + var api = new RolesAPI(db); + + Assert.False(await api.GetDeveloperRole("developer", 6)); + + var profile = await db.Profiles.FirstAsync(x => x.accountId == 6); + profile.role = "developer"; + await db.SaveChangesAsync(); + + Assert.True(await api.GetDeveloperRole("developer", 6)); + } +} diff --git a/ServerTests/RoomCommentTests.cs b/ServerTests/RoomCommentTests.cs new file mode 100644 index 0000000..eab88a1 --- /dev/null +++ b/ServerTests/RoomCommentTests.cs @@ -0,0 +1,317 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.Extensions.Logging.Abstractions; +using RadicadeServer.Admin; +using RadicadeServer.API.Compatibility; +using RadicadeServer.DB; +using RadicadeServer.Migrations; +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Room comments (POST roomcomments/comments/create/{roomId}). +/// +/// The route answered 200 and stored nothing - the client considers the comment posted, drops the +/// draft, and the comment is gone on reload. Silent, and indistinguishable from the room page +/// being broken. These cover the three things that make a stored comment real: the route the +/// client actually builds, a row with the caller as its author, and a table that a migration +/// creates. +/// +public class RoomCommentTests +{ + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static CompatibilityAPI NewApi(RecDBContext db, long? callerId = 205) + { + var identity = new ClaimsIdentity("TestAuth"); + if (callerId != null) + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, callerId.Value.ToString())); + + return new CompatibilityAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + } + + private static void SetBody(ControllerBase controller, string body, string contentType) + { + var request = controller.ControllerContext.HttpContext.Request; + request.ContentType = contentType; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + } + + private static async Task WithRoomAsync(long roomId = 42) + { + var db = NewDb(); + db.Rooms.Add(new RoomData + { + RoomId = roomId, + Name = "TestRoom", + CreatorAccountId = 1, + CreatedAt = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + return db; + } + + // ----------------------------------------------------------------------------------- + // The route, and who is allowed to post + // ----------------------------------------------------------------------------------- + + [Fact] + public void TheRouteIsTheClientsAbsolutePathAndRequiresAUser() + { + var method = typeof(CompatibilityAPI).GetMethod(nameof(CompatibilityAPI.CreateRoomComment))!; + + var template = Assert.Single(method.GetCustomAttributes()).Template; + Assert.Equal("/roomcomments/comments/create/{roomId:long}", template); + + // Absolute: a relative template would be concatenated onto the controller's own prefix and + // answer a path no client asks for, which is this project's most repeated failure mode. + Assert.StartsWith("/", template); + + // A comment has to have an author, and the author comes from the token. + Assert.NotEmpty(method.GetCustomAttributes()); + } + + [Fact] + public async Task AnAnonymousCommentIsRefusedRatherThanStoredUnderSomeone() + { + using var db = await WithRoomAsync(); + var api = NewApi(db, callerId: null); + SetBody(api, """{"Comment":"hi"}""", "application/json"); + + Assert.IsType(await api.CreateRoomComment(42)); + Assert.Empty(db.RoomComments); + } + + // ----------------------------------------------------------------------------------- + // Storage + // ----------------------------------------------------------------------------------- + + [Fact] + public async Task AJSONCommentIsStoredWithTheCallerAsItsAuthor() + { + using var db = await WithRoomAsync(); + var api = NewApi(db); + SetBody(api, """{"Comment":"great room!","CommentType":"Feedback"}""", "application/json"); + + Assert.IsType(await api.CreateRoomComment(42)); + + var comment = await db.RoomComments.SingleAsync(); + Assert.Equal(42, comment.RoomId); + Assert.Equal(205, comment.CreatorAccountId); // the caller, never a body field + Assert.Equal("great room!", comment.Comment); + Assert.Equal("Feedback", comment.CommentType); + Assert.NotEqual(default, comment.CreatedAt); + } + + [Fact] + public async Task TheBodySpellingsTheClientMightUseAllLandInTheSameColumn() + { + using var db = await WithRoomAsync(); + var api = NewApi(db); + // A single-field DTO serializes to a bare JSON string, which is not an object at all. + SetBody(api, "\"bare string body\"", "application/json"); + + Assert.IsType(await api.CreateRoomComment(42)); + + Assert.Equal("bare string body", (await db.RoomComments.SingleAsync()).Comment); + } + + [Fact] + public async Task AFormPostIsStoredToo() + { + using var db = await WithRoomAsync(); + var api = NewApi(db); + SetBody(api, "Comment=form+comment&commentType=Report", "application/x-www-form-urlencoded"); + + Assert.IsType(await api.CreateRoomComment(42)); + + var comment = await db.RoomComments.SingleAsync(); + Assert.Equal("form comment", comment.Comment); + Assert.Equal("Report", comment.CommentType); + } + + [Fact] + public async Task ACommentSentInTheQueryStringIsStored() + { + using var db = await WithRoomAsync(); + var api = NewApi(db); + + // The client's own call carries the comment as a query parameter and posts no body at all, + // which is why a real comment came back as "rejected: empty body" - only the body was read. + api.ControllerContext.HttpContext.Request.QueryString = new QueryString("?comment=query%20comment"); + + Assert.IsType(await api.CreateRoomComment(42)); + + Assert.Equal("query comment", (await db.RoomComments.SingleAsync()).Comment); + } + + [Fact] + public async Task AQueryStringCommentStillTakesItsTypeFromTheForm() + { + using var db = await WithRoomAsync(); + var api = NewApi(db); + + // Both carriers at once: whichever one names the text, the type must not be lost. + api.ControllerContext.HttpContext.Request.QueryString = new QueryString("?comment=mixed"); + SetBody(api, "commentType=Report", "application/x-www-form-urlencoded"); + + Assert.IsType(await api.CreateRoomComment(42)); + + var comment = await db.RoomComments.SingleAsync(); + Assert.Equal("mixed", comment.Comment); + Assert.Equal("Report", comment.CommentType); + } + + [Fact] + public async Task AnEmptyCommentIsRefusedSoTheClientKeepsTheDraft() + { + using var db = await WithRoomAsync(); + var api = NewApi(db); + SetBody(api, """{"Comment":" "}""", "application/json"); + + // Non-2xx on purpose: the client keeps what the player typed instead of dropping it. + Assert.IsType(await api.CreateRoomComment(42)); + Assert.Empty(db.RoomComments); + } + + [Fact] + public async Task ACommentWithNoTypeFallsBackToTheClientsOwnDefault() + { + using var db = await WithRoomAsync(); + var api = NewApi(db); + SetBody(api, """{"Comment":"no type"}""", "application/json"); + + Assert.IsType(await api.CreateRoomComment(42)); + + Assert.Equal("Feedback", (await db.RoomComments.SingleAsync()).CommentType); + } + + [Fact] + public async Task ThePositionIsKeptWhenTheClientSendsOneAndNullWhenItDoesNot() + { + using var db = await WithRoomAsync(); + + var withPosition = NewApi(db); + SetBody(withPosition, """{"Comment":"here","Position":{"x":1.5,"y":2,"z":-3}}""", "application/json"); + Assert.IsType(await withPosition.CreateRoomComment(42)); + + var withoutPosition = NewApi(db); + SetBody(withoutPosition, """{"Comment":"anywhere"}""", "application/json"); + Assert.IsType(await withoutPosition.CreateRoomComment(42)); + + var rows = await db.RoomComments.OrderBy(c => c.Id).ToListAsync(); + Assert.Equal(1.5f, rows[0].PositionX); + Assert.Equal(2f, rows[0].PositionY); + Assert.Equal(-3f, rows[0].PositionZ); + Assert.Null(rows[1].PositionX); + } + + // ----------------------------------------------------------------------------------- + // The table itself + // ----------------------------------------------------------------------------------- + + [Fact] + public void TheCommentsTableIsInTheModelAndAMigrationCreatesIt() + { + using var db = NewDb(); + Assert.NotNull(db.RoomComments); + + // A DbSet with no migration is a column the running database never gets: the code compiles, + // the query works in memory, and every insert fails on the server. Migrate() runs Up(), so + // running it here is the same statement the server will execute. + // Up() is protected (EF calls it itself), so it is invoked the way the framework does. + var up = typeof(AddRoomComments).GetMethod("Up", BindingFlags.NonPublic | BindingFlags.Instance)!; + var builder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + up.Invoke(new AddRoomComments(), new object[] { builder }); + + var statements = builder.Operations + .OfType() + .Select(op => op.Sql) + .ToArray(); + + Assert.NotEmpty(statements); + Assert.Contains(statements, op => op.Contains("CREATE TABLE", StringComparison.Ordinal)); + Assert.Contains(statements, op => op.Contains("[dbo].[RoomComments]", StringComparison.Ordinal)); + + // Guarded, like every other hand-written migration here: a second run over a database that + // already has the table is a no-op instead of a startup crash. + Assert.Contains(statements, op => op.Contains("OBJECT_ID", StringComparison.Ordinal)); + } + + // ----------------------------------------------------------------------------------- + // The admin panel side + // ----------------------------------------------------------------------------------- + + + // The panel's routes reject an unauthenticated caller - the branch's admin sign-in + // guards every one of them with AdminUtils.IsAdminKey - so these tests act as a + // signed-in operator by presenting the key the server has configured. + private const string TestAdminKey = "test-admin-key"; + + private static async Task AsAdminAsync(Func> call) + { + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = TestAdminKey; + try + { + return await call(); + } + finally + { + DBConfig.AdminApiKey = original; + } + } + + [Fact] + public async Task ThePanelListsWhatPlayersActuallyWrote() + { + using var db = await WithRoomAsync(); + + var api = NewApi(db); + SetBody(api, """{"Comment":"panel should see this"}""", "application/json"); + Assert.IsType(await api.CreateRoomComment(42)); + + var panel = new AdminPanelAPI(db); + var ok = Assert.IsType(await AsAdminAsync(() => panel.List("room_comments", TestAdminKey, null, null, null, true))); + var json = System.Text.Json.JsonSerializer.SerializeToElement(ok.Value); + + var row = Assert.Single(json.EnumerateArray()); + Assert.Equal("panel should see this", row.GetProperty("comment").GetString()); + Assert.Equal(42, row.GetProperty("room_id").GetInt64()); + Assert.Equal(205, row.GetProperty("creator_account_id").GetInt64()); + } + + [Fact] + public async Task ThePanelCanDeleteAComment() + { + using var db = await WithRoomAsync(); + + var api = NewApi(db); + SetBody(api, """{"Comment":"delete me"}""", "application/json"); + Assert.IsType(await api.CreateRoomComment(42)); + var id = (await db.RoomComments.SingleAsync()).Id; + + var panel = new AdminPanelAPI(db); + Assert.IsType(await AsAdminAsync(() => panel.Delete("room_comments", TestAdminKey, null, id.ToString(), "id", id.ToString(), null, null, null))); + + Assert.Empty(db.RoomComments); + } +} diff --git a/ServerTests/RoomDeveloperOwnedTests.cs b/ServerTests/RoomDeveloperOwnedTests.cs new file mode 100644 index 0000000..effac39 --- /dev/null +++ b/ServerTests/RoomDeveloperOwnedTests.cs @@ -0,0 +1,250 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// IsDeveloperOwned is the flag that files a room under the client's Dev Rooms row, and +/// the room payload never carried it at all - so every room read as "not developer-owned" and a +/// developer's own rooms were missing from that list. It is a property of the room, stamped when +/// the room is made (a player makes a room by CLONING a template - there is no separate create +/// route) from whether the maker holds the developer role. +/// +/// These pin the three ways it can come out wrong: stamped from nothing (always false), stamped +/// on a dorm (a dev's dorm is a home, not a project), and stamping the room row while the room +/// the client is handed still defaults to false. +/// +[Collection(DbFactoryCollection.Name)] +public class RoomDeveloperOwnedTests +{ + private const long DeveloperId = 2; + private const long PlainPlayerId = 3; + private const long MakerRoomTemplateId = 23; + private const long DormTemplateId = 1; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + /// + /// The shape a player's create-room flow runs against: a clonable template (room 23 is the + /// Maker Room the client clones) and the per-player dorm template. Room 1 is what + /// GetOrCreateDorm clones, so it has to be a dorm for the dorm case to be real. + /// + private static async Task<(TestDbContextFactory Factory, RecDBContext Db)> TemplatesAsync(bool withDeveloperProfile = true) + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + + var db = factory.CreateDbContext(); + + db.Rooms.Add(new RoomData + { + RoomId = MakerRoomTemplateId, + Name = "MakerRoom", + CloningAllowed = true, + Accessibility = Accessibility.Public, + CreatorAccountId = 1, + MaxPlayers = 8, + CreatedAt = DateTime.UtcNow, + }); + db.SubRooms.Add(new SubRoom + { + RoomId = MakerRoomTemplateId, + Name = "Home", + UnitySceneId = SceneLocations.MakerRoom, + Accessibility = Accessibility.Public, + MaxPlayers = 8, + DataBlob = "", + }); + + db.Rooms.Add(new RoomData + { + RoomId = DormTemplateId, + Name = "DormRoom", + IsDorm = true, + CloningAllowed = true, + Accessibility = Accessibility.Private, + CreatorAccountId = 1, + MaxPlayers = 4, + CreatedAt = DateTime.UtcNow, + }); + db.SubRooms.Add(new SubRoom + { + RoomId = DormTemplateId, + Name = "DormRoom", + UnitySceneId = "76d98498-60a1-430c-ab76-b54a29b7a163", + Accessibility = Accessibility.Private, + MaxPlayers = 4, + DataBlob = "", + }); + + db.Profiles.Add(new Profile + { + accountId = DeveloperId, + role = withDeveloperProfile ? "developer" : null, + username = "devplayer", + displayName = "Dev Player", + profileImage = DBConfig.DefaultProfileImage, + }); + await db.SaveChangesAsync(); + + return (factory, db); + } + + [Fact] + public async Task ARoomMadeByADeveloperIsDeveloperOwned() + { + var (_, db) = await TemplatesAsync(); + + var made = await RoomUtils.CloneRoom(MakerRoomTemplateId, DeveloperId, "My Dev Room"); + + Assert.NotNull(made); + Assert.Equal(DeveloperId, made!.CreatorAccountId); + Assert.True(made.IsDeveloperOwned); + + // The room the client is handed and the row it came from have to agree - the response is + // what the Dev Rooms row is built from, and the row is what the next read answers with. + Assert.True(db.Rooms.Single(x => x.Name == "My Dev Room").IsDeveloperOwned); + } + + [Fact] + public async Task ARoomMadeByAPlainPlayerIsNotDeveloperOwned() + { + var (_, db) = await TemplatesAsync(); + + var made = await RoomUtils.CloneRoom(MakerRoomTemplateId, PlainPlayerId, "Plain Room"); + + Assert.NotNull(made); + Assert.False(made!.IsDeveloperOwned); + Assert.False(db.Rooms.Single(x => x.Name == "Plain Room").IsDeveloperOwned); + } + + [Fact] + public async Task ADevelopersDormIsNotDeveloperOwned() + { + var (_, db) = await TemplatesAsync(); + + var dorm = await RoomUtils.GetOrCreateDorm(DeveloperId); + + Assert.NotNull(dorm); + Assert.True(dorm!.IsDorm); + + // The dorm is cloned by the same code path that creates rooms, so without the dorm guard + // a developer's own dorm came back developer-owned. + Assert.False(dorm.IsDeveloperOwned); + Assert.False(db.Rooms.Single(x => x.IsDorm && x.CreatorAccountId == DeveloperId).IsDeveloperOwned); + } + + [Fact] + public async Task TheRoomTheClientReadsCarriesTheFlag() + { + await TemplatesAsync(); + await RoomUtils.CloneRoom(MakerRoomTemplateId, DeveloperId, "Read Back"); + + var read = await RoomUtils.GetRoom("Read Back"); + + Assert.NotNull(read); + Assert.True(read!.IsDeveloperOwned); + + // The 2023 client reads room payloads by their declared names (Program.cs pins the + // serializer to no naming policy), the same PascalCase the real API answers with. + var json = JsonSerializer.Serialize(read); + Assert.Contains("\"IsDeveloperOwned\":true", json); + } + + [Fact] + public void APayloadWithoutTheFieldReadsAsFalse() + { + // Every room that predates the column, and every room an importer does not say otherwise + // about, serializes the field explicitly - a missing property is what makes the client's + // parser fall back to its own default. + var json = JsonSerializer.Serialize(new FullRoom { Name = "Imported" }); + + Assert.Contains("\"IsDeveloperOwned\":false", json); + } + + // ---- The admin create route, the path every import uses ------------------------------- + + private const string Secret = "test-bot-secret"; + + private static async Task CreateRoomAsync(RecDBContext db, long newOwner, string json) + { + var api = new AdminRoomAPI(db); + var original = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + return await api.CreateRoom(Secret, newOwner, JsonNode.Parse(json)!); + } + finally + { + DBConfig.AdminApiKey = original; + } + } + + [Fact] + public async Task AnImportedRoomWithNoOpinionTakesTheOwnersRoles() + { + var factory = new TestDbContextFactory(); + var db = factory.CreateDbContext(); + db.Profiles.Add(new Profile + { + accountId = DeveloperId, + role = "developer", + username = "devplayer", + displayName = "Dev Player", + profileImage = DBConfig.DefaultProfileImage, + }); + await db.SaveChangesAsync(); + + Assert.IsType(await CreateRoomAsync(db, DeveloperId, """{ "Name": "DerivedDevRoom" }""")); + + Assert.True(db.Rooms.Single(x => x.Name == "DerivedDevRoom").IsDeveloperOwned); + } + + [Fact] + public async Task AnImportedRoomThatSaysItIsDeveloperOwnedIsBelieved() + { + var factory = new TestDbContextFactory(); + var db = factory.CreateDbContext(); + + // A captured RRO room is developer-owned on the real service even though it is recreated + // here under a local (non-developer) owner, so an explicit value wins. + Assert.IsType(await CreateRoomAsync(db, PlainPlayerId, + """{ "Name": "ImportedRRO", "IsRRO": true, "IsDeveloperOwned": true }""")); + + Assert.True(db.Rooms.Single(x => x.Name == "ImportedRRO").IsDeveloperOwned); + } + + [Fact] + public async Task ANonDeveloperOwnerMakesAPlainRoom() + { + var factory = new TestDbContextFactory(); + var db = factory.CreateDbContext(); + + Assert.IsType(await CreateRoomAsync(db, PlainPlayerId, + """{ "Name": "PlayerRoom", "IsDeveloperOwned": false }""")); + + Assert.False(db.Rooms.Single(x => x.Name == "PlayerRoom").IsDeveloperOwned); + } +} diff --git a/ServerTests/RoomEconomyPersistenceTests.cs b/ServerTests/RoomEconomyPersistenceTests.cs new file mode 100644 index 0000000..2d7a120 --- /dev/null +++ b/ServerTests/RoomEconomyPersistenceTests.cs @@ -0,0 +1,168 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Rooms; +using RadicadeServer.API.Rooms.Currency.V1; +using RadicadeServer.API.Rooms.Keys.V1; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +public class RoomEconomyPersistenceTests +{ + [Fact] + public async Task CurrencyAndConsumableCreationPersistForTheCurrentRoom() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var db = new RecDBContext(options); + const long accountId = 71; + const long roomId = 902; + var instance = new RoomInstance { roomId = roomId }; + db.RoomInstances.Add(instance); + db.Presences.Add(new RealPresence + { + playerId = accountId, + isOnline = true, + roomInstance = instance.roomInstanceId, + }); + await db.SaveChangesAsync(); + + var currencyApi = new RoomCurrencyAPI(db); + SetUserAndJsonBody(currencyApi, accountId, "{\"Name\":\"Test currency\",\"Description\":\"A test currency\",\"Color\":\"#112233\"}"); + var currencyResult = Assert.IsType(await currencyApi.CreateCurrency()); + var currency = Assert.IsType>(currencyResult.Value); + Assert.Equal(roomId, currency["RoomId"]); + Assert.Equal("#112233", currency["Color"]); + + var consumableApi = new RoomConsumablesAPI(db); + SetUserAndJsonBody(consumableApi, accountId, "{\"Name\":\"Test consumable\",\"Description\":\"A test consumable\"}"); + var consumableResult = Assert.IsType(await consumableApi.CreateRoomConsumable()); + var consumable = Assert.IsType>(consumableResult.Value); + Assert.Equal(roomId, consumable["RoomId"]); + + var listedCurrency = Assert.IsType(await currencyApi.GetRoomCurrencies(roomId)); + var listedCurrencies = Assert.IsType>>(listedCurrency.Value); + Assert.Single(listedCurrencies); + Assert.Equal(currency["CurrencyId"], listedCurrencies[0]["CurrencyId"]); + + var listedConsumable = Assert.IsType(await consumableApi.GetRoomConsumablesInRoom(roomId)); + var listedConsumables = Assert.IsType>>(listedConsumable.Value); + Assert.Single(listedConsumables); + Assert.Equal(consumable["ConsumableId"], listedConsumables[0]["ConsumableId"]); + + var storedConfig = await db.ServerConfig.SingleAsync(config => config.Key == "RoomAssets"); + Assert.Contains("Test currency", storedConfig.Value); + Assert.Contains("Test consumable", storedConfig.Value); + + // The list routes load records from persisted JSON, not the original request + // dictionaries; nested JSON values must remain intact after deserialization. + await using var restartedDb = new RecDBContext(options); + var reloadedCurrencies = Assert.IsType( + await new RoomCurrencyAPI(restartedDb).GetRoomCurrencies(roomId)); + var reloadedCurrency = Assert.Single( + Assert.IsAssignableFrom>>(reloadedCurrencies.Value)); + Assert.Equal("#112233", reloadedCurrency["Color"]?.ToString()); + + var reloadedConsumables = Assert.IsType( + await new RoomConsumablesAPI(restartedDb).GetRoomConsumablesInRoom(roomId)); + Assert.Equal("Test consumable", + Assert.Single(Assert.IsAssignableFrom>>(reloadedConsumables.Value))["Name"]?.ToString()); + } + + [Fact] + public async Task RoomKeyCreationUsesTheCurrentRoomAndPersistsTheKey() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var db = new RecDBContext(options); + const long accountId = 81; + const long roomId = 903; + var instance = new RoomInstance { roomId = roomId }; + db.RoomInstances.Add(instance); + db.Presences.Add(new RealPresence + { + playerId = accountId, + isOnline = true, + roomInstance = instance.roomInstanceId, + }); + await db.SaveChangesAsync(); + + var api = new RoomKeysAPI(db); + SetUserAndJsonBody(api, accountId, "{\"Name\":\"Test key\",\"Description\":\"A test key\",\"Price\":5}"); + + var created = Assert.IsType(await api.CreateRoomKey()); + var key = Assert.IsType(created.Value); + Assert.Equal(roomId, key.RoomId); + Assert.Equal("Test key", key.Name); + Assert.Equal(key.RoomKeyId, Assert.Single(db.RoomKeys).RoomKeyId); + var listed = Assert.IsType(api.GetRoomKeysForRoom(roomId)); + Assert.Equal(key.RoomKeyId, Assert.Single(Assert.IsAssignableFrom>(listed.Value)).RoomKeyId); + var missingRoomId = Assert.IsType(api.GetRoomKeysForRoom(null)); + Assert.Empty(Assert.IsAssignableFrom>(missingRoomId.Value)); + + // The client endpoint takes the RoomKeyId in its path and answers a bare bool. + // Creating a room key doesn't purchase it; model ownership explicitly. + db.Unlocks.Add(new PlayerUnlocks + { + AccountId = accountId, + RoomKeys = JsonSerializer.Serialize(new[] { key.RoomKeyId }) + }); + await db.SaveChangesAsync(); + SetUserAndJsonBody(api, accountId, string.Empty); + Assert.True(Assert.IsType(Assert.IsType(await api.IsRoomKeyPurchased(key.RoomKeyId)).Value)); + Assert.False(Assert.IsType(Assert.IsType(await api.IsRoomKeyPurchased(key.RoomKeyId + 1000)).Value)); + Assert.False(Assert.IsType(Assert.IsType(await api.IsRoomKeyPurchased(key.RoomKeyId + 1)).Value)); + } + + [Fact] + public void BalanceLookupsAnswerWithAZeroBalanceRatherThanNotFound() + { + // The 2023 client asks getBalance right after getAllBalances on every room join and + // retries an answer it cannot use, so a 404 here is a join that spends a fresh TLS + // connection and a round trip (~100 ms RTT) per retry before the room finishes + // loading. This server tracks no balances, so the answer is a zero balance. + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + using var db = new RecDBContext(options); + + var api = new RoomCurrencyAPI(db); + SetUserAndJsonBody(api, 91, "{}"); + + var result = Assert.IsType(api.GetBalance("d0618354-d68a-4364-a693-b8cf09879973", 91, 25)); + var balance = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)).RootElement; + Assert.Equal("d0618354-d68a-4364-a693-b8cf09879973", balance.GetProperty("CurrencyId").GetString()); + Assert.Equal(91, balance.GetProperty("AccountId").GetInt64()); + Assert.Equal(25, balance.GetProperty("RoomId").GetInt64()); + Assert.Equal(0, balance.GetProperty("Balance").GetInt32()); + + // The client does not always send the account it is asking about; the caller's own + // id is the only sensible substitute. + var anonymous = new RoomCurrencyAPI(db); + SetUserAndJsonBody(anonymous, 92, "{}"); + var inferred = Assert.IsType(anonymous.GetBalance(null, null, null)); + var inferredBalance = JsonDocument.Parse(JsonSerializer.Serialize(inferred.Value)).RootElement; + Assert.Equal(92, inferredBalance.GetProperty("AccountId").GetInt64()); + Assert.Equal(string.Empty, inferredBalance.GetProperty("CurrencyId").GetString()); + } + + private static void SetUserAndJsonBody(Controller controller, long accountId, string json) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + var request = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }.Request; + request.ContentType = "application/json"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json)); + request.ContentLength = request.Body.Length; + controller.ControllerContext = new ControllerContext { HttpContext = request.HttpContext }; + } +} diff --git a/ServerTests/RoomJoinGiftFromTests.cs b/ServerTests/RoomJoinGiftFromTests.cs new file mode 100644 index 0000000..1d408b1 --- /dev/null +++ b/ServerTests/RoomJoinGiftFromTests.cs @@ -0,0 +1,258 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Who a room's join box is from. The box a player receives on joining already travelled with +/// RecNet's Gift.FromPlayerId - it is the field the client reads to put a name on the box - +/// but a room's join gift left it at zero, so every join box arrived from nobody. +/// +/// The room's Room Details modal now carries a FromPlayerId, which lands in +/// Rooms.JoinGiftFromPlayerId and from there on the box. The value crosses a JSON boundary +/// twice, and an account id is eighteen digits, so the panel and the route keep it as digits in a +/// string rather than as a number: a JSON number is read back as a double and its last digits are +/// rounded away, which would name a different account. +/// +/// These tests drive the real admin routes and then read the box the way a client does - +/// GET api/avatar/v2/gifts - rather than inspecting the row the route answered with. +[Collection(DbFactoryCollection.Name)] +public class RoomJoinGiftFromTests +{ + private const string Secret = "room-join-gift-from-key"; + private const long RoomId = 247; + private const long Joiner = 114903603133502227; + private const long Sender = 733901112234567890; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + /// A room that hands every player a box on join, which is what makes the sender matter. + private static async Task RoomWithJoinGiftAsync() + { + var db = NewDb(); + db.Rooms.Add(new RoomData + { + RoomId = RoomId, + Name = "RecCenter", + CreatorAccountId = 1, + JoinGiftEnabled = true, + JoinGiftOncePerPlayer = true, + JoinGiftTitle = "Room Visit Gift", + JoinGiftMessage = "Thanks for visiting!", + JoinGiftCurrencyType = RadicadeServer.RecNet.Enums.Store.CurrencyType.RecCenterTokens, + JoinGiftCurrencyAmount = 25 + }); + await db.SaveChangesAsync(); + return db; + } + + /// Patches one room the way the panel does, and returns the answer's status. + private static async Task SaveRoomAsync(RecDBContext db, string payloadJson) + { + var api = new AdminPanelAPI(db); + var originalKey = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + var payload = JsonDocument.Parse(payloadJson).RootElement; + return await api.Update("rooms", RoomId, Secret, payload); + } + finally + { + DBConfig.AdminApiKey = originalKey; + } + } + + /// The box as the recipient's client reads it: GET api/avatar/v2/gifts. + private static async Task> ClientGiftsAsync(RecDBContext db, long playerId) + { + var api = new AvatarV2(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, playerId.ToString())], "test")) + } + } + }; + + var result = await api.GetGifts(); + var ok = Assert.IsType(result); + return Assert.IsAssignableFrom>(ok.Value).ToList(); + } + + /// The one box the joiner's client reads, with the server's own naming on the wire. + private static async Task JoinerBoxOnTheWireAsync(RecDBContext db) + { + Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + + var read = Assert.Single(await ClientGiftsAsync(db, Joiner)); + return JsonDocument.Parse(JsonSerializer.Serialize(read)).RootElement; + } + + [Fact] + public async Task TheSenderThePanelSavesIsTheSenderOnTheJoinBox() + { + var db = await RoomWithJoinGiftAsync(); + + var result = await SaveRoomAsync(db, $$""" + { + "join_gift_from_player_id": "{{Sender}}", + "join_gift_title": "Room Visit Gift" + } + """); + Assert.IsType(result); + + var stored = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); + Assert.Equal(Sender, stored.JoinGiftFromPlayerId); + + // And what the joiner's client reads off the box it was handed. + var wire = await JoinerBoxOnTheWireAsync(db); + Assert.Equal(Sender, wire.GetProperty("FromPlayerId").GetInt64()); + } + + [Fact] + public async Task ASeventeenDigitSenderIsNotRoundedOnItsWayThroughThePanel() + { + var db = await RoomWithJoinGiftAsync(); + Assert.True(Sender > int.MaxValue); + + await SaveRoomAsync(db, $$""" + { "join_gift_from_player_id": "{{Sender}}" } + """); + + // The panel reads the id back as digits in a string, so the modal shows the digits that + // were saved rather than a rounded double, and saving again cannot drift the account. + var api = new AdminPanelAPI(db); + var originalKey = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + string? asPanelReadsIt; + try + { + var details = Assert.IsType(await api.RoomPanelDetails(RoomId, Secret)); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(details.Value)); + var field = doc.RootElement.GetProperty("room").GetProperty("join_gift_from_player_id"); + Assert.Equal(JsonValueKind.String, field.ValueKind); + asPanelReadsIt = field.GetString(); + } + finally + { + DBConfig.AdminApiKey = originalKey; + } + + Assert.Equal(Sender.ToString(), asPanelReadsIt); + } + + [Fact] + public async Task AChangedSenderReissuesTheBox() + { + var db = await RoomWithJoinGiftAsync(); + var before = (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftVersion; + + await SaveRoomAsync(db, $$""" + { "join_gift_from_player_id": "{{Sender}}" } + """); + + // A new version means a new gift drop id, so a player who already opened the old box is + // handed the one that now names a sender. + var after = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); + Assert.True(after.JoinGiftVersion > before); + } + + [Fact] + public async Task ASaveThatLeavesTheSenderAloneDoesNotReissueTheBox() + { + var db = await RoomWithJoinGiftAsync(); + await SaveRoomAsync(db, $$""" + { "join_gift_from_player_id": "{{Sender}}" } + """); + var afterFirstSave = (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftVersion; + + // The same id again - the modal posts every field on every save. + await SaveRoomAsync(db, $$""" + { "join_gift_from_player_id": "{{Sender}}", "join_gift_enabled": true, "join_gift_once_per_player": true } + """); + + var again = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); + Assert.Equal(afterFirstSave, again.JoinGiftVersion); + Assert.Equal(Sender, again.JoinGiftFromPlayerId); + } + + [Fact] + public async Task AClearedSenderLeavesTheBoxWithNoSender() + { + var db = await RoomWithJoinGiftAsync(); + await SaveRoomAsync(db, $$""" + { "join_gift_from_player_id": "{{Sender}}" } + """); + + // The blank field is what the modal sends for no sender, and it has to clear the room + // rather than leave the old account on the box. + Assert.IsType(await SaveRoomAsync(db, """{ "join_gift_from_player_id": "" }""")); + + Assert.Equal(0, (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftFromPlayerId); + + var wire = await JoinerBoxOnTheWireAsync(db); + Assert.Equal(0, wire.GetProperty("FromPlayerId").GetInt64()); + } + + [Fact] + public async Task ARoomNoPanelFieldArrivedForKeepsItsSender() + { + var db = await RoomWithJoinGiftAsync(); + await SaveRoomAsync(db, $$""" + { "join_gift_from_player_id": "{{Sender}}" } + """); + + // An older panel, an import or a partial save leaves the field out entirely; that is not + // a request to clear it. + Assert.IsType(await SaveRoomAsync(db, """{ "min_level": 5 }""")); + + Assert.Equal(Sender, (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftFromPlayerId); + } + + [Fact] + public async Task ASenderThatIsNotAnAccountIdIsRefusedRatherThanKept() + { + var db = await RoomWithJoinGiftAsync(); + await SaveRoomAsync(db, $$""" + { "join_gift_from_player_id": "{{Sender}}" } + """); + + // The panel refuses this before it posts; if it ever reaches the route anyway, the room + // must not be left believing the sender changed. + var result = await SaveRoomAsync(db, """{ "join_gift_from_player_id": "not-a-player" }"""); + + var bad = Assert.IsType(result); + Assert.Contains("join_gift_from_player_id", bad.Value?.ToString()); + Assert.Equal(Sender, (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftFromPlayerId); + } + + [Fact] + public async Task ARoomThatNamesNoSenderStillHandsOutItsBox() + { + var db = await RoomWithJoinGiftAsync(); + + // Nothing here sets a sender: the box is still delivered, it just names nobody, which is + // how every join box behaved before the room could name a sender. + var wire = await JoinerBoxOnTheWireAsync(db); + + Assert.Equal(0, wire.GetProperty("FromPlayerId").GetInt64()); + Assert.Equal(25, wire.GetProperty("Currency").GetInt32()); + } +} diff --git a/ServerTests/RoomJoinGiftListTests.cs b/ServerTests/RoomJoinGiftListTests.cs new file mode 100644 index 0000000..873b274 --- /dev/null +++ b/ServerTests/RoomJoinGiftListTests.cs @@ -0,0 +1,400 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.Admin; +using RadicadeServer.API.Avatars.V2; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A room hands players one or more boxes when they join. The boxes live in RoomJoinGifts, one row +/// each, and the panel edits them as a list behind Room Details. +/// +/// A room saved before that list existed still describes one box on its own Rooms row; it keeps +/// handing that out, which is what every room did before the list existed. A room with a list is +/// read from the list alone, and each box is claimed on its own, so editing one box re-issues that +/// box without silencing the others. +/// +/// These tests drive the real admin route and then read the boxes the way a client does - +/// GET api/avatar/v2/gifts - rather than inspecting the rows the route answered with. +[Collection(DbFactoryCollection.Name)] +public class RoomJoinGiftListTests +{ + private const string Secret = "room-join-gift-list-key"; + private const long RoomId = 911; + private const long Joiner = 114903603133502227; + private const long Sender = 733901112234567890; + + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static async Task NewRoomAsync(string name = "RecCenter") + { + var db = NewDb(); + db.Rooms.Add(new RoomData + { + RoomId = RoomId, + Name = name, + CreatorAccountId = 1 + }); + await db.SaveChangesAsync(); + return db; + } + + /// Patches the room the way the panel does, and returns the answer. + private static async Task SaveRoomAsync(RecDBContext db, string payloadJson) + { + var api = new AdminPanelAPI(db); + var originalKey = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + try + { + var payload = JsonDocument.Parse(payloadJson).RootElement; + return await api.Update("rooms", RoomId, Secret, payload); + } + finally + { + DBConfig.AdminApiKey = originalKey; + } + } + + private static Task SaveGiftsAsync(RecDBContext db, params object[] gifts) + { + var payload = JsonSerializer.Serialize(new { join_gifts = gifts }); + return SaveRoomAsync(db, payload); + } + + /// One box as the panel sends it. + private static object Gift(long id, string title, int currency, bool enabled = true, + bool oncePerPlayer = true, string fromPlayerId = "", string message = "") + { + return new + { + id, + enabled, + once_per_player = oncePerPlayer, + title, + message, + from_player_id = fromPlayerId, + avatar_item = "", + avatar_item_type = "Outfit", + equipment_item = "", + equipment_modification_guid = "", + consumable_item = "", + currency_type = "RecCenterTokens", + currency_amount = currency, + xp = 0, + context = "Game_Drop", + rarity = "Common" + }; + } + + /// The boxes as the recipient's client reads them: GET api/avatar/v2/gifts. + private static async Task> ClientGiftsAsync(RecDBContext db, long playerId) + { + var api = new AvatarV2(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.NameIdentifier, playerId.ToString())], "test")) + } + } + }; + + var result = await api.GetGifts(); + var ok = Assert.IsType(result); + return Assert.IsAssignableFrom>(ok.Value).ToList(); + } + + [Fact] + public async Task TwoBoxesAreBothHandedToTheJoiner() + { + var db = await NewRoomAsync(); + Assert.IsType(await SaveGiftsAsync(db, + Gift(0, "Welcome!", 25), + Gift(0, "Thanks!", 5))); + + Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + + var boxes = await ClientGiftsAsync(db, Joiner); + Assert.Equal(2, boxes.Count); + Assert.Equal(new[] { "Welcome!", "Thanks!" }, boxes.Select(x => x.FriendlyName).OrderBy(x => x == "Welcome!" ? 0 : 1)); + Assert.Equal(2, boxes.Select(x => x.GiftDropId).Distinct().Count()); + Assert.Equal([25, 5], boxes.Select(x => x.Currency).OrderByDescending(x => x)); + } + + [Fact] + public async Task AnOrderedListIsHandedOutInTheOrderThePanelShowsIt() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, + Gift(0, "First", 1), + Gift(0, "Second", 2), + Gift(0, "Third", 3)); + + await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId); + + // Each box names itself, so the client's list can be read in the order the room stores. + var stored = await db.RoomJoinGifts.Where(x => x.RoomId == RoomId) + .OrderBy(x => x.SortOrder).ToListAsync(); + Assert.Equal(new[] { "First", "Second", "Third" }, stored.Select(x => x.Title)); + Assert.Equal(new[] { 0, 1, 2 }, stored.Select(x => x.SortOrder)); + } + + [Fact] + public async Task EachBoxIsClaimedOnItsOwn() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5)); + + Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + // Nothing changed, so the second visit is silent. + Assert.False(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + + Assert.Equal(2, (await ClientGiftsAsync(db, Joiner)).Count); + } + + [Fact] + public async Task AChangedBoxIsHandedOutAgainWithoutReissuingTheOther() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5)); + await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId); + + var saved = (await db.RoomJoinGifts.Where(x => x.RoomId == RoomId) + .OrderBy(x => x.SortOrder).ToListAsync()); + var firstId = saved[0].RoomJoinGiftId; + var secondId = saved[1].RoomJoinGiftId; + + // The panel posts the whole list every save: only the second box's amount changed. + await SaveGiftsAsync(db, + Gift(firstId, "Welcome!", 25), + Gift(secondId, "Thanks!", 50)); + + Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + + var boxes = await ClientGiftsAsync(db, Joiner); + Assert.Equal(3, boxes.Count); + Assert.Equal(3, boxes.Select(x => x.GiftDropId).Distinct().Count()); + Assert.Single(boxes, x => x.Currency == 50); + + // The box that did not change was not handed out a second time. + var unchanged = await db.RoomJoinGifts.SingleAsync(x => x.RoomJoinGiftId == firstId); + Assert.Equal(1, unchanged.Version); + Assert.Equal(2, (await db.RoomJoinGifts.SingleAsync(x => x.RoomJoinGiftId == secondId)).Version); + } + + [Fact] + public async Task ABoxLeftOutOfTheListIsNoLongerHandedOut() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5)); + var first = (await db.RoomJoinGifts.OrderBy(x => x.SortOrder).FirstAsync()).RoomJoinGiftId; + + await SaveGiftsAsync(db, Gift(first, "Welcome!", 25)); + + Assert.Single(await db.RoomJoinGifts.Where(x => x.RoomId == RoomId).ToListAsync()); + await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId); + var boxes = Assert.Single(await ClientGiftsAsync(db, Joiner)); + Assert.Equal("Welcome!", boxes.FriendlyName); + } + + [Fact] + public async Task ADisabledBoxIsSkippedAndTheRestStillArrive() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, + Gift(0, "Welcome!", 25), + Gift(0, "Thanks!", 5, enabled: false)); + + Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + + var boxes = Assert.Single(await ClientGiftsAsync(db, Joiner)); + Assert.Equal("Welcome!", boxes.FriendlyName); + } + + [Fact] + public async Task ABoxThatGivesNothingIsNotAHandout() + { + var db = await NewRoomAsync(); + // The editor always keeps at least one card, and a blank one gives nothing at all. + await SaveGiftsAsync(db, Gift(0, "Room Visit Gift", 0)); + + Assert.False(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + Assert.Empty(await ClientGiftsAsync(db, Joiner)); + } + + [Fact] + public async Task ARoomSavedBeforeTheListStillHandsOutItsOwnBox() + { + var db = await NewRoomAsync(); + var room = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); + room.JoinGiftEnabled = true; + room.JoinGiftOncePerPlayer = true; + room.JoinGiftTitle = "Room Visit Gift"; + room.JoinGiftCurrencyAmount = 25; + await db.SaveChangesAsync(); + + Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + + var box = Assert.Single(await ClientGiftsAsync(db, Joiner)); + Assert.Equal(25, box.Currency); + Assert.Empty(await db.RoomJoinGifts.ToListAsync()); + } + + [Fact] + public async Task TheRoomsOwnFieldsDescribeTheFirstBox() + { + var db = await NewRoomAsync(); + Assert.IsType(await SaveGiftsAsync(db, + Gift(0, "Welcome!", 25, fromPlayerId: Sender.ToString()), + Gift(0, "Thanks!", 5, enabled: false))); + + var room = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); + Assert.Equal("Welcome!", room.JoinGiftTitle); + Assert.Equal(25, room.JoinGiftCurrencyAmount); + Assert.Equal(Sender, room.JoinGiftFromPlayerId); + // The room is still handing boxes out even though only its first one is enabled. + Assert.True(room.JoinGiftEnabled); + } + + [Fact] + public async Task AListWithEveryBoxDisabledClosesTheRoom() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, + Gift(0, "Welcome!", 25, enabled: false), + Gift(0, "Thanks!", 5, enabled: false)); + + var room = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); + Assert.False(room.JoinGiftEnabled); + Assert.False(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); + } + + [Fact] + public async Task ThePanelReadsTheWholeListBackWithSendersAsDigits() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, + Gift(0, "Welcome!", 25, fromPlayerId: Sender.ToString()), + Gift(0, "Thanks!", 5)); + + var api = new AdminPanelAPI(db); + var originalKey = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + string? firstSender; + int count; + try + { + var details = Assert.IsType(await api.RoomPanelDetails(RoomId, Secret)); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(details.Value)); + var gifts = doc.RootElement.GetProperty("room").GetProperty("join_gifts"); + count = gifts.GetArrayLength(); + var sender = gifts[0].GetProperty("from_player_id"); + Assert.Equal(JsonValueKind.String, sender.ValueKind); + firstSender = sender.GetString(); + } + finally + { + DBConfig.AdminApiKey = originalKey; + } + + Assert.Equal(2, count); + Assert.Equal(Sender.ToString(), firstSender); + } + + [Fact] + public async Task TheRoomsListThePanelLoadsCarriesEveryBox() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5)); + + // The panel's room table is what fills the editor, so the list has to travel with it - the + // details route is not what opens the modal. + var api = new AdminPanelAPI(db); + var originalKey = DBConfig.AdminApiKey; + DBConfig.AdminApiKey = Secret; + JsonElement room; + try + { + var result = Assert.IsType(await api.List("rooms", Secret, null, null, null)); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)); + room = doc.RootElement.EnumerateArray() + .Single(x => x.GetProperty("id").GetInt64() == RoomId) + .Clone(); + } + finally + { + DBConfig.AdminApiKey = originalKey; + } + + var gifts = room.GetProperty("join_gifts"); + Assert.Equal(2, gifts.GetArrayLength()); + Assert.Equal("Welcome!", gifts[0].GetProperty("title").GetString()); + Assert.Equal(25, gifts[0].GetProperty("currency_amount").GetInt32()); + Assert.Equal("Thanks!", gifts[1].GetProperty("title").GetString()); + } + + [Fact] + public async Task ASenderThatIsNotAnAccountIdRefusesTheWholeList() + { + var db = await NewRoomAsync(); + + var result = await SaveGiftsAsync(db, + Gift(0, "Welcome!", 25), + Gift(0, "Thanks!", 5, fromPlayerId: "not-a-player")); + + var bad = Assert.IsType(result); + Assert.Contains("join_gifts[1].from_player_id", bad.Value?.ToString()); + // Nothing was written: a save the panel could not make sense of leaves no half-list behind. + Assert.Empty(await db.RoomJoinGifts.ToListAsync()); + Assert.Equal(0, (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftCurrencyAmount); + } + + [Fact] + public async Task ABoxIdFromAnotherRoomIsRefused() + { + var db = await NewRoomAsync(); + db.RoomJoinGifts.Add(new RoomJoinGift + { + RoomId = 4242, + SortOrder = 0, + Title = "Someone else's", + CurrencyAmount = 1 + }); + await db.SaveChangesAsync(); + var foreignId = (await db.RoomJoinGifts.SingleAsync()).RoomJoinGiftId; + + var result = await SaveGiftsAsync(db, Gift(foreignId, "Welcome!", 25)); + + Assert.IsType(result); + Assert.Empty(await db.RoomJoinGifts.Where(x => x.RoomId == RoomId).ToListAsync()); + } + + [Fact] + public async Task ReorderingTheListReissuesNothing() + { + var db = await NewRoomAsync(); + await SaveGiftsAsync(db, Gift(0, "First", 1), Gift(0, "Second", 2)); + var ids = (await db.RoomJoinGifts.OrderBy(x => x.SortOrder).ToListAsync()) + .Select(x => x.RoomJoinGiftId).ToArray(); + + await SaveGiftsAsync(db, Gift(ids[1], "Second", 2), Gift(ids[0], "First", 1)); + + var stored = await db.RoomJoinGifts.OrderBy(x => x.SortOrder).ToListAsync(); + Assert.Equal(new[] { ids[1], ids[0] }, stored.Select(x => x.RoomJoinGiftId)); + Assert.All(stored, x => Assert.Equal(1, x.Version)); + } +} diff --git a/ServerTests/RoomRolesTests.cs b/ServerTests/RoomRolesTests.cs new file mode 100644 index 0000000..fb2384c --- /dev/null +++ b/ServerTests/RoomRolesTests.cs @@ -0,0 +1,358 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using RadicadeServer; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RoomServer; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// A player's room role is what decides everything they may do in that room, and the client +/// reads it out of the room payload. These pin the ways that went wrong: the role list being +/// served empty (which silently removed every co-owner's and moderator's permissions), the +/// owner missing from it, an invite matching the target's role in a DIFFERENT room, and the +/// Creator tier being grantable through the role route. +/// +[Collection(DbFactoryCollection.Name)] +public class RoomRolesTests +{ + private const long CreatorId = 11; + private const long RoomId = 5; + private const long MemberId = 22; + private const long StrangerId = 999; + + private static readonly JsonSerializerOptions ServerJson = new() + { + PropertyNamingPolicy = null, + WriteIndented = true, + }; + + private static string Serialize(object? value) => JsonSerializer.Serialize(value, ServerJson); + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + // A unique database name isolates each test; the store is NOT given its own + // InMemoryDatabaseRoot, because a distinct root makes Entity Framework build a + // distinct internal service provider every time and it throws once the process + // has more than twenty of them. + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + return (factory, factory.CreateDbContext()); + } + + private static void SetUser(Controller controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetFormBody(Controller controller, string body) + { + var request = controller.ControllerContext.HttpContext.Request; + request.ContentType = "application/x-www-form-urlencoded"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + } + + private static void SetQuery(Controller controller, string query) + => controller.ControllerContext.HttpContext.Request.QueryString = new QueryString(query); + + private static async Task SeedRoomAsync(RecDBContext db, long roomId = RoomId, long creatorId = CreatorId) + { + var creator = await db.Players.FirstOrDefaultAsync(x => x.AccountId == creatorId); + if (creator == null) + db.Players.Add(new PlayerData { AccountId = creatorId, Hashword = "x" }); + + var member = await db.Players.FirstOrDefaultAsync(x => x.AccountId == MemberId); + if (member == null) + db.Players.Add(new PlayerData { AccountId = MemberId, Hashword = "x" }); + + db.Rooms.Add(new RoomData + { + RoomId = roomId, + Name = $"Room {roomId}", + CreatorAccountId = creatorId, + Accessibility = Accessibility.Public, + MaxPlayers = 8, + CreatedAt = DateTime.UtcNow, + }); + + await db.SaveChangesAsync(); + } + + private static async Task ReadPersistedRoleAsync(TestDbContextFactory factory, long roomId, long accountId) + { + await using var db = factory.CreateDbContext(); + return await db.RoomRoles.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId && x.AccountId == accountId); + } + + [Fact] + public async Task RolesAreServedEvenWhenTheHighRanksConfigIsMissing() + { + var (_, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + db.RoomRoles.Add(new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.CoOwner, InvitedRole = RoleType.None }); + await db.SaveChangesAsync(); + + // No ServerConfig "HighRanks" row: the role list used to come back empty here, which + // took every co-owner's and moderator's permissions with it. + var roles = await RoomUtils.GetRoles(RoomId); + + Assert.Contains(roles, x => x.AccountId == MemberId && x.Role == RoleType.CoOwner); + Assert.True(await MatchmakingUtils.IsAtLeastRole(MemberId, RoomId, RoleType.CoOwner)); + } + + [Fact] + public async Task TheRoomsCreatorAlwaysAppearsAsCreator() + { + var (_, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + + var roles = await RoomUtils.GetRoles(RoomId); + var row = Assert.Single(roles); + Assert.Equal(CreatorId, row.AccountId); + Assert.Equal(RoleType.Creator, row.Role); + + // ...and the room the client loads carries the same entry, so the owner's own watch + // shows them as the owner. + var room = await RoomUtils.GetRoom(RoomId); + Assert.Contains(room!.Roles, x => x.AccountId == CreatorId && x.Role == RoleType.Creator); + } + + [Fact] + public async Task RoleLookupUsesTheHighestRoleAnAccountHolds() + { + var (_, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + db.RoomRoles.AddRange( + new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.Moderator, InvitedRole = RoleType.None }, + new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.Creator, InvitedRole = RoleType.None }); + await db.SaveChangesAsync(); + + var role = await RoomUtils.GetRoleForPlayer(MemberId, RoomId); + + Assert.NotNull(role); + Assert.Equal(RoleType.Creator, role!.Role); + Assert.True(await MatchmakingUtils.IsAtLeastRole(MemberId, RoomId, RoleType.CoOwner)); + } + + [Fact] + public async Task InvitingAPlayerOnlyTouchesThisRoomsRole() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db, RoomId, CreatorId); + await SeedRoomAsync(db, 6, CreatorId); + db.RoomRoles.Add(new RoomRole { RoomId = 6, AccountId = MemberId, Role = RoleType.CoOwner, InvitedRole = RoleType.None }); + await db.SaveChangesAsync(); + + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, "role=30"); + + var result = await api.InvitePlayerToRole(RoomId, MemberId); + using var doc = JsonDocument.Parse(Serialize(result)); + Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); + + // The other room's role is untouched... + var other = await ReadPersistedRoleAsync(factory, 6, MemberId); + Assert.NotNull(other); + Assert.Equal(RoleType.CoOwner, other!.Role); + Assert.Null(other.LastChangedByAccountId); + + // ...and this room has its own pending invite. + var invited = await ReadPersistedRoleAsync(factory, RoomId, MemberId); + Assert.NotNull(invited); + Assert.Equal(RoleType.None, invited!.Role); + Assert.Equal(RoleType.CoOwner, invited.InvitedRole); + Assert.Equal(CreatorId, invited.LastChangedByAccountId); + } + + [Fact] + public async Task SettingARoleRecordsWhoChangedItAndReturnsTheRoom() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetQuery(api, "?role=20"); + + var result = await api.SetPlayerRole(RoomId, MemberId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal(RoomId, doc.RootElement.GetProperty("value").GetProperty("RoomId").GetInt64()); + + var granted = await ReadPersistedRoleAsync(factory, RoomId, MemberId); + Assert.Equal(RoleType.Moderator, granted!.Role); + Assert.Equal(RoleType.None, granted.InvitedRole); + Assert.Equal(CreatorId, granted.LastChangedByAccountId); + } + + [Fact] + public async Task RoleEntriesCarryLastChangedByAccountIdOnTheWire() + { + var (_, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + db.RoomRoles.Add(new RoomRole + { + RoomId = RoomId, + AccountId = MemberId, + Role = RoleType.CoOwner, + InvitedRole = RoleType.None, + LastChangedByAccountId = CreatorId, + }); + await db.SaveChangesAsync(); + + var room = await RoomUtils.GetRoom(RoomId); + using var doc = JsonDocument.Parse(Serialize(room!.Roles)); + var entry = doc.RootElement.EnumerateArray() + .Single(x => x.GetProperty("AccountId").GetInt64() == MemberId); + + Assert.Equal(CreatorId, entry.GetProperty("LastChangedByAccountId").GetInt64()); + Assert.True(entry.TryGetProperty("InvitedRole", out _)); + } + + [Fact] + public async Task TheCreatorTierCannotBeGrantedThroughTheRoleRoute() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, "role=255"); + + var result = await api.SetPlayerRole(RoomId, MemberId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Null(await ReadPersistedRoleAsync(factory, RoomId, MemberId)); + } + + [Fact] + public async Task ARequestWithNoRoleIsRejectedInsteadOfDemotingThePlayer() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + db.RoomRoles.Add(new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.CoOwner, InvitedRole = RoleType.None }); + await db.SaveChangesAsync(); + + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, ""); // no role field at all + + var result = await api.SetPlayerRole(RoomId, MemberId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Contains("valid role", doc.RootElement.GetProperty("error").GetString()!); + + // An unbound role must never read as None, which would strip the co-owner. + var unchanged = await ReadPersistedRoleAsync(factory, RoomId, MemberId); + Assert.Equal(RoleType.CoOwner, unchanged!.Role); + } + + [Fact] + public async Task DemotingAPlayerRemovesTheirRoleEntry() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + db.RoomRoles.Add(new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.CoOwner, InvitedRole = RoleType.None }); + await db.SaveChangesAsync(); + + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, "role=0"); + + var result = await api.SetPlayerRole(RoomId, MemberId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Null(await ReadPersistedRoleAsync(factory, RoomId, MemberId)); + } + + [Fact] + public async Task SettingARoleRequiresCoOwnerRights() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db, RoomId, StrangerId); + + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, "role=30"); + + var result = await api.SetPlayerRole(RoomId, MemberId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Null(await ReadPersistedRoleAsync(factory, RoomId, MemberId)); + } + + [Fact] + public async Task TheRoomsCreatorCannotBeGivenAnotherRole() + { + var (_, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, "role=20"); + + var result = await api.SetPlayerRole(RoomId, CreatorId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); + } +} diff --git a/ServerTests/RoomSaveTests.cs b/ServerTests/RoomSaveTests.cs new file mode 100644 index 0000000..73d846a --- /dev/null +++ b/ServerTests/RoomSaveTests.cs @@ -0,0 +1,650 @@ +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using RadicadeServer; +using RadicadeServer.DB; +using RadicadeServer.API.Studio; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RoomServer; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Room saves are the one path where losing a request loses a creator's work, and the +/// failure is silent: the client reports a successful save to the player, and the room +/// reopens with nothing in it. These pin the two things that caused exactly that - the +/// save body not binding, and a save with no uploaded blob being recorded anyway - plus +/// the history fallback that keeps a room loadable when one blob goes missing. +/// +[Collection(DbFactoryCollection.Name)] +public class RoomSaveTests +{ + private const long CreatorId = 7; + private const long RoomId = 1; + + private static readonly JsonSerializerOptions ServerJson = new() + { + PropertyNamingPolicy = null, + WriteIndented = true, + }; + + private static string Serialize(object? value) => JsonSerializer.Serialize(value, ServerJson); + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + // A unique database name isolates each test; the store is NOT given its own + // InMemoryDatabaseRoot, because a distinct root makes Entity Framework build a + // distinct internal service provider every time and it throws once the process + // has more than twenty of them. + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + private static void SetUser(Controller controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetJsonBody(Controller controller, string json) + { + var request = controller.ControllerContext.HttpContext.Request; + request.ContentType = "application/json"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json)); + } + + private static void SetFormBody(Controller controller, string body) + { + var request = controller.ControllerContext.HttpContext.Request; + request.ContentType = "application/x-www-form-urlencoded"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + } + + /// Puts a blob in CDN storage, the way a completed upload would. + private static string WriteBlob(string name) + { + Directory.CreateDirectory(DBConfig.CDNStorage); + var path = Path.Join(DBConfig.CDNStorage, name); + File.WriteAllBytes(path, new byte[] { 1, 2, 3 }); + return path; + } + + private static void DeleteBlob(string name) + { + var path = Path.Join(DBConfig.CDNStorage, name); + if (File.Exists(path)) File.Delete(path); + } + + /// + /// Reads a value back from the store through a NEW context. A context that has already + /// materialized an entity keeps serving its own copy, so reusing the seeding context + /// would report pre-repair values even after the repair was persisted. + /// + private static async Task ReadPersistedSubRoomAsync(TestDbContextFactory factory, long subRoomId) + { + await using var db = factory.CreateDbContext(); + return await db.SubRooms.AsNoTracking().FirstAsync(x => x.SubRoomId == subRoomId); + } + + private static async Task SeedRoomAsync(RecDBContext db, long creatorId = CreatorId, string dataBlob = "") + { + db.Rooms.Add(new RoomData + { + RoomId = RoomId, + Name = "Build Room", + CreatorAccountId = creatorId, + Accessibility = Accessibility.Public, + MaxPlayers = 8, + CreatedAt = DateTime.UtcNow, + }); + + var subRoom = new SubRoom + { + RoomId = RoomId, + Name = "Build Room", + UnitySceneId = "BuildRoomScene", + MaxPlayers = 8, + Accessibility = Accessibility.Public, + DataBlob = dataBlob, + }; + + db.SubRooms.Add(subRoom); + await db.SaveChangesAsync(); + + return subRoom.SubRoomId; + } + + private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + return (factory, factory.CreateDbContext()); + } + + [Fact] + public async Task SaveAcceptsTheJsonEnvelopeThe2023ClientPosts() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, """ + { + "RoomData": { "Filename": "meta-1.bin" }, + "SubRoomData": { "Filename": "scene-1.bin", "Hash": null }, + "UnityAssetId": null, + "PersistenceVersion": 4, + "InventionUsage": "", + "AutoPublish": true + } + """); + + WriteBlob("scene-1.bin"); + try + { + var result = await api.UploadRoomData(RoomId, subRoomId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); + + // Studio's Result.EEKDNHEOFFD requires Success == (Error == null), so unlike the + // shared RecNet envelope this save endpoint must return a null error on success. + Assert.Equal(JsonValueKind.Null, doc.RootElement.GetProperty("error").ValueKind); + + // The loader downloads the room's `CurrentSave.DataBlob`; a save that records + // anything else opens empty. + var saved = doc.RootElement.GetProperty("value").GetProperty("room").GetProperty("SubRooms")[0]; + Assert.Equal("scene-1.bin", saved.GetProperty("CurrentSave").GetProperty("DataBlob").GetString()); + + // The save the response hands back is the client's OWN projection: camelCase, and + // a different field set from that CurrentSave. Serving the room's save here leaves + // every field the client reads back null, and its save handling NREs on them. + var save = doc.RootElement.GetProperty("value").GetProperty("subRoomDataSave"); + Assert.Equal("scene-1.bin", save.GetProperty("dataBlob").GetString()); + Assert.Equal(subRoomId, save.GetProperty("subRoomId").GetInt64()); + Assert.Equal(save.GetProperty("subRoomDataSaveId").GetInt64(), saved.GetProperty("CurrentSave").GetProperty("SubRoomDataSaveId").GetInt64()); + Assert.Equal(JsonValueKind.Null, save.GetProperty("dataBlobHash").ValueKind); + Assert.Equal(JsonValueKind.Null, save.GetProperty("unityAsset").ValueKind); + Assert.False(save.TryGetProperty("DataBlob", out _)); + + // A subroom the client is shown has to name a creator: it NREs on a null one, so + // the account that saved it is reported from here on. + Assert.Equal(CreatorId, saved.GetProperty("CreatorAccountId").GetInt64()); + + // ...and both room spellings are present for the client's saver to read back. + Assert.Equal(RoomId, doc.RootElement.GetProperty("value").GetProperty("room").GetProperty("RoomId").GetInt64()); + Assert.Equal(RoomId, doc.RootElement.GetProperty("value").GetProperty("RoomId").GetInt64()); + + Assert.Equal("scene-1.bin", (await ReadPersistedSubRoomAsync(factory, subRoomId)).DataBlob); + + await using var verify = factory.CreateDbContext(); + Assert.True(await verify.SubRoomHistory.AnyAsync(x => x.SubRoomId == subRoomId)); + } + finally { DeleteBlob("scene-1.bin"); } + } + + [Fact] + public async Task SaveResponseEchoesTheBlobHashItWasGiven() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, """ + { + "SubRoomData": { "Filename": "hashed.bin", "Hash": "9f2c1a" }, + "RoomData": { "Filename": "meta-2.bin" }, + "AutoPublish": true + } + """); + + WriteBlob("hashed.bin"); + try + { + var result = await api.UploadRoomData(RoomId, subRoomId); + using var doc = JsonDocument.Parse(Serialize(result)); + + // The hash goes out on the save the response names, not on the room: it is the + // only digest either side has of the scene blob that save points at. + var save = doc.RootElement.GetProperty("value").GetProperty("subRoomDataSave"); + Assert.Equal("hashed.bin", save.GetProperty("dataBlob").GetString()); + Assert.Equal("9f2c1a", save.GetProperty("dataBlobHash").GetString()); + } + finally { DeleteBlob("hashed.bin"); } + } + + /// + /// A subroom that has never been saved has no creator and says so, rather than leaving the + /// key out: the client reads a missing key and a null the same way. + /// + [Fact] + public async Task AnUnsavedSubRoomReportsNoCreator() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db); + + var room = await RoomUtils.GetRoom(RoomId); + Assert.NotNull(room); + + var subRoom = room!.SubRooms.Single(x => x.SubRoomId == subRoomId); + Assert.Null(subRoom.CreatorAccountId); + } + + [Fact] + public async Task SaveAcceptsTheLegacyFormFields() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetFormBody(api, "filename=legacy.bin&inventionUsage=&savedByAccountId=999"); + + WriteBlob("legacy.bin"); + try + { + var result = await api.UploadRoomData(RoomId, subRoomId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal("legacy.bin", (await ReadPersistedSubRoomAsync(factory, subRoomId)).DataBlob); + } + finally { DeleteBlob("legacy.bin"); } + } + + [Fact] + public async Task SaveTakesTheSaverFromTheTokenNotTheBodyOrAPresenceRow() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db); + // No presence row at all: editing a room does not require one, and the old required + // `savedByAccountId` presence lookup turned an otherwise valid save into "Invalid auth". + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, """{ "SubRoomData": { "Filename": "scene-2.bin" }, "SavedByAccountId": 999 }"""); + + WriteBlob("scene-2.bin"); + try + { + var result = await api.UploadRoomData(RoomId, subRoomId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal(CreatorId, (await ReadPersistedSubRoomAsync(factory, subRoomId)).SavedByAccountId); + } + finally { DeleteBlob("scene-2.bin"); } + } + + [Fact] + public async Task StudioSourceManifestRouteReturnsHashAndHidesPrivateRoomSource() + { + var previousStorage = DBConfig.CDNStorage; + var isolatedStorage = Path.Combine(Path.GetTempPath(), "studio-source-route-" + Guid.NewGuid()); + DBConfig.CDNStorage = isolatedStorage; + Directory.CreateDirectory(isolatedStorage); + var (_, db) = CreateContext(); + await using var _db = db; + var subRoomId = await SeedRoomAsync(db); + const string manifestId = "0123456789abcdef0123456789abcdef01234567"; + var manifestBytes = Encoding.UTF8.GetBytes("{\"Version\":2,\"Files\":[]}"); + await File.WriteAllBytesAsync(Path.Combine(isolatedStorage, manifestId), manifestBytes); + var subRoom = await db.SubRooms.SingleAsync(x => x.SubRoomId == subRoomId); + subRoom.UnityAssetId = manifestId; + await db.SaveChangesAsync(); + try + { + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + var result = Assert.IsType(await api.GetStudioUnityAsset(RoomId, subRoomId)); + var asset = Assert.IsType(result.Value); + Assert.Equal(manifestId, asset.UnityAssetId); + Assert.Equal(manifestId, asset.Filename); + Assert.Equal(Convert.ToBase64String(SHA256.HashData(manifestBytes)), asset.Hash); + Assert.Empty(asset.BakedUnityAssets); + + var room = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); + room.Accessibility = Accessibility.Private; + await db.SaveChangesAsync(); + SetUser(api, 42); + Assert.IsType(await api.GetStudioUnityAsset(RoomId, subRoomId)); + } + finally + { + DBConfig.CDNStorage = previousStorage; + if (Directory.Exists(isolatedStorage)) Directory.Delete(isolatedStorage, true); + } + } + + [Fact] + public async Task SaveWithAMissingStudioManifestIsRejectedWithoutMutatingCurrentSaveOrHistory() + { + var previousStorage = DBConfig.CDNStorage; + var isolatedStorage = Path.Combine(Path.GetTempPath(), "studio-manifest-test-" + Guid.NewGuid()); + DBConfig.CDNStorage = isolatedStorage; + Directory.CreateDirectory(isolatedStorage); + var (factory, db) = CreateContext(); + await using var _db = db; + var subRoomId = await SeedRoomAsync(db, dataBlob: "keep-manifest.bin"); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, """{ "SubRoomData": { "Filename": "next-manifest.bin" }, "UnityAsset": { "Filename": "0000000000000000000000000000000000000000" } }"""); + WriteBlob("keep-manifest.bin"); + WriteBlob("next-manifest.bin"); + try + { + var result = await api.UploadRoomData(RoomId, subRoomId); + using var doc = JsonDocument.Parse(Serialize(result)); + Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal("Unity source manifest has not been uploaded.", doc.RootElement.GetProperty("error").GetString()); + Assert.Equal("keep-manifest.bin", (await ReadPersistedSubRoomAsync(factory, subRoomId)).DataBlob); + Assert.Equal(string.Empty, (await ReadPersistedSubRoomAsync(factory, subRoomId)).UnityAssetId); + await using var verify = factory.CreateDbContext(); + Assert.False(await verify.SubRoomHistory.AnyAsync(x => x.SubRoomId == subRoomId)); + } + finally + { + DBConfig.CDNStorage = previousStorage; + if (Directory.Exists(isolatedStorage)) Directory.Delete(isolatedStorage, true); + } + } + + [Fact] + public async Task SaveWithoutABlobNameIsRejectedAndLeavesTheCurrentSave() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db, dataBlob: "keep.bin"); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, """{ "AutoPublish": true }"""); + + WriteBlob("keep.bin"); + try + { + var result = await api.UploadRoomData(RoomId, subRoomId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal("keep.bin", (await ReadPersistedSubRoomAsync(factory, subRoomId)).DataBlob); + + await using var verify = factory.CreateDbContext(); + Assert.False(await verify.SubRoomHistory.AnyAsync(x => x.SubRoomId == subRoomId)); + } + finally { DeleteBlob("keep.bin"); } + } + + [Fact] + public async Task SaveRequiresCoOwnerRights() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db, creatorId: 999, dataBlob: "keep.bin"); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, """{ "SubRoomData": { "Filename": "scene-3.bin" } }"""); + + WriteBlob("scene-3.bin"); + try + { + var result = await api.UploadRoomData(RoomId, subRoomId); + using var doc = JsonDocument.Parse(Serialize(result)); + + Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); + Assert.Equal("keep.bin", (await ReadPersistedSubRoomAsync(factory, subRoomId)).DataBlob); + } + finally { DeleteBlob("scene-3.bin"); } + } + + [Fact] + public async Task RoomLoadFallsBackToTheNewestSaveStillOnDisk() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db, dataBlob: "gone.bin"); + + var newer = new RestorableSubRoom + { + RoomId = RoomId, + SubRoomId = subRoomId, + DataBlob = "also-gone.bin", + SavedByAccountId = CreatorId, + CreatedAt = DateTime.UtcNow, + }; + var older = new RestorableSubRoom + { + RoomId = RoomId, + SubRoomId = subRoomId, + DataBlob = "old-1.bin", + SavedByAccountId = CreatorId, + CreatedAt = DateTime.UtcNow.AddHours(-1), + }; + db.SubRoomHistory.AddRange(newer, older); + await db.SaveChangesAsync(); + + WriteBlob("old-1.bin"); + try + { + var room = await RoomUtils.GetRoom(RoomId); + + Assert.NotNull(room); + Assert.Equal("old-1.bin", room!.SubRooms.Single(x => x.SubRoomId == subRoomId).DataBlob); + + // The repair is persisted: it is a recovery, not a per-read patch-up. + Assert.Equal("old-1.bin", (await ReadPersistedSubRoomAsync(factory, subRoomId)).DataBlob); + } + finally { DeleteBlob("old-1.bin"); } + } + + /// Seeds a subroom with a live save plus two revisions behind it. + private static async Task SeedSaveHistoryAsync(RecDBContext db) + { + var subRoomId = await SeedRoomAsync(db, dataBlob: "live.bin"); + + db.SubRoomHistory.AddRange( + new RestorableSubRoom + { + RoomId = RoomId, + SubRoomId = subRoomId, + DataBlob = "newer.bin", + SavedByAccountId = CreatorId, + CreatedAt = DateTime.UtcNow.AddMinutes(-5), + }, + new RestorableSubRoom + { + RoomId = RoomId, + SubRoomId = subRoomId, + DataBlob = "older.bin", + SavedByAccountId = CreatorId, + CreatedAt = DateTime.UtcNow.AddHours(-2), + }); + + await db.SaveChangesAsync(); + + return subRoomId; + } + + private static List BlobsIn(JsonElement page) => + page.GetProperty("Results").EnumerateArray() + .Select(x => x.GetProperty("DataBlob").GetString()) + .ToList(); + + /// + /// The save list the client asks for on entering a room. A 404 here is the whole history + /// screen broken, and any revision missing from it is a save nobody can restore. + /// + [Fact] + public async Task SavesPageListsTheLiveRevisionAndItsHistoryNewestFirst() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedSaveHistoryAsync(db); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + + var ok = Assert.IsType(await api.GetSubRoomSaves(RoomId, subRoomId)); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + + // The live save is what the room is running, so it leads; the revisions follow, + // newest first. + Assert.Equal(new List { "live.bin", "newer.bin", "older.bin" }, BlobsIn(doc.RootElement)); + + // Both spellings of the total, because the client's paged DTO and the reference + // disagree on the name - and it counts the history, not the page. + Assert.Equal(3, doc.RootElement.GetProperty("TotalResults").GetInt32()); + Assert.Equal(3, doc.RootElement.GetProperty("TotalCount").GetInt32()); + + // Ids are what the client keys the list by: the live save is 1 (the id the room + // payload's CurrentSave already carries) and every revision is distinct. + var ids = doc.RootElement.GetProperty("Results").EnumerateArray() + .Select(x => x.GetProperty("SubRoomDataSaveId").GetInt64()).ToList(); + Assert.Equal(3, ids.Distinct().Count()); + Assert.Equal(1, ids[0]); + Assert.All(ids, id => Assert.True(id >= 1)); + + // A page is a window onto the history, not the whole of it. + var paged = Assert.IsType(await api.GetSubRoomSaves(RoomId, subRoomId, 1, 1)); + using var pagedDoc = JsonDocument.Parse(Serialize(paged.Value)); + Assert.Equal(new List { "newer.bin" }, BlobsIn(pagedDoc.RootElement)); + Assert.Equal(3, pagedDoc.RootElement.GetProperty("TotalResults").GetInt32()); + + // The lighter twin carries the same rows. + var light = Assert.IsType(await api.GetSubRoomSavesWithoutUnityAssets(RoomId, subRoomId)); + using var lightDoc = JsonDocument.Parse(Serialize(light.Value)); + Assert.Equal(new List { "live.bin", "newer.bin", "older.bin" }, BlobsIn(lightDoc.RootElement)); + Assert.False(lightDoc.RootElement.GetProperty("Results")[0].TryGetProperty("UnitySubAssets", out _)); + } + + [Fact] + public async Task SavesPageIsEmptyRatherThanMissingForAnUnknownSubroom() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + await SeedRoomAsync(db); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + + // An unknown id answers an empty page, so a client holding a stale one still renders. + var ok = Assert.IsType(await api.GetSubRoomSaves(RoomId, 9999)); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + + Assert.Empty(doc.RootElement.GetProperty("Results").EnumerateArray()); + Assert.Equal(0, doc.RootElement.GetProperty("TotalResults").GetInt32()); + } + + [Fact] + public async Task SavesAreReadableByAStrangerOnlyWhileTheyAreInTheRoom() + { + const long visitorId = 42; + + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedSaveHistoryAsync(db); + var api = new RoomServerAPI(db); + SetUser(api, visitorId); + + // Not the creator and holding no role: the list exposes unpublished work, so from + // outside the room it is a 403. + var refused = Assert.IsType(await api.GetSubRoomSaves(RoomId, subRoomId)); + Assert.Equal(StatusCodes.Status403Forbidden, refused.StatusCode); + + // Standing in the room is its own grant: the client resolves which version to load + // from this list, so a visitor who cannot read it cannot load the room. + var instance = new RoomInstance { roomId = RoomId, subRoomId = subRoomId }; + db.RoomInstances.Add(instance); + await db.SaveChangesAsync(); + + db.Presences.Add(new RealPresence { playerId = visitorId, isOnline = true, roomInstance = instance.roomInstanceId }); + await db.SaveChangesAsync(); + + var ok = Assert.IsType(await api.GetSubRoomSaves(RoomId, subRoomId)); + using var doc = JsonDocument.Parse(Serialize(ok.Value)); + Assert.Equal(3, doc.RootElement.GetProperty("TotalResults").GetInt32()); + + // A visitor in a DIFFERENT room is not in this one. + instance.roomId = RoomId + 1; + await db.SaveChangesAsync(); + + var elsewhere = Assert.IsType(await api.GetSubRoomSaves(RoomId, subRoomId)); + Assert.Equal(StatusCodes.Status403Forbidden, elsewhere.StatusCode); + } + + [Fact] + public async Task RestoreKeepsTheRevisionItReplaced() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db, dataBlob: "current.bin"); + var api = new RoomServerAPI(db); + SetUser(api, CreatorId); + SetJsonBody(api, """{ "Filename": "older.bin" }"""); + + WriteBlob("older.bin"); + try + { + var result = await api.RestoreRoomData(RoomId, subRoomId); + using var doc = JsonDocument.Parse(Serialize(result)); + Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); + + // Restoring is the one path that can drop a save outright: the revision it + // replaced has to be in the history afterwards, or it is gone. + await using var verify = factory.CreateDbContext(); + var history = await verify.SubRoomHistory.Where(x => x.SubRoomId == subRoomId).ToListAsync(); + Assert.Contains(history, x => x.DataBlob == "current.bin"); + Assert.Equal("older.bin", (await ReadPersistedSubRoomAsync(factory, subRoomId)).DataBlob); + } + finally { DeleteBlob("older.bin"); } + } + + [Fact] + public async Task RoomLoadClearsTheSaveOnlyWhenNothingIsRecoverable() + { + var (factory, db) = CreateContext(); + await using var _db = db; + + var subRoomId = await SeedRoomAsync(db, dataBlob: "gone.bin"); + + var room = await RoomUtils.GetRoom(RoomId); + + Assert.NotNull(room); + Assert.Equal(string.Empty, room!.SubRooms.Single(x => x.SubRoomId == subRoomId).DataBlob); + } +} diff --git a/ServerTests/RouteAmbiguityTests.cs b/ServerTests/RouteAmbiguityTests.cs new file mode 100644 index 0000000..dd4b016 --- /dev/null +++ b/ServerTests/RouteAmbiguityTests.cs @@ -0,0 +1,107 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Two endpoints that claim the same method + path make ASP.NET throw +/// AmbiguousMatchException (HTTP 500) for EVERY request to that path, before any +/// authorization or handler code runs. That is how a club "set minimum level" or "remove member" +/// click became an unhandled exception in the server log instead of an answer. +/// +/// The route table is therefore asserted directly: a controller-level test cannot see two +/// endpoints fighting over one URL, because it calls the handler it wants by name. +/// +/// The classic way this happens here is an action carrying both the class-relative template +/// ("{clubId}/minlevel" under [Route("clubs/club")]) and an absolute +/// "~/clubs/club/{clubId}/minlevel" copy of it: the same path, registered twice. +/// +public class RouteAmbiguityTests +{ + /// + /// {id} and {id:long} both match a numeric segment with the same route + /// precedence, so ASP.NET reports ambiguity for that pair as well - compare on the + /// constraint-free form to catch both kinds. + /// + private static string Unconstrained(string template) + { + var result = new System.Text.StringBuilder(template.Length); + var index = 0; + + while (true) + { + var start = template.IndexOf('{', index); + if (start < 0) + { + result.Append(template, index, template.Length - index); + return result.ToString(); + } + + var end = template.IndexOf('}', start); + if (end < 0) + { + result.Append(template, index, template.Length - index); + return result.ToString(); + } + + var name = template.Substring(start + 1, end - start - 1).TrimStart('*'); + var cut = name.IndexOfAny(new[] { ':', '(' }); + if (cut >= 0) name = name.Substring(0, cut); + + result.Append(template, index, start - index).Append('{').Append(name).Append('}'); + index = end + 1; + } + } + + private static IEnumerable<(string Verb, string Template, string Owner)> Routes() + { + var assembly = typeof(RadicadeServer.Clubs.ClubsAPI).Assembly; + + foreach (var type in assembly.GetTypes()) + { + if (!typeof(ControllerBase).IsAssignableFrom(type) || type.IsAbstract) continue; + + var classRoutes = type.GetCustomAttributes(inherit: false) + .Select(attribute => (attribute.Template ?? string.Empty).Trim('/')) + .ToArray(); + + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + foreach (var attribute in method.GetCustomAttributes(inherit: false)) + { + var template = attribute.Template ?? string.Empty; + + if (template.StartsWith("~/", StringComparison.Ordinal)) + { + foreach (var verb in attribute.HttpMethods) + yield return (verb, template.Substring(2), $"{type.Name}.{method.Name}"); + continue; + } + + var prefixes = classRoutes.Length == 0 ? new[] { string.Empty } : classRoutes; + foreach (var prefix in prefixes) + { + var combined = prefix.Length == 0 ? template : $"{prefix}/{template.TrimStart('/')}"; + foreach (var verb in attribute.HttpMethods) + yield return (verb, combined, $"{type.Name}.{method.Name}"); + } + } + } + } + } + + [Fact] + public void NoTwoEndpointsClaimTheSameMethodAndPath() + { + var duplicates = Routes() + .GroupBy(route => (route.Verb, Path: Unconstrained(route.Template))) + .Where(group => group.Select(route => (route.Owner, route.Template)).Distinct().Count() > 1) + .Select(group => $"{group.Key.Verb} /{group.Key.Path} is claimed by " + + string.Join(" and ", group.Select(route => $"{route.Owner} [{route.Template}]").Distinct())) + .ToArray(); + + Assert.Empty(duplicates); + } +} diff --git a/ServerTests/RroReacquisitionTests.cs b/ServerTests/RroReacquisitionTests.cs new file mode 100644 index 0000000..03a4b99 --- /dev/null +++ b/ServerTests/RroReacquisitionTests.cs @@ -0,0 +1,348 @@ +using System.Text.Json; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Players.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// What a completion pays once the player owns everything the activity hands out. The reward can no +/// longer be a first-time one, so it becomes a reacquisition: a selection of ordinary items for a +/// map game, a non-rare consumable for a quest. +/// +public class RroReacquisitionTests +{ + private static string ShippedCatalogueJson() => + File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json")); + + private static string ConsumableCatalogueJson() => JsonSerializer.Serialize(new List + { + new() { ConsumableItemDesc = "consumable-a", FriendlyName = "Consumable A", Rarity = Rarity.Common }, + new() { ConsumableItemDesc = "consumable-b", FriendlyName = "Consumable B", Rarity = Rarity.Uncommon }, + }); + + private static LevelUpRewardPool.Pool ConsumablePool() => new() + { + Consumables = ["consumable-a", "consumable-b"] + }; + + // --------------------------------------------------------------------------------- + // The repeat is filed under its own context + // --------------------------------------------------------------------------------- + + [Fact] + public void ARepeatIsFiledUnderTheReacquisitionContext() + { + // Not under the activity's own context: the client has to be able to tell a duplicate from + // a first-time reward. + Assert.Equal(20, (int)RroRewardPools.ReacquisitionContext); + Assert.Equal(GiftContext.Reacquisition, RroRewardPools.ReacquisitionContext); + Assert.False(RroRewardPools.IsDeprecated(RroRewardPools.ReacquisitionContext)); + } + + // --------------------------------------------------------------------------------- + // A map game offers a selection of ordinary items + // --------------------------------------------------------------------------------- + + [Fact] + public void AMapGameOffersASelectionOfOrdinaryItems() + { + var drops = RroRewardPools.BuildFallbackDrops( + selection: true, owned: null, random: new Random(4), catalogueJson: ShippedCatalogueJson()); + + Assert.Equal(3, drops.Count); + Assert.Equal(3, drops.Select(x => x.AvatarItemDesc).Distinct(StringComparer.Ordinal).Count()); + Assert.All(drops, drop => Assert.Equal(GiftContext.Reacquisition, drop.Context)); + Assert.All(drops, drop => Assert.Equal(20, drop.GiftContext)); + Assert.All(drops, drop => Assert.Contains(drop.Rarity, new[] { Rarity.Uncommon, Rarity.Rare })); + } + + [Fact] + public void TheOrdinaryPoolIsTheTwoLowestRaritiesAndNothingDenied() + { + var json = ShippedCatalogueJson(); + var pool = RroRewardPools.ReacquisitionPool(json); + + // The shipped scale is 2/3/4/5 stars as rarity 10/20/30/50; the ordinary tiers are the two + // lowest, which is a few hundred items rather than the whole catalogue. + Assert.True(pool.Count > 300, $"only {pool.Count} ordinary items"); + Assert.All(pool, item => Assert.Contains(item.Rarity, new[] { 10, 20 })); + Assert.All(pool, item => Assert.False(LevelUpRewardPool.IsAlwaysDenied(item))); + Assert.All(pool, item => Assert.False(string.IsNullOrWhiteSpace(item.AvatarItemDesc))); + Assert.All(pool, item => Assert.False(item.IsBaseAvatarItem)); + } + + [Fact] + public void TheGenericPoolIsNotAnotherActivitysRewardList() + { + // A dodgeball clear must not offer a Crimson Cauldron piece as a consolation prize: nothing + // from any reward set belongs in the generic draw. + var json = ShippedCatalogueJson(); + var pool = RroRewardPools.ReacquisitionPool(json); + + Assert.All(pool, item => Assert.False(RroRewardPools.IsRewardSetItem(item), + $"{item.FriendlyName} is a reward-set item and should not be offered generically")); + Assert.All(pool, item => Assert.DoesNotContain("Witch Hunter", item.FriendlyName, StringComparison.OrdinalIgnoreCase)); + Assert.All(pool, item => Assert.DoesNotContain("Angler", item.FriendlyName, StringComparison.OrdinalIgnoreCase)); + Assert.All(pool, item => Assert.DoesNotContain("Knight", item.FriendlyName, StringComparison.OrdinalIgnoreCase)); + + // And the ordinary items a player actually saw are still eligible. + Assert.Contains(pool, item => item.FriendlyName == "Turtleneck (White)"); + Assert.Contains(pool, item => item.FriendlyName == "Bike Gloves (Blue)"); + } + + [Fact] + public void TheFilterCatchesRewardItemsThePoolsDoNot() + { + // The pools only name the colours a rank pays. A colour they do not name is still that + // activity's item and must not leak into a generic reward either. + Assert.True(RroRewardPools.IsRewardSetItem(new AvatarItem { FriendlyName = "Stunt Singlet (Pink)" })); + Assert.True(RroRewardPools.IsRewardSetItem(new AvatarItem { FriendlyName = "Wizard Robes (Green)" })); + Assert.True(RroRewardPools.IsRewardSetItem(new AvatarItem { FriendlyName = "Runner Harness (Orange)" })); + Assert.True(RroRewardPools.IsRewardSetItem(new AvatarItem { FriendlyName = "Angler Hat (Gray)" })); + + Assert.False(RroRewardPools.IsRewardSetItem(new AvatarItem { FriendlyName = "Turtleneck (White)" })); + Assert.False(RroRewardPools.IsRewardSetItem(new AvatarItem { FriendlyName = "Bike Gloves (Blue)" })); + } + + [Fact] + public void AnItemBoxCarriesNoCurrencyType() + { + var json = ShippedCatalogueJson(); + var quest = RroRewardPools.BuildGift(GiftContext.Quest_Goblin_A, null, new Random(1), json); + Assert.NotNull(quest); + Assert.Equal(CurrencyType.Invalid, quest!.CurrencyType); + + var reacquired = RroRewardPools.BuildFallbackDrops(true, null, new Random(1), json); + Assert.All(reacquired, drop => Assert.Equal(CurrencyType.Invalid, drop.CurrencyType)); + Assert.All(reacquired, drop => Assert.Equal(0, drop.Currency)); + } + + [Fact] + public void TheSelectionOpensUpTheCatalogueBeforeItRepeats() + { + // The point of preferring what is unowned: a player who has finished Golden Trophy should be + // offered something they have never had, not one of the two pieces they already own. + var json = ShippedCatalogueJson(); + var pool = RroRewardPools.ReacquisitionPool(json); + var keep = pool.Take(2).ToList(); + var owned = pool.Skip(2) + .Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var drops = RroRewardPools.BuildFallbackDrops(true, owned, new Random(6), json); + + Assert.Equal(2, drops.Count); + var expected = keep.Select(x => x.AvatarItemDesc).ToHashSet(StringComparer.Ordinal); + Assert.All(drops, drop => Assert.Contains(drop.AvatarItemDesc, expected)); + } + + [Fact] + public void APlayerWhoOwnsEverythingIsStillOfferedASelection() + { + // A repeat has to terminate: once the catalogue itself is owned, the draw repeats rather + // than leaving the player with nothing. + var json = ShippedCatalogueJson(); + var owned = RroRewardPools.ReacquisitionPool(json) + .Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var drops = RroRewardPools.BuildFallbackDrops(true, owned, new Random(8), json); + Assert.Equal(3, drops.Count); + Assert.Equal(3, drops.Select(x => x.AvatarItemDesc).Distinct(StringComparer.Ordinal).Count()); + } + + // --------------------------------------------------------------------------------- + // A quest is handed a consumable instead + // --------------------------------------------------------------------------------- + + [Fact] + public void AQuestIsHandedANonRareConsumableInsteadOfAChoice() + { + var drops = RroRewardPools.BuildFallbackDrops( + selection: false, + owned: null, + random: new Random(2), + catalogueJson: ShippedCatalogueJson(), + consumableCatalogJson: ConsumableCatalogueJson(), + consumablePool: ConsumablePool()); + + Assert.Single(drops); + Assert.False(string.IsNullOrWhiteSpace(drops[0].ConsumableItemDesc)); + Assert.True(string.IsNullOrWhiteSpace(drops[0].AvatarItemDesc)); + Assert.Equal(GiftContext.Reacquisition, drops[0].Context); + Assert.Equal(0, drops[0].Currency); + Assert.Equal(0, drops[0].Xp); + } + + [Fact] + public void TheConsumablePrefersOneThePlayerDoesNotHave() + { + var owned = new HashSet(StringComparer.OrdinalIgnoreCase) + { + LevelUpRewardPool.OwnershipKey(ConsumableCatalog.ToClientDesc("consumable-a")) + }; + + var gift = RroRewardPools.BuildFallbackConsumable( + owned, new Random(3), ConsumableCatalogueJson(), ConsumablePool()); + + Assert.NotNull(gift); + Assert.Equal(ConsumableCatalog.ToClientDesc("consumable-b"), gift!.ConsumableItemDesc); + } + + [Fact] + public void AnExhaustedConsumablePoolStillPaysOne() + { + var owned = new[] { "consumable-a", "consumable-b" } + .Select(x => LevelUpRewardPool.OwnershipKey(ConsumableCatalog.ToClientDesc(x))) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var gift = RroRewardPools.BuildFallbackConsumable( + owned, new Random(3), ConsumableCatalogueJson(), ConsumablePool()); + + Assert.NotNull(gift); + Assert.Equal(GiftContext.Reacquisition, gift!.Context); + } + + [Fact] + public void WithNothingToDrawFromTheFallbackIsEmptyRatherThanAWrongItem() + { + // An empty consumable catalogue leaves the caller to its token fallback. It must not reach + // for an ordinary item here: a quest is not a selection. + Assert.Null(RroRewardPools.BuildFallbackConsumable(null, new Random(1), "[]", new LevelUpRewardPool.Pool())); + Assert.Empty(RroRewardPools.BuildFallbackDrops( + selection: false, owned: null, random: new Random(1), + consumableCatalogJson: "[]", consumablePool: new LevelUpRewardPool.Pool())); + + // And a catalogue that cannot satisfy the ordinary tiers yields no selection. + Assert.Empty(RroRewardPools.BuildFallbackDrops( + selection: true, owned: null, random: new Random(1), catalogueJson: "[]")); + } + + // --------------------------------------------------------------------------------- + // A completion whose own pool cannot pay still offers items, with tokens as one of them + // --------------------------------------------------------------------------------- + + [Fact] + public void AStuntRunnerCompletionPaysItsOwnItems() + { + // The reward request for a finished Stunt Runner run arrives after the player has left the + // room, so the room the visit was recorded against names the Rec Center rather than the + // activity. The context the client claims is what names it, and the consumable the client + // files a run with nothing new to give under belongs to the same activity, not to tokens. + Assert.Equal("StuntRunner", RroRewardPools.RoomForContext(GiftContext.StuntRunner_TheMainEvent_C)); + Assert.Equal("StuntRunner", RroRewardPools.RoomForContext(GiftContext.StuntRunner_TheMainEvent_SS)); + Assert.Equal("StuntRunner", RroRewardPools.RoomForContext(GiftContext.StuntRunner_TheMainEvent_Consumable)); + Assert.True(RroRewardPools.IsRewardActivity("StuntRunner")); + + // A quest's pool is served by rank, and a consumable is no rank: the activity's lowest rank + // is what answers it. + Assert.True(RroRewardPools.TryFloorContext("StuntRunner", out var floor)); + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_C, floor); + + var drops = RroRewardPools.BuildDrops(GiftContext.StuntRunner_TheMainEvent_C, + selection: false, owned: null, random: new Random(3), catalogueJson: ShippedCatalogueJson()); + + var box = Assert.Single(drops); + Assert.False(string.IsNullOrWhiteSpace(box.AvatarItemDesc)); + // An item is not a token box: the run pays a Stunt Runner item, never the token package. + Assert.Equal(CurrencyType.Invalid, box.CurrencyType); + Assert.Equal(0, box.Currency); + } + + [Fact] + public void ACompletionNothingOwnOfItsOwnCanPayIsNotPaidInTokensAlone() + { + // Rec Royale's Season 1 rewards came from the Frontier Pass, which is not implemented, so a + // completed match used to be handed the token package as its whole reward. + var drops = RroRewardPools.BuildFallbackSelection(random: new Random(6), catalogueJson: ShippedCatalogueJson()); + + Assert.Equal(RroRewardPools.SelectionSize, drops.Count); + Assert.Equal(RroRewardPools.SelectionSize, + drops.Count(drop => !string.IsNullOrWhiteSpace(drop.AvatarItemDesc))); + + // An item box is an item: none of them is a token box with an item's name on it. + Assert.All(drops, drop => Assert.Equal(0, drop.Currency)); + Assert.All(drops, drop => Assert.Equal(CurrencyType.Invalid, drop.CurrencyType)); + } + + /// + /// The token box used to be in EVERY fallback selection - a fixed one-in-three chance of being + /// handed tokens on a completion that had nothing of its own left to give - which is what "I keep + /// getting these" was. It is now the draw, and this is the draw's own budget. + /// + [Fact] + public void TheTokenBoxIsTheExceptionInAFallbackSelection() + { + var json = ShippedCatalogueJson(); + var withTokens = 0; + const int draws = 400; + + for (var seed = 1; seed <= draws; seed++) + { + var drops = RroRewardPools.BuildFallbackSelection(random: new Random(seed), catalogueJson: json); + Assert.Equal(RroRewardPools.SelectionSize, drops.Count); + if (drops.Any(drop => drop.CurrencyType == CurrencyType.RecCenterTokens)) withTokens++; + } + + Assert.True(withTokens > 0, "the token option is never offered at all"); + Assert.True(withTokens < draws / 4, + $"the token option is in {withTokens} of {draws} selections; it is meant to be the exception"); + } + + [Fact] + public void TheTokenBoxIsTitledWithTheAmountItHolds() + { + var tokens = RroRewardPools.TokenGift(GiftContext.GameRewards_Tokens, RroRewardPools.CompletionTokenAmount); + + // Every token box used to be called "Rec Center Tokens" whatever it carried, so a 50 and a + // 150 looked like the same reward twice; the amount is the title now. + Assert.Equal("50 Tokens", tokens.FriendlyName); + Assert.Equal(50, tokens.Currency); + Assert.Equal(CurrencyType.RecCenterTokens, tokens.CurrencyType); + Assert.Equal("200 Tokens", RroRewardPools.TokenGift(GiftContext.GameRewards_Tokens, 200).FriendlyName); + } + + [Fact] + public void TheTokenBoxFillsTheSlotTheOrdinaryPoolCannot() + { + // A catalogue with two ordinary items cannot fill a three-box selection, so the slot it + // cannot fill is the token option rather than a short selection. + var drops = RroRewardPools.BuildFallbackSelection(random: new Random(1), catalogueJson: TwoItemCatalogueJson()); + + Assert.Equal(RroRewardPools.SelectionSize, drops.Count); + var tokens = Assert.Single(drops, drop => drop.CurrencyType == CurrencyType.RecCenterTokens); + Assert.Equal(RroRewardPools.CompletionTokenAmount, tokens.Currency); + Assert.Equal("50 Tokens", tokens.FriendlyName); + Assert.Equal(GiftContext.GameRewards_Tokens, tokens.Context); + } + + [Fact] + public void ACatalogueWithNothingOrdinaryStillPaysTheTokenOption() + { + var drops = RroRewardPools.BuildFallbackSelection(random: new Random(1), catalogueJson: "[]"); + + // Nothing to offer but tokens, so tokens it is - the caller's own token package is the same + // answer, and this only has to be the one box rather than a failure. + var tokens = Assert.Single(drops); + Assert.Equal(CurrencyType.RecCenterTokens, tokens.CurrencyType); + Assert.Equal("50 Tokens", tokens.FriendlyName); + } + + /// Two ordinary items, which is what a selection of three cannot be filled from. + private static string TwoItemCatalogueJson() => JsonSerializer.Serialize(new List + { + new() + { + AvatarItemDesc = "aa11,bb22", FriendlyName = "Plain Shirt (Grey)", Rarity = 10, + AvatarItemId = 1, CreatedAt = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc) + }, + new() + { + AvatarItemDesc = "cc33,dd44", FriendlyName = "Plain Hat (Grey)", Rarity = 20, + AvatarItemId = 2, CreatedAt = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc) + }, + }); +} diff --git a/ServerTests/RroRewardPoolsTests.cs b/ServerTests/RroRewardPoolsTests.cs new file mode 100644 index 0000000..99973f3 --- /dev/null +++ b/ServerTests/RroRewardPoolsTests.cs @@ -0,0 +1,673 @@ +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The historical RRO reward pools. The catalogue these assertions run against is the shipped +/// 2022-avatar.json fixture - the same one the catalogue tests read - so a set whose items are not +/// in the catalogue fails here rather than at runtime. +/// +public class RroRewardPoolsTests +{ + private static string ShippedCatalogueJson() => + File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json")); + + // --------------------------------------------------------------------------------- + // The context values themselves + // --------------------------------------------------------------------------------- + + [Theory] + // Quests: each activity keeps its own block of values. + [InlineData(GiftContext.Quest_Goblin_A, 4000)] + [InlineData(GiftContext.Quest_Goblin_B, 4001)] + [InlineData(GiftContext.Quest_Goblin_C, 4002)] + [InlineData(GiftContext.Quest_Goblin_S, 4003)] + [InlineData(GiftContext.Quest_Goblin_Consumable, 4004)] + [InlineData(GiftContext.Quest_Cauldron_A, 4010)] + [InlineData(GiftContext.Quest_Cauldron_B, 4011)] + [InlineData(GiftContext.Quest_Cauldron_C, 4012)] + [InlineData(GiftContext.Quest_Cauldron_S, 4013)] + [InlineData(GiftContext.Quest_Cauldron_Consumable, 4014)] + [InlineData(GiftContext.Quest_Pirate1_A, 4100)] + [InlineData(GiftContext.Quest_Pirate1_B, 4101)] + [InlineData(GiftContext.Quest_Pirate1_C, 4102)] + [InlineData(GiftContext.Quest_Pirate1_S, 4103)] + [InlineData(GiftContext.Quest_Pirate1_X, 4104)] + [InlineData(GiftContext.Quest_Pirate1_Consumable, 4105)] + [InlineData(GiftContext.Quest_Dracula1_A, 4200)] + [InlineData(GiftContext.Quest_Dracula1_B, 4201)] + [InlineData(GiftContext.Quest_Dracula1_C, 4202)] + [InlineData(GiftContext.Quest_Dracula1_S, 4203)] + [InlineData(GiftContext.Quest_Dracula1_X, 4204)] + [InlineData(GiftContext.Quest_Dracula1_Consumable, 4205)] + [InlineData(GiftContext.Quest_Dracula1_SS, 4206)] + [InlineData(GiftContext.Quest_SciFi_A, 4500)] + [InlineData(GiftContext.Quest_SciFi_B, 4501)] + [InlineData(GiftContext.Quest_SciFi_C, 4502)] + [InlineData(GiftContext.Quest_SciFi_S, 4503)] + [InlineData(GiftContext.Quest_Scifi_Consumable, 4504)] + // Stunt Runner. + [InlineData(GiftContext.StuntRunner_TheMainEvent_A, 11000)] + [InlineData(GiftContext.StuntRunner_TheMainEvent_B, 11001)] + [InlineData(GiftContext.StuntRunner_TheMainEvent_C, 11002)] + [InlineData(GiftContext.StuntRunner_TheMainEvent_D, 11003)] + [InlineData(GiftContext.StuntRunner_TheMainEvent_S, 11004)] + [InlineData(GiftContext.StuntRunner_TheMainEvent_X, 11005)] + [InlineData(GiftContext.StuntRunner_TheMainEvent_Consumable, 11006)] + [InlineData(GiftContext.StuntRunner_TheMainEvent_SS, 11007)] + // Map games. + [InlineData(GiftContext.Paintball_ClearCut, 2010)] + [InlineData(GiftContext.Paintball_Homestead, 2011)] + [InlineData(GiftContext.Paintball_Quarry, 2012)] + [InlineData(GiftContext.Paintball_River, 2013)] + [InlineData(GiftContext.Paintball_Dam, 2014)] + [InlineData(GiftContext.Paintball_DriveIn, 2015)] + [InlineData(GiftContext.Discgolf_Propulsion, 3010)] + [InlineData(GiftContext.Discgolf_Lake, 3011)] + [InlineData(GiftContext.Charades, 5001)] + [InlineData(GiftContext.Soccer, 6001)] + [InlineData(GiftContext.Paddleball, 7001)] + [InlineData(GiftContext.Dodgeball, 8001)] + [InlineData(GiftContext.Lasertag, 9001)] + [InlineData(GiftContext.Bowling, 10001)] + [InlineData(GiftContext.RecRoyale, 1200)] + // The generic pair, for packages with no specialised activity. + [InlineData(GiftContext.GameRewards, 50)] + [InlineData(GiftContext.GameRewards_Tokens, 51)] + // Deprecated: kept so old data still reads, never chosen for a new reward. + [InlineData(GiftContext.DEPRECATED_Paintball_ClearCut, 2000)] + [InlineData(GiftContext.DEPRECATED_Paintball_Homestead, 2001)] + [InlineData(GiftContext.DEPRECATED_Paintball_Quarry, 2002)] + [InlineData(GiftContext.DEPRECATED_Paintball_River, 2003)] + [InlineData(GiftContext.DEPRECATED_Paintball_Dam, 2004)] + [InlineData(GiftContext.DEPRECATED_Paintball_DriveIn, 2005)] + [InlineData(GiftContext.DEPRECATED_Discgolf_Propulsion, 3000)] + [InlineData(GiftContext.DEPRECATED_Discgolf_Lake, 3001)] + [InlineData(GiftContext.DEPRECATED_Charades, 5000)] + [InlineData(GiftContext.DEPRECATED_Soccer, 6000)] + [InlineData(GiftContext.DEPRECATED_Paddleball, 7000)] + [InlineData(GiftContext.DEPRECATED_Dodgeball, 8000)] + [InlineData(GiftContext.DEPRECATED_Lasertag, 9000)] + [InlineData(GiftContext.DEPRECATED_Bowling, 10000)] + public void TheContextKeepsItsHistoricalValue(GiftContext context, int expected) + { + Assert.Equal(expected, (int)context); + } + + [Fact] + public void NoDeprecatedContextIsRewardable() + { + foreach (var context in RroRewardPools.Deprecated) + { + Assert.True(RroRewardPools.IsDeprecated(context)); + // A deprecated context may have no pool at all; it must never have a non-empty one. + Assert.Empty(RroRewardPools.Resolve(context, ShippedCatalogueJson())); + } + + Assert.Equal(RroRewardPools.Deprecated.Count, RroRewardPools.Deprecated.Distinct().Count()); + } + + // --------------------------------------------------------------------------------- + // Which room pays which context + // --------------------------------------------------------------------------------- + + [Theory] + [InlineData("GoldenTrophy", "S", GiftContext.Quest_Goblin_S)] + [InlineData("GoldenTrophy", "C", GiftContext.Quest_Goblin_C)] + [InlineData("TheRiseofJumbotron", "A", GiftContext.Quest_SciFi_A)] + [InlineData("CrimsonCauldron", "C", GiftContext.Quest_Cauldron_C)] + [InlineData("Crescendo", "S", GiftContext.Quest_Dracula1_S)] + [InlineData("StuntRunner", "B", GiftContext.StuntRunner_TheMainEvent_B)] + [InlineData("IsleOfLostSkulls", "S", GiftContext.Quest_Pirate1_S)] + // The map games take no rank. + [InlineData("Clearcut", null, GiftContext.Paintball_ClearCut)] + [InlineData("Spillway", null, GiftContext.Paintball_Dam)] + [InlineData("DriveIn", null, GiftContext.Paintball_DriveIn)] + [InlineData("DiscGolfPropulsion", null, GiftContext.Discgolf_Propulsion)] + [InlineData("DiscGolfLake", null, GiftContext.Discgolf_Lake)] + [InlineData("3DCharades", null, GiftContext.Charades)] + [InlineData("Soccer", null, GiftContext.Soccer)] + [InlineData("Paddleball", null, GiftContext.Paddleball)] + [InlineData("Dodgeball", null, GiftContext.Dodgeball)] + [InlineData("LaserTag", null, GiftContext.Lasertag)] + [InlineData("Bowling", null, GiftContext.Bowling)] + [InlineData("RecRoyaleSquads", null, GiftContext.RecRoyale)] + public void TheRoomNamesTheContext(string room, string? rank, GiftContext expected) + { + Assert.True(RroRewardPools.TryContext(room, rank, out var context)); + Assert.Equal(expected, context); + } + + [Theory] + [InlineData("DormRoom")] + [InlineData("RecCenter")] + [InlineData("SomeonesInvention")] + [InlineData("")] + [InlineData(null)] + public void ARoomThatIsNotARewardActivityPaysNothing(string? room) + { + Assert.False(RroRewardPools.IsRewardActivity(room)); + Assert.False(RroRewardPools.TryContext(room, "S", out var context)); + Assert.Equal(GiftContext.None, context); + } + + [Fact] + public void ARankAQuestDoesNotDefinePaysNothing() + { + // A rank the definitions do not name must not resolve: Golden Trophy never pays a D or an + // SS, and Isle of Lost Skulls has no C. + Assert.False(RroRewardPools.TryContext("GoldenTrophy", "SS", out _)); + Assert.False(RroRewardPools.TryContext("GoldenTrophy", "D", out _)); + // Stunt Runner does define a tier above S, under both of the names the client has used + // for it; D is the one it does not. + Assert.False(RroRewardPools.TryContext("StuntRunner", "D", out _)); + Assert.False(RroRewardPools.TryContext("Crescendo", "SS", out _)); + + // A ranked quest still refuses a context from another activity, because the room is what + // decides: Golden Trophy cannot file a Jumbotron reward. + Assert.Equal( + GiftContext.Quest_Goblin_S, + RroRewardPools.ContextsForRoom("GoldenTrophy")["S"]); + } + + [Fact] + public void TheStuntRunnerTopTierPaysFeatheredHairUnderEitherName() + { + // The definitions call this tier SS and the watch shows it as S+; both names have to reach + // the same context, and the tier has to hand out the item it names. + var json = ShippedCatalogueJson(); + var pool = RroRewardPools.Resolve(GiftContext.StuntRunner_TheMainEvent_SS, json); + + Assert.Single(pool); + Assert.Equal("Feathered Hair", pool[0].FriendlyName); + + Assert.True(RroRewardPools.TryContext("StuntRunner", "SS", out var viaSs)); + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_SS, viaSs); + Assert.Equal(11007, (int)viaSs); + + Assert.True(RroRewardPools.TryContext("StuntRunner", "S+", out var viaSPlus)); + Assert.Equal(GiftContext.StuntRunner_TheMainEvent_SS, viaSPlus); + + // And the tier is reachable, which is what makes a claim of it honoured. + Assert.Contains(GiftContext.StuntRunner_TheMainEvent_SS, + RroRewardPools.ContextsForRoom("StuntRunner").Values); + } + + [Fact] + public void NoRoomEverPaysADeprecatedContext() + { + foreach (var room in new[] { "GoldenTrophy", "TheRiseofJumbotron", "CrimsonCauldron", "Crescendo", + "IsleOfLostSkulls", "StuntRunner", "Clearcut", "Spillway", "River", "DriveIn", + "DiscGolfPropulsion", "3DCharades", "Soccer", "Paddleball", "Dodgeball", "LaserTag", + "Bowling", "RecRoyaleSolos" }) + { + foreach (var context in RroRewardPools.ContextsForRoom(room).Values) + { + Assert.False(RroRewardPools.IsDeprecated(context), + $"{room} pays the deprecated {context}"); + } + } + } + + // --------------------------------------------------------------------------------- + // The pools resolve against the shipped catalogue + // --------------------------------------------------------------------------------- + + [Fact] + public void EveryPoolThatNamesASetResolvesToShippedItems() + { + var json = ShippedCatalogueJson(); + var catalogue = (json.FromJson>() ?? []) + .ToDictionary(x => x.AvatarItemDesc, StringComparer.Ordinal); + + foreach (var context in RroRewardPools.RewardableContexts) + { + var sets = RroRewardPools.SetsFor(context); + if (sets.Count == 0) continue; + + var items = RroRewardPools.Resolve(context, json); + Assert.True(items.Count > 0, $"{context} names {sets.Count} set(s) but resolves to no item"); + + // Every item is a real catalogue row, and it came from one of the context's own sets. + foreach (var item in items) + { + Assert.True(catalogue.ContainsKey(item.AvatarItemDesc), + $"{context} resolved {item.AvatarItemDesc}, which is not a catalogue row"); + Assert.False(string.IsNullOrWhiteSpace(item.FriendlyName)); + Assert.Contains(sets, set => + RroRewardPools.Resolve(set, json).Any(x => x.AvatarItemDesc == item.AvatarItemDesc)); + } + } + } + + [Fact] + public void EachQuestPaysEveryRankItDefinesAndNoItemTwice() + { + var json = ShippedCatalogueJson(); + + // Each quest's ranks, so a rank can be checked against the quest it belongs to. + var quests = new Dictionary + { + ["Golden Trophy"] = [GiftContext.Quest_Goblin_C, GiftContext.Quest_Goblin_B, + GiftContext.Quest_Goblin_A, GiftContext.Quest_Goblin_S], + ["Jumbotron"] = [GiftContext.Quest_SciFi_C, GiftContext.Quest_SciFi_B, + GiftContext.Quest_SciFi_A, GiftContext.Quest_SciFi_S], + ["Crimson Cauldron"] = [GiftContext.Quest_Cauldron_C, GiftContext.Quest_Cauldron_B, + GiftContext.Quest_Cauldron_A, GiftContext.Quest_Cauldron_S], + ["Crescendo"] = [GiftContext.Quest_Dracula1_C, GiftContext.Quest_Dracula1_B, + GiftContext.Quest_Dracula1_A, GiftContext.Quest_Dracula1_S], + ["Stunt Runner"] = [GiftContext.StuntRunner_TheMainEvent_C, GiftContext.StuntRunner_TheMainEvent_B, + GiftContext.StuntRunner_TheMainEvent_A, GiftContext.StuntRunner_TheMainEvent_S], + }; + + foreach (var (quest, ranks) in quests) + { + foreach (var rank in ranks) + { + Assert.True(RroRewardPools.Resolve(rank, json).Count > 0, + $"{quest} pays nothing at one of its ranks ({rank})"); + } + + // Nothing here asserts that a quest's ranks are disjoint: the verified Isle of Lost + // Skulls tables deliberately pay the same piece at two ranks. What must not happen is a + // piece crossing from one activity to another, which NoItemIsPaidByTwoActivities covers. + } + } + + [Fact] + public void NoItemIsPaidByTwoActivities() + { + // The rule that matters most: a Golden Trophy item must never be handed out as a Jumbotron + // or a paintball reward. A quest's own ranks may overlap - the verified Isle of Lost Skulls + // tables do - so the comparison is between activities, not between their tiers. + var json = ShippedCatalogueJson(); + var owner = new Dictionary(StringComparer.Ordinal); + + foreach (var (room, ranks) in new[] + { + ("GoldenTrophy", new[] { GiftContext.Quest_Goblin_C, GiftContext.Quest_Goblin_B, GiftContext.Quest_Goblin_A, GiftContext.Quest_Goblin_S }), + ("Jumbotron", new[] { GiftContext.Quest_SciFi_C, GiftContext.Quest_SciFi_B, GiftContext.Quest_SciFi_A, GiftContext.Quest_SciFi_S }), + ("CrimsonCauldron", new[] { GiftContext.Quest_Cauldron_C, GiftContext.Quest_Cauldron_B, GiftContext.Quest_Cauldron_A, GiftContext.Quest_Cauldron_S }), + ("Crescendo", new[] { GiftContext.Quest_Dracula1_C, GiftContext.Quest_Dracula1_B, GiftContext.Quest_Dracula1_A, GiftContext.Quest_Dracula1_S, GiftContext.Quest_Dracula1_SS }), + ("StuntRunner", new[] { GiftContext.StuntRunner_TheMainEvent_C, GiftContext.StuntRunner_TheMainEvent_B, GiftContext.StuntRunner_TheMainEvent_A, GiftContext.StuntRunner_TheMainEvent_S, GiftContext.StuntRunner_TheMainEvent_SS }), + ("IsleOfLostSkulls", new[] { GiftContext.Quest_Pirate1_C, GiftContext.Quest_Pirate1_B, GiftContext.Quest_Pirate1_A, GiftContext.Quest_Pirate1_S }), + ("Paintball", new[] { GiftContext.Paintball_ClearCut, GiftContext.Paintball_Homestead, GiftContext.Paintball_Quarry, GiftContext.Paintball_River, GiftContext.Paintball_Dam, GiftContext.Paintball_DriveIn }), + ("DiscGolf", new[] { GiftContext.Discgolf_Propulsion, GiftContext.Discgolf_Lake }), + ("Charades", new[] { GiftContext.Charades }), + ("Soccer", new[] { GiftContext.Soccer }), + ("Paddleball", new[] { GiftContext.Paddleball }), + ("Dodgeball", new[] { GiftContext.Dodgeball }), + ("Bowling", new[] { GiftContext.Bowling }), + }) + { + // A quest's ranks may pay the same piece - the verified Isle of Lost Skulls tables do - + // so an activity's pieces are gathered up before they are compared with another's. + var paidByRoom = ranks + .SelectMany(context => RroRewardPools.Resolve(context, json)) + .Select(x => x.AvatarItemDesc) + .ToHashSet(StringComparer.Ordinal); + + foreach (var descriptor in paidByRoom) + { + Assert.True(!owner.TryGetValue(descriptor, out var other), + $"{descriptor} is paid by both {other} and {room}"); + owner[descriptor] = room; + } + } + + Assert.NotEmpty(owner); + } + + [Fact] + public void APoolTheDefinitionsNameButTheCatalogueCannotSatisfyPaysNothing() + { + var json = ShippedCatalogueJson(); + + // The tiers that name no reward at all. + Assert.Empty(RroRewardPools.Resolve(GiftContext.Quest_Dracula1_X, json)); + Assert.Empty(RroRewardPools.Resolve(GiftContext.Quest_Pirate1_X, json)); + Assert.Empty(RroRewardPools.Resolve(GiftContext.StuntRunner_TheMainEvent_D, json)); + Assert.Empty(RroRewardPools.Resolve(GiftContext.StuntRunner_TheMainEvent_X, json)); + Assert.Empty(RroRewardPools.Resolve(GiftContext.RecRoyale, json)); + + // The Scallywag tiers pay now; only X names nothing. + Assert.Empty(RroRewardPools.Resolve(GiftContext.Quest_Pirate1_X, json)); + } + + [Fact] + public void EveryGapIsWrittenDownRatherThanGuessed() + { + // The brief's rule: an item that cannot be verified is left unresolved, not invented. Every + // context that pays nothing has to be accounted for in the list, so a silently empty pool + // cannot slip through. + var known = string.Join("\n", RroRewardPools.Unresolved); + + // The sets the catalogue genuinely cannot satisfy, and nothing else. Every other reward set + // turned out to be in the catalogue under its own family name. + Assert.Contains("Brown Witch Hunter", known); + Assert.Contains("Frontier Pass", known); + Assert.Contains("Isle of Lost Skulls X", known); + Assert.Contains("Stunt Runner D and X", known); + Assert.Contains("Crescendo X", known); + Assert.Contains("Rec Royale", known); + + // And a set that was found must not still be described as missing. + Assert.DoesNotContain("Feathered Hair", known); + Assert.DoesNotContain("Dude Sweater", known); + Assert.DoesNotContain("Dracula's Set", known); + Assert.DoesNotContain("Farmer set is not", known); + Assert.DoesNotContain("Golfer set is not", known); + + Assert.Equal(RroRewardPools.Unresolved.Count, RroRewardPools.Unresolved.Distinct().Count()); + } + + [Theory] + // Every set that the catalogue turned out to carry, named by a piece it must contain. These are + // the pools that were once wrongly written off as missing. + [InlineData(GiftContext.Paintball_Homestead, "Farmer Hat")] + [InlineData(GiftContext.Paintball_Homestead, "Farmer Overalls")] + [InlineData(GiftContext.Paintball_Quarry, "Construction Helmet (Yellow)")] + [InlineData(GiftContext.Paintball_Quarry, "Construction Shirt (Blue)")] + [InlineData(GiftContext.Discgolf_Lake, "Golfer Hat (Blue)")] + [InlineData(GiftContext.Discgolf_Lake, "Golfer Shirt (Tan)")] + [InlineData(GiftContext.Dodgeball, "Dodgeball Shirt (Orange)")] + [InlineData(GiftContext.Bowling, "Dude Sweater")] + [InlineData(GiftContext.Quest_Dracula1_SS, "Dracula Cape")] + [InlineData(GiftContext.Quest_Dracula1_SS, "Dracula Jacket")] + [InlineData(GiftContext.Quest_Pirate1_A, "Pirate Captain Jacket (Blue)")] + [InlineData(GiftContext.Quest_Pirate1_S, "Pirate Captain Jacket (Gold)")] + public void ASetTheCatalogueCarriesPaysItsPieces(GiftContext context, string expected) + { + var names = RroRewardPools.Resolve(context, ShippedCatalogueJson()) + .Select(x => x.FriendlyName) + .ToHashSet(StringComparer.Ordinal); + + Assert.NotEmpty(names); + Assert.Contains(expected, names); + } + + [Fact] + public void TheHomesteadSetIsTwoPiecesAndTheBeardIsNotOne() + { + // The Farmer set is the hat and the overalls; the beard was a store item and must not be + // handed out as a Homestead reward. + var names = RroRewardPools.Resolve(GiftContext.Paintball_Homestead, ShippedCatalogueJson()) + .Select(x => x.FriendlyName) + .ToHashSet(StringComparer.Ordinal); + + Assert.Equal(2, names.Count); + Assert.Contains("Farmer Hat", names); + Assert.Contains("Farmer Overalls", names); + Assert.DoesNotContain("Farmer Beard", names); + } + + [Fact] + public void TheGolferSetIsThreePiecesInThreeColoursAndNothingElse() + { + // Three clothing types in tan, blue and grey. The catalogue's Golfer Hat also comes in red, + // which is a store colour rather than a reward one. + var names = RroRewardPools.Resolve(GiftContext.Discgolf_Lake, ShippedCatalogueJson()) + .Select(x => x.FriendlyName) + .ToHashSet(StringComparer.Ordinal); + + Assert.Equal(9, names.Count); + Assert.Contains("Golfer Hat (Tan)", names); + Assert.Contains("Golfer Shirt (Grey)", names); + Assert.Contains("Golfer Gloves (Blue)", names); + Assert.DoesNotContain("Golfer Hat (Red)", names); + } + + [Fact] + public void TheDodgerSetIsTheThreeCataloguePiecesUnderTheShirtName() + { + // The definitions call it the Dodger Vest in cyan, purple and orange; the catalogue carries + // the same three as Dodgeball Shirt in orange, purple and teal. + var names = RroRewardPools.Resolve(GiftContext.Dodgeball, ShippedCatalogueJson()) + .Select(x => x.FriendlyName) + .ToHashSet(StringComparer.Ordinal); + + Assert.Equal(3, names.Count); + Assert.Contains("Dodgeball Shirt (Orange)", names); + Assert.Contains("Dodgeball Shirt (Purple)", names); + Assert.Contains("Dodgeball Shirt (Teal)", names); + } + + [Fact] + public void BowlingPaysItsSweaterOnceInTwoHundredAndFiftyGames() + { + Assert.Equal(1.0 / 250.0, RroRewardPools.DropChanceFor(GiftContext.Bowling), 10); + // Every other activity pays what its pool holds whenever it is earned. + Assert.Equal(1.0, RroRewardPools.DropChanceFor(GiftContext.Dodgeball)); + Assert.Equal(1.0, RroRewardPools.DropChanceFor(GiftContext.Quest_Goblin_S)); + } + + [Fact] + public void LaserTagPaysTicketsInsteadOfDrawingACosmetic() + { + // Laser Tag is not a random-drop activity: its context exists for the record, and its + // reward is tickets for its own shop. + Assert.True(RroRewardPools.PaysTickets("LaserTag")); + Assert.False(RroRewardPools.PaysTickets("Dodgeball")); + Assert.False(RroRewardPools.PaysTickets(null)); + + Assert.Empty(RroRewardPools.Resolve(GiftContext.Lasertag, ShippedCatalogueJson())); + Assert.Empty(RroRewardPools.BuildDrops(GiftContext.Lasertag, selection: false, null, new Random(1), ShippedCatalogueJson())); + + Assert.Equal(25, RroRewardPools.LaserTagTicketsPerGame); + Assert.Equal(100, RroRewardPools.LaserTagFirstGamesBonus); + Assert.Equal(5, RroRewardPools.LaserTagBonusGamesPerDay); + } + + [Theory] + // Every rank of Isle of Lost Skulls, named by the pieces the verified tables give it. + [InlineData(GiftContext.Quest_Pirate1_C, "Scallywag Bracers (Brown)")] + [InlineData(GiftContext.Quest_Pirate1_C, "Scallywag Belt (Red)")] + [InlineData(GiftContext.Quest_Pirate1_C, "Pirate Captain Cuffs (Green)")] + [InlineData(GiftContext.Quest_Pirate1_B, "Scallywag Vest (Brown)")] + [InlineData(GiftContext.Quest_Pirate1_B, "Scallywag Vest (Red)")] + [InlineData(GiftContext.Quest_Pirate1_B, "Pirate Captain Jacket (Blue)")] + [InlineData(GiftContext.Quest_Pirate1_A, "Scallywag Hat (Brown)")] + [InlineData(GiftContext.Quest_Pirate1_A, "Scallywag Hat (Red)")] + [InlineData(GiftContext.Quest_Pirate1_A, "Pirate Captain Hat (Red)")] + [InlineData(GiftContext.Quest_Pirate1_S, "Pirate Captain Hat (Gold)")] + [InlineData(GiftContext.Quest_Pirate1_S, "Pirate Eyepatch (Black)")] + [InlineData(GiftContext.Quest_Pirate1_S, "Pirate Eyepatch (Skull)")] + public void EveryIsleOfLostSkullsRankPaysThePiecesItNames(GiftContext context, string expected) + { + var names = RroRewardPools.Resolve(context, ShippedCatalogueJson()) + .Select(x => x.FriendlyName) + .ToHashSet(StringComparer.Ordinal); + + Assert.NotEmpty(names); + Assert.Contains(expected, names); + } + + [Fact] + public void TheBlackScallywagSetIsNotACompletionReward() + { + // It is a shop set, so no Isle of Lost Skulls rank names it. + var json = ShippedCatalogueJson(); + + foreach (var context in new[] + { + GiftContext.Quest_Pirate1_C, GiftContext.Quest_Pirate1_B, + GiftContext.Quest_Pirate1_A, GiftContext.Quest_Pirate1_S, + }) + { + Assert.All( + RroRewardPools.Resolve(context, json).Where(x => x.FriendlyName.StartsWith("Scallywag", StringComparison.Ordinal)), + item => Assert.DoesNotContain("(Black)", item.FriendlyName, StringComparison.Ordinal)); + } + } + + [Fact] + public void AQuestFallsBackToItsOwnLowerRanks() + { + // Strongest first, and never to another activity. + Assert.Equal( + new[] { GiftContext.Quest_Goblin_A, GiftContext.Quest_Goblin_B, GiftContext.Quest_Goblin_C }, + RroRewardPools.LowerRanksOf(GiftContext.Quest_Goblin_S)); + + Assert.Equal( + new[] { GiftContext.Quest_Pirate1_B, GiftContext.Quest_Pirate1_C }, + RroRewardPools.LowerRanksOf(GiftContext.Quest_Pirate1_A)); + + // The weakest rank of a quest has nothing beneath it, and a map game ranks nothing at all. + Assert.Empty(RroRewardPools.LowerRanksOf(GiftContext.Quest_Goblin_C)); + Assert.Empty(RroRewardPools.LowerRanksOf(GiftContext.Quest_Pirate1_C)); + Assert.Empty(RroRewardPools.LowerRanksOf(GiftContext.Paintball_ClearCut)); + Assert.Empty(RroRewardPools.LowerRanksOf(GiftContext.Dodgeball)); + } + + [Fact] + public void TheLowerRanksHandedBackAreAlwaysTheSameActivitys() + { + foreach (var context in RroRewardPools.RewardableContexts) + { + foreach (var weaker in RroRewardPools.LowerRanksOf(context)) + { + var sameRoom = RroRewardPools.ContextsForRoom("GoldenTrophy").Values.Contains(weaker) + || RroRewardPools.ContextsForRoom("CrimsonCauldron").Values.Contains(weaker) + || RroRewardPools.ContextsForRoom("IsleOfLostSkulls").Values.Contains(weaker) + || RroRewardPools.ContextsForRoom("Crescendo").Values.Contains(weaker) + || RroRewardPools.ContextsForRoom("StuntRunner").Values.Contains(weaker) + || RroRewardPools.ContextsForRoom("TheRiseofJumbotron").Values.Contains(weaker); + + Assert.True(sameRoom, $"{weaker} came back as a lower rank of {context} but is not a quest rank"); + } + } + } + + [Fact] + public void RecRoyalePaysThroughItsPassRatherThanADrop() + { + Assert.True(RroRewardPools.PaysNoItemReward("RecRoyaleSolos")); + Assert.True(RroRewardPools.PaysNoItemReward("RecRoyaleSquads")); + Assert.False(RroRewardPools.PaysNoItemReward("Dodgeball")); + Assert.False(RroRewardPools.PaysNoItemReward(null)); + + Assert.Empty(RroRewardPools.Resolve(GiftContext.RecRoyale, ShippedCatalogueJson())); + } + + [Fact] + public void GreyAndGrayAreOneColour() + { + // The catalogue spells it both ways depending on the item, so a set named one way still has + // to find the pieces spelled the other. + var json = ShippedCatalogueJson(); + var grey = RroRewardPools.Resolve(new RroRewardPools.ItemSet("Knight", "Grey"), json); + var gray = RroRewardPools.Resolve(new RroRewardPools.ItemSet("Knight", "Gray"), json); + + Assert.NotEmpty(grey); + Assert.Equal( + grey.Select(x => x.AvatarItemDesc).OrderBy(x => x, StringComparer.Ordinal), + gray.Select(x => x.AvatarItemDesc).OrderBy(x => x, StringComparer.Ordinal)); + } + + // --------------------------------------------------------------------------------- + // Picking an item + // --------------------------------------------------------------------------------- + + [Fact] + public void ThePickSkipsWhatThePlayerAlreadyOwns() + { + var json = ShippedCatalogueJson(); + var pool = RroRewardPools.Resolve(GiftContext.Quest_Goblin_S, json); + Assert.True(pool.Count >= 2); + + // Everything but the last item is owned, so the pick can only be that one. + var owned = pool.Take(pool.Count - 1) + .Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var pick = RroRewardPools.Pick(GiftContext.Quest_Goblin_S, owned, new Random(1), json); + Assert.NotNull(pick); + Assert.Equal(pool[^1].AvatarItemDesc, pick!.AvatarItemDesc); + } + + [Fact] + public void AnExhaustedPoolPicksNothingRatherThanRepeating() + { + var json = ShippedCatalogueJson(); + var pool = RroRewardPools.Resolve(GiftContext.Quest_Goblin_S, json); + + var owned = pool + .Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.Null(RroRewardPools.Pick(GiftContext.Quest_Goblin_S, owned, new Random(1), json)); + Assert.Null(RroRewardPools.BuildGift(GiftContext.Quest_Goblin_S, owned, new Random(1), json)); + } + + [Fact] + public void AContextThatPaysNothingPicksNothing() + { + var json = ShippedCatalogueJson(); + Assert.Null(RroRewardPools.Pick(GiftContext.Quest_Pirate1_X, null, new Random(1), json)); + Assert.Null(RroRewardPools.Pick(GiftContext.DEPRECATED_Bowling, null, new Random(1), json)); + Assert.Null(RroRewardPools.Pick(GiftContext.GameRewards, null, new Random(1), json)); + } + + [Fact] + public void TheBoxCarriesTheContextItWasFiledUnder() + { + var json = ShippedCatalogueJson(); + var gift = RroRewardPools.BuildGift(GiftContext.Quest_Goblin_S, null, new Random(7), json); + + Assert.NotNull(gift); + Assert.Equal(GiftContext.Quest_Goblin_S, gift!.Context); + // The wire field the client reads, and the one the admin panel stores, both agree. + Assert.Equal(4003, gift.GiftContext); + Assert.False(string.IsNullOrWhiteSpace(gift.AvatarItemDesc)); + Assert.Equal(0, gift.Currency); + Assert.Equal(0, gift.Xp); + } + + [Fact] + public void APaintballRewardIsFiledUnderItsOwnMap() + { + var json = ShippedCatalogueJson(); + + Assert.True(RroRewardPools.TryContext("Clearcut", null, out var clearcut)); + var gift = RroRewardPools.BuildGift(clearcut, null, new Random(3), json); + + Assert.NotNull(gift); + Assert.Equal(2010, gift!.GiftContext); + // The item is a Lumberjack piece, not a quest reward. + Assert.Contains("Lumberjack", gift.FriendlyName, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void EveryRewardableContextEitherPaysItemsOrIsAWrittenDownGap() + { + // A pool that quietly loses all of its items is the failure this guards against: a context + // with pools must resolve, and one without must be a gap this file names on purpose. + var json = ShippedCatalogueJson(); + + GiftContext[] deliberatelyEmpty = + [ + GiftContext.RecRoyale, + GiftContext.Lasertag, + GiftContext.Quest_Dracula1_X, + GiftContext.Quest_Pirate1_X, + GiftContext.StuntRunner_TheMainEvent_D, + GiftContext.StuntRunner_TheMainEvent_X, + ]; + + foreach (var context in RroRewardPools.RewardableContexts) + { + if (RroRewardPools.Resolve(context, json).Count > 0) continue; + + Assert.True(deliberatelyEmpty.Contains(context), + $"{context} pays nothing and is not one of the written-down gaps"); + } + + // And the reverse: none of the written-down gaps gained a pool behind our back. + foreach (var context in deliberatelyEmpty) + Assert.Empty(RroRewardPools.Resolve(context, json)); + } +} diff --git a/ServerTests/RroRewardSelectionTests.cs b/ServerTests/RroRewardSelectionTests.cs new file mode 100644 index 0000000..8eca9b6 --- /dev/null +++ b/ServerTests/RroRewardSelectionTests.cs @@ -0,0 +1,198 @@ +using RadicadeServer.RecNet.Enums.Store; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// How a completion turns into reward boxes: which activities offer the player a choice, what the +/// server falls back to when the context the client named is not one the activity pays, and what +/// the boxes contain. +/// +public class RroRewardSelectionTests +{ + private static string ShippedCatalogueJson() => + File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json")); + + // --------------------------------------------------------------------------------- + // Choice or no choice + // --------------------------------------------------------------------------------- + + [Theory] + // The map games hand the player a watch selection. + [InlineData("Clearcut", true)] + [InlineData("River", true)] + [InlineData("Spillway", true)] + [InlineData("DriveIn", true)] + [InlineData("DiscGolfPropulsion", true)] + [InlineData("3DCharades", true)] + [InlineData("Soccer", true)] + [InlineData("Paddleball", true)] + // A quest pays the rank the run earned; there is nothing to choose. + [InlineData("GoldenTrophy", false)] + [InlineData("TheRiseofJumbotron", false)] + [InlineData("CrimsonCauldron", false)] + [InlineData("Crescendo", false)] + [InlineData("IsleOfLostSkulls", false)] + [InlineData("StuntRunner", false)] + // Not a reward activity at all. + [InlineData("DormRoom", false)] + [InlineData("RecCenter", false)] + [InlineData(null, false)] + public void OnlyTheMapGamesOfferAChoice(string? room, bool offersSelection) + { + Assert.Equal(offersSelection, RroRewardPools.OffersSelection(room)); + } + + // --------------------------------------------------------------------------------- + // What an out-of-place claim falls back to + // --------------------------------------------------------------------------------- + + [Theory] + [InlineData("GoldenTrophy", GiftContext.Quest_Goblin_C)] // 4002, the quest's lowest rank + [InlineData("TheRiseofJumbotron", GiftContext.Quest_SciFi_C)] + [InlineData("CrimsonCauldron", GiftContext.Quest_Cauldron_C)] + [InlineData("Crescendo", GiftContext.Quest_Dracula1_C)] + [InlineData("IsleOfLostSkulls", GiftContext.Quest_Pirate1_C)] // its lowest defined rank + [InlineData("StuntRunner", GiftContext.StuntRunner_TheMainEvent_C)] + [InlineData("Clearcut", GiftContext.Paintball_ClearCut)] // the map's only context + [InlineData("3DCharades", GiftContext.Charades)] + public void AClaimTheRoomDoesNotPayFallsToTheLowestRankItDoes(string room, GiftContext expected) + { + Assert.True(RroRewardPools.TryFloorContext(room, out var floor)); + Assert.Equal(expected, floor); + } + + [Fact] + public void TheFloorIsNeverAboveWhatTheRoomWouldPay() + { + // The point of the floor: an inflated claim can only ever drop a player to the activity's + // lowest rank, never lift them, and it can never hand out another activity's context. + Assert.False(RroRewardPools.TryFloorContext("DormRoom", out var none)); + Assert.Equal(GiftContext.None, none); + + foreach (var room in new[] { "GoldenTrophy", "CrimsonCauldron", "Clearcut", "Soccer" }) + { + Assert.True(RroRewardPools.TryFloorContext(room, out var floor)); + Assert.Contains(floor, RroRewardPools.ContextsForRoom(room).Values); + } + } + + // --------------------------------------------------------------------------------- + // What a completion pays + // --------------------------------------------------------------------------------- + + [Fact] + public void AQuestPaysOneBoxUnderItsOwnRankContext() + { + var json = ShippedCatalogueJson(); + var drops = RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_S, selection: false, null, new Random(5), json); + + Assert.Single(drops); + Assert.Equal(GiftContext.Quest_Goblin_S, drops[0].Context); + Assert.Equal(4003, drops[0].GiftContext); + Assert.False(string.IsNullOrWhiteSpace(drops[0].AvatarItemDesc)); + } + + [Fact] + public void AMapGameFillsThreeBoxesWithThreeDifferentItems() + { + var json = ShippedCatalogueJson(); + // Clear Cut pays seven Lumberjack pieces, so a selection of three is always available. + var drops = RroRewardPools.BuildDrops(GiftContext.Paintball_ClearCut, selection: true, null, new Random(11), json); + + Assert.Equal(3, drops.Count); + Assert.Equal(3, drops.Select(x => x.AvatarItemDesc).Distinct(StringComparer.Ordinal).Count()); + Assert.All(drops, drop => Assert.Equal(GiftContext.Paintball_ClearCut, drop.Context)); + Assert.All(drops, drop => Assert.Contains("Lumberjack", drop.FriendlyName, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void ASelectionStopsAtWhatThePoolHasLeft() + { + var json = ShippedCatalogueJson(); + var pool = RroRewardPools.Resolve(GiftContext.Quest_Dracula1_S, json); + + // Crescendo's S rank pays two pieces, so a selection can offer at most those two. + Assert.Equal(2, pool.Count); + Assert.Equal(2, RroRewardPools.BuildDrops(GiftContext.Quest_Dracula1_S, selection: true, null, new Random(3), json).Count); + } + + [Fact] + public void ABoxIsNeverOfferedForSomethingThePlayerAlreadyOwns() + { + var json = ShippedCatalogueJson(); + var pool = RroRewardPools.Resolve(GiftContext.Paintball_ClearCut, json); + + // Everything but two pieces is owned, so a selection of three can only offer those two. + var owned = pool.Take(pool.Count - 2) + .Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var drops = RroRewardPools.BuildDrops(GiftContext.Paintball_ClearCut, selection: true, owned, new Random(9), json); + Assert.Equal(2, drops.Count); + + var remaining = pool.Skip(pool.Count - 2).Select(x => x.AvatarItemDesc).ToHashSet(StringComparer.Ordinal); + Assert.All(drops, drop => Assert.Contains(drop.AvatarItemDesc, remaining)); + Assert.All(drops, drop => Assert.DoesNotContain( + LevelUpRewardPool.OwnershipKey(drop.AvatarItemDesc), owned)); + } + + [Fact] + public void AnExhaustedPoolPaysNoBoxesAtAll() + { + var json = ShippedCatalogueJson(); + var owned = RroRewardPools.Resolve(GiftContext.Quest_Goblin_S, json) + .Select(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.Empty(RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_S, selection: false, owned, new Random(1), json)); + } + + [Fact] + public void AGapInTheDefinitionsPaysNoBoxesRatherThanAStandIn() + { + var json = ShippedCatalogueJson(); + + // A tier the definitions name but the catalogue cannot satisfy pays nothing here, which is + // what sends the caller to the fallback rather than to a guessed item. + Assert.Empty(RroRewardPools.BuildDrops(GiftContext.Quest_Pirate1_X, selection: false, null, new Random(1), json)); + Assert.Empty(RroRewardPools.BuildDrops(GiftContext.StuntRunner_TheMainEvent_D, selection: false, null, new Random(1), json)); + Assert.Empty(RroRewardPools.BuildDrops(GiftContext.RecRoyale, selection: true, null, new Random(1), json)); + } + + [Fact] + public void TheSameCompletionTwiceDoesNotRepeatItself() + { + // Two runs of the same quest must not hand out the same piece: once the first is owned, the + // second has to come from what is left. + var json = ShippedCatalogueJson(); + var first = RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_B, selection: false, null, new Random(4), json); + Assert.Single(first); + + var owned = new HashSet(StringComparer.OrdinalIgnoreCase) + { + LevelUpRewardPool.OwnershipKey(first[0].AvatarItemDesc) + }; + + var second = RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_B, selection: false, owned, new Random(4), json); + Assert.Single(second); + Assert.NotEqual(first[0].AvatarItemDesc, second[0].AvatarItemDesc); + } + + [Fact] + public void TheBoxesCarryNoCurrencyOrXp() + { + // A pool reward is the item and nothing else: it must not also pay tokens, or a quest clear + // would quietly become a currency source. + var json = ShippedCatalogueJson(); + var drops = RroRewardPools.BuildDrops(GiftContext.Quest_Goblin_A, selection: false, null, new Random(2), json); + + Assert.Single(drops); + Assert.Equal(0, drops[0].Currency); + Assert.Equal(0, drops[0].Xp); + // And nothing for the client to paint as currency: with the default CurrencyType an item box + // rendered as a stack of coins with "Value: 0" under the item's name. + Assert.Equal(CurrencyType.Invalid, drops[0].CurrencyType); + } +} diff --git a/ServerTests/SceneLocationsTests.cs b/ServerTests/SceneLocationsTests.cs new file mode 100644 index 0000000..51c00e8 --- /dev/null +++ b/ServerTests/SceneLocationsTests.cs @@ -0,0 +1,61 @@ +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Tests the scene-location rules behind . The client +/// rejects a room-entry response whose "location" isn't a known Unity scene GUID +/// ("RecNet room scene contains unknown scene location ID"), so subrooms must never carry +/// null/empty/placeholder/non-GUID scene ids. +/// +public class SceneLocationsTests +{ + [Theory] + [InlineData("a75f7547-79eb-47c6-8986-6767abcb4f92")] + [InlineData("76d98498-60a1-430c-ab76-b54a29b7a163")] + public void IsValid_AcceptsRealSceneGuids(string id) + { + Assert.True(SceneLocations.IsValid(id)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("unknown")] + [InlineData("UNKNOWN")] + [InlineData("none")] + [InlineData("null")] + [InlineData("not-a-guid")] + public void IsValid_RejectsNullEmptyPlaceholderAndGarbage(string? id) + { + Assert.False(SceneLocations.IsValid(id)); + } + + [Fact] + public void ResolveFallback_PicksFirstValidSibling() + { + var result = SceneLocations.ResolveFallback( + new[] { "unknown", null, "acc06e66-c2d0-4361-b0cd-46246a4c455c" }, + isDorm: false); + + Assert.Equal("acc06e66-c2d0-4361-b0cd-46246a4c455c", result); + } + + [Fact] + public void ResolveFallback_UsesDormSceneForDorms() + { + var result = SceneLocations.ResolveFallback(new string?[] { "unknown", "" }, isDorm: true); + + Assert.Equal(SceneLocations.Dorm, result); + } + + [Fact] + public void ResolveFallback_UsesMakerRoomForNonDorms() + { + var result = SceneLocations.ResolveFallback(new string?[] { null }, isDorm: false); + + Assert.Equal(SceneLocations.MakerRoom, result); + } +} diff --git a/ServerTests/Server.Tests.csproj b/ServerTests/Server.Tests.csproj new file mode 100644 index 0000000..df06926 --- /dev/null +++ b/ServerTests/Server.Tests.csproj @@ -0,0 +1,37 @@ + + + + net9.0 + enable + enable + false + true + RadicadeServer.Tests + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + diff --git a/ServerTests/SimilarRoomsTests.cs b/ServerTests/SimilarRoomsTests.cs new file mode 100644 index 0000000..a5e10bb --- /dev/null +++ b/ServerTests/SimilarRoomsTests.cs @@ -0,0 +1,137 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using RadicadeServer.DB; +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RoomServer; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The "Similar Rooms" rail (GET rooms/rooms/{id}/similar). +/// +/// The route answered a BARE ARRAY, which the client's list DTO cannot parse: its rows are objects +/// (PagedResultsDTO<RoomDTO>), so the response came back as +/// Malformed Response: '[]' in Player.log and the rail rendered empty. An unknown room was a +/// 404 on top of that, which stalls the screen the rail sits on rather than emptying one row. +/// +[Collection(DbFactoryCollection.Name)] +public class SimilarRoomsTests +{ + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + var root = new InMemoryDatabaseRoot(); + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString(), root) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + + public RecDBContext Create() => new(options); + } + + private static (TestDbContextFactory Factory, RecDBContext Db) NewContext() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + return (factory, factory.Create()); + } + + private static V2_RoomsAPI NewApi(RecDBContext db, long callerId = 2) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, callerId.ToString())); + + return new V2_RoomsAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + } + + private static RoomData Room(long id, string name, string tags, Accessibility accessibility = Accessibility.Public) => new() + { + RoomId = id, + Name = name, + CreatorAccountId = 2, + Accessibility = accessibility, + Tags = tags, + State = RoomState.Active, + }; + + [Fact] + public async Task SimilarRoomsAnswerAPagedObjectAndNotABareArray() + { + var (_, db) = NewContext(); + db.Rooms.Add(Room(10, "Source", "import,0|horror,0")); + db.Rooms.Add(Room(11, "SharesTag", "import,0")); + db.Rooms.Add(Room(12, "Unrelated", "art,0")); + await db.SaveChangesAsync(); + + var result = await NewApi(db).GetSimilarRooms(10, 0, 100); + + var ok = Assert.IsType(result); + var page = Assert.IsType(ok.Value); + Assert.Equal(1, page.TotalResults); + Assert.Single(page.Results); + Assert.Equal(11, page.Results[0].RoomId); + } + + [Fact] + public async Task AnUnknownRoomAnswersAnEmptyPageRatherThan404() + { + var (_, db) = NewContext(); + + var result = await NewApi(db).GetSimilarRooms(99999, 0, 100); + + var ok = Assert.IsType(result); + var page = Assert.IsType(ok.Value); + Assert.Equal(0, page.TotalResults); + Assert.Empty(page.Results); + } + + [Fact] + public async Task PaginationPagesTheRowsAndCountsThemAll() + { + var (_, db) = NewContext(); + db.Rooms.Add(Room(20, "Source", "import,0")); + for (var id = 21; id < 25; id++) db.Rooms.Add(Room(id, "Shares" + id, "import,0")); + await db.SaveChangesAsync(); + + var result = await NewApi(db).GetSimilarRooms(20, 1, 2); + + var page = Assert.IsType(Assert.IsType(result).Value); + Assert.Equal(4, page.TotalResults); + Assert.Equal(2, page.Results.Count); + } + + [Fact] + public async Task RoomsTheCallerMayNotSeeAreNeverInTheRail() + { + var (_, db) = NewContext(); + db.Rooms.Add(Room(30, "Source", "import,0")); + db.Rooms.Add(Room(31, "Hidden", "import,0", Accessibility.Private)); + await db.SaveChangesAsync(); + + var page = Assert.IsType( + Assert.IsType(await NewApi(db).GetSimilarRooms(30, 0, 100)).Value); + + Assert.Equal(0, page.TotalResults); + } +} diff --git a/ServerTests/SplitTestCompatibilityTests.cs b/ServerTests/SplitTestCompatibilityTests.cs new file mode 100644 index 0000000..758d8a4 --- /dev/null +++ b/ServerTests/SplitTestCompatibilityTests.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Config; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Config; +using Xunit; + +namespace RadicadeServer.Tests; + +public class SplitTestCompatibilityTests +{ + [Fact] + public void EveryForcedExperimentAndSegmentExistsInMarch2023Client() + { + var catalog = JsonSerializer.Deserialize>( + File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "split-tests-20230301.json")))!; + using var db = new RecDBContext(TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + db.GameConfig.Add(new ConfigVal("splitTestHardOverrides", SplitTestConfigs.HardOverrides)); + db.SaveChanges(); + var response = Assert.IsType(new GameConfigAPI(db).GetAllGameConfigs()); + var configs = Assert.IsType>(response.Value); + var overrides = configs.Single(x => x.Key == "splitTestHardOverrides").Value; + // DB seeds hold the reference-era set, including experiments this client + // lacks enums for; the API filters those out before responding. + Assert.False(string.IsNullOrWhiteSpace(overrides)); + foreach (var entry in overrides.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + var pair = entry.Split('=', 2); + Assert.Equal(2, pair.Length); + Assert.True(catalog.ContainsKey(pair[0]), $"Unknown client experiment: {pair[0]}"); + Assert.Contains(pair[1], catalog[pair[0]]); + } + } +} diff --git a/ServerTests/SqlServerInvitePersistenceTests.cs b/ServerTests/SqlServerInvitePersistenceTests.cs new file mode 100644 index 0000000..9748aaf --- /dev/null +++ b/ServerTests/SqlServerInvitePersistenceTests.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Notifications; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Messages; +using Xunit; + +namespace RadicadeServer.Tests; + +public sealed class SqlServerInviteFactAttribute : FactAttribute +{ + public SqlServerInviteFactAttribute() + { + if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("RADICADE_SQL_TEST_CONNECTION"))) + Skip = "Set RADICADE_SQL_TEST_CONNECTION to a SQL Server with permission to create a scratch database."; + } +} + +[Collection(DbFactoryCollection.Name)] +public class SqlServerInvitePersistenceTests +{ + private sealed class Factory(string connection) : IDbContextFactory + { + public RecDBContext CreateDbContext() => new(TestDbOptions.Create() + .UseSqlServer(connection, sql => sql.EnableRetryOnFailure(3)).Options); + } + + [SqlServerInviteFact] + public async Task InvitePayloadCommitsWithSqlServerRetriesEnabled() + { + // Never write test invitations into the user's database or notify real players. + var database = "RadicadeInviteTest_" + Guid.NewGuid().ToString("N"); + var connection = new SqlConnectionStringBuilder( + Environment.GetEnvironmentVariable("RADICADE_SQL_TEST_CONNECTION")); + connection.InitialCatalog = "master"; + await using var admin = new SqlConnection(connection.ConnectionString); + await admin.OpenAsync(); + await using (var create = new SqlCommand($"CREATE DATABASE [{database}]", admin)) + await create.ExecuteNonQueryAsync(); + var previous = Notify.DbFactory; + try + { + connection.InitialCatalog = database; + var factory = new Factory(connection.ConnectionString); + await using var db = factory.CreateDbContext(); + await db.Database.ExecuteSqlRawAsync(""" + CREATE TABLE [Messages] ( + [Id] bigint IDENTITY PRIMARY KEY, + [FromPlayerId] bigint NOT NULL, [ToPlayerId] bigint NOT NULL, + [SentTime] datetime2 NOT NULL, [Type] int NOT NULL, + [Data] nvarchar(max) NOT NULL, [Details] nvarchar(max) NOT NULL, + [RoomId] bigint NULL, [PlayerEventId] bigint NULL, + [IsDismissed] bit NOT NULL); + """); + Notify.DbFactory = factory; + var message = new Message + { + FromPlayerId = long.MaxValue - 1, RoomId = 45, Type = MessageType.GameInviteV2 + }; + await Notify.SendMessage(long.MaxValue, message, prepareWithId: stored => + stored.Data = JsonSerializer.Serialize(new RoomInviteDetails + { + InviteId = stored.Id, Name = "SQL invite test", RoomInstanceId = 123 + })); + var saved = await db.Messages.AsNoTracking().SingleAsync(); + Assert.True(saved.Id > 0); + Assert.Equal(saved.Id, JsonSerializer.Deserialize(saved.Data)!.InviteId); + Assert.Equal(123L, RoomInviteDetails.GetInstanceId(saved)); + Assert.Equal(long.MaxValue, saved.ToPlayerId); + } + finally + { + Notify.DbFactory = previous; + SqlConnection.ClearAllPools(); + await using var drop = new SqlCommand( + $"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{database}]", admin); + await drop.ExecuteNonQueryAsync(); + } + } +} diff --git a/ServerTests/StartupApiCompatibilityTests.cs b/ServerTests/StartupApiCompatibilityTests.cs new file mode 100644 index 0000000..eab1336 --- /dev/null +++ b/ServerTests/StartupApiCompatibilityTests.cs @@ -0,0 +1,500 @@ +using System.Reflection; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Compatibility; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Auth.Cached; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.API.Keepsakes; +using RadicadeServer.RecNet.Keepsakes; +using RadicadeServer.API.IncentivizedReferrals; +using RadicadeServer.API.InfluencerPartnerProgram; +using RadicadeServer.API.Progression.SubscriptionSeasons.V1; +using RadicadeServer.API.Storefront; +using RadicadeServer.API.Storefront.V1; +using RadicadeServer.RoomServer; +using Xunit; + +namespace RadicadeServer.Tests; + +public class StartupApiCompatibilityTests +{ + [Fact] + public void KeepsakeCategoriesReturnsCountedResults() + { + var previous = KeepsakeRoomConfig.Override; + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""); + try + { + var response = Assert.IsType(new KeepsakesAPI(null!).Categories()); + var json = JsonSerializer.SerializeToElement(response.Value); + + // A COUNTED set, whatever it holds: a bare list fails the client's parser outright + // ("expected:'{', actual:'['", logged as "Failed to get keepsake categories") and takes + // the keepsake load down with it. The entries themselves are the real catalog now. + Assert.Equal(JsonValueKind.Object, json.ValueKind); + var results = json.GetProperty("Results"); + Assert.Equal(JsonValueKind.Array, results.ValueKind); + Assert.Equal(results.GetArrayLength(), json.GetProperty("TotalResults").GetInt32()); + Assert.Equal(KeepsakeCatalog.Results().Length, results.GetArrayLength()); + } + finally + { + KeepsakeRoomConfig.Override = previous; + } + } + + /// + /// The disabled state, route by route: no keepsake room configured means the whole feature is + /// off (), and every answer a client could read a keepsake from has + /// to say so - an empty counted catalog rather than a list, NO CONTENT for the room, no active + /// event, and a placement refused instead of stored. + /// + [Fact] + public async Task WithNoKeepsakeRoomsTheFeatureAnswersOffEverywhere() + { + var previousRooms = KeepsakeRoomConfig.Override; + var previousFeature = KeepsakeFeature.Override; + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse(null); + KeepsakeFeature.Override = null; + try + { + var config = JsonSerializer.SerializeToElement( + Assert.IsType(new KeepsakesAPI(null!).GlobalConfig()).Value); + Assert.False(config.GetProperty("KeepsakeFeatureEnabled").GetBoolean()); + Assert.Equal(0, config.GetProperty("KeepsakeRoomLimit").GetInt32()); + + var catalog = JsonSerializer.SerializeToElement( + Assert.IsType(new KeepsakesAPI(null!).Categories()).Value); + Assert.Equal(0, catalog.GetProperty("Results").GetArrayLength()); + Assert.Equal(0, catalog.GetProperty("TotalResults").GetInt32()); + + Assert.IsType(await new KeepsakesAPI(null!).Room(25)); + Assert.IsType(await new KeepsakesAPI(null!).Place(25)); + } + finally + { + KeepsakeRoomConfig.Override = previousRooms; + KeepsakeFeature.Override = previousFeature; + } + } + + /// + /// The emptied catalog is still a counted set rather than a bare list - the shape the endpoint + /// has to keep even when there is nothing in it. + /// + [Fact] + public void AnEmptyKeepsakeCatalogIsStillACountedSet() + { + var json = JsonSerializer.SerializeToElement(KeepsakeCatalog.Results(enabled: false)); + + Assert.Equal(JsonValueKind.Array, json.ValueKind); + Assert.Equal(0, json.GetArrayLength()); + } + + /// + /// A room's keepsakes answer with NO body. The client reads any body here as keepsakes it + /// has to resolve, so an empty list is not the safe answer for it - the absence of one is. + /// A room listed in is the deliberate exception and answers + /// with the room fields, because that answer is the only place its keepsake permission can + /// come from - see KeepsakeRoomConfigTests. + /// + [Fact] + public async Task RoomKeepsakesAnswerNoContent() + { + var previous = KeepsakeRoomConfig.Override; + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse(null); + try + { + Assert.IsType(await new KeepsakesAPI(null!).Room(25)); + } + finally + { + KeepsakeRoomConfig.Override = previous; + } + } + + /// + /// The active progression event is the event's ID and nothing else - a bare number, which is + /// what the client's no-argument call returns (`MFBKDKPGMNP`). An array, an object or a + /// whole record does not deserialize into a nullable long, and every one of those shapes has + /// been answered here across these runs, each logged by the client as + /// `Malformed Response: '`body`'`. + /// + [Fact] + public void TheActiveProgressionEventIsTheIdTheRoomNames() + { + var previous = KeepsakeRoomConfig.Override; + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse( + """{ "RoomIds": [25], "RoomListId": 7, "RoomLimit": 5, "EventId": 42, "EventName": "Test Event" }"""); + try + { + var response = Assert.IsType( + new CompatibilityAPI(null!).ActiveProgressionEvents()); + var json = JsonSerializer.SerializeToElement(response.Value); + + Assert.Equal(JsonValueKind.Number, json.ValueKind); + Assert.Equal(42, json.GetInt64()); + } + finally + { + KeepsakeRoomConfig.Override = previous; + } + } + + /// + /// The caller's current subscription season is absent, not `{}`. The client's DTO for this route + /// has six members (`Guid`, `string`, `string`, `DateTime`, `DateTime?`, `List<…>` - the + /// 2023-09-12 reference's `ALGDPMHOPGG`), so an empty object is a body it cannot bind: the + /// 2026-10-01 21:37 log's `Malformed Response: '{}'` is thrown right after this route is fetched. + /// No season exists on this server, so "none" is answered at the HTTP level instead of inventing + /// six member names and putting a season in front of the player. + /// + [Fact] + public void TheCurrentSubscriptionSeasonIsAbsentRatherThanAnEmptyObject() + { + Assert.IsType(new SubscriptionSeasonsAPI().GetCurrentSeason()); + + var method = typeof(SubscriptionSeasonsAPI).GetMethod(nameof(SubscriptionSeasonsAPI.GetCurrentSeason))!; + var template = Assert.Single(method.GetCustomAttributes()).Template; + Assert.Equal("seasons/current", template); + + var classRoutes = typeof(SubscriptionSeasonsAPI).GetCustomAttributes() + .Select(route => route.Template) + .ToArray(); + Assert.Contains("econ/api/subscriptionseasons/v1", classRoutes); + } + + /// + /// The event itself, by id: the record the client fetches once it has the id above, carrying + /// the same one-list entry the room's own keepsake payload carries. The list is what the client + /// applies to the room, so a room the event does not name would be overridden back to having no + /// keepsake permission. + /// + [Fact] + public void TheProgressionEventByIdCarriesItsKeepsakeRoomLists() + { + var previous = KeepsakeRoomConfig.Override; + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse( + """{ "RoomIds": [25], "RoomListId": 7, "RoomLimit": 5, "EventId": 42, "EventName": "Test Event" }"""); + try + { + var api = new CompatibilityAPI(null!); + + // The definition owns the room lists. The player record has a separate DTO. + foreach (var (route, response) in new (string, IActionResult)[] + { + ("event", api.ProgressionEventDefinition(42)), + ("event", api.ProgressionEvent(42)), + }) + { + var json = JsonSerializer.SerializeToElement(Assert.IsType(response).Value); + + Assert.Equal(JsonValueKind.Object, json.ValueKind); + Assert.Equal(route, json.GetProperty("_route").GetString()); + Assert.Equal(42, json.GetProperty("ProgressionEventId").GetInt64()); + Assert.Equal("Test Event", json.GetProperty("Name").GetString()); + Assert.True(json.GetProperty("IsEnabled").GetBoolean()); + + // Running, or the client applies nothing. + Assert.True(json.GetProperty("StartTime").GetDateTimeOffset() < DateTimeOffset.UtcNow); + Assert.True(json.GetProperty("EndTime").GetDateTimeOffset() > DateTimeOffset.UtcNow); + + var list = Assert.Single(json.GetProperty("KeepsakeRoomLists").EnumerateArray()); + Assert.Equal(7, list.GetProperty("KeepsakeRoomListId").GetInt64()); + Assert.Equal(5, list.GetProperty("KeepsakeRoomLimit").GetInt32()); + Assert.Equal( + new long[] { 25 }, + list.GetProperty("KeepsakeRooms").EnumerateArray().Select(e => e.GetProperty("RoomId").GetInt64()).ToArray()); + } + + // The record also carries the definition nested, which is the other shape a record takes + // in this API - an unknown member there is ignored, so carrying both costs nothing. + var record = JsonSerializer.SerializeToElement( + Assert.IsType(api.ProgressionEventRecord(42)).Value); + Assert.Equal(0, record.GetProperty("Xp").GetInt32()); + Assert.Equal(0, record.GetProperty("RewardsCollected").GetInt32()); + Assert.Equal(JsonValueKind.Null, record.GetProperty("XpBoostLastPurchasedAt").ValueKind); + Assert.False(record.TryGetProperty("ProgressionEvent", out _)); + + // An id this server does not have an event for is a 404, not a made-up record. + Assert.IsType(api.ProgressionEvent(43)); + Assert.IsType(api.ProgressionEventDefinition(43)); + Assert.IsType(api.ProgressionEventRecord(43)); + } + finally + { + KeepsakeRoomConfig.Override = previous; + } + } + + /// + /// With no keepsake room configured there is no event to report, and the endpoint answers the + /// JSON literal `null` - the body its declared `long?` reads as "no event". The empty array it + /// used to answer is logged by the client as `Malformed Response: '[]'` (2026-10-01 17:54 and + /// 21:37 runs), which is a parse failure and not a "nothing running" answer, and a 404 is an + /// empty body the client reports as a failed call it retries. + /// + [Fact] + public void NoKeepsakeRoomsMeansNoActiveEvent() + { + var previous = KeepsakeRoomConfig.Override; + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse(null); + try + { + var response = Assert.IsType( + new CompatibilityAPI(null!).ActiveProgressionEvents()); + + Assert.Equal("null", response.Content); + Assert.Equal("application/json", response.ContentType); + Assert.True(response.StatusCode is null or 200); + } + finally + { + KeepsakeRoomConfig.Override = previous; + } + } + + [Fact] + public void KeepsakeConfigUsesTheClientFieldNames() + { + var previous = KeepsakeRoomConfig.Override; + KeepsakeRoomConfig.Override = KeepsakeRoomConfig.Parse("""{ "AllRooms": true }"""); + try + { + var response = Assert.IsType(new KeepsakesAPI(null!).GlobalConfig()); + var json = JsonSerializer.SerializeToElement(response.Value); + + // These are the names the client's parser reads. A config carrying anything else + // leaves every switch at its default instead of the value that was sent. + Assert.True(json.GetProperty("KeepsakeFeatureEnabled").GetBoolean()); + Assert.Equal(10, json.GetProperty("KeepsakeRoomLimit").GetInt32()); + Assert.False(json.GetProperty("SocialXpBoostEnabled").GetBoolean()); + } + finally + { + KeepsakeRoomConfig.Override = previous; + } + } + + /// + /// A room's XP settings are a BARE two-key object - the room page parses this one + /// directly, so an envelope around it would fail the same way the keepsake catalog does. + /// A 404 here is what the client logs as `HTTP Error 404` as the room page opens, with the + /// maker buttons on it never built. + /// + [Fact] + public void RoomExperienceAnswersTheTwoKeysTheClientReads() + { + var response = Assert.IsType(new V2_RoomsAPI(null!).GetRoomExperience(25)); + var json = JsonSerializer.SerializeToElement(response.Value); + + Assert.False(json.GetProperty("Enabled").GetBoolean()); + Assert.Equal(1000, json.GetProperty("DailyLimit").GetInt32()); + Assert.False(json.TryGetProperty("value", out _)); + } + + /// + /// The caller's per-room progress is an empty LIST: the client reads that as "no progress + /// in this room", where a 404 is an HTTP error and stalls the room page it is loading. + /// + [Fact] + public void RoomExperienceForPlayerAnswersAnEmptyList() + { + var response = Assert.IsType(new V2_RoomsAPI(null!).GetRoomExperienceForPlayer(25)); + var json = JsonSerializer.SerializeToElement(response.Value); + + Assert.Equal(JsonValueKind.Array, json.ValueKind); + Assert.Equal(0, json.GetArrayLength()); + } + + /// + /// A room's gift-drop shops are an empty LIST. The client asks for this route as a room + /// page opens and re-asks when it 404s, which is what leaves the page's shop entries + /// waiting while it logs an HTTP error. + /// + [Fact] + public void RoomGiftDropShopsAnswerAnEmptyList() + { + var response = Assert.IsType(new RoomGiftDropShopAPI().GetRoomGiftDropShops(25)); + var json = JsonSerializer.SerializeToElement(response.Value); + + Assert.Equal(JsonValueKind.Array, json.ValueKind); + Assert.Equal(0, json.GetArrayLength()); + } + + [Fact] + public void UserSegmentCompatibilityRouteIsRegisteredAndReturnsTheThreeSegmentMembers() + { + var route = typeof(CommonQueryAPI).GetMethod(nameof(CommonQueryAPI.GetUserSegment))! + .GetCustomAttributes() + .Single(); + Assert.Equal("/data/commonQuery/userSegment", route.Template); + + // The empty object this route used to answer is a `Malformed Response: '{}'` in the client + // (no member to bind), so the shape under test is "an object with the segment members", + // not merely "an object". + var response = Assert.IsType(new CommonQueryAPI().GetUserSegment()); + var json = JsonSerializer.SerializeToElement(response.Value); + Assert.Equal(JsonValueKind.Object, json.ValueKind); + Assert.Equal( + new[] { "User_segment_algo_name", "User_segment_algo_version", "User_segment_value" }, + json.EnumerateObject().Select(property => property.Name).ToArray()); + } + + [Fact] + public void PurchaseCampaignsReturnABareList() + { + // CompatibilityAPI now takes the database (it stores room comments), which this shape + // check does not touch - a null context is enough for a route that reads no data. + var response = Assert.IsType(new CompatibilityAPI(null!).CurrentPurchaseCampaigns()); + var json = JsonSerializer.SerializeToElement(response.Value); + + // PurchaseCampaignManager holds these as List; the paged shape fails its list reader. + Assert.Equal(JsonValueKind.Array, json.ValueKind); + } + + [Theory] + [InlineData(typeof(ReferralsAPI), "incentivizedreferrals", "GetReferrealProgress", "progress")] + [InlineData(typeof(InfluencerPartnerProgramAPI), "influencerpartnerprogram", "GetInfluencers", "influencers")] + public void EconomyStartupEndpointsExposeBothRoutePrefixes( + Type controller, string resource, string method, string action) + { + var routes = controller.GetCustomAttributes() + .SelectMany(prefix => controller.GetMethod(method)!.GetCustomAttributes() + .Select(suffix => $"{prefix.Template}/{suffix.Template}")) + .ToArray(); + + Assert.Contains($"api/{resource}/{action}", routes); + Assert.Contains($"econ/api/{resource}/{action}", routes); + Assert.Contains($"econ/api/{resource}/v1/{action}", routes); + } + + [Fact] + public void StorefrontCarouselExposesEconomyRoute() + { + var routes = typeof(StorefrontsV1).GetCustomAttributes() + .SelectMany(prefix => typeof(StorefrontsV1).GetMethod("GetAdCarouselItems")! + .GetCustomAttributes() + .Select(suffix => $"{prefix.Template}/{suffix.Template}")) + .ToArray(); + + Assert.Contains("api/storefronts/v1/adcarouselitems", routes); + Assert.Contains("econ/api/storefronts/v1/adcarouselitems", routes); + } + + [Fact] + public async Task CachedLoginDeleteRemovesOnlyTheAuthenticatedPlatformLogin() + { + var options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var db = new RecDBContext(options); + db.CachedLogins.AddRange( + new CachedLogin { accountId = 71, platform = Platforms.Steam, platformId = "steam-71" }, + new CachedLogin { accountId = 71, platform = Platforms.Oculus, platformId = "quest-71" }, + new CachedLogin { accountId = 72, platform = Platforms.Steam, platformId = "steam-71" }); + await db.SaveChangesAsync(); + + var api = new CompatibilityAPI(db); + var identity = new System.Security.Claims.ClaimsIdentity("test"); + identity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "71")); + identity.AddClaim(new System.Security.Claims.Claim("rn.platid", "steam-71")); + identity.AddClaim(new System.Security.Claims.Claim("rn.plat", ((int)Platforms.Steam).ToString())); + api.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new System.Security.Claims.ClaimsPrincipal(identity) } + }; + + Assert.IsType(await api.DeleteCurrentCachedLogin()); + Assert.Equal(new[] { (71L, Platforms.Oculus), (72L, Platforms.Steam) }, + await db.CachedLogins.OrderBy(login => login.accountId).ThenBy(login => login.platform) + .Select(login => new ValueTuple(login.accountId, login.platform)).ToArrayAsync()); + } + + [Fact] + public async Task InfluencerRemovalUsesTheCapturedEconomyRouteAndRequestShape() + { + var method = typeof(InfluencerPartnerProgramAPI).GetMethod(nameof(InfluencerPartnerProgramAPI.RemoveInfluencer))!; + Assert.Contains(method.GetCustomAttributes(), attribute => attribute.Template == "remove"); + + // No action parameter on purpose: the economy client posts this required body + // urlencoded, and binding it with [FromBody] answered 415 Unsupported Media Type. + Assert.Empty(method.GetParameters()); + var accountId = typeof(InfluencerRemoveRequest).GetProperty("influencerAccountId"); + Assert.NotNull(accountId); + Assert.Equal(typeof(int), accountId!.PropertyType); + + var body = System.Text.Encoding.UTF8.GetBytes("influencerAccountId=7"); + var api = new InfluencerPartnerProgramAPI + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + Request = + { + ContentType = "application/x-www-form-urlencoded", + Body = new MemoryStream(body) + } + } + } + }; + api.HttpContext.Request.ContentLength = body.Length; + + var result = Assert.IsType(await api.RemoveInfluencer()); + var response = JsonSerializer.SerializeToElement(result.Value); + Assert.True(response.GetProperty("success").GetBoolean()); + } + + /// + /// Both influencer reads answer a BARE NUMBER, because that is what the client's own signatures + /// return: `HAHEDHADMKF(int) -> KDCMFGDLEKL<int?>` for `influencer?accountId=` and + /// `BJLFMAMBEEJ() -> KDCMFGDLEKL<int?>` for `myinfluencer`. The captured real answer to the + /// first is a bare number (`28848275`, "Influencer Support" in + /// `research/RecNet API.postman_collection.json`); sending `{AccountId, Code}` there made every + /// profile open with `Exception: Malformed Response: '{ "AccountId": 2, "Code": "COACH" }'`, + /// and `myinfluencer` then collected BOTH of its own rejections - `'{}'` for the empty object + /// and `'{ "influencerAccountId": 0 }'` for the one-member object it was replaced with, which + /// is still in the log on every boot. 0 means "nobody". + /// + [Fact] + public void BothInfluencerReadsServeABareNumber() + { + var previousInfluencers = DBConfig.TestInfluencerAccountIds; + DBConfig.TestInfluencerAccountIds = new List { 2 }; + try + { + var api = new InfluencerPartnerProgramAPI + { + ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } + }; + + Assert.Equal(2L, Assert.IsType(api.GetInfluencer(2)).Value); + Assert.IsType(api.GetInfluencer(999)); + + api.ControllerContext.HttpContext.User = new ClaimsPrincipal( + new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "2") }, "TestAuth")); + + var mine = Assert.IsType(api.GetMyInfluencer()); + var mineJson = JsonSerializer.SerializeToElement(mine.Value); + Assert.Equal(JsonValueKind.Number, mineJson.ValueKind); + Assert.Equal(2L, mineJson.GetInt64()); + + // A caller who supports nobody gets the number 0, not an object carrying it. + api.ControllerContext.HttpContext.User = new ClaimsPrincipal( + new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "7") }, "TestAuth")); + var none = Assert.IsType(api.GetMyInfluencer()); + var noneJson = JsonSerializer.SerializeToElement(none.Value); + Assert.Equal(JsonValueKind.Number, noneJson.ValueKind); + Assert.Equal(0L, noneJson.GetInt64()); + } + finally + { + DBConfig.TestInfluencerAccountIds = previousInfluencers; + } + } +} diff --git a/ServerTests/StorefrontRewardConfigTests.cs b/ServerTests/StorefrontRewardConfigTests.cs new file mode 100644 index 0000000..cc7ac48 --- /dev/null +++ b/ServerTests/StorefrontRewardConfigTests.cs @@ -0,0 +1,154 @@ +using System.Text.Json; +using RadicadeServer.API.Storefront.V3; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.RecNet.Enums.Players; +using RadicadeServer.RecNet.Store; +using RadicadeServer.RecNet.Storefront; +using Xunit; + +namespace RadicadeServer.Tests; + +public class StorefrontRewardConfigTests +{ + [Fact] + public void QuestStorefrontProjectionHidesOnlyUnsupportedAvatarOnlyItems() + { + var supported = Purchasable(10, "supported,"); + var unsupported = Purchasable(11, "not-in-quest-build,"); + var mixedReward = Purchasable(12, "also-unsupported,"); + var originalGift = mixedReward.GiftDrops[0]; + originalGift.ConsumableItemDesc = "consumable"; + originalGift.Currency = 25; + + var projected = StorefrontsV3.ProjectStoreItemsForClient( + [supported, unsupported, mixedReward], + ((int)Platforms.Standalone).ToString(), + description => description == "supported,"); + + Assert.Equal(new[] { 10, 12 }, projected.Select(item => item.PurchasableItemId)); + Assert.Equal(250, Assert.Single(projected[0].Prices).Price); + Assert.Equal(string.Empty, projected[1].GiftDrops[0].AvatarItemDesc); + Assert.Equal("consumable", projected[1].GiftDrops[0].ConsumableItemDesc); + Assert.Equal(25, projected[1].GiftDrops[0].Currency); + + // The response is a projection only; purchase resolution still sees the original gifts. + Assert.Equal("also-unsupported,", originalGift.AvatarItemDesc); + Assert.Equal("not-in-quest-build,", unsupported.GiftDrops[0].AvatarItemDesc); + } + + [Fact] + public void QuestStorefrontProjectionLeavesOtherPlatformsAndResolvableGiftDropsAlone() + { + var item = Purchasable(20, "supported,"); + item.GiftDrops.Add(new Gift { GiftDropId = 201, AvatarItemDesc = "unsupported," }); + + var nonStandalone = StorefrontsV3.ProjectStoreItemsForClient( + [item], + ((int)Platforms.Oculus).ToString(), + _ => false); + var standalone = StorefrontsV3.ProjectStoreItemsForClient( + [item], + ((int)Platforms.Standalone).ToString(), + description => description == "supported,"); + + Assert.Same(item, Assert.Single(nonStandalone)); + Assert.Single(Assert.Single(standalone).GiftDrops); + Assert.Equal("supported,", standalone[0].GiftDrops[0].AvatarItemDesc); + } + + [Fact] + public void QuestStorefrontProjectionFiltersCapturedCatalogWithoutChangingPurchaseLookup() + { + using var lookup = JsonDocument.Parse(File.ReadAllText( + Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar-item-lookup.json"))); + var resolvable = lookup.RootElement.GetProperty("descriptions") + .EnumerateArray() + .Select(value => AvatarItemDescription.Normalize(value.GetString())) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var canonicalItems = StorefrontCatalog.GetItems((long)StorefrontTypes.Watch); + var supportedItem = canonicalItems.First(item => + resolvable.Contains(AvatarItemDescription.Normalize(item.GiftDrop.AvatarItemDesc))); + var unsupportedItem = canonicalItems.First(item => + !string.IsNullOrWhiteSpace(item.GiftDrop.AvatarItemDesc) + && !resolvable.Contains(AvatarItemDescription.Normalize(item.GiftDrop.AvatarItemDesc))); + var projected = StorefrontsV3.ProjectStoreItemsForClient( + canonicalItems, + ((int)Platforms.Standalone).ToString(), + description => resolvable.Contains(AvatarItemDescription.Normalize(description))); + + Assert.Contains(projected, item => item.PurchasableItemId == supportedItem.PurchasableItemId); + Assert.DoesNotContain(projected, item => item.PurchasableItemId == unsupportedItem.PurchasableItemId); + Assert.All(projected.SelectMany(item => item.GiftDrops) + .Where(gift => !string.IsNullOrWhiteSpace(gift.AvatarItemDesc)), + gift => Assert.Contains( + AvatarItemDescription.Normalize(gift.AvatarItemDesc), resolvable)); + + var canonicalUnsupported = StorefrontCatalog.FindItem( + (long)StorefrontTypes.Watch, unsupportedItem.PurchasableItemId); + Assert.NotNull(canonicalUnsupported); + Assert.Equal(unsupportedItem.GiftDrop.AvatarItemDesc, canonicalUnsupported.GiftDrop.AvatarItemDesc); + } + + private static PurchasableGiftDrop Purchasable(int itemId, string avatarItemDesc) => new() + { + PurchasableItemId = itemId, + Prices = [new PurchasablePrice { Price = 250 }], + GiftDrops = [new Gift { GiftDropId = itemId + 100, AvatarItemDesc = avatarItemDesc }] + }; + + [Fact] + public void PartialMultiplierSerializesAsNumber() + { + var json = JsonSerializer.SerializeToElement(new StorefrontBalanceAddConfigDTO()); + Assert.Equal(JsonValueKind.Number, json.GetProperty("MaxPartialMultiplier").ValueKind); + Assert.Equal(1f, json.GetProperty("MaxPartialMultiplier").GetSingle()); + Assert.Equal(JsonValueKind.False, json.GetProperty("IgnorePartialMultiplier").ValueKind); + Assert.Equal(JsonValueKind.False, json.GetProperty("BalanceInGiftBox").ValueKind); + } + + // The client asks for a storefront by number and the MVC binder drops any id that is not a + // member of StorefrontTypes down to None, so those ids have to stay defined and keep the + // exact numbers the 20230831 build sends (RecNet.Runtime's StorefrontTypes). + [Theory] + [InlineData(StorefrontTypes.LaserTag, 1)] + [InlineData(StorefrontTypes.Watch, 3)] + [InlineData(StorefrontTypes.Quest_GoldenTrophy, 102)] + [InlineData(StorefrontTypes.Quest_CrimsonCauldron, 103)] + [InlineData(StorefrontTypes.Paintball_SunsetDriveIn, 406)] + [InlineData(StorefrontTypes.StuntRunner, 600)] + [InlineData(StorefrontTypes.DormMirror, 700)] + [InlineData(StorefrontTypes.InventionStore, 800)] + [InlineData(StorefrontTypes.Player_Profile, 1000)] + [InlineData(StorefrontTypes.PopUpShop_1, 1700)] + [InlineData(StorefrontTypes.CustomAvatarItems, 1800)] + [InlineData(StorefrontTypes.ProgressionEventPurchasableXpBoosts, 1900)] + [InlineData(StorefrontTypes.PopUpShop_ProgressionEvent_1, 2000)] + [InlineData(StorefrontTypes.PopUpShop_ProgressionEvent_5, 2004)] + public void GameStorefrontIdStaysBound(StorefrontTypes type, int clientId) + { + Assert.Equal(clientId, (int)type); + Assert.True( + Enum.IsDefined(typeof(StorefrontTypes), clientId), + $"storefront {clientId} must stay a defined StorefrontTypes member or the route binder serves it as None"); + } + + // Every storefront the client knows carries a capture (RadicadeServer-Config/storefronts/sf{id}.json, + // embedded into the server at build), so the catalog covers the whole game and every storefront is + // editable there. A storefront with no items of its own still serves the shared storefront 0 list. + [Fact] + public void EveryGameStorefrontHasACapture() + { + var captured = StorefrontCatalog.ReadCaptures() + .Select(capture => capture.StorefrontId) + .ToHashSet(); + + var storefronts = Enum.GetValues() + .Where(type => type != StorefrontTypes.None) + .Select(type => (long)type) + .ToList(); + + Assert.Equal(44, storefronts.Count); + Assert.Empty(storefronts.Where(id => !captured.Contains(id))); + } +} diff --git a/ServerTests/StringToolsTests.cs b/ServerTests/StringToolsTests.cs new file mode 100644 index 0000000..49d7667 --- /dev/null +++ b/ServerTests/StringToolsTests.cs @@ -0,0 +1,72 @@ +using RadicadeServer.RecNet; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +public class StringToolsTests +{ + [Fact] + public void InvalidRoomTagDoesNotBreakRoomLists() + { + var tags = StringTools.ReadTags("rro,2|action"); + var tag = Assert.Single(tags); + Assert.Equal("rro", tag.Tag); + } + + // Beta-enabled rooms (e.g. the imported Showdown room) carry a "beta" tag with + // TagType.Auto - matching captured real RecNet responses - which is what flips + // the client's "Allow Creative Tools Beta Content" room setting on. A round-trip + // through the stored tag string must preserve it or the toggle stays greyed out. + [Fact] + public void BetaTag_SurvivesTagStringRoundTrip() + { + var stored = StringTools.CreateTagString(new List + { + new ItemTag { Tag = "rro", Type = TagType.AGOnly }, + new ItemTag { Tag = "games", Type = TagType.General }, + new ItemTag { Tag = "beta", Type = TagType.Auto }, + }); + + Assert.Equal("rro,2|games,0|beta,1", stored); + + var read = StringTools.ReadTags(stored); + Assert.Equal(3, read.Count); + var beta = read.Single(t => t.Tag == "beta"); + Assert.Equal(TagType.Auto, beta.Type); + } + + // The blacklisted words are the PATTERNS and the caller's text is the input. When + // the arguments were the other way round, the player's own text was compiled as a + // regex: anything with an unbalanced bracket or parenthesis threw an + // ArgumentException straight out of the request (a bio of "Cute (" was enough), and + // ordinary text stood a chance of being rejected as a false positive. + [Theory] + [InlineData("Cool person")] + [InlineData("Cute (")] + [InlineData("[unclosed")] + [InlineData("a.b*c")] + [InlineData("bob")] + public void IsPure_AcceptsOrdinaryTextThatLooksLikeARegex(string text) + { + Assert.True(text.IsPure()); + } + + [Theory] + [InlineData("nigger")] + [InlineData("well, nigger123")] + [InlineData("NIGGER")] + [InlineData("a fag")] + public void IsPure_RejectsTextContainingABlacklistedWord(string text) + { + Assert.False(text.IsPure()); + } + + [Fact] + public void Sanitize_MasksBlacklistedWordsRegardlessOfCase() + { + Assert.Equal("****", "Nigger".Sanitize()); + Assert.Equal("hello there", "hello there".Sanitize()); + } +} diff --git a/ServerTests/StudioCloudBuildTests.cs b/ServerTests/StudioCloudBuildTests.cs new file mode 100644 index 0000000..6508db7 --- /dev/null +++ b/ServerTests/StudioCloudBuildTests.cs @@ -0,0 +1,153 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using RadicadeServer.API.Studio; +using RadicadeServer.DB; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class StudioCloudBuildTests +{ + private sealed class TestDbFactory : IDbContextFactory + { + private readonly DbContextOptions options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + public RecDBContext CreateDbContext() => new(options); + public Task CreateDbContextAsync(CancellationToken cancellationToken = default) => + Task.FromResult(CreateDbContext()); + } + + private static (StudioAPI Api, StudioBuildWorker Worker) CreateController(long accountId = 2, long? roomOwnerId = null) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim("userId", accountId.ToString())); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + var worker = new StudioBuildWorker( + NullLogger.Instance, + new ConfigurationBuilder().Build()); + var dbFactory = new TestDbFactory(); + using (var db = dbFactory.CreateDbContext()) + { + db.Rooms.Add(new RadicadeServer.RecNet.Rooms.RoomData + { + RoomId = 99, + Name = "Build Test Room", + CreatorAccountId = roomOwnerId ?? accountId + }); + db.SaveChanges(); + } + var api = new StudioAPI(NullLogger.Instance, worker, dbFactory) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + return (api, worker); + } + + private static async Task WaitForBuild(StudioAPI api, string cloudBuildId) + { + for (var attempt = 0; attempt < 100; attempt++) + { + var response = Assert.IsType(await api.CloudBuild(cloudBuildId)); + var build = System.Text.Json.JsonSerializer.Deserialize( + System.Text.Json.JsonSerializer.Serialize(response.Value)); + Assert.NotNull(build); + if (build!.CompletedAt != null) return build; + await Task.Delay(20); + } + throw new TimeoutException("The queued local build did not report a result."); + } + + [Fact] + public async Task CreateQueuesBuildAndReturnsTheWorkerFailureForMissingUnityConfiguration() + { + var (controller, worker) = CreateController(); + await worker.StartAsync(CancellationToken.None); + try + { + var response = Assert.IsType(await controller.CreateCloudBuild(99, 109, 1, autoPublish: true, buildBranch: null)); + var json = System.Text.Json.JsonSerializer.SerializeToElement(response.Value); + Assert.True(json.GetProperty("Success").GetBoolean()); + Assert.Null(json.GetProperty("Error").GetString()); + var queued = json.GetProperty("Value"); + var cloudBuildId = queued.GetProperty("CloudBuildId").GetString(); + Assert.False(string.IsNullOrWhiteSpace(cloudBuildId)); + Assert.Equal(System.Text.Json.JsonValueKind.Null, queued.GetProperty("CompletedAt").ValueKind); + Assert.Equal(2, queued.GetProperty("CreatedByAccountId").GetInt32()); + + var completed = await WaitForBuild(controller, cloudBuildId!); + Assert.True(completed.CompletedAt >= completed.StartedAt); + Assert.Contains("Radicade:Studio:UnityPath", completed.Error); + Assert.Empty(completed.UnityAssetId); + + var list = Assert.IsType(await controller.CloudBuildsForRoom(99, 109, 0, 30)); + var listJson = System.Text.Json.JsonSerializer.SerializeToElement(list.Value); + Assert.Contains(listJson.GetProperty("Results").EnumerateArray(), + item => item.GetProperty("CloudBuildId").GetString() == cloudBuildId); + } + finally + { + await worker.StopAsync(CancellationToken.None); + worker.Dispose(); + } + } + + [Fact] + public async Task CreateAcceptsStudioUrlEncodedPostFormFields() + { + var (controller, worker) = CreateController(); + await worker.StartAsync(CancellationToken.None); + try + { + var context = controller.HttpContext; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes( + "roomId=99&subRoomId=109&subRoomDataSaveId=1&autoPublish=true&buildBranch=main")); + context.Request.ContentLength = context.Request.Body.Length; + + var response = Assert.IsType(await controller.CreateCloudBuild()); + var json = System.Text.Json.JsonSerializer.SerializeToElement(response.Value); + Assert.True(json.GetProperty("Success").GetBoolean()); + var createdId = json.GetProperty("Value").GetProperty("CloudBuildId").GetString()!; + var completed = await WaitForBuild(controller, createdId); + Assert.Contains("Radicade:Studio:UnityPath", completed.Error); + + var page = Assert.IsType(await controller.CloudBuildsForRoom(99, 109, 0, 100)); + var pageJson = System.Text.Json.JsonSerializer.SerializeToElement(page.Value); + Assert.Contains(pageJson.GetProperty("Results").EnumerateArray(), + build => build.GetProperty("CloudBuildId").GetString() == createdId); + } + finally + { + await worker.StopAsync(CancellationToken.None); + worker.Dispose(); + } + } + + [Fact] + public async Task CreateRejectsMissingSaveReference() + { + var (controller, worker) = CreateController(); + var result = Assert.IsType(await controller.CreateCloudBuild(99, 109, 0)); + Assert.NotNull(result.Value); + worker.Dispose(); + } + + [Fact] + public async Task CreateRejectsAccountsWithoutRoomBuildPermission() + { + var (controller, worker) = CreateController(accountId: 2, roomOwnerId: 7); + var result = Assert.IsType(await controller.CreateCloudBuild(99, 109, 1)); + Assert.NotNull(result); + worker.Dispose(); + } +} diff --git a/ServerTests/StudioUploadTests.cs b/ServerTests/StudioUploadTests.cs new file mode 100644 index 0000000..4c74373 --- /dev/null +++ b/ServerTests/StudioUploadTests.cs @@ -0,0 +1,184 @@ +using System.Security.Cryptography; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer; +using RadicadeServer.CDN; +using RadicadeServer.RecNet.CDN; +using RadicadeServer.Storage; +using RadicadeServer.API.Studio; +using RadicadeServer.RoomServer; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Rooms; +using Xunit; + +namespace RadicadeServer.Tests; + +[Collection(DbFactoryCollection.Name)] +public class StudioUploadTests +{ + [Fact] + public async Task BatchPreservesOrderAndStoresActualContentsIncludingEmptyFiles() + { + var previous = DBConfig.CDNStorage; + var directory = Path.Combine(Path.GetTempPath(), "studio-upload-" + Guid.NewGuid()); + DBConfig.CDNStorage = directory; + try + { + var payloads = new[] { new byte[] { 1, 2, 3 }, Array.Empty() }; + var boundary = "test"; + var body = new MemoryStream(); + foreach (var payload in payloads) + { + var header = System.Text.Encoding.ASCII.GetBytes( + $"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"asset.bin\"\r\nContent-Type: application/octet-stream\r\n\r\n"); + body.Write(header); + body.Write(payload); + body.Write(System.Text.Encoding.ASCII.GetBytes("\r\n")); + } + body.Write(System.Text.Encoding.ASCII.GetBytes($"--{boundary}--\r\n")); + body.Position = 0; + var context = new DefaultHttpContext(); + context.Request.ContentType = $"multipart/form-data; boundary={boundary}"; + context.Request.Body = body; + context.Request.ContentLength = body.Length; + var controller = new StorageAPI { ControllerContext = new ControllerContext { HttpContext = context } }; + var response = Assert.IsType(await controller.UploadUnityAssets()); + var json = System.Text.Json.JsonSerializer.SerializeToElement(response.Value); + Assert.Equal(2, json.GetArrayLength()); + for (var i = 0; i < payloads.Length; i++) + { + var name = json[i].GetProperty("Filename").GetString()!; + Assert.Equal(40, name.Length); + Assert.Equal(payloads[i], await File.ReadAllBytesAsync(Path.Combine(directory, name))); + Assert.Equal(Convert.ToBase64String(SHA256.HashData(payloads[i])), json[i].GetProperty("Hash").GetString()); + Assert.IsType(new StudioAssetsCDN().Download(name)); + } + Assert.Equal(2, Directory.GetFiles(directory).Length); + } + finally + { + DBConfig.CDNStorage = previous; + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } + + [Fact] + public async Task UploadParsesGzipCompressedMultipartFilesLargerThanDefault16KiBLimit() + { + var previous = DBConfig.CDNStorage; + var directory = Path.Combine(Path.GetTempPath(), "studio-upload-large-" + Guid.NewGuid()); + DBConfig.CDNStorage = directory; + try + { + const string boundary = "studio-upload-boundary"; + var payload = Enumerable.Range(0, 32 * 1024).Select(i => (byte)(i % 251)).ToArray(); + var multipart = System.Text.Encoding.ASCII.GetBytes( + new string('p', 20 * 1024) + "\r\n" + + $"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"asset.bin\"\r\nContent-Type: application/octet-stream\r\n\r\n") + .Concat(payload) + .Concat(System.Text.Encoding.ASCII.GetBytes($"\r\n--{boundary}--\r\n")) + .ToArray(); + var body = new MemoryStream(); + await using (var gzip = new System.IO.Compression.GZipStream(body, System.IO.Compression.CompressionLevel.Optimal, leaveOpen: true)) + await gzip.WriteAsync(multipart); + body.Position = 0; + var context = new DefaultHttpContext(); + context.Request.ContentType = $"multipart/form-data; boundary=\"{boundary}\""; + context.Request.Headers["Content-Encoding"] = "gzip"; + context.Request.Body = body; + context.Request.ContentLength = body.Length; + var controller = new StorageAPI { ControllerContext = new ControllerContext { HttpContext = context } }; + + var response = Assert.IsType(await controller.UploadUnityAssets()); + var json = System.Text.Json.JsonSerializer.SerializeToElement(response.Value); + var filename = json[0].GetProperty("Filename").GetString()!; + Assert.Equal(payload, await File.ReadAllBytesAsync(Path.Combine(directory, filename))); + } + finally + { + DBConfig.CDNStorage = previous; + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } + + [Fact] + public async Task CompressedSingleFileUploadReturnsLegacyFilenameResponse() + { + var previous = DBConfig.CDNStorage; + var directory = Path.Combine(Path.GetTempPath(), "studio-upload-single-" + Guid.NewGuid()); + DBConfig.CDNStorage = directory; + try + { + const string boundary = "legacy-upload-boundary"; + var payload = Enumerable.Range(0, 4096).Select(i => (byte)(i % 253)).ToArray(); + var multipart = System.Text.Encoding.ASCII.GetBytes( + "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"asset.bin\"\r\nContent-Type: application/octet-stream\r\n\r\n") + .Concat(payload) + .Concat(System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n")) + .ToArray(); + var body = new MemoryStream(); + await using (var gzip = new System.IO.Compression.GZipStream(body, System.IO.Compression.CompressionLevel.Optimal, leaveOpen: true)) + await gzip.WriteAsync(multipart); + body.Position = 0; + var context = new DefaultHttpContext(); + context.Request.ContentType = $"multipart/form-data; boundary=\"{boundary}\""; + context.Request.Headers["Content-Encoding"] = "gzip"; + context.Request.Body = body; + context.Request.ContentLength = body.Length; + var controller = new StorageAPI { ControllerContext = new ControllerContext { HttpContext = context } }; + + var response = await controller.Upload(); + var value = Assert.IsType(response.Value); + Assert.Equal(40, value.filename.Length); + Assert.Equal(payload, await File.ReadAllBytesAsync(Path.Combine(directory, value.filename))); + } + finally + { + DBConfig.CDNStorage = previous; + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } + + [Fact] + public async Task RegistryAndGameMetadataExposeBothWindowsBundlesAndRawAssetDownload() + { + var previous = DBConfig.CDNStorage; + var directory = Path.Combine(Path.GetTempPath(), "studio-baked-" + Guid.NewGuid()); + DBConfig.CDNStorage = directory; + Directory.CreateDirectory(directory); + try + { + const string manifestId = "0123456789abcdef0123456789abcdef01234567"; + await File.WriteAllTextAsync(Path.Combine(directory, manifestId), "{\"Version\":2,\"MainSceneProjectRelativePath\":\"Assets/Room.unity\",\"Files\":[]}"); + await using var sceneBundle = new MemoryStream(new byte[] { 1, 2, 3, 4 }); + var sceneUpload = await StudioAssetStorage.Save(sceneBundle, CancellationToken.None); + await using var rejectedBundle = new MemoryStream(new byte[] { 5, 6, 7 }); + var rejectedUpload = await StudioAssetStorage.Save(rejectedBundle, CancellationToken.None); + await StudioUnityAssetRegistry.AddBakedAssetAsync(manifestId, + new StudioBakedUnityAsset(manifestId, 0, 1, sceneUpload.Filename, rejectedUpload.Filename)); + + var api = new UnityAssetsCDN { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } }; + var response = Assert.IsType(await api.GetUnityAsset(manifestId, 0, 1)); + using var metadata = System.Text.Json.JsonDocument.Parse(response.Content!); + Assert.Equal(sceneUpload.Filename, metadata.RootElement.GetProperty("Filename").GetString()); + Assert.Equal(rejectedUpload.Filename, metadata.RootElement.GetProperty("RejectedDependenciesFilename").GetString()); + Assert.IsType(new DataCDN().GetDataBlob(sceneUpload.Filename)); + Assert.IsType(new DataCDN().GetDataBlob(rejectedUpload.Filename)); + + var studio = new StudioAssetsCDN { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } }; + Assert.IsType(studio.Download(sceneUpload.Filename)); + } + finally + { + DBConfig.CDNStorage = previous; + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } + + [Theory] + [InlineData("../secret")] + [InlineData("..\\secret")] + [InlineData("C:secret")] + public void DownloadRejectsPaths(string filename) => + Assert.IsType(new StudioAssetsCDN().Download(filename)); +} diff --git a/ServerTests/SubRoomPermissionTests.cs b/ServerTests/SubRoomPermissionTests.cs new file mode 100644 index 0000000..c1c6416 --- /dev/null +++ b/ServerTests/SubRoomPermissionTests.cs @@ -0,0 +1,445 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums; +using RadicadeServer.RecNet.Enums.Rooms; +using RadicadeServer.RecNet.Rooms; +using RadicadeServer.RoomServer; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// The Moderator Permission Settings screen writes a subroom's overrides, and +/// rooms/photon_access_token is the only thing that ever reads them back to the client. +/// Both halves failed before: the PUT was a stub that stored nothing, and the token answered an +/// empty table, so every box in that screen came up unchecked no matter what a creator had +/// saved. These pin the pair - that a stored override survives the round trip, that unchecking +/// the box removes it, that (Permission, Role) is the identity so re-sending overwrites, and +/// that the token table the client actually applies carries the override over the default. +/// +[Collection(DbFactoryCollection.Name)] +public class SubRoomPermissionTests +{ + private const long CreatorId = 7; + private const long StrangerId = 99; + private const long RoomId = 1; + private const long SubRoomId = 40; + + private static readonly JsonSerializerOptions ServerJson = new() + { + PropertyNamingPolicy = null, + WriteIndented = true, + }; + + private sealed class TestDbContextFactory : IDbContextFactory + { + private readonly DbContextOptions options; + + public TestDbContextFactory() + { + options = TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + } + + public RecDBContext CreateDbContext() => new(options); + } + + private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext() + { + var factory = new TestDbContextFactory(); + RoomUtils.DbFactory = factory; + MatchmakingUtils.DbFactory = factory; + return (factory, factory.CreateDbContext()); + } + + private static void SetUser(Controller controller, long accountId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + }; + } + + private static void SetJsonBody(Controller controller, string json) + { + var request = controller.ControllerContext.HttpContext.Request; + request.ContentType = "application/json"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json)); + } + + private static RoomServerAPI Api(RecDBContext db, long accountId) + { + var api = new RoomServerAPI(db); + SetUser(api, accountId); + return api; + } + + private static async Task SeedRoomAsync(RecDBContext db, long creatorId = CreatorId) + { + db.Rooms.Add(new RoomData + { + RoomId = RoomId, + Name = "Rec Center", + CreatorAccountId = creatorId, + Accessibility = Accessibility.Public, + MaxPlayers = 16, + CreatedAt = DateTime.UtcNow, + }); + + db.SubRooms.Add(new SubRoom + { + SubRoomId = SubRoomId, + RoomId = RoomId, + Name = "Rec Center", + UnitySceneId = "RecCenterScene", + MaxPlayers = 16, + Accessibility = Accessibility.Public, + DataBlob = "", + }); + + await db.SaveChangesAsync(); + } + + /// + /// The permission entries a route answered with. The GET and the token table both hand back + /// objects whose keys are the client's own field names, so they are read as JSON. + /// + private static List PermissionList(object? value) + { + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(value, ServerJson)); + return doc.RootElement.EnumerateArray().Select(x => x.Clone()).ToList(); + } + + private static List RootPropertyList(object? value, string property) + { + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(value, ServerJson)); + return doc.RootElement.GetProperty(property).EnumerateArray().Select(x => x.Clone()).ToList(); + } + + private static (string Permission, long Role) Pair(JsonElement entry) => + (entry.GetProperty("Permission").GetString() ?? "", entry.GetProperty("Role").GetInt64()); + + private static async Task> StoredRowsAsync(TestDbContextFactory factory) + { + await using var db = factory.CreateDbContext(); + return await db.SubRoomPermissions.AsNoTracking().OrderBy(x => x.Id).ToListAsync(); + } + + /// + /// A stored row is the override, so reads always answer Override: true; a pair with no + /// row is simply absent. The GET route is what the settings screen loads its checkboxes from. + /// + [Fact] + public async Task StoredOverridesComeBackAsTheyWereSaved() + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + var api = Api(db, CreatorId); + SetJsonBody(api, """ + [ + { "Permission": "CAN_SPAWN_INVENTIONS", "Role": 0, "Override": true, "Type": 0, "Value": "False" }, + { "Permission": "CAN_INVITE", "Role": 0, "Override": true, "Type": 0, "Value": "True" } + ] + """); + + Assert.IsType(await api.SetSubRoomPermissions(RoomId, SubRoomId)); + + var read = Api(db, CreatorId); + var result = Assert.IsType(await read.GetSubRoomPermissions(RoomId, SubRoomId)); + var entries = PermissionList(result.Value); + + Assert.Equal(2, entries.Count); + Assert.Equal(("CAN_SPAWN_INVENTIONS", 0L), Pair(entries[0])); + Assert.Equal("False", entries[0].GetProperty("Value").GetString()); + Assert.Equal(("CAN_INVITE", 0L), Pair(entries[1])); + Assert.Equal("True", entries[1].GetProperty("Value").GetString()); + + // A stored row IS an override, so the box beside it reads as checked. + Assert.All(entries, x => Assert.True(x.GetProperty("Override").GetBoolean())); + } + + /// + /// Unchecking a box is the ABSENCE of a row, not a row that says false - otherwise the + /// default table would be overridden by a value the creator just asked to stop overriding. + /// + [Fact] + public async Task UncheckingTheOverrideBoxFallsBackToTheDefault() + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + var set = Api(db, CreatorId); + SetJsonBody(set, """ + [ { "Permission": "CAN_SAVE_INVENTIONS", "Role": 30, "Override": true, "Type": 0, "Value": "False" } ] + """); + Assert.IsType(await set.SetSubRoomPermissions(RoomId, SubRoomId)); + Assert.Single(await StoredRowsAsync(factory)); + + var unset = Api(db, CreatorId); + SetJsonBody(unset, """ + [ { "Permission": "CAN_SAVE_INVENTIONS", "Role": 30, "Override": false, "Type": 0, "Value": "False" } ] + """); + Assert.IsType(await unset.SetSubRoomPermissions(RoomId, SubRoomId)); + + Assert.Empty(await StoredRowsAsync(factory)); + + var read = Api(db, CreatorId); + var result = Assert.IsType(await read.GetSubRoomPermissions(RoomId, SubRoomId)); + Assert.Empty(PermissionList(result.Value)); + } + + /// + /// The client re-sends the whole table when one checkbox changes, so a pair that arrives + /// twice must update its row. Appending instead would leave two rows for one pair, and the + /// token table would carry whichever the query happened to return last. + /// + [Fact] + public async Task ResendingAPairReplacesItsRowInsteadOfAddingASecond() + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + var first = Api(db, CreatorId); + SetJsonBody(first, """ + [ { "Permission": "CAN_USE_DELETE_ALL_BUTTON", "Role": 0, "Override": true, "Type": 0, "Value": "False" } ] + """); + await first.SetSubRoomPermissions(RoomId, SubRoomId); + + var second = Api(db, CreatorId); + SetJsonBody(second, """ + [ { "Permission": "CAN_USE_DELETE_ALL_BUTTON", "Role": 0, "Override": true, "Type": 3, "Value": "True" } ] + """); + await second.SetSubRoomPermissions(RoomId, SubRoomId); + + var rows = await StoredRowsAsync(factory); + var row = Assert.Single(rows); + Assert.Equal("CAN_USE_DELETE_ALL_BUTTON", row.permission); + Assert.Equal(3, row.type); + Assert.Equal("True", row.value); + } + + /// Deciding what a role may do is a co-owner decision. + [Fact] + public async Task OnlyACoOwnerCanChangeThePermissions() + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + var stranger = Api(db, StrangerId); + SetJsonBody(stranger, """ + [ { "Permission": "CAN_USE_MAKER_PEN", "Role": 0, "Override": true, "Type": 0, "Value": "True" } ] + """); + + Assert.IsType(await stranger.SetSubRoomPermissions(RoomId, SubRoomId)); + Assert.Empty(await StoredRowsAsync(factory)); + } + + [Fact] + public async Task AnUnknownSubroomIsNotFound() + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + var api = Api(db, CreatorId); + SetJsonBody(api, "[]"); + + Assert.IsType(await api.SetSubRoomPermissions(RoomId, SubRoomId + 1)); + Assert.IsType(await api.GetSubRoomPermissions(RoomId, SubRoomId + 1)); + } + + /// + /// The whole point of storing them: the table the client applies when it spawns has to carry + /// the subroom's override over the default for that pair, and a pair the defaults do not + /// carry has to be added - otherwise a creator's checkbox is saved and then ignored. + /// + [Fact] + public async Task TheTokenTableCarriesTheSubroomsOverrides() + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + const long instanceId = 900; + db.RoomInstances.Add(new RoomInstance + { + roomInstanceId = instanceId, + roomId = RoomId, + subRoomId = SubRoomId, + maxCapacity = 16, + }); + db.Presences.Add(new RealPresence + { + playerId = CreatorId, + isOnline = true, + roomInstance = instanceId, + }); + await db.SaveChangesAsync(); + + var set = Api(db, CreatorId); + SetJsonBody(set, """ + [ + { "Permission": "CAN_SPAWN_INVENTIONS", "Role": 0, "Override": true, "Type": 0, "Value": "False" }, + { "Permission": "CAN_INVITE", "Role": 0, "Override": true, "Type": 0, "Value": "True" } + ] + """); + await set.SetSubRoomPermissions(RoomId, SubRoomId); + + var token = Api(db, CreatorId); + var result = Assert.IsType(await token.GetPhotonAccessToken()); + var table = RootPropertyList(result.Value, "Permissions"); + + // The default table includes CAN_INVITE (permission id 4), which the keepsake + // authorization gate also reads; both overrides replace defaults in place. + Assert.Equal(12, table.Count); + + using (var doc = JsonDocument.Parse(JsonSerializer.Serialize(result.Value, ServerJson))) + Assert.Equal(instanceId, doc.RootElement.GetProperty("RoomInstanceId").GetInt64()); + + var spawn = Assert.Single(table.Where(x => + x.GetProperty("Permission").GetString() == "CAN_SPAWN_INVENTIONS" && x.GetProperty("Role").GetInt64() == 0)); + Assert.Equal("False", spawn.GetProperty("Value").GetString()); + Assert.True(spawn.GetProperty("Override").GetBoolean()); + + // Replaced where it stood rather than appended, so the order cannot shift under the client + // and no pair is listed twice with two different values. + Assert.Equal(("CAN_SPAWN_INVENTIONS", 0L), Pair(table[3])); + Assert.Equal(("CAN_INVITE", 0L), Pair(table[5])); + Assert.Equal("True", table[5].GetProperty("Value").GetString()); + + // The default that ships as Override:false is the client's "this is not an override", and a + // subroom override must not have disturbed it. It follows the new CAN_INVITE row. + Assert.Equal(("CAN_USE_MAKER_PEN", 30L), Pair(table[6])); + Assert.False(table[6].GetProperty("Override").GetBoolean()); + } + + /// + /// Every spelling of the same payload has to land on the same stored entry, because the + /// table that matters is keyed by the permission's NAME. A body carrying the enum's number, + /// or the client's own `PermissionType` field, or the role tier as a name, would otherwise + /// save an entry that matches no default: the checkbox would look saved and change nothing. + /// + [Theory] + [InlineData(""" + [ { "Permission": 20, "Role": 0, "Override": true, "Type": 0, "Value": "False" } ] + """, "CAN_SPAWN_INVENTIONS", 0L)] + [InlineData(""" + [ { "PermissionType": "CAN_SPAWN_INVENTIONS", "Role": 0, "Override": true, "Type": 0, "Value": "False" } ] + """, "CAN_SPAWN_INVENTIONS", 0L)] + [InlineData(""" + [ { "PermissionTypeOld": "CAN_SPAWN_INVENTIONS", "Role": "0", "Override": true, "Type": 0, "Value": "False" } ] + """, "CAN_SPAWN_INVENTIONS", 0L)] + [InlineData(""" + [ { "Permission": "CAN_SPAWN_INVENTIONS", "Role": "CoOwner", "Override": true, "Type": 0, "Value": "False" } ] + """, "CAN_SPAWN_INVENTIONS", 30L)] + public async Task ASpellingOfTheSameEntryStoresTheSameKey(string body, string permission, long role) + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + var api = Api(db, CreatorId); + SetJsonBody(api, body); + Assert.IsType(await api.SetSubRoomPermissions(RoomId, SubRoomId)); + + var row = Assert.Single(await StoredRowsAsync(factory)); + Assert.Equal(permission, row.permission); + Assert.Equal(role, row.role); + Assert.Equal("False", row.value); + } + + /// + /// A subroom override is the creator's decision for that room and wins over the developer + /// grant, so nobody can build their way around it by being staff. + /// + [Fact] + public async Task ASubRoomOverrideBeatsTheDeveloperGrant() + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + const long instanceId = 901; + db.Profiles.Add(new Profile + { + accountId = CreatorId, + username = "dev", + displayName = "dev", + role = "developer", + profileImage = DBConfig.DefaultProfileImage, + }); + db.RoomInstances.Add(new RoomInstance + { + roomInstanceId = instanceId, + roomId = RoomId, + subRoomId = SubRoomId, + }); + db.Presences.Add(new RealPresence + { + playerId = CreatorId, + isOnline = true, + roomInstance = instanceId, + }); + await db.SaveChangesAsync(); + + var plain = Api(db, CreatorId); + var beforeResult = Assert.IsType(await plain.GetPhotonAccessToken()); + var before = RootPropertyList(beforeResult.Value, "Permissions"); + Assert.Equal(("CAN_USE_MAKER_PEN", 0L), Pair(before[0])); + Assert.True(before[0].GetProperty("Override").GetBoolean()); + Assert.Equal(13, before.Count); + + var set = Api(db, CreatorId); + SetJsonBody(set, """ + [ { "Permission": "CAN_USE_MAKER_PEN", "Role": 0, "Override": true, "Type": 0, "Value": "False" } ] + """); + await set.SetSubRoomPermissions(RoomId, SubRoomId); + + var after = Api(db, CreatorId); + var afterResult = Assert.IsType(await after.GetPhotonAccessToken()); + var table = RootPropertyList(afterResult.Value, "Permissions"); + + Assert.Equal("False", Assert.Single(table.Where(x => + x.GetProperty("Permission").GetString() == "CAN_USE_MAKER_PEN" && x.GetProperty("Role").GetInt64() == 0)) + .GetProperty("Value").GetString()); + Assert.Equal(13, table.Count); + } + + /// + /// A player standing in no instance still gets a usable table: the default, without the + /// subroom merge. An empty one is what left every permission unchecked before. + /// + [Fact] + public async Task StandingNowhereStillAnswersTheDefaultTable() + { + var (factory, db) = CreateContext(); + await using var _db = db; + await SeedRoomAsync(db); + + var api = Api(db, CreatorId); + var result = Assert.IsType(await api.GetPhotonAccessToken()); + var table = RootPropertyList(result.Value, "Permissions"); + + Assert.Equal(12, table.Count); + var invite = Assert.Single(table.Where(x => + x.GetProperty("Permission").GetString() == "CAN_INVITE" && x.GetProperty("Role").GetInt64() == 0)); + Assert.True(invite.GetProperty("Override").GetBoolean()); + Assert.Equal("True", invite.GetProperty("Value").GetString()); + } +} diff --git a/ServerTests/TestDbOptions.cs b/ServerTests/TestDbOptions.cs new file mode 100644 index 0000000..590d8e4 --- /dev/null +++ b/ServerTests/TestDbOptions.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using RadicadeServer.DB; + +namespace RadicadeServer.Tests; + +/// +/// Builds the options every test context uses. +/// +/// Each test asks for its own InMemory database, so each one needs its own +/// and EF builds a separate internal service +/// provider for it. Past twenty of those EF raises +/// as an exception, which +/// fails whichever tests happen to run after the twentieth provider rather than the test +/// that built it. That count is a property of how the harness allocates contexts, not of +/// the behaviour under test, so the warning is ignored once here instead of per test. +/// +internal static class TestDbOptions +{ + public static DbContextOptionsBuilder Create() => + new DbContextOptionsBuilder() + .ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)); +} diff --git a/ServerTests/UnityAssetMetadataTests.cs b/ServerTests/UnityAssetMetadataTests.cs new file mode 100644 index 0000000..32e238c --- /dev/null +++ b/ServerTests/UnityAssetMetadataTests.cs @@ -0,0 +1,271 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RadicadeServer; +using RadicadeServer.API.Studio; +using RadicadeServer.CDN; +using RadicadeServer.DB; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Step one of the client's bundle download is +/// GET /rooms/unity_assets/{assetId}/{target}/{version} for a file name, step two is +/// /data/{Filename} for the bytes. An id arrives in two shapes and the room load needs +/// the one the subroom carries: +/// +/// - a Studio manifest id (40 alphanumerics), whose bundles are named in the manifest's own +/// metadata; +/// - a legacy bundle stored under its file name, with or without the .assetbundle suffix +/// an import appends - which is every imported room, every built-in room and every bundle the +/// admin panel binds. +/// +/// Gating both shapes on the 40-character test answered 400 {"success":false,"error":"bad +/// request"} for every legacy bundle on the server, and the client reports one such answer as +/// "Error downloading room assets! Try again later or on a better connection." for the whole +/// room - the failure looked like a connection problem and hit every player, because it was +/// server-side and room-independent. These pin both shapes and the two names that must never be +/// served at all. +/// +[Collection(DbFactoryCollection.Name)] +public class UnityAssetMetadataTests : IDisposable +{ + private const string LegacyBundle = "Showdown_0_1.assetbundle"; + private const string StudioManifestId = "0123456789abcdef0123456789abcdef01234567"; + private static readonly string BakedBundle = new('a', 40); + private static readonly string RejectedBundle = new('b', 40); + + private readonly string root = Directory.CreateTempSubdirectory("radicade-unity-assets-").FullName; + private readonly string? previousStorage; + + public UnityAssetMetadataTests() + { + previousStorage = DBConfig.CDNStorage; + DBConfig.CDNStorage = root; + } + + public void Dispose() + { + DBConfig.CDNStorage = previousStorage!; + + try + { + Directory.Delete(root, true); + } + catch (IOException) + { + } + } + + private string Write(string name, string contents = "bundle") + { + var path = Path.Join(root, name); + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); + File.WriteAllText(path, contents); + return path; + } + + private static UnityAssetsCDN Metadata() => new() + { + ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } + }; + + private static T WithHttpContext(T controller) where T : Controller + { + controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; + return controller; + } + + private static JsonElement Body(IActionResult result) + { + var content = Assert.IsType(result); + Assert.Equal("application/json", content.ContentType); + return JsonSerializer.Deserialize(content.Content!); + } + + // ---------------------------------------------------------------- legacy ids + + [Theory] + [InlineData("Showdown_0_1.assetbundle", "Showdown_0_1.assetbundle")] + [InlineData("Showdown_0_1", "Showdown_0_1.assetbundle")] + [InlineData("8aas7b5ivtaggn9swhe3wkgdj.assetbundle", "8aas7b5ivtaggn9swhe3wkgdj.assetbundle")] + [InlineData("8aas7b5ivtaggn9swhe3wkgdj", "8aas7b5ivtaggn9swhe3wkgdj.assetbundle")] + public async Task ALegacyBundleResolvesUnderBothSpellingsOfItsId(string requested, string expected) + { + Write("Showdown_0_1.assetbundle"); + Write("8aas7b5ivtaggn9swhe3wkgdj.assetbundle"); + + var json = Body(await Metadata().GetUnityAsset(requested, 0, 1)); + + Assert.Equal(requested, json.GetProperty("UnityAssetId").GetString()); + Assert.Equal(expected, json.GetProperty("Filename").GetString()); + Assert.Equal(0, json.GetProperty("Target").GetInt32()); + Assert.Equal(1, json.GetProperty("Version").GetInt32()); + } + + [Fact] + public async Task TheFilenameTheClientIsHandedIsTheFileItCanDownload() + { + // The two steps have to agree: step two has no other way to reach the bytes. + Write(LegacyBundle); + var filename = Body(await Metadata().GetUnityAsset(LegacyBundle, 0, 1)).GetProperty("Filename").GetString()!; + + var file = Assert.IsType(new DataCDN().GetDataBlob(filename)); + + Assert.Equal(Path.Join(root, LegacyBundle), file.FileName); + Assert.True(file.EnableRangeProcessing); + } + + [Fact] + public async Task AnUnknownBundleStillAnswersNotFound() + { + Write(LegacyBundle); + + Assert.IsType(await Metadata().GetUnityAsset("not-on-disk", 0, 1)); + Assert.IsType(await Metadata().GetUnityAsset("not-on-disk.assetbundle", 0, 1)); + } + + [Fact] + public async Task TheIdsWithoutTargetOrVersionAnswersTheDefaultPair() + { + // /rooms/unity_assets/{assetId} is the short form; it must not silently be a 400 either. + Write(LegacyBundle); + + var json = Body(await Metadata().GetUnityAsset(LegacyBundle)); + + Assert.Equal(LegacyBundle, json.GetProperty("Filename").GetString()); + Assert.Equal(1, json.GetProperty("Version").GetInt32()); + } + + // ---------------------------------------------------------------- Studio manifests + + [Fact] + public async Task AStudioManifestStillAnswersTheBundleItsMetadataNames() + { + Write(StudioManifestId, "{\"Version\":2,\"MainSceneProjectRelativePath\":\"Assets/Room.unity\",\"Files\":[]}"); + Write(BakedBundle); + Write(RejectedBundle); + await StudioUnityAssetRegistry.AddBakedAssetAsync(StudioManifestId, + new StudioBakedUnityAsset(StudioManifestId, 0, 1, BakedBundle, RejectedBundle)); + + var json = Body(await Metadata().GetUnityAsset(StudioManifestId, 0, 1)); + + Assert.Equal(BakedBundle, json.GetProperty("Filename").GetString()); + Assert.Equal(RejectedBundle, json.GetProperty("RejectedDependenciesFilename").GetString()); + } + + [Fact] + public async Task AStudioManifestIsNeverHandedOverAsItsOwnBundle() + { + // The manifest is the file stored under the id, so a target/version it does not name + // must be a 404: answering with the manifest would have the client parse JSON metadata + // as an asset bundle. + Write(StudioManifestId, "{\"Version\":2,\"MainSceneProjectRelativePath\":\"Assets/Room.unity\",\"Files\":[]}"); + Write(LegacyBundle); + + Assert.IsType(await Metadata().GetUnityAsset(StudioManifestId, 0, 1)); + Assert.IsType(await Metadata().GetUnityAsset(StudioManifestId, 5, 9)); + } + + [Fact] + public async Task ALegacyBundleWhoseIdLooksLikeAManifestStillResolves() + { + // 40 alphanumerics with no manifest file behind it is a bundle name like any other, and + // a 40-character id is exactly what the importer names a Studio asset, so this is the + // shape a bare-id bundle arrives in. + Write(BakedBundle + ".assetbundle"); + + var json = Body(await Metadata().GetUnityAsset(BakedBundle, 0, 1)); + + Assert.Equal(BakedBundle + ".assetbundle", json.GetProperty("Filename").GetString()); + } + + // ---------------------------------------------------------------- names never served + + [Theory] + [InlineData("sub/" + LegacyBundle)] + [InlineData("..\\" + LegacyBundle)] + [InlineData(LegacyBundle + ".uploading")] + [InlineData("Showdown_0_1.uploading")] + public async Task ANameThatMustNotBeServedIsRefused(string requested) + { + // Every one of these is on disk as literally as the platform allows, so the refusal has + // to be the rule rather than the file being absent. + Write(LegacyBundle); + Write(LegacyBundle + ".uploading"); + Write("Showdown_0_1.uploading"); + Write("sub/" + LegacyBundle); + + Assert.IsType(await Metadata().GetUnityAsset(requested, 0, 1)); + } + + [Fact] + public async Task ANameThatLeavesTheStorageDirectoryIsRefused() + { + // This one escapes the storage root and re-enters it, so the file is genuinely reachable + // by path and only the rule stands between the request and the bytes. + Write(LegacyBundle); + var escaping = "../" + Path.GetFileName(root) + "/" + LegacyBundle; + + Assert.IsType(await Metadata().GetUnityAsset(escaping, 0, 1)); + } + + [Theory] + [InlineData(256, 1)] + [InlineData(-1, 1)] + [InlineData(0, -1)] + public async Task ATargetOrVersionOutsideTheContractIsRefused(int target, int version) + { + Write(LegacyBundle); + + Assert.IsType(await Metadata().GetUnityAsset(LegacyBundle, target, version)); + } + + // ---------------------------------------------------------------- the bytes routes + + [Fact] + public void TheBytesRouteAnswersBothSpellingsOfABundle() + { + Write(LegacyBundle); + + var exact = Assert.IsType( + WithHttpContext(new RoomsCDN()).GetUnityAssetRaw(LegacyBundle, 0, 1)); + Assert.Equal(Path.Join(root, LegacyBundle), exact.FileName); + + // /unity/{id} is the id with no extension, while an import stores .assetbundle. + var bare = Assert.IsType( + WithHttpContext(new RoomsCDN()).GetUnityAssetRaw("Showdown_0_1", 0, 1)); + Assert.Equal(Path.Join(root, LegacyBundle), bare.FileName); + } + + [Fact] + public void NoBlobRouteServesAHalfImportedPart() + { + // Admin part uploads stage .uploading beside the final file and promote it only + // when the last part lands, so a truncated bundle must be unreachable by name even + // though it is sitting in the storage directory. + Write(LegacyBundle); + Write(LegacyBundle + ".uploading"); + + Assert.IsType(new DataCDN().GetDataBlob(LegacyBundle + ".uploading")); + Assert.IsType(new RoomsCDN().GetDataBlob(LegacyBundle + ".uploading")); + Assert.IsType(WithHttpContext(new RoomsCDN()).GetUnityAssetRaw(LegacyBundle + ".uploading", 0, 1)); + Assert.IsType(new InventionCDN().GetDataBlob(LegacyBundle + ".uploading")); + + // ... while the promoted file beside it is served as usual. + Assert.IsType(new DataCDN().GetDataBlob(LegacyBundle)); + } + + [Fact] + public void NoBlobRouteServesANameThatLeavesTheStorageDirectory() + { + Write(LegacyBundle); + + Assert.IsType(new DataCDN().GetDataBlob("../" + LegacyBundle)); + Assert.IsType(new DataCDN().GetDataBlob("sub/" + LegacyBundle)); + Assert.IsType(new RoomsCDN().GetDataBlob("../" + LegacyBundle)); + Assert.IsType(WithHttpContext(new RoomsCDN()).GetUnityAssetRaw("../" + LegacyBundle, 0, 1)); + } +} diff --git a/ServerTests/VoteKickDeveloperTests.cs b/ServerTests/VoteKickDeveloperTests.cs new file mode 100644 index 0000000..9e05bca --- /dev/null +++ b/ServerTests/VoteKickDeveloperTests.cs @@ -0,0 +1,182 @@ +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using RadicadeServer.API.Moderation; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Auth; +using RadicadeServer.RecNet.Enums.Moderation; +using Xunit; + +namespace RadicadeServer.Tests; + +/// +/// Vote-to-kick, specifically the developer case (POST api/PlayerReporting/v2/voteToKick). +/// +/// Three votes used to kick anyone, developers included, with no way back: the moderation row that +/// blocks matchmaking was written the moment the threshold was reached, so a developer who never +/// agreed to the kick was banned anyway and the kick could not be undone by declining. +/// +/// A developer's kick now STANDS DOWN until they accept it - nothing is written at the threshold, +/// the kick is parked, their client is handed the prompt, and their answer on the same route +/// (PlayerId = themselves) decides it: accept applies the kick, decline drops it and leaves the +/// database exactly as it was. +/// +/// These pin the decision, which is the part that was wrong. The prompt itself is delivered by the +/// same KickOrPromptAsync/Notify path the ban flow already uses. +/// +[Collection(DbFactoryCollection.Name)] +public class VoteKickDeveloperTests +{ + private static RecDBContext NewDb() => new( + TestDbOptions.Create() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + private static PlayerReportingAPI NewApi(RecDBContext db, long callerId) + { + var identity = new ClaimsIdentity("TestAuth"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, callerId.ToString())); + + return new PlayerReportingAPI(db) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } + } + }; + } + + private static void SetJson(Controller controller, string json) + { + var request = controller.ControllerContext.HttpContext.Request; + request.ContentType = "application/json"; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json)); + } + + private static string VoteBody(long targetId, bool response, long sessionId, string reason = "Cheating") => + $$"""{"PlayerId":{{targetId}},"Response":{{(response ? "true" : "false")}},"GameSessionId":{{sessionId}},"Reason":"{{reason}}"}"""; + + /// One vote, from a distinct voter (the vote count is per voter, not per call). + private static async Task VoteAsync(RecDBContext db, long voterId, string body) + { + var api = NewApi(db, voterId); + SetJson(api, body); + + // The action returns ActionResult, so Ok(...) arrives as its Result; a vote is + // always a 200, including the one that decides a developer's parked kick. + var result = await api.VoteToKick(); + Assert.IsType(result.Result); + } + + private static async Task WithTargetAsync(long targetId, string? roles) + { + var db = NewDb(); + db.Profiles.Add(new Profile + { + accountId = targetId, + role = roles, + username = $"player{targetId}", + displayName = $"Player {targetId}", + profileImage = DBConfig.DefaultProfileImage + }); + await db.SaveChangesAsync(); + return db; + } + + // ----------------------------------------------------------------------------------- + // The behaviour being kept: an ordinary player is kicked by the vote + // ----------------------------------------------------------------------------------- + + [Fact] + public async Task ANonDeveloperIsKickedAsSoonAsTheVotePasses() + { + using var db = await WithTargetAsync(205, roles: null); + const long session = 910001; + + await VoteAsync(db, 1, VoteBody(205, true, session)); + await VoteAsync(db, 2, VoteBody(205, true, session)); + Assert.Empty(db.Moderation); + + await VoteAsync(db, 3, VoteBody(205, true, session)); + + var moderation = await db.Moderation.SingleAsync(); + Assert.True(moderation.IsBan); + Assert.Equal("Cheating", moderation.Message); + } + + // ----------------------------------------------------------------------------------- + // The behaviour that changed: a developer's kick waits for them + // ----------------------------------------------------------------------------------- + + [Fact] + public async Task ADevelopersVoteKickIsParkedInsteadOfApplied() + { + using var db = await WithTargetAsync(206, "developer"); + const long session = 910002; + + await VoteAsync(db, 1, VoteBody(206, true, session)); + await VoteAsync(db, 2, VoteBody(206, true, session)); + await VoteAsync(db, 3, VoteBody(206, true, session)); + + // Nothing written: the moderation row is what blocks matchmaking, so writing it here is + // exactly the "kicked a developer who never agreed to it" bug. + Assert.Empty(db.Moderation); + } + + [Fact] + public async Task ADeveloperAcceptingTheParkedKickIsKickedWheneverTheyAnswer() + { + using var db = await WithTargetAsync(207, "developer"); + const long session = 910003; + + for (long voter = 1; voter <= 3; voter++) + await VoteAsync(db, voter, VoteBody(207, true, session, "Being AFK")); + Assert.Empty(db.Moderation); + + // Their own answer on the same route: PlayerId is themselves. + await VoteAsync(db, 207, VoteBody(207, true, session, "Being AFK")); + + var moderation = await db.Moderation.SingleAsync(); + Assert.True(moderation.IsBan); + Assert.Equal("Being AFK", moderation.Message); + } + + [Fact] + public async Task ADeveloperDecliningTheParkedKickLeavesNothingBehind() + { + using var db = await WithTargetAsync(208, "developer"); + const long session = 910004; + + for (long voter = 1; voter <= 3; voter++) + await VoteAsync(db, voter, VoteBody(208, true, session)); + Assert.Empty(db.Moderation); + + await VoteAsync(db, 208, VoteBody(208, false, session)); + + // "Stands down" has to mean the database is untouched, not that a row was written and then + // apologised for - the row is what stops them joining a room later. + Assert.Empty(db.Moderation); + + // And the parked kick is gone, so a later answer cannot kick them out of nowhere. + await VoteAsync(db, 208, VoteBody(208, true, session)); + Assert.Empty(db.Moderation); + } + + [Fact] + public async Task ADevelopersOwnAnswerIsNotCountedAsAVote() + { + using var db = await WithTargetAsync(209, "developer"); + const long session = 910005; + + // Answering a prompt they were never sent must not add a vote towards their own kick. + await VoteAsync(db, 209, VoteBody(209, true, session)); + await VoteAsync(db, 209, VoteBody(209, true, session)); + await VoteAsync(db, 209, VoteBody(209, true, session)); + + Assert.Empty(db.Moderation); + } +} diff --git a/ServerTests/WatchWireCompatibilityTests.cs b/ServerTests/WatchWireCompatibilityTests.cs new file mode 100644 index 0000000..9740b63 --- /dev/null +++ b/ServerTests/WatchWireCompatibilityTests.cs @@ -0,0 +1,160 @@ +using System.Globalization; +using System.Text.Json; +using RadicadeServer.API.Notifications; +using RadicadeServer.Clubs; +using RadicadeServer.Extensions; +using RadicadeServer.RecNet.Enums.Messages; +using RadicadeServer.RecNet.Notifications; +using Xunit; + +namespace RadicadeServer.Tests; + +public class WatchWireCompatibilityTests +{ + // Notifs and NotificationType carry the same values as the client's own enum + // (RelationshipChanged = 1, MessageReceived = 2), so both must reach the wire the same way. + [Theory] + [InlineData(Notifs.RelationshipChanged, "1")] + [InlineData(Notifs.MessageReceived, "2")] + public void FriendNotificationUsesNumericStringIdWithNumericPayload(Notifs eventType, string expectedId) + { + var wire = new Notification + { + Id = eventType, + Msg = new { Type = MessageType.FriendInvite, AccountId = 2 } + }.ToJson(); + using var json = JsonDocument.Parse(wire); + Assert.Equal(expectedId, json.RootElement.GetProperty("Id").GetString()); + Assert.Equal(4, json.RootElement.GetProperty("Msg").GetProperty("Type").GetInt32()); + Assert.Equal(2, json.RootElement.GetProperty("Msg").GetProperty("AccountId").GetInt32()); + } + + // The 2023 client requires a STRING at the notification Id field AND dispatches on the + // number inside it. A quoted number is the only form that satisfies both: a bare number + // throws in the client's deserializer ("expected:'String Begin Token', actual:'1'"), and + // the enum's name parses but matches no registered handler, so the frame is ignored. + [Theory] + [InlineData(NotificationType.GiftPackageReceivedImmediate, 31)] + [InlineData(NotificationType.SubscriptionUpdatePresence, 12)] + [InlineData(NotificationType.ModerationKick, 22)] + [InlineData(NotificationType.MessageReceived, 2)] + [InlineData(NotificationType.RefreshLogin, 5)] + public void NotificationIdIsTheClientEnumNumberAsAString(NotificationType eventType, int expectedId) + { + using var json = JsonDocument.Parse(new Notification { Id = eventType, Msg = new { AccountId = 2 } }.ToJson()); + var id = json.RootElement.GetProperty("Id"); + Assert.Equal(JsonValueKind.String, id.ValueKind); + Assert.Equal(expectedId.ToString(CultureInfo.InvariantCulture), id.GetString()); + Assert.Equal((int)eventType, expectedId); + Assert.Equal(2, json.RootElement.GetProperty("Msg").GetProperty("AccountId").GetInt32()); + } + + // Both halves of the contract, stated as the client would experience them: the name is + // never what goes out, and the number is never sent bare. + [Theory] + [InlineData(NotificationType.MessageReceived, "2", "MessageReceived")] + [InlineData(NotificationType.ModerationKick, "22", "ModerationKick")] + public void NotificationIdIsNeitherTheNameNorABareNumber(NotificationType eventType, string expectedNumber, string name) + { + var notification = new Notification { Id = eventType, Msg = new { } }; + var wire = notification.ToJson(); + using var json = JsonDocument.Parse(wire); + Assert.NotEqual(JsonValueKind.Number, json.RootElement.GetProperty("Id").ValueKind); + // Through the property, exactly as the push log does it: the log has to show the token + // the client will parse, not what a boxed enum's ToString() happens to be. + Assert.Equal($"\"{expectedNumber}\"", Notify.WireId(notification.Id)); + Assert.DoesNotContain($"\"{name}\"", wire); + } + + // An explicit string id set by a caller is passed through untouched. + [Fact] + public void ExplicitStringNotificationIdIsUnchanged() + { + using var json = JsonDocument.Parse(new Notification { Id = "ReputationUpdate", Msg = new { } }.ToJson()); + Assert.Equal("ReputationUpdate", json.RootElement.GetProperty("Id").GetString()); + } + + // These ids are NOT members of the client's enum, so they are named events and must keep + // travelling as strings (the reference server sends exactly these spellings). + [Theory] + [InlineData("PresenceUpdate")] + [InlineData("AccountUpdate")] + [InlineData("SelfAccountUpdate")] + [InlineData("ReputationUpdate")] + public void NonEnumNotificationIdStaysAString(string eventName) + { + using var json = JsonDocument.Parse(new Notification { Id = eventName, Msg = new { AccountId = 2 } }.ToJson()); + Assert.Equal(JsonValueKind.String, json.RootElement.GetProperty("Id").ValueKind); + Assert.Equal(eventName, json.RootElement.GetProperty("Id").GetString()); + } + + // The push log prints the id with the same token the client will parse, so the server log + // shows the frame's real shape. A boxed enum prints its NAME through ToString() no matter + // how the frame was serialized, which is exactly why a log line reading "ModerationKick" + // looked healthy while every frame went out unparseable. + [Theory] + [InlineData(NotificationType.ModerationKick, "22")] + [InlineData(NotificationType.MessageReceived, "2")] + [InlineData(NotificationType.RefreshLogin, "5")] + public void PushLogShowsTheWireId(NotificationType eventType, string expected) + => Assert.Equal(expected, Notify.WireId(eventType)); + + [Theory] + [InlineData("PresenceUpdate")] + [InlineData("AccountUpdate")] + [InlineData("SelfAccountUpdate")] + public void PushLogQuotesANamedId(string eventName) + => Assert.Equal($"\"{eventName}\"", Notify.WireId(eventName)); + + // The diagnostic A/B (/diag/notify) has to be able to produce all three forms, because the + // point of it is to show the live client accepting one and rejecting the other two. + [Fact] + public void IdFormJsonProducesTheCanonicalQuotedNumberByDefault() + { + Assert.Equal("\"22\"", Notify.IdFormJson(null, NotificationType.ModerationKick)); + Assert.Equal("\"22\"", Notify.IdFormJson("string", NotificationType.ModerationKick)); + } + + [Theory] + [InlineData("number", "22")] + [InlineData("name", "\"ModerationKick\"")] + public void IdFormJsonCanProduceTheFormsTheClientRejects(string form, string expected) + => Assert.Equal(expected, Notify.IdFormJson(form, NotificationType.ModerationKick)); + + // The A/B envelope keeps the real Msg bytes and only swaps the Id token, so what the client + // is reacting to is genuinely just the id's form. + [Fact] + public void DiagnosticEnvelopeSwapsOnlyTheIdToken() + { + var message = new Notification + { + Id = NotificationType.MessageReceived, + Msg = new { Type = MessageType.CoachMessage, Data = "hello" } + }; + + using var json = JsonDocument.Parse(Notify.EnvelopeWithIdForm(message, "\"2\"")); + Assert.Equal("2", json.RootElement.GetProperty("Id").GetString()); + Assert.Equal(100, json.RootElement.GetProperty("Msg").GetProperty("Type").GetInt32()); + Assert.Equal("hello", json.RootElement.GetProperty("Msg").GetProperty("Data").GetString()); + } + + [Theory] + [InlineData(2, 100)] + [InlineData(3, 10)] + [InlineData(0, 0)] + public void ClubMembershipUsesClientEnumValues(long viewerId, int expected) + { + var club = new ClubRecord + { + clubId = 1, + ownerPlayerId = 2, + members = viewerId == 3 + ? [new ClubMemberRecord { accountId = viewerId, membershipType = ClubMembershipTypes.Member }] + : [] + }; + using var details = JsonDocument.Parse(JsonSerializer.Serialize(ClubStore.BuildClubDetails(club, viewerId))); + using var membership = JsonDocument.Parse(JsonSerializer.Serialize(ClubStore.BuildMembership(club, viewerId))); + Assert.Equal(expected, details.RootElement.GetProperty("MyMembershipType").GetInt32()); + Assert.Equal(expected, membership.RootElement.GetProperty("membershipType").GetInt32()); + } +} diff --git a/ServerTests/WeeklyChallengeTests.cs b/ServerTests/WeeklyChallengeTests.cs new file mode 100644 index 0000000..c3c1439 --- /dev/null +++ b/ServerTests/WeeklyChallengeTests.cs @@ -0,0 +1,305 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RadicadeServer.API.Progression.Challenges.V2; +using RadicadeServer.API.Storefront; +using RadicadeServer.DB; +using RadicadeServer.RecNet.Accounts; +using RadicadeServer.RecNet.Avatar; +using RadicadeServer.Utils; +using Xunit; + +namespace RadicadeServer.Tests; + +public class WeeklyChallengeTests +{ + [Fact] + public async Task AdminLibrarySchedulesNextWeekAndPlayerEndpointUsesSavedLineup() + { + await using var db = Db(); + var oldKey = DBConfig.AdminApiKey; + var oldLogs = DBConfig.PersistentLogs; + var oldEquipment = DBConfig.DefaultEquipmentItems; + var oldAvatars = DBConfig.DefaultAvatarCatalog; + DBConfig.DefaultEquipmentItems = JsonSerializer.Serialize(new[] { new Equipment { PrefabName = "Disc", ModificationGuid = "test-skin", FriendlyName = "Test skin" } }); + DBConfig.DefaultAvatarCatalog = "[]"; + var folder = Path.Combine(Path.GetTempPath(), "radicade-weekly-tests-" + Guid.NewGuid().ToString("N")); + DBConfig.AdminApiKey = "weekly-test-key"; + DBConfig.PersistentLogs = folder; + try + { + var api = new RadicadeServer.Admin.AdminPanelAPI(db); + var now = DateTime.UtcNow; + var current = FixtureMap(now); + var next = FixtureMap(current.EndAt); + var ids = new List(); + foreach (var definition in current.Challenges.Take(3)) + { + var payload = JsonSerializer.SerializeToElement(new { name = "Admin " + definition.Name, + description = definition.Description, tooltip = definition.Tooltip, config = definition.Config }); + var result = Assert.IsType(await api.Insert("challenges", "weekly-test-key", payload)); + ids.Add(Assert.IsType(result.Value)["id"]!.GetValue()); + } + var request = new RadicadeServer.Admin.WeeklyScheduleRequest { ChallengeMapId = next.ChallengeMapId, ChallengeIds = ids, RewardKey = "equipment:Disc:test-skin" }; + Assert.IsType(await api.SaveWeeklySchedule("wrong", request)); + Assert.IsType(await api.SaveWeeklySchedule("weekly-test-key", request)); + Assert.Equal(5, (await WeeklyChallenges.GetAsync(db, 42, now)).Challenges.Count); + var scheduled = await WeeklyChallenges.GetAsync(db, 42, current.EndAt); + Assert.Equal(3, scheduled.Challenges.Count); + Assert.All(scheduled.Challenges, c => Assert.StartsWith("Admin ", c.Name)); + // A library edit doesn't change the already-published snapshot. + await api.Update("challenges", ids[0], "weekly-test-key", JsonSerializer.SerializeToElement(new { name = "Draft change" })); + Assert.Equal(scheduled.Challenges[0].Name, (await WeeklyChallenges.GetAsync(db, 42, current.EndAt)).Challenges[0].Name); + foreach (var id in ids) await Complete(db, (int)id, now: current.EndAt); + Assert.Single(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data)); + var published = await WeeklyChallenges.GetDefinitionAsync(db, current.EndAt); + Assert.IsType(await api.SaveWeeklySchedule("weekly-test-key", request)); + request.ChallengeMapId = current.ChallengeMapId; + DBConfig.DefaultAvatarCatalog = JsonSerializer.Serialize(new[] { new AvatarItem { AvatarItemDesc = "admin-avatar", FriendlyName = "Admin avatar", AvatarItemType = 0 } }); + request.RewardKey = "avatar:admin-avatar"; + Assert.IsType(await api.SaveWeeklySchedule("weekly-test-key", request)); + Assert.Equal("admin-avatar", (await WeeklyChallenges.GetAsync(db, 42, now)).Gift.AvatarItemDesc); + foreach (var id in ids) await Complete(db, (int)id, now: now); + Assert.Contains(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data), g => g.AvatarItemDesc == "admin-avatar"); + request.Revision = (await WeeklyChallenges.GetDefinitionAsync(db, now)).Revision; + request.RewardKey = "avatar:nonexistent"; + Assert.IsType(await api.SaveWeeklySchedule("weekly-test-key", request)); + request.RewardKey = "equipment:Disc:test-skin"; + request.ChallengeMapId = current.ChallengeMapId - 1; + Assert.IsType(await api.SaveWeeklySchedule("weekly-test-key", request)); + request.ChallengeMapId = next.ChallengeMapId; + request.ChallengeIds = [ids[0], ids[0], ids[1]]; + Assert.IsType(await api.SaveWeeklySchedule("weekly-test-key", request)); + request.ChallengeIds = [ids[0], ids[1], 99999]; + Assert.IsType(await api.SaveWeeklySchedule("weekly-test-key", request)); + } + finally + { + DBConfig.DefaultEquipmentItems = oldEquipment; + DBConfig.DefaultAvatarCatalog = oldAvatars; + DBConfig.AdminApiKey = oldKey; + DBConfig.PersistentLogs = oldLogs; + if (Directory.Exists(folder)) Directory.Delete(folder, true); + } + } + + [Fact] + public async Task UnpublishedWeekHasNoContentAndRejectsProgress() + { + await using var db = Db(); + db.ServerConfig.RemoveRange(db.ServerConfig); await db.SaveChangesAsync(); + var map = await WeeklyChallenges.GetAsync(db, 42, Now); + Assert.False(map.Enabled); Assert.Empty(map.Challenges); + Assert.Empty(map.Gift.EquipmentModificationGuid); + Assert.Equal("weekly_not_published", await WeeklyChallenges.UpdateAsync(db, 42, map.ChallengeMapId, 1, "{}", true, Now)); + } + + [Fact] + public async Task CurrentEditsPreserveMatchingProgressAndNeverReaward() + { + await using var db = Db(); + await Complete(db, 1); + var definition = FixtureMap(Now); + definition.Gift = new Gift { AvatarItemDesc = "test-avatar", FriendlyName = "Chosen avatar", Context = RadicadeServer.RecNet.Enums.Store.GiftContext.All_Weekly_Challenge_Complete }; + var record = await db.ServerConfig.SingleAsync(x => x.Key == WeeklyChallenges.ScheduleKey(definition.ChallengeMapId)); + record.Value = JsonSerializer.Serialize(definition); await db.SaveChangesAsync(); + Assert.True((await WeeklyChallenges.GetAsync(db, 42, Now)).Challenges[0].Complete); + await Complete(db, 2); await Complete(db, 3); + Assert.Equal("test-avatar", Assert.Single(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data)).AvatarItemDesc); + definition.Challenges[0].Config = definition.Challenges[3].Config; + record = await db.ServerConfig.SingleAsync(x => x.Key == WeeklyChallenges.ScheduleKey(definition.ChallengeMapId)); + record.Value = JsonSerializer.Serialize(definition); await db.SaveChangesAsync(); + var updated = await WeeklyChallenges.GetAsync(db, 42, Now); + Assert.False(updated.Challenges[0].Complete); Assert.True(updated.CompletedRequired); + await Complete(db, 1); + Assert.Single(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data)); + definition.Enabled = false; + record = await db.ServerConfig.SingleAsync(x => x.Key == WeeklyChallenges.ScheduleKey(definition.ChallengeMapId)); + record.Value = JsonSerializer.Serialize(definition); await db.SaveChangesAsync(); + Assert.Empty((await WeeklyChallenges.GetAsync(db, 42, Now)).Challenges); + Assert.Equal("weekly_not_published", await WeeklyChallenges.UpdateAsync(db, 42, definition.ChallengeMapId, 1, definition.Challenges[0].Config, false, Now)); + } + + [Fact] + public async Task PendingAvatarGetsConfiguredFallback() + { + await using var db = Db(); + var map = FixtureMap(Now); + map.Gift = new Gift { AvatarItemDesc = "test-avatar", Context = RadicadeServer.RecNet.Enums.Store.GiftContext.All_Weekly_Challenge_Complete }; + map.FallbackTokens = 725; + (await db.ServerConfig.SingleAsync(x => x.Key == WeeklyChallenges.ScheduleKey(map.ChallengeMapId))).Value = JsonSerializer.Serialize(map); + await db.SaveChangesAsync(); + await StorefrontShared.PersistGiftBoxAsync(db, 42, new Gift { GiftDropId = 99, AvatarItemDesc = "test-avatar" }, notify: false); + await Complete(db, 1); await Complete(db, 2); await Complete(db, 3); + var gifts = StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data); + Assert.Equal(2, gifts.Count); Assert.Equal(725, gifts.Single(x => x.GiftDropId != 99).Currency); + } + + [Fact] + public void WeeklyDefinitionValidationRejectsCompletedAndUnrecognizedPredicates() + { + foreach (var goal in FixtureMap(Now).Challenges) + Assert.True(WeeklyChallenges.IsSupportedDefinition(goal.Config)); + Assert.False(WeeklyChallenges.IsSupportedDefinition("{}")); + Assert.False(WeeklyChallenges.IsSupportedDefinition("{\"ct\":0,\"rc\":[{\"ct\":6,\"vs\":[999]}]}")); + Assert.False(WeeklyChallenges.IsSupportedDefinition(Completed(FixtureMap(Now).Challenges[0].Config))); + } + private static readonly DateTime Now = new(2026, 10, 8, 12, 0, 0, DateTimeKind.Utc); + private static RecDBContext Db(string? name = null) + { + var db = new RecDBContext(TestDbOptions.Create().UseInMemoryDatabase(name ?? Guid.NewGuid().ToString()).Options); + foreach (var time in new[] { Now, DateTime.UtcNow }) + { + var map = FixtureMap(time); + var key = WeeklyChallenges.ScheduleKey(map.ChallengeMapId); + if (!db.ServerConfig.Any(x => x.Key == key)) db.ServerConfig.Add(new RadicadeServer.RecNet.Config.ServerConfigVal(key, JsonSerializer.Serialize(map))); + db.SaveChanges(); + } + return db; + } + private static RadicadeServer.RecNet.Progression.Challenges.WeeklyChallengeMap FixtureMap(DateTime now) + { + var map = WeeklyChallenges.CreateMap(now); + map.Enabled = true; + map.Challenges = new[] { 2, 4, 5, 10, 8 }.Select((e, i) => new RadicadeServer.RecNet.Progression.Challenges.WeeklyChallenge { + ChallengeId = i + 1, Name = "Goal " + (i + 1), Description = "Test goal", Config = JsonSerializer.Serialize(new { ct = 0, rc = new[] { new { ct = 6, vs = new[] { e } } } }) }).ToList(); + map.Gift = new Gift { EquipmentPrefabName = "Disc", EquipmentModificationGuid = "9d2a3618-12b3-4ca0-be59-ed15d7926d77", Context = RadicadeServer.RecNet.Enums.Store.GiftContext.All_Weekly_Challenge_Complete }; + return map; + } + private static string Completed(string config) + { + var node = JsonNode.Parse(config)!; + node["c"] = true; + node["ipc"] = false; + node["wc"] = new JsonArray(); + return node.ToJsonString(); + } + private static async Task Complete(RecDBContext db, int id, long player = 42, DateTime? now = null) + { + var time = now ?? Now; + var map = await WeeklyChallenges.GetAsync(db, player, time); + Assert.Null(await WeeklyChallenges.UpdateAsync(db, player, map.ChallengeMapId, id, + Completed(map.Challenges.Single(x => x.ChallengeId == id).Config), true, time)); + } + + [Fact] + public async Task ThreeOfFivePaysOnceIncludingRetriesAndAllFive() + { + await using var db = Db(); + await Complete(db, 1); await Complete(db, 2); + Assert.Empty(await db.Gifts.ToListAsync()); + await Complete(db, 3); await Complete(db, 3); await Complete(db, 4); await Complete(db, 5); + var gifts = StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data); + Assert.Single(gifts); + Assert.Equal("9d2a3618-12b3-4ca0-be59-ed15d7926d77", gifts[0].EquipmentModificationGuid); + Assert.True((await WeeklyChallenges.GetAsync(db, 42, Now)).CompletedRequired); + Assert.Equal(gifts[0].GiftDropId, (await db.WeeklyChallenges.SingleAsync()).RewardGiftDropId); + } + + [Fact] + public async Task ReconnectPersistsAndPlayersAreIsolated() + { + var name = Guid.NewGuid().ToString(); + await using (var db = Db(name)) await Complete(db, 1); + await using var reopened = Db(name); + Assert.True((await WeeklyChallenges.GetAsync(reopened, 42, Now)).Challenges[0].Complete); + Assert.False((await WeeklyChallenges.GetAsync(reopened, 43, Now)).Challenges[0].Complete); + } + + [Fact] + public async Task RolloverResetsAndRejectsOldWeek() + { + await using var db = Db(); + var before = FixtureMap(Now); + await Complete(db, 1); + var next = await WeeklyChallenges.GetAsync(db, 42, before.EndAt); + Assert.Equal(before.ChallengeMapId + 1, next.ChallengeMapId); + Assert.All(next.Challenges, x => Assert.False(x.Complete)); + Assert.False(next.Enabled); + Assert.Empty(next.Challenges); + Assert.Equal("stale_challenge_map", await WeeklyChallenges.UpdateAsync(db, 42, before.ChallengeMapId, 1, + Completed(before.Challenges[0].Config), true, before.EndAt)); + } + + [Fact] + public async Task RejectsAlteredDefinitionsAndInconsistentCompletion() + { + await using var db = Db(); + var map = FixtureMap(Now); + var changed = JsonNode.Parse(Completed(map.Challenges[0].Config))!; + changed["rc"]![0]!["vs"]![0] = 0; + Assert.Equal("invalid_challenge_config", await WeeklyChallenges.UpdateAsync(db, 42, map.ChallengeMapId, 1, changed.ToJsonString(), true, Now)); + Assert.Equal("invalid_challenge_config", await WeeklyChallenges.UpdateAsync(db, 42, map.ChallengeMapId, 1, map.Challenges[0].Config, true, Now)); + Assert.Equal("unknown_challenge", await WeeklyChallenges.UpdateAsync(db, 42, map.ChallengeMapId, 100, "{}", true, Now)); + Assert.Empty(await db.WeeklyChallenges.ToListAsync()); + } + + [Fact] + public async Task OwnedSkinGetsOneTokenBox() + { + await using var db = Db(); + var gift = FixtureMap(Now).Gift; + db.Unlocks.Add(new PlayerUnlocks { AccountId = 42, Equipment = JsonSerializer.Serialize(new[] + { new Equipment { PrefabName = gift.EquipmentPrefabName, ModificationGuid = gift.EquipmentModificationGuid } }) }); + await db.SaveChangesAsync(); + await Complete(db, 1); await Complete(db, 2); await Complete(db, 3); await Complete(db, 3); + var box = Assert.Single(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data)); + Assert.Equal(300, box.Currency); + Assert.True(string.IsNullOrEmpty(box.EquipmentModificationGuid)); + } + + [Fact] + public async Task ConsumedBoxIsNeverRecreatedAndCompletionCannotRegress() + { + await using var db = Db(); + await Complete(db, 1); await Complete(db, 2); await Complete(db, 3); + var row = await db.Gifts.SingleAsync(); row.Data = "[]"; await db.SaveChangesAsync(); + var fresh = FixtureMap(Now); + Assert.Null(await WeeklyChallenges.UpdateAsync(db, 42, fresh.ChallengeMapId, 1, fresh.Challenges[0].Config, false, Now)); + await Complete(db, 3); + Assert.Empty(StorefrontShared.ParseGiftList((await db.Gifts.SingleAsync()).Data)); + Assert.True((await WeeklyChallenges.GetAsync(db, 42, Now)).Challenges[0].Complete); + } + + [Fact] + public async Task ConcurrentCompletionsDoNotLoseProgressOrDuplicateReward() + { + var name = Guid.NewGuid().ToString(); + await using (var seed = Db(name)) { } + await Task.WhenAll(Enumerable.Range(1, 5).Select(async id => + { + await using var db = Db(name); await Complete(db, id); + })); + await using var check = Db(name); + Assert.All((await WeeklyChallenges.GetAsync(check, 42, Now)).Challenges, x => Assert.True(x.Complete)); + Assert.Single(StorefrontShared.ParseGiftList((await check.Gifts.SingleAsync()).Data)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task EndpointAcceptsClientFormAndJson(bool form) + { + await using var db = Db(); + var now = DateTime.UtcNow; + var map = FixtureMap(now); + var fields = new Dictionary { ["ChallengeMapId"] = map.ChallengeMapId.ToString(), + ["ChallengeId"] = "1", ["Complete"] = "True", ["Config"] = Completed(map.Challenges[0].Config) }; + var body = form ? await new FormUrlEncodedContent(fields).ReadAsStringAsync() : JsonSerializer.Serialize(fields); + var context = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] + { new Claim(ClaimTypes.NameIdentifier, "42") }, "test")) }; + context.Request.ContentType = form ? "application/x-www-form-urlencoded" : "application/json"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + var api = new ChallengeV2(db) { ControllerContext = new ControllerContext { HttpContext = context } }; + Assert.IsType(await api.UpdateProgress()); + var response = Assert.IsType(await api.GetCurrent()); + var wire = JsonSerializer.SerializeToElement(response.Value); + Assert.Equal(JsonValueKind.String, wire.GetProperty("Challenges")[0].GetProperty("Config").ValueKind); + Assert.True(wire.GetProperty("Challenges")[0].GetProperty("Complete").GetBoolean()); + Assert.Equal(5, wire.GetProperty("Challenges").GetArrayLength()); + } +} diff --git a/Tools/AugustInterop/.gitignore b/Tools/AugustInterop/.gitignore new file mode 100644 index 0000000..4ded7c4 --- /dev/null +++ b/Tools/AugustInterop/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/obj/ diff --git a/Tools/AugustInterop/AugustInterop.csproj b/Tools/AugustInterop/AugustInterop.csproj new file mode 100644 index 0000000..f759a82 --- /dev/null +++ b/Tools/AugustInterop/AugustInterop.csproj @@ -0,0 +1,14 @@ + + + Exe + net9.0 + enable + C:\RecRoom\2023-08-29T06-53-33Z\BepInEx\core + + + $(BepInExCore)\Iced.dll + + $(BepInExCore)\Mono.Cecil.dll + + + diff --git a/Tools/AugustInterop/LiteralFinder.cs b/Tools/AugustInterop/LiteralFinder.cs new file mode 100644 index 0000000..0cc8290 --- /dev/null +++ b/Tools/AugustInterop/LiteralFinder.cs @@ -0,0 +1,134 @@ +using System.Reflection; +using System.Reflection.PortableExecutable; +using Iced.Intel; + +// Find the il2cpp methods that reference a given string literal. +// +// Neither the bytes nor the index of a literal appear directly in the code: on +// metadata v27+ the code loads a pointer from a metadata-usage slot, and statically +// that slot holds the encoded usage token `(type << 29) | (index << 1) | 1` with +// type 5 for string literals. So the search is: +// 1. resolve the literal's index from its text, +// 2. scan the image for the 8-byte token (this finds the slots), +// 3. scan the executable sections for instructions whose rip-relative operand +// lands on one of those slots. The displacement is the tail of such an +// instruction, so a cheap arithmetic pre-check narrows candidates and Iced +// then confirms by decoding from the candidate's start. +internal static class LiteralFinder +{ + const ulong TokenBase = 5UL << 29; + + static object Prop(object o, string n) => o.GetType().GetProperty(n)!.GetValue(o)!; + + sealed class LineOutput : FormatterOutput + { + public string Value = ""; + public override void Write(string text, FormatterTextKind kind) => Value += text; + } + + public static void Run(object main, object metadata, string binaryPath, string needle) + { + var defs = (System.Collections.IEnumerable)metadata.GetType().GetField("methodDefs")!.GetValue(metadata)!; + var methods = new List<(object Method, ulong Address)>(); + foreach (var m in defs.Cast()) + { + var address = (ulong)Prop(m, "MethodPointer"); + if (address != 0) methods.Add((m, address)); + } + methods.Sort((a, b) => a.Address.CompareTo(b.Address)); + + var table = metadata.GetType().GetField("stringLiterals", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!.GetValue(metadata) as Array; + var getLiteralText = metadata.GetType().GetMethod("GetStringLiteralFromIndex")!; + + var tokens = new List(); + for (int i = 0; i < (table?.Length ?? 0); i++) + { + var text = (string)getLiteralText.Invoke(metadata, new object[] { (uint)i })!; + if (!text.Contains(needle, StringComparison.Ordinal)) continue; + // Both low-bit spellings resolve to the same index downstream. + tokens.Add(TokenBase | ((ulong)i << 1) | 1); + tokens.Add(TokenBase | ((ulong)i << 1)); + Console.WriteLine($" literal #{i} = \"{text}\" token 0x{TokenBase | ((ulong)i << 1):X}"); + } + if (tokens.Count == 0) + { + Console.WriteLine($"no literal contains \"{needle}\""); + return; + } + + var bytes = File.ReadAllBytes(binaryPath); + using var pe = new PEReader(new MemoryStream(bytes)); + var imageBase = pe.PEHeaders.PEHeader!.ImageBase; + + // 1. slots: 8-byte tokens, so only probe 8-byte aligned positions. + var slots = new List<(string Section, ulong Address, ulong Value)>(); + foreach (var section in pe.PEHeaders.SectionHeaders) + { + var size = Math.Min(section.SizeOfRawData, Math.Max(0, bytes.Length - section.PointerToRawData)); + for (int i = section.PointerToRawData; i < section.PointerToRawData + size - 8; i += 8) + { + var value = BitConverter.ToUInt64(bytes, i); + if (!tokens.Contains(value)) continue; + var slot = imageBase + (ulong)section.VirtualAddress + (ulong)(i - section.PointerToRawData); + slots.Add((section.Name, slot, value)); + Console.WriteLine($" slot 0x{slot:X} ({section.Name}, raw 0x{i:X}) = 0x{value:X}"); + } + } + if (slots.Count == 0) + { + Console.WriteLine("no metadata-usage slot found"); + return; + } + var slotSet = slots.Select(s => s.Address).ToHashSet(); + + // 2. references: pre-check the displacement, confirm by decoding from the start. + var formatter = new IntelFormatter(); + int hits = 0, candidates = 0; + foreach (var section in pe.PEHeaders.SectionHeaders.Where(s => (s.SectionCharacteristics & SectionCharacteristics.MemExecute) != 0)) + { + var rawToVa = (long)imageBase + section.VirtualAddress - section.PointerToRawData; + var size = Math.Min(section.SizeOfRawData, Math.Max(0, bytes.Length - section.PointerToRawData)); + for (int i = section.PointerToRawData; i < section.PointerToRawData + size - 4; i++) + { + var disp = BitConverter.ToInt32(bytes, i); + var target = (ulong)((long)i + 4 + rawToVa + disp); + if (!slotSet.Contains(target)) continue; + candidates++; + for (int back = 0; back <= 7; back++) + { + var candidateRaw = i - back; + if (candidateRaw < section.PointerToRawData) break; + var reader = new ByteArrayCodeReader(bytes, candidateRaw, Math.Min(16, bytes.Length - candidateRaw)); + var decoder = Decoder.Create(64, reader); + decoder.IP = (ulong)((long)candidateRaw + rawToVa); + decoder.Decode(out var instruction); + if (instruction.IsInvalid || !instruction.IsIPRelativeMemoryOperand) continue; + if (instruction.IPRelativeMemoryAddress != target) continue; + hits++; + var method = Nearest(methods, instruction.IP); + var name = method == null + ? "" + : Prop(Prop(method, "DeclaringType"), "Name") + "::" + Prop(method, "Name"); + var offset = method == null ? 0 : instruction.IP - (ulong)Prop(method, "MethodPointer"); + var output = new LineOutput(); + formatter.Format(instruction, output); + Console.WriteLine($" REF 0x{instruction.IP:X} -> slot 0x{target:X} in {name} +0x{offset:X} [{output.Value}]"); + break; + } + } + } + Console.WriteLine($"slots {slots.Count}; displacement candidates {candidates}; confirmed references {hits}"); + } + + static object? Nearest(List<(object Method, ulong Address)> methods, ulong address) + { + object? best = null; + foreach (var (method, addr) in methods) + { + if (addr > address) break; + best = method; + } + return best; + } +} diff --git a/Tools/AugustInterop/NativeInspect.cs b/Tools/AugustInterop/NativeInspect.cs new file mode 100644 index 0000000..43ef5df --- /dev/null +++ b/Tools/AugustInterop/NativeInspect.cs @@ -0,0 +1,69 @@ +using System.Reflection; +using System.Collections; +using System.Reflection.PortableExecutable; +using Iced.Intel; + +internal static class NativeInspect +{ + static object Prop(object o, string n) => o.GetType().GetProperty(n)!.GetValue(o)!; + static string Label(object m) => m == null ? "" : Prop(Prop(m,"DeclaringType"),"Name") + "::" + Prop(m,"Name"); + public static void Run(Type main, object metadata, string binary, string typeName, string methodName) + { + var methods = ((IEnumerable)metadata.GetType().GetField("methodDefs")!.GetValue(metadata)!).Cast() + .Select(m => (Method:m, Address:(ulong)Prop(m,"MethodPointer"))).Where(m=>m.Address!=0).OrderBy(m=>m.Address).ToArray(); + // An address target (decimal or 0x hex) disassembles that instruction window directly. + var raw = typeName.Replace("0x", "").Trim(); + if (raw.Length > 4 && + (ulong.TryParse(raw, out var fixedAddress) || + ulong.TryParse(raw, System.Globalization.NumberStyles.HexNumber, null, out fixedAddress))) + { + DisassembleOne(main, metadata, binary, fixedAddress, methods); + return; + } + var bytes=File.ReadAllBytes(binary); + using var pe=new PEReader(new MemoryStream(bytes)); + ulong imageBase=pe.PEHeaders.PEHeader!.ImageBase; + int Raw(ulong address) { var rva=address-imageBase; var s=pe.PEHeaders.SectionHeaders.First(s=>rva>=(ulong)s.VirtualAddress && rva<(ulong)(s.VirtualAddress+s.SizeOfRawData)); return (int)(rva-(ulong)s.VirtualAddress)+s.PointerToRawData; } + object Nearest(ulong address) => methods.LastOrDefault(m=>m.Address<=address).Method; + foreach(var item in methods.Where(m=>Prop(Prop(m.Method,"DeclaringType"),"Name").ToString()==typeName && (methodName=="" || Prop(m.Method,"Name").ToString()==methodName))) + { + Console.WriteLine($"\nMETHOD {Label(item.Method)} 0x{item.Address:X}"); + Emit(main, pe, imageBase, bytes, Raw, Nearest, methods, item.Address); + } + } + + static void DisassembleOne(Type main, object metadata, string binary, ulong address, (object Method, ulong Address)[] methods) + { + var bytes=File.ReadAllBytes(binary); + using var pe=new PEReader(new MemoryStream(bytes)); + ulong imageBase=pe.PEHeaders.PEHeader!.ImageBase; + int Raw(ulong address) { var rva=address-imageBase; var s=pe.PEHeaders.SectionHeaders.First(s=>rva>=(ulong)s.VirtualAddress && rva<(ulong)(s.VirtualAddress+s.SizeOfRawData)); return (int)(rva-(ulong)s.VirtualAddress)+s.PointerToRawData; } + object Nearest(ulong address) => methods.LastOrDefault(m=>m.Address<=address).Method; + Console.WriteLine($"\nMETHOD {Label(Nearest(address))}+0x{address - (Nearest(address)!=null ? (ulong)Prop(Nearest(address),"MethodPointer") : 0):X} 0x{address:X}"); + Emit(main, pe, imageBase, bytes, Raw, Nearest, methods, address); + } + + static void Emit(Type main, PEReader pe, ulong imageBase, byte[] bytes, Func Raw, Func Nearest, + (object Method, ulong Address)[] methods, ulong start) + { + var item = (Method: Nearest(start), Address: start); + foreach(var section in pe.PEHeaders.SectionHeaders.Where(s=>(s.SectionCharacteristics & SectionCharacteristics.MemExecute)!=0)) + for(int i=section.PointerToRawData;i m.Address > item.Address).Address; + for(int i=0;i<1500 && (next == 0 || decoder.IP < next);i++) + { + decoder.Decode(out var instruction); output.Value=""; formatter.Format(instruction,output); + string note=""; + if(instruction.FlowControl==FlowControl.Call && instruction.Op0Kind==OpKind.NearBranch64) + { var called=Nearest(instruction.NearBranchTarget); if(called!=null && (ulong)Prop(called,"MethodPointer")==instruction.NearBranchTarget) note=" ; "+Label(called); } + if(instruction.IsIPRelativeMemoryOperand) + { try { var literal=main.GetMethod("GetLiteralByAddress")!.Invoke(null,new object[]{instruction.IPRelativeMemoryAddress}); if(literal!=null) note+=" ; literal: "+literal; } catch {} } + Console.WriteLine($"{instruction.IP:X}: {output.Value}{note}"); + } + } + sealed class LineOutput:FormatterOutput { public string Value=""; public override void Write(string text,FormatterTextKind kind)=>Value+=text; } +} diff --git a/Tools/AugustInterop/Program.cs b/Tools/AugustInterop/Program.cs new file mode 100644 index 0000000..0075317 --- /dev/null +++ b/Tools/AugustInterop/Program.cs @@ -0,0 +1,64 @@ +using System.Reflection; +using System.Runtime.Loader; +using Mono.Cecil; +using Mono.Cecil.Cil; + +if (args.Length == 3 && args[0] == "--patch-loader") +{ + if (Path.GetFullPath(args[1]).Equals(Path.GetFullPath(args[2]), StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException("Write a separate loader copy; do not overwrite the source."); + using var assembly = AssemblyDefinition.ReadAssembly(args[1]); + int changes = 0; + foreach (var type in assembly.MainModule.Types) + foreach (var method in type.Methods.Where(m => m.HasBody)) + foreach (var instruction in method.Body.Instructions) + if (type.FullName == "BepInEx.Unity.IL2CPP.Il2CppInteropManager" && + instruction.OpCode == OpCodes.Ldstr && (string)instruction.Operand == "global-metadata.dat") + { + instruction.Operand = "global-metadata.interop.dat"; + Console.WriteLine($"Redirected metadata input in {method.Name}"); + changes++; + } + if (changes != 1) throw new InvalidOperationException($"Expected one loader metadata reference, found {changes}."); + assembly.Write(args[2]); + Console.WriteLine($"Created loader copy with {changes} metadata path reference(s) changed."); + return; +} + +if (args.Length is < 2 or > 5) +{ + Console.Error.WriteLine("Usage: AugustInterop [GameAssembly.dll]"); + Console.Error.WriteLine(" or: AugustInterop --patch-loader "); + Environment.ExitCode = 1; + return; +} + +var core = args[0]; +AssemblyLoadContext.Default.Resolving += (_, name) => +{ + var path = Path.Combine(core, name.Name + ".dll"); + return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null; +}; +var library = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(core, "LibCpp2IL.dll")); +if (args.Length > 1) +{ + var primitives = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(core, "AssetRipper.Primitives.dll")); + var versionType = primitives.GetType("AssetRipper.Primitives.UnityVersion")!; + var version = versionType.GetMethod("Parse", new[] { typeof(string) })!.Invoke(null, new object[] { "2020.3.33f1" }); + var type = library.GetType("LibCpp2IL.Metadata.Il2CppMetadata")!; + var metadata = type.GetMethod("ReadFrom")!.Invoke(null, new object[] { File.ReadAllBytes(args[1]), version }); + Console.WriteLine("Metadata parsed successfully."); + foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public)) + if (field.GetValue(metadata) is Array array) Console.WriteLine($"{field.Name}: {array.Length}"); + if (args.Length > 2) + { + var registry = library.GetTypes().Single(t => t.Name == "LibCpp2IlBinaryRegistry"); + registry.GetMethod("RegisterBuiltInBinarySupport")!.Invoke(null, null); + var main = library.GetType("LibCpp2IL.LibCpp2IlMain")!; + var result = main.GetMethod("LoadFromFile")!.Invoke(null, new[] { (object)args[2], Path.GetFullPath(args[1]), version }); + Console.WriteLine($"Binary + metadata initialization: {result}"); + if (args.Length > 3 && args[3] == "--literal-find" && args.Length > 4) + LiteralFinder.Run(main, metadata, args[2], args[4]); + else if (args.Length > 3) NativeInspect.Run(main, metadata, args[2], args[3], args.Length > 4 ? args[4] : ""); + } +} diff --git a/Tools/AugustInterop/README.md b/Tools/AugustInterop/README.md new file mode 100644 index 0000000..220dbe5 --- /dev/null +++ b/Tools/AugustInterop/README.md @@ -0,0 +1,47 @@ +# August 2023 interop repair + +For the exact Rec Room 20230825 metadata header fingerprint and the installed +BepInEx 6.0.0-be.697 loader. This fixes interop generation, not login or gameplay. +It does not change GameAssembly.dll or the metadata used by the native game. + +Requirements: Node.js, .NET 9 SDK, and the August build's BepInEx core libraries. +Run from the repository root, with the game closed during installation. + +```powershell +$gameRoot = 'C:\RecRoom\2023-08-29T06-53-33Z' +$core = Join-Path $gameRoot 'BepInEx\core' +$metadataDir = Join-Path $gameRoot 'RecRoom_Data\il2cpp_data\Metadata' +node tools/AugustInterop/normalize.cjs "$metadataDir\global-metadata.dat" artifacts/august-normalized-metadata.dat +dotnet run --project tools/AugustInterop -- "$core" artifacts/august-normalized-metadata.dat "$gameRoot\GameAssembly.dll" +``` + +The normalizer checks the exact shuffled header, table bounds, image/type counts, +and every repaired module row. All other records remain unchanged. The C# tool +uses the installed LibCpp2IL to verify metadata and native registration parsing. + +To reproduce the loader modification, use the original loader backup as input: + +```powershell +dotnet run --project tools/AugustInterop -- --patch-loader "$core\BepInEx.Unity.IL2CPP.dll.before-august-interop" artifacts/BepInEx.Unity.IL2CPP.dll +``` + +For a different installation path, pass `-p:BepInExCore=` when +building. The patch tool requires a separate output file and exactly one matching +metadata filename instruction. Preserve the original loader before installation. + +Installation performed locally: + +- Normalized copy: `RecRoom_Data/il2cpp_data/Metadata/global-metadata.interop.dat`. +- Modified loader: `BepInEx/core/BepInEx.Unity.IL2CPP.dll`. +- Original loader: same path plus `.before-august-interop`. +- Native `global-metadata.dat`: unchanged by this interop repair. + +Launch `RecRoom.exe +forcemode:screen`. Successful generation ends with +`Il2CppInteropGen ... Done!` and `Chainloader startup complete`, and writes +`BepInEx/interop/assembly-hash.txt`. Keep `UpdateInteropAssemblies = true`. +Do not copy March-generated interop assemblies into this build. + +Rollback: close the game and restore the original loader from its backup. The +separate normalized file can remain on disk; the original loader never reads it. +The old loader will again reject this release's custom metadata if regeneration +is needed. If the game build changes, this exact mapping must be revalidated. diff --git a/Tools/AugustInterop/normalize.cjs b/Tools/AugustInterop/normalize.cjs new file mode 100644 index 0000000..9f5d3ee --- /dev/null +++ b/Tools/AugustInterop/normalize.cjs @@ -0,0 +1,63 @@ +// Diagnostic copy only. Never overwrite the metadata consumed by the game. +const fs = require('node:fs'); +const crypto = require('node:crypto'); +const [source, destination] = process.argv.slice(2); +if (!source || !destination || require('node:path').resolve(source) === require('node:path').resolve(destination)) + throw new Error('Specify separate source and diagnostic output files.'); +const input = fs.readFileSync(source); +const header = Buffer.from(input.subarray(0, 256)); +header.fill(0, 0xc0, 0xc4); +header.fill(0, 0xc8, 0xcc); +if (crypto.createHash('sha256').update(header).digest('hex') !== '2af65a04fe43613b66fafbe269264a6ec2b4c834304db272f0df68e7503b59e2') + throw new Error('Not the verified 20230825 header.'); +// Standard v27 section order. Each pair is the offset/size FIELD POSITION +// in the shuffled header, not a hardcoded section address. +const fields = [ + [0x2c,0xbc], [0xc8,0xc0], [0xe8,0xc4], [0x20,0xa8], + [0x24,0x68], [0x44,0xb0], [0x64,0xec], [0xf0,0x04], + [0x38,0x3c], [0x28,0x80], [0xa0,0x50], [0xf8,0xd4], + [0xe0,0x5c], [0x08,0x00], [0x18,0x58], [0x4c,0xb4], + [0x30,0x9c], [0x34,0x98], [0x6c,0x8c], [0x14,0x48], + [0xd8,0xcc], [0x0c,0xb8], [0x88,0x70], [0x7c,0xd0], + [0xa4,0x90], [0x78,0x94], [0xe4,0xf4], [0x54,0x40], + [0xfc,0x10], [0x74,0x84], [0xac,0xdc] +]; +const output = Buffer.from(input); +output.writeUInt32LE(0xfab11baf, 0); +output.writeUInt32LE(27, 4); +for (let i=0; i input.length) throw new Error(`Section ${i} out of bounds`); + output.writeUInt32LE(offset, 8+i*8); + output.writeUInt32LE(size, 12+i*8); +} +if (!input.subarray(256).equals(output.subarray(256))) throw new Error('Payload changed'); +// This release also places invalid generic-container indices in each image's +// synthetic type. Those types cannot have generic parameters. Repair +// only verified module rows in the tooling copy; game metadata is untouched. +const types = output.readUInt32LE(8 + 19*8); +const typeCount = output.readUInt32LE(12 + 19*8) / 88; +const images = output.readUInt32LE(8 + 20*8); +const imageCount = output.readUInt32LE(12 + 20*8) / 40; +const strings = output.readUInt32LE(24); +if (imageCount !== 305 || typeCount !== 42537 || output.subarray(strings+42, strings+51).toString() !== '\0') + throw new Error('Unexpected August type/image structure'); +const modules = new Set(); +for (let i=0; i= typeCount || modules.has(index) || + output.readUInt32LE(p+84) !== 0x02000001 || output.readInt32LE(p) !== 0 || + output.readInt32LE(p+24) < 6673) + throw new Error(`Unexpected module row for image ${i}`); + modules.add(index); + output.writeInt32LE(42, p); + output.writeInt32LE(-1, p+24); +} +for (let i=0; i= 6673) throw new Error(`Invalid generic container on type ${i}`); +} +fs.writeFileSync(destination, output); +console.log(`Wrote tooling copy: ${destination}; normalized header and ${modules.size} synthetic module rows.`); diff --git a/Tools/ColliderGuard.Tests/.gitignore b/Tools/ColliderGuard.Tests/.gitignore new file mode 100644 index 0000000..cd42ee3 --- /dev/null +++ b/Tools/ColliderGuard.Tests/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/Tools/ColliderGuard.Tests/ColliderGuard.Tests.csproj b/Tools/ColliderGuard.Tests/ColliderGuard.Tests.csproj new file mode 100644 index 0000000..0f7c504 --- /dev/null +++ b/Tools/ColliderGuard.Tests/ColliderGuard.Tests.csproj @@ -0,0 +1,13 @@ + + + Exe + net6.0 + enable + C:\RecRoom\2023-09-06T06-53-47Z\BepInEx + + + + + + + diff --git a/Tools/ColliderGuard.Tests/Program.cs b/Tools/ColliderGuard.Tests/Program.cs new file mode 100644 index 0000000..6684e34 --- /dev/null +++ b/Tools/ColliderGuard.Tests/Program.cs @@ -0,0 +1,84 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using HarmonyLib; + +var harmony = new Harmony("radicade.collider.regression"); +foreach (var name in new[] { "DisposeBody", "Move", "Attach", "Unrelated" }) + ColliderCastGuardPatch.Install(harmony, typeof(Targets).GetMethod(name)); +Targets.DisposeBody(); +Targets.Move(1, 2); +Targets.Attach(null, true); +try +{ + Targets.Unrelated(); + throw new Exception("Unrelated exception was swallowed"); +} +catch (InvalidOperationException ex) when (ex.Message == "unrelated") { } +var dispose = Harmony.GetPatchInfo(typeof(Targets).GetMethod("DisposeBody")); +var move = Harmony.GetPatchInfo(typeof(Targets).GetMethod("Move")); +var attach = Harmony.GetPatchInfo(typeof(Targets).GetMethod("Attach")); +if (dispose.Prefixes.Count != 0 || move.Prefixes.Count != 0 || attach.Prefixes.Count != 1) + throw new Exception("Prefix was bound to an incompatible signature"); +if (Targets.Calls != 3) throw new Exception("Original methods did not execute"); +Console.WriteLine("PASS: zero-argument disposal, movement, collider prefix, cast handling, unrelated exception propagation"); + +var tool = new Tool(); +var physical = new UnityEngine.Collider { Owner = tool, gameObject = new() { layer = 13 } }; +var trigger = new UnityEngine.Collider { Owner = tool, isTrigger = true, gameObject = new() { layer = 13 } }; +var nested = new UnityEngine.Collider { Owner = new Tool(), gameObject = new() { layer = 13 } }; +var blocker = new UnityEngine.Collider { Owner = tool, gameObject = new() { layer = 14 } }; +tool.Colliders = new[] { physical, trigger, nested, blocker }; +ToolCollisionLayerRestore.Restore(tool); +if (physical.gameObject.layer != 10 || trigger.gameObject.layer != 13 + || nested.gameObject.layer != 13 || blocker.gameObject.layer != 14) + throw new Exception("Collision-layer restoration changed the wrong colliders"); +physical.gameObject.layer = 13; +tool.MEDCLKFANNC = true; +ToolCollisionLayerRestore.Restore(tool); +if (physical.gameObject.layer != 13) throw new Exception("Collision-layer lock was ignored"); +Console.WriteLine("PASS: dropped-item layer reset; triggers, nested tools, blockers and locks preserved"); + +public static class Targets +{ + public static int Calls; + [MethodImpl(MethodImplOptions.NoInlining)] + public static void DisposeBody() { Calls++; throw new InvalidCastException("Unable to cast object of type CBKENBPBBPK"); } + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Move(float a, float b) { Calls++; } + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Attach(AOCLEIAJHKP collider, bool enabled) { Calls++; } + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Unrelated() => throw new InvalidOperationException("unrelated"); +} + +// Minimal game stand-ins: these tests exercise real Harmony registration without launching Unity. +public class AOCLEIAJHKP { } +public class CBKENBPBBPK { public UnityEngine.GameObject JPBNIBGFKJO => null; } +public class BPJMFIFDPJO { public UnityEngine.GameObject JPBNIBGFKJO => null; } +namespace UnityEngine +{ + public class GameObject { public string name = "test"; public int layer; } + public class Collider + { + public bool isTrigger; + public GameObject gameObject; + public Tool Owner; + public T GetComponentInParent() where T : class => Owner as T; + } +} +public class Tool +{ + public bool MEDCLKFANNC; + public int EHNBBIAIJIK = 10; + public int HeldCollisionLayer = 13, EquippedCollisionLayer = 18, AttachedCollisionLayer = 23; + public UnityEngine.GameObject gameObject = new(); + public UnityEngine.Collider[] Colliders = Array.Empty(); + public T[] GetComponentsInChildren(bool includeInactive) => Colliders.Cast().ToArray(); +} +internal static class RadicadeEacBypassPlugin { internal static readonly TestLog LogSource = new(); } +internal class TestLog +{ + public void LogInfo(object value) { } + public void LogWarning(object value) { } + public void LogError(object value) => throw new Exception(value.ToString()); +} diff --git a/Tools/CommentStripper/CommentStripper.csproj b/Tools/CommentStripper/CommentStripper.csproj new file mode 100644 index 0000000..4250f3f --- /dev/null +++ b/Tools/CommentStripper/CommentStripper.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + enable + enable + CommentStripper + CommentStripper + + + + + + + diff --git a/Tools/CommentStripper/Program.cs b/Tools/CommentStripper/Program.cs new file mode 100644 index 0000000..d7d0626 --- /dev/null +++ b/Tools/CommentStripper/Program.cs @@ -0,0 +1,186 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommentStripper; + +/// +/// Removes every comment from the given C# files. +/// +/// This is deliberately not a text scan. A line-based strip corrupts anything that merely looks +/// like a comment - this codebase is full of URLs and paths inside string and interpolated +/// literals, verbatim strings, char literals and C# 11 raw strings. The file is parsed with the +/// real C# parser instead, comment trivia is dropped from each token, and the result is written +/// back. Before a file is written, the tool re-parses its own output and checks that the sequence +/// of token kinds and token text is unchanged: if only trivia moved, the rewrite touched no code. +/// +/// Usage: CommentStripper [--dry-run] <path> [path...] +/// +internal static class Program +{ + private static readonly HashSet SkippedDirectories = new(StringComparer.OrdinalIgnoreCase) + { + "bin", "obj", ".git", ".vs", "node_modules", "artifacts" + }; + + private static int Main(string[] args) + { + var dryRun = args.Contains("--dry-run"); + var roots = args.Where(argument => !argument.StartsWith("--", StringComparison.Ordinal)).ToArray(); + if (roots.Length == 0) + { + Console.Error.WriteLine("usage: CommentStripper [--dry-run] [path...]"); + return 2; + } + + var filesChanged = 0; + var commentsRemoved = 0; + var linesRemoved = 0; + var filesSeen = 0; + var failures = 0; + + foreach (var file in roots.SelectMany(EnumerateSources).Distinct(StringComparer.OrdinalIgnoreCase)) + { + filesSeen++; + try + { + var result = Strip(file, dryRun); + if (result == null) continue; + + filesChanged++; + commentsRemoved += result.Value.Comments; + linesRemoved += result.Value.Lines; + } + catch (Exception exception) + { + failures++; + Console.Error.WriteLine($"failed: {file}: {exception.Message}"); + } + } + + Console.WriteLine($"{(dryRun ? "would change" : "changed")} {filesChanged} of {filesSeen} files; " + + $"{commentsRemoved} comments, {linesRemoved} lines"); + return failures == 0 ? 0 : 1; + } + + private static IEnumerable EnumerateSources(string root) + { + if (File.Exists(root)) + { + if (root.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) yield return root; + yield break; + } + + if (!Directory.Exists(root)) yield break; + + foreach (var file in Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories)) + { + var segments = file.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (segments.Any(segment => SkippedDirectories.Contains(segment))) continue; + yield return file; + } + } + + private static (int Comments, int Lines)? Strip(string path, bool dryRun) + { + var bytes = File.ReadAllBytes(path); + var hasByteOrderMark = bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF; + var original = new UTF8Encoding(false).GetString(bytes, hasByteOrderMark ? 3 : 0, bytes.Length - (hasByteOrderMark ? 3 : 0)); + + var tree = CSharpSyntaxTree.ParseText(original, new CSharpParseOptions(LanguageVersion.Latest)); + var root = tree.GetRoot(); + + var stripper = new CommentStripper(); + var rewritten = stripper.Visit(root) ?? root; + var stripped = rewritten.ToFullString(); + + if (stripper.Removed == 0 && stripped == original) return null; + + // Blank out the whitespace that propped up a removed comment so the file does not fill with + // trailing-space-only lines. The split keeps each line's own terminator, because a verbatim + // string in this codebase carries CRLF of its own and rewriting it to LF would change the + // literal - which the token check below would then (correctly) refuse. + var cleaned = string.Concat(LinePattern.Split(stripped).Select(chunk => + { + var body = chunk.TrimEnd('\r', '\n'); + return body.Trim().Length == 0 ? chunk[body.Length..] : chunk; + })); + + // Refuse to write anything that changed a token. Parsing the output and comparing the token + // stream is the check that this was trivia surgery and not code surgery. + var reparsed = CSharpSyntaxTree.ParseText(cleaned, new CSharpParseOptions(LanguageVersion.Latest)); + var errors = reparsed.GetDiagnostics() + .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToList(); + if (errors.Count > 0) + throw new InvalidOperationException("output does not parse: " + errors[0].GetMessage()); + + var before = Tokens(root); + var after = Tokens(reparsed.GetRoot()); + if (before.Count != after.Count) + throw new InvalidOperationException($"token count changed: {before.Count} -> {after.Count}"); + for (var index = 0; index < before.Count; index++) + { + if (before[index] != after[index]) + throw new InvalidOperationException($"token {index} changed: {before[index]} -> {after[index]}"); + } + + var removedLines = CountLines(original) - CountLines(cleaned); + if (!dryRun) + File.WriteAllText(path, cleaned, new UTF8Encoding(hasByteOrderMark)); + + return (stripper.Removed, removedLines); + } + + private static List Tokens(SyntaxNode root) => + root.DescendantTokens(descendIntoTrivia: false) + .Select(token => token.RawKind + ":" + token.Text) + .ToList(); + + private static int CountLines(string text) => text.Length == 0 ? 0 : text.Split('\n').Length; + + /// Splits into chunks that each keep their own line terminator. + private static readonly System.Text.RegularExpressions.Regex LinePattern = new("(?<=\\n)", System.Text.RegularExpressions.RegexOptions.Compiled); + + private sealed class CommentStripper : CSharpSyntaxRewriter + { + public int Removed { get; private set; } + + public override SyntaxToken VisitToken(SyntaxToken token) + { + if (token.LeadingTrivia.Count == 0 && token.TrailingTrivia.Count == 0) return token; + + return token + .WithLeadingTrivia(Clean(token.LeadingTrivia)) + .WithTrailingTrivia(Clean(token.TrailingTrivia)); + } + + private SyntaxTriviaList Clean(SyntaxTriviaList trivia) + { + if (trivia.Count == 0) return trivia; + + var kept = new List(trivia.Count); + foreach (var item in trivia) + { + if (IsComment(item)) + { + Removed++; + continue; + } + + kept.Add(item); + } + + return kept.Count == trivia.Count ? trivia : SyntaxFactory.TriviaList(kept); + } + + private static bool IsComment(SyntaxTrivia trivia) => trivia.Kind() switch + { + SyntaxKind.SingleLineCommentTrivia => true, + SyntaxKind.MultiLineCommentTrivia => true, + SyntaxKind.SingleLineDocumentationCommentTrivia => true, + SyntaxKind.MultiLineDocumentationCommentTrivia => true, + _ => false + }; + } +} diff --git a/Tools/InteropInspect/InteropInspect.csproj b/Tools/InteropInspect/InteropInspect.csproj new file mode 100644 index 0000000..1cad418 --- /dev/null +++ b/Tools/InteropInspect/InteropInspect.csproj @@ -0,0 +1,8 @@ + + + Exe + net9.0 + enable + enable + + diff --git a/Tools/InteropInspect/Program.cs b/Tools/InteropInspect/Program.cs new file mode 100644 index 0000000..17b23ed --- /dev/null +++ b/Tools/InteropInspect/Program.cs @@ -0,0 +1,118 @@ +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text; + + +if (args.Length < 2) +{ + Console.Error.WriteLine("Usage: InteropInspect ... | --type "); + return 1; +} + +var path = args[0]; +var detail = false; +string detailType = null; +var keywords = new List(); +for (int i = 1; i < args.Length; i++) +{ + if (args[i] == "--type" && i + 1 < args.Length) { detail = true; detailType = args[++i]; } + else keywords.Add(args[i]); +} + +using var fs = File.OpenRead(path); +using var pe = new PEReader(fs); +var md = pe.GetMetadataReader(); + +string GetString(StringHandle h) => md.GetString(h); +string GetNs(TypeDefinition t) +{ + var ns = GetString(t.Namespace); + var decl = t.GetDeclaringType(); + if (!decl.IsNil) + { + var outer = md.GetTypeDefinition(decl); + return GetNs(outer) + "+" + GetString(outer.Name); + } + return ns; +} + +foreach (var tdh in md.TypeDefinitions) +{ + var td = md.GetTypeDefinition(tdh); + var fullName = GetNs(td) + "." + GetString(td.Name); + + bool match; + if (detail) + match = fullName.Equals(detailType, StringComparison.OrdinalIgnoreCase); + else + match = keywords.Any(k => fullName.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0 || + (k.StartsWith("field:") && td.GetFields().Any(f => + GetString(md.GetFieldDefinition(f).Name).Contains(k[6..], StringComparison.OrdinalIgnoreCase)))); + + if (!match) continue; + + if (!detail) + { + Console.WriteLine(fullName); + continue; + } + + var sb = new StringBuilder(); + sb.AppendLine($"TYPE {fullName}"); + if (!td.BaseType.IsNil) + { + try { sb.AppendLine($" base: {ResolveName(md, td.BaseType)}"); } catch { } + } + foreach (var iih in td.GetInterfaceImplementations()) + { + var ii = md.GetInterfaceImplementation(iih); + try { sb.AppendLine($" impl: {ResolveName(md, ii.Interface)}"); } catch { } + } + sb.AppendLine(" fields:"); + foreach (var fh in td.GetFields()) + { + var f = md.GetFieldDefinition(fh); + var constantHandle = f.GetDefaultValue(); + var constantValue = ""; + if (!constantHandle.IsNil) + { + var constant = md.GetConstant(constantHandle); + var reader = md.GetBlobReader(constant.Value); + constantValue = " = " + reader.ReadConstant(constant.TypeCode); + } + sb.AppendLine($" {GetString(f.Name)}{constantValue}"); + } + sb.AppendLine(" methods:"); + foreach (var mh in td.GetMethods()) + { + var m = md.GetMethodDefinition(mh); + var attrs = m.Attributes; + var prms = m.GetParameters().Select(ph => md.GetParameter(ph)) + .Select(pp => $"{GetString(pp.Name)}"); + sb.AppendLine($" {GetString(m.Name)}({string.Join(", ", prms)}) [{attrs}]"); + } + sb.AppendLine(" properties:"); + foreach (var ph in td.GetProperties()) + { + var p = md.GetPropertyDefinition(ph); + sb.AppendLine($" {GetString(p.Name)}"); + } + Console.Write(sb.ToString()); +} + +return 0; + +static string ResolveName(MetadataReader md, EntityHandle h) +{ + if (h.Kind == HandleKind.TypeReference) + { + var tr = md.GetTypeReference((TypeReferenceHandle)h); + return $"{md.GetString(tr.Namespace)}.{md.GetString(tr.Name)}"; + } + if (h.Kind == HandleKind.TypeDefinition) + { + var td2 = md.GetTypeDefinition((TypeDefinitionHandle)h); + return $"{md.GetString(td2.Namespace)}.{md.GetString(td2.Name)}"; + } + return h.Kind.ToString(); +} diff --git a/Tools/__pycache__/patch-0831-client-metadata.cpython-314.pyc b/Tools/__pycache__/patch-0831-client-metadata.cpython-314.pyc new file mode 100644 index 0000000..da29e41 Binary files /dev/null and b/Tools/__pycache__/patch-0831-client-metadata.cpython-314.pyc differ diff --git a/Tools/august-client-triage.ps1 b/Tools/august-client-triage.ps1 new file mode 100644 index 0000000..0d246d7 --- /dev/null +++ b/Tools/august-client-triage.ps1 @@ -0,0 +1,431 @@ +<# + Read-only triage for a Radicade-patched August 2023 Rec Room install. + + Usage: + powershell -ExecutionPolicy Bypass -File august-client-triage.ps1 + powershell -ExecutionPolicy Bypass -File august-client-triage.ps1 -GameDir "C:\path\to\2023-08-29T06-53-33Z" + + It only reads files and opens two TCP sockets to localhost. It writes + august-client-report.txt next to this script (override with -OutFile). + Send that file back verbatim; it names exactly which piece of the stack is missing. +#> +[CmdletBinding()] +param( + [string]$GameDir, + [string]$OutFile +) + +$ErrorActionPreference = 'Continue' +$script:Report = New-Object System.Collections.Generic.List[string] +$script:Failures = 0 +$script:Warnings = 0 + +function Add-Line { + param([string]$Text = '') + $script:Report.Add($Text) +} + +function Write-Both { + param([string]$Text = '') + Write-Host $Text + Add-Line $Text +} + +function Add-Verdict { + param( + [ValidateSet('OK', 'WARN', 'MISSING')][string]$State, + [string]$Text + ) + if ($State -eq 'MISSING') { $script:Failures++ } + if ($State -eq 'WARN') { $script:Warnings++ } + Add-Line ("[{0,-7}] {1}" -f $State, $Text) + if ($State -ne 'OK') { Write-Host ("[{0}] {1}" -f $State, $Text) } +} + +function Get-Md5 { + param([string]$Path) + try { return (Get-FileHash -LiteralPath $Path -Algorithm MD5).Hash.ToLower() } + catch { return 'unreadable' } +} + +function Get-ByteString { + param([string]$Path) + try { + $bytes = [System.IO.File]::ReadAllBytes($Path) + return [System.Text.Encoding]::GetEncoding(28591).GetString($bytes) + } + catch { return $null } +} + +function Test-Utf16Literal { + param([string]$Path, [string]$Literal) + try { + $bytes = [System.IO.File]::ReadAllBytes($Path) + $needle = [System.Text.Encoding]::Unicode.GetBytes($Literal) + for ($i = 0; $i -le ($bytes.Length - $needle.Length); $i++) { + if ($bytes[$i] -ne $needle[0]) { continue } + $hit = $true + for ($j = 1; $j -lt $needle.Length; $j++) { + if ($bytes[$i + $j] -ne $needle[$j]) { $hit = $false; break } + } + if ($hit) { return $true } + } + } + catch { } + return $false +} + +function Test-Tcp { + param([int]$Port) + $client = New-Object System.Net.Sockets.TcpClient + try { + $task = $client.ConnectAsync('127.0.0.1', $Port) + if ($task.Wait(1500) -and $client.Connected) { return $true } + return $false + } + catch { return $false } + finally { $client.Close() } +} + +function Get-LogMarkers { + param([string]$Path, [string[]]$Markers) + $result = @{} + foreach ($m in $Markers) { $result[$m] = 0 } + if (-not (Test-Path -LiteralPath $Path)) { return $result } + try { + $text = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop + foreach ($m in $Markers) { + $result[$m] = ([regex]::Matches($text, [regex]::Escape($m))).Count + } + } + catch { } + return $result +} + +# --- locate the install ------------------------------------------------------- + +if (-not $GameDir) { + $candidates = @( + (Join-Path $env:USERPROFILE 'Downloads\2023-08-29T06-53-33Z'), + (Join-Path $env:USERPROFILE 'Desktop\2023-08-29T06-53-33Z'), + 'C:\2023-08-29T06-53-33Z' + ) + $downloads = Join-Path $env:USERPROFILE 'Downloads' + if (Test-Path -LiteralPath $downloads) { + $candidates += (Get-ChildItem -LiteralPath $downloads -Directory -ErrorAction SilentlyContinue | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'RecRoom.exe') } | + ForEach-Object { $_.FullName }) + } + foreach ($c in $candidates) { + if ($c -and (Test-Path -LiteralPath (Join-Path $c 'RecRoom.exe'))) { $GameDir = $c; break } + } +} + +if (-not $GameDir) { $GameDir = Read-Host 'Path to the Rec Room folder that contains RecRoom.exe' } +$GameDir = $GameDir.TrimEnd('\') + +if (-not $OutFile) { + if ($PSScriptRoot) { $OutFile = Join-Path $PSScriptRoot 'august-client-report.txt' } + else { $OutFile = Join-Path (Get-Location) 'august-client-report.txt' } +} + +$playerLog = Join-Path $env:USERPROFILE 'AppData\LocalLow\Against Gravity\Rec Room\Player.log' +$bepLog = Join-Path $GameDir 'BepInEx\LogOutput.log' +$metadata = Join-Path $GameDir 'Recroom_Data\il2cpp_data\Metadata\global-metadata.dat' +$interopDat = Join-Path $GameDir 'Recroom_Data\il2cpp_data\Metadata\global-metadata.interop.dat' +$bootConfig = Join-Path $GameDir 'Recroom_Data\boot.config' +$patchJson = Join-Path $GameDir 'Recroom_Data\radicade-patch.json' +$winhttp = Join-Path $GameDir 'winhttp.dll' +$doorstop = Join-Path $GameDir 'doorstop_config.ini' +$loader = Join-Path $GameDir 'BepInEx\core\BepInEx.Unity.IL2CPP.dll' +$plugin = Join-Path $GameDir 'BepInEx\plugins\RadicadeAugustClient.dll' +$assembly = Join-Path $GameDir 'GameAssembly.dll' +$interopDir = Join-Path $GameDir 'BepInEx\interop' + +# Expected values taken from the reference install that logs in and loads rooms. +$expected = @{ + MetadataSize = 38654794 + MetadataMd5 = 'cc9c32014b5fc3dd1a2a8fb12376be15' + InteropSize = 38654794 + InteropMd5 = '4a1a28078f957af470f3b31951b32648' + BootSize = 173 + BootMd5 = 'dc2e5f7af7d5df736a4cc3477b4b1a1f' + WinhttpSize = 26112 + WinhttpMd5 = 'b2a4d0cb04bf8f5a27ccab237ecc2586' + LoaderSize = 60928 + LoaderMd5 = '9430bb84349d52b753a181788a86c5e2' + PluginSize = 62464 + PluginMd5 = '8718a45dd42d7cb72b3e46e04b744d91' + InteropCount = 335 + AssemblySize = 167390208 +} + +# --- header ------------------------------------------------------------------ + +Write-Both 'Radicade August 2023 client triage' +Write-Both ("generated : {0}" -f (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) +Write-Both ("game folder : {0}" -f $GameDir) +Write-Both ("powershell : {0}" -f $PSVersionTable.PSVersion) +Write-Both '' +Write-Both 'NOTE: "Failed to load Type RecRoom.Analytics.Statsig.StatsigDebugOverrides" in' +Write-Both 'Player.log is NORMAL. It appears in a completely unpatched launch of this' +Write-Both 'build and in every working install. It is not a symptom of anything.' +Write-Both '' + +# --- install identity -------------------------------------------------------- + +Write-Both '== game build ==' +if (Test-Path -LiteralPath $assembly) { + $info = Get-Item -LiteralPath $assembly + $md5 = Get-Md5 $assembly + Write-Both ("GameAssembly.dll : {0} b md5 {1}" -f $info.Length, $md5) + Write-Both (" file version : {0}" -f $info.VersionInfo.FileVersion) + if ($info.Length -eq $expected.AssemblySize) { + Add-Verdict 'OK' ('GameAssembly.dll size matches the reference build ({0} b)' -f $expected.AssemblySize) + } else { + Add-Verdict 'WARN' ("GameAssembly.dll is {0} b, the reference build is {1} b - a different build will not accept this patch stack" -f $info.Length, $expected.AssemblySize) + } +} else { + Add-Verdict 'MISSING' 'GameAssembly.dll - this is not a Rec Room install folder?' +} +if (Test-Path -LiteralPath $playerLog) { + $firstLine = (Get-Content -LiteralPath $playerLog -TotalCount 1 -ErrorAction SilentlyContinue) + Write-Both ("Player.log line 1 : {0}" -f $firstLine) + if ($firstLine -notmatch '2020\.3\.33f1') { + Add-Verdict 'WARN' 'Player.log does not report Unity 2020.3.33f1 - unexpected engine build' + } +} else { + Add-Verdict 'WARN' ('Player.log not found at {0}' -f $playerLog) +} +Write-Both '' + +# --- the patched data files -------------------------------------------------- + +Write-Both '== patched data files ==' +if (Test-Path -LiteralPath $metadata) { + $info = Get-Item -LiteralPath $metadata + $md5 = Get-Md5 $metadata + $text = Get-ByteString $metadata + $local = if ($text) { ([regex]::Matches($text, 'localhost:3030')).Count } else { -1 } + $recnet = if ($text) { ([regex]::Matches($text, 'rec\.net')).Count } else { -1 } + Write-Both ("global-metadata.dat : {0} b md5 {1}" -f $info.Length, $md5) + Write-Both (" localhost:3030 : {0} occurrence(s) (reference: 28)" -f $local) + Write-Both (" rec.net literals : {0} occurrence(s) (reference: 50)" -f $recnet) + if ($info.Length -eq $expected.MetadataSize -and $md5 -eq $expected.MetadataMd5) { + Add-Verdict 'OK' 'global-metadata.dat is the Radicade-patched file' + } elseif ($local -eq 0) { + Add-Verdict 'MISSING' 'global-metadata.dat is UNPATCHED - the client points at the official servers, the plugin cannot fix that' + } else { + Add-Verdict 'WARN' 'global-metadata.dat differs from the reference file (size or md5)' + } +} else { + Add-Verdict 'MISSING' 'Recroom_Data\il2cpp_data\Metadata\global-metadata.dat' +} +$metadataOrig = $metadata + '.orig' +if (Test-Path -LiteralPath $metadataOrig) { + $origInfo = Get-Item -LiteralPath $metadataOrig + Add-Line (" global-metadata.dat.orig: {0} b (a 32039456 b copy is the pristine vanilla file the patcher backed up)" -f $origInfo.Length) +} + +if (Test-Path -LiteralPath $interopDat) { + $info = Get-Item -LiteralPath $interopDat + $md5 = Get-Md5 $interopDat + if ($info.Length -eq $expected.InteropSize -and $md5 -eq $expected.InteropMd5) { + Add-Verdict 'OK' 'global-metadata.interop.dat matches the reference' + } else { + Add-Verdict 'WARN' ("global-metadata.interop.dat is {0} b md5 {1} (reference {2} b {3})" -f $info.Length, $md5, $expected.InteropSize, $expected.InteropMd5) + } +} else { + Add-Verdict 'MISSING' 'Recroom_Data\il2cpp_data\Metadata\global-metadata.interop.dat - the patched BepInEx loader needs it to build interop assemblies' +} + +if (Test-Path -LiteralPath $bootConfig) { + $info = Get-Item -LiteralPath $bootConfig + $md5 = Get-Md5 $bootConfig + Write-Both ("boot.config : {0} b md5 {1}" -f $info.Length, $md5) + Write-Both (' content : ' + ((Get-Content -LiteralPath $bootConfig) -join ' | ')) + if ($info.Length -eq $expected.BootSize -and $md5 -eq $expected.BootMd5) { + Add-Verdict 'OK' 'boot.config is the Radicade variant' + } else { + Add-Verdict 'WARN' 'boot.config differs from the reference (vr-enabled=0 / player-connection-mode=Listen)' + } +} else { + Add-Verdict 'MISSING' 'Recroom_Data\boot.config' +} + +if (Test-Path -LiteralPath $patchJson) { + Add-Line 'radicade-patch.json :' + foreach ($line in (Get-Content -LiteralPath $patchJson)) { Add-Line (' ' + $line) } + Add-Verdict 'OK' 'Recroom_Data\radicade-patch.json present - the metadata was patched on this machine' +} else { + Add-Verdict 'MISSING' 'Recroom_Data\radicade-patch.json - no evidence this install was ever patched' +} +Write-Both '' + +# --- loader stack ------------------------------------------------------------ + +Write-Both '== loader stack ==' +if (Test-Path -LiteralPath $winhttp) { + $info = Get-Item -LiteralPath $winhttp + $md5 = Get-Md5 $winhttp + if ($info.Length -eq $expected.WinhttpSize -and $md5 -eq $expected.WinhttpMd5) { + Add-Verdict 'OK' 'winhttp.dll is the Doorstop 4.3.0 proxy' + } else { + Add-Verdict 'WARN' ("winhttp.dll is {0} b md5 {1} (reference {2} b {3}) - another proxy or a disabled/renamed copy?" -f $info.Length, $md5, $expected.WinhttpSize, $expected.WinhttpMd5) + } +} else { + Add-Verdict 'MISSING' 'winhttp.dll - without the Doorstop proxy nothing loads BepInEx' + if (Test-Path -LiteralPath (Join-Path $GameDir 'winhttp.dll.baseline-disabled')) { + Add-Verdict 'MISSING' 'winhttp.dll is renamed to winhttp.dll.baseline-disabled - rename it back' + } +} + +if (Test-Path -LiteralPath $doorstop) { + $cfg = Get-Content -LiteralPath $doorstop -Raw + if ($cfg -match 'enabled\s*=\s*true' -or $cfg -match '^\s*enabled\s*=\s*1') { + Add-Verdict 'OK' 'doorstop_config.ini has enabled = true' + } else { + Add-Verdict 'MISSING' 'doorstop_config.ini does not set enabled = true' + } + if ($cfg -match 'BepInEx\\core\\BepInEx\.Unity\.IL2CPP\.dll') { + Add-Verdict 'OK' 'doorstop_config.ini targets BepInEx\core\BepInEx.Unity.IL2CPP.dll' + } else { + Add-Verdict 'WARN' 'doorstop_config.ini target_assembly is not the IL2CPP preloader' + } +} else { + Add-Verdict 'MISSING' 'doorstop_config.ini' +} +if (Test-Path -LiteralPath (Join-Path $GameDir '.doorstop_version')) { + Add-Line (' .doorstop_version : ' + (Get-Content -LiteralPath (Join-Path $GameDir '.doorstop_version') -Raw).Trim()) +} + +if (Test-Path -LiteralPath $loader) { + $info = Get-Item -LiteralPath $loader + $md5 = Get-Md5 $loader + $patched = Test-Utf16Literal -Path $loader -Literal 'global-metadata.interop.dat' + if ($info.Length -eq $expected.LoaderSize -and $md5 -eq $expected.LoaderMd5) { + Add-Verdict 'OK' 'BepInEx.Unity.IL2CPP.dll is the patched loader (reads global-metadata.interop.dat)' + } elseif (-not $patched) { + Add-Verdict 'MISSING' 'BepInEx.Unity.IL2CPP.dll is UNPATCHED - Cpp2IL will reject this build''s metadata header' + } else { + Add-Verdict 'WARN' ("BepInEx.Unity.IL2CPP.dll is {0} b md5 {1} (reference {2} b {3})" -f $info.Length, $md5, $expected.LoaderSize, $expected.LoaderMd5) + } +} else { + Add-Verdict 'MISSING' 'BepInEx\core\BepInEx.Unity.IL2CPP.dll' +} + +if (Test-Path -LiteralPath $interopDir) { + $count = (Get-ChildItem -LiteralPath $interopDir -File -ErrorAction SilentlyContinue).Count + if ($count -ge $expected.InteropCount) { + Add-Verdict 'OK' ("BepInEx\interop holds {0} assemblies" -f $count) + } else { + Add-Verdict 'WARN' ("BepInEx\interop holds {0} assemblies (reference {1}) - generation may have failed halfway" -f $count, $expected.InteropCount) + } +} else { + Add-Verdict 'MISSING' 'BepInEx\interop - the game''s managed proxy assemblies are absent' +} + +if (Test-Path -LiteralPath $plugin) { + $info = Get-Item -LiteralPath $plugin + $md5 = Get-Md5 $plugin + if ($info.Length -eq $expected.PluginSize -and $md5 -eq $expected.PluginMd5) { + Add-Verdict 'OK' 'RadicadeAugustClient.dll matches the reference plugin build' + } else { + Add-Verdict 'WARN' ("RadicadeAugustClient.dll is {0} b md5 {1} (reference {2} b {3}) - a different build" -f $info.Length, $md5, $expected.PluginSize, $expected.PluginMd5) + } +} else { + Add-Verdict 'MISSING' 'BepInEx\plugins\RadicadeAugustClient.dll' +} +$otherPlugins = Get-ChildItem -LiteralPath (Join-Path $GameDir 'BepInEx\plugins') -File -Filter *.dll -ErrorAction SilentlyContinue | + Where-Object { $_.Name -ne 'RadicadeAugustClient.dll' } +foreach ($p in $otherPlugins) { + Add-Verdict 'WARN' ('extra plugin in BepInEx\plugins: {0} ({1} b) - it may not be August-compatible' -f $p.Name, $p.Length) +} +Write-Both '' + +# --- server reachability ----------------------------------------------------- + +Write-Both '== local server ==' +$https = Test-Tcp -Port 3030 +$http = Test-Tcp -Port 3031 +if ($https) { Add-Verdict 'OK' 'something is listening on https://127.0.0.1:3030' } +else { Add-Verdict 'MISSING' 'nothing is listening on port 3030 - the client is hardwired to https://localhost:3030, so the Radicade server must run on THIS machine' } +if ($http) { Add-Verdict 'OK' 'something is listening on http://127.0.0.1:3031' } +else { Add-Verdict 'WARN' 'nothing is listening on port 3031' } +Write-Both '' + +# --- logs -------------------------------------------------------------------- + +Write-Both '== BepInEx log ==' +if (Test-Path -LiteralPath $bepLog) { + $markers = Get-LogMarkers -Path $bepLog -Markers @( + 'Chainloader startup complete', + 'Radicade August Client', + 'Bypassing native launch validator', + 'global-metadata.interop.dat', + 'Cpp2IL', + '[Error', + '[Warning' + ) + foreach ($k in $markers.Keys) { Add-Line (" {0,-38}: {1}" -f $k, $markers[$k]) } + if ($markers['Chainloader startup complete'] -gt 0) { Add-Verdict 'OK' 'BepInEx chainloader came up' } + else { Add-Verdict 'MISSING' 'BepInEx chainloader never completed - read the log tail below' } + if ($markers['Radicade August Client'] -gt 0) { Add-Verdict 'OK' 'the Radicade plugin logged under its own name' } + else { Add-Verdict 'MISSING' 'the Radicade plugin never logged - it did not load' } + Add-Line '' + Add-Line ' last 25 lines:' + foreach ($line in (Get-Content -LiteralPath $bepLog -Tail 25 -ErrorAction SilentlyContinue)) { Add-Line (' ' + $line) } +} else { + Add-Verdict 'MISSING' ('BepInEx\LogOutput.log not found at {0} - BepInEx never started' -f $bepLog) +} +Write-Both '' + +Write-Both '== Player.log ==' +if (Test-Path -LiteralPath $playerLog) { + $markers = Get-LogMarkers -Path $playerLog -Markers @( + 'Failed to load Type "RecRoom.Analytics.Statsig.StatsigDebugOverrides"', + 'Could not resolve host', + "Could not resolve host 'ns.rec.net'", + 'Launch validation failed', + 'FailedToSpawnPlayer', + 'Error downloading save data', + 'ns.rec.net', + 'localhost:3030' + ) + foreach ($k in $markers.Keys) { Add-Line (" {0,-72}: {1}" -f $k, $markers[$k]) } + if ($markers["Could not resolve host 'ns.rec.net'"] -gt 0 -or $markers['ns.rec.net'] -gt 0) { + Add-Verdict 'MISSING' 'the client still talks to ns.rec.net - the metadata host literals are unpatched (or the plugin''s nameserver redirect never installed)' + } elseif ($markers['localhost:3030'] -gt 0) { + Add-Verdict 'OK' 'the client''s requests go to localhost:3030' + } + if ($markers['Launch validation failed'] -gt 0) { + Add-Verdict 'MISSING' 'the native launch validator rejected the install and no bypass ran' + } + Add-Line '' + Add-Line ' last 25 lines:' + foreach ($line in (Get-Content -LiteralPath $playerLog -Tail 25 -ErrorAction SilentlyContinue)) { Add-Line (' ' + $line) } +} else { + Add-Verdict 'MISSING' ('Player.log not found at {0}' -f $playerLog) +} + +# --- verdict ----------------------------------------------------------------- + +Write-Both '' +Write-Both ('RESULT: {0} missing piece(s), {1} warning(s)' -f $script:Failures, $script:Warnings) +if ($script:Failures -eq 0) { + Write-Both 'Every required piece is present and the local server answers. If the game still' + Write-Both 'fails, the report above (BepInEx log tail + Player.log tail) names the next cause.' +} else { + Write-Both 'The [MISSING] lines above are the install steps that did not happen.' +} +Write-Both '' +Write-Both 'Reminder: the StatsigDebugOverrides line is expected noise in every launch.' + +try { + Set-Content -LiteralPath $OutFile -Value ($script:Report -join "`r`n") -Encoding UTF8 + Write-Host '' + Write-Host ("Report written to {0}" -f $OutFile) +} +catch { + Write-Host ("Could not write {0}: {1}" -f $OutFile, $_.Exception.Message) +} diff --git a/Tools/check-obfuscation-safety.py b/Tools/check-obfuscation-safety.py new file mode 100644 index 0000000..579c781 --- /dev/null +++ b/Tools/check-obfuscation-safety.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Check that obfuscating the September client cannot break a by-name lookup. + +Obfuscar renames types and members, but it cannot see reflection. Anywhere this plugin hands a +*name* to a reflection API for one of its OWN types, that name has to survive the rename - hence +the SkipMethod rules in 2023Patch/RadicadePatch/obfuscar.xml. + +This script enumerates the names that must survive (from the C# sources) and then checks each one +against Obfuscar's Mapping.txt to prove it was kept. Run after an obfuscation pass: + + Tools/check-obfuscation-safety.py + +Exit status is 0 only when every name was preserved. +""" + +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +CLIENT = ROOT / "2023Patch" / "RadicadePatch" +SHARED = ROOT / "2023Patch" / "RadicadeEacBypass" +MAPPING = CLIENT / "bin" / "obfuscated" / "Mapping.txt" + +SOURCES = [p for d in (CLIENT, SHARED) for p in d.rglob("*.cs") if "/obj/" not in p.as_posix() and "/bin/" not in p.as_posix()] + + +def source_texts(): + for path in SOURCES: + yield path, path.read_text(encoding="utf-8", errors="replace") + + +def our_type_names(): + """Type names declared in the plugin and the shared folder (the ones Obfuscar can rename).""" + names = set() + pattern = re.compile(r"^\s*(?:public|internal|private|protected|static|sealed|abstract|partial|\s)*\b(?:class|struct|enum|interface|record)\s+([A-Za-z_]\w*)", re.M) + for _, text in source_texts(): + names.update(pattern.findall(text)) + return names + + +def names_resolved_at_runtime(): + """Every member name handed to reflection for one of our own types.""" + ours = our_type_names() + found = {} + + def add(name, where, why): + found.setdefault(name, (where, why)) + + for path, text in source_texts(): + lines = text.splitlines() + for index, line in enumerate(lines, 1): + for match in re.finditer(r"nameof\(\s*([A-Za-z_][\w\.]*)\s*\)", line): + qualified = match.group(1) + owner = qualified.split(".")[0] if "." in qualified else None + member = qualified.split(".")[-1] + # nameof(SomeGameType.Member) is the game's own name and survives untouched. + if owner is not None and owner not in ours: + continue + add(member, f"{path.name}:{index}", f"nameof({qualified})") + + # A literal handed to a reflection call on one of our own types. + for match in re.finditer(r"(?:typeof\(\s*([A-Za-z_]\w*)\s*\)|GetType\(\s*\))[^;\n]*?[\"']([A-Za-z_]\w*)[\"']", line): + owner, literal = match.group(1), match.group(2) + if owner in ours: + add(literal, f"{path.name}:{index}", f'"{literal}" on {owner}') + + return found + + +def harmony_patch_methods(): + """Methods carrying a Harmony attribute. + + Harmony resolves the special parameters __result/__instance/__state/__args/___field by NAME, + and Obfuscar nulls a parameter's name whenever it renames the declaring method (see + ShouldSkipParams). A renamed patch method therefore turns its own __result into P_0 and the + patch throws at Apply time - which aborts the rest of Plugin.Load, so later patches never arm. + Keeping the method name keeps the parameter names with it, so every one of these must be + pinned. + """ + attribute = re.compile(r"^\s*\[\s*Harmony") + declaration = re.compile(r"^\s*(?:[\w<>,\[\]\.\?]+\s+)+(?= 0 and len(above) < 6: + stripped = lines[cursor].strip() + if not stripped: + cursor -= 1 + continue + above.append(stripped) + if stripped.startswith("["): + cursor -= 1 + continue + break + if any(attribute.match(a) for a in above): + found.setdefault(name, f"{path.name}:{index + 1}") + return found + + +def kept_by_obfuscar(): + """Members Obfuscar reported as skipped (i.e. not renamed).""" + kept = set() + for line in MAPPING.read_text(encoding="utf-8", errors="replace").splitlines(): + if "skipped:" not in line: + continue + match = re.search(r"::([A-Za-z_]\w*)(?:\[|\()", line) + if match: + kept.add(match.group(1)) + return kept + + +def renamed_by_obfuscar(): + renamed = set() + for line in MAPPING.read_text(encoding="utf-8", errors="replace").splitlines(): + if " -> " not in line or "::" not in line: + continue + match = re.search(r"::([A-Za-z_]\w*)(?:\[|\()", line) + if match: + renamed.add(match.group(1)) + return renamed + + +def main(): + if not MAPPING.exists(): + print(f"no mapping at {MAPPING}; run obfuscar first", file=sys.stderr) + return 2 + + required = names_resolved_at_runtime() + patches = harmony_patch_methods() + for name, where in patches.items(): + required.setdefault(name, (where, "Harmony patch method (keeps __parameters)")) + + kept = kept_by_obfuscar() + renamed = renamed_by_obfuscar() + + broken = sorted(n for n in required if n in renamed and n not in kept) + print(f"by-name lookups that must survive : {len(required)}") + print(f" of which Harmony patch methods : {len(patches)}") + print(f"members Obfuscar kept : {len(kept)}") + for name in sorted(required): + where, why = required[name] + status = "BROKEN" if name in broken else "kept" + print(f" [{status:6}] {name:36} {why:46} {where}") + + if broken: + print(f"\nFAIL: {len(broken)} name(s) renamed but still looked up at runtime: {', '.join(broken)}") + return 1 + + print("\nOK: every by-name lookup points at a name Obfuscar preserved.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/client-exit-triage.ps1 b/Tools/client-exit-triage.ps1 new file mode 100644 index 0000000..559dd50 --- /dev/null +++ b/Tools/client-exit-triage.ps1 @@ -0,0 +1,38 @@ +# Triage the last Rec Room run: crash vs orderly exit. +# +# The Unity log ends without a shutdown sequence in both cases, so the Windows +# Application log is what separates a fault (Application Error / WER) or a freeze +# (Application Hang) from a clean process end. +# +# Usage: +# powershell -NoProfile -File tools/client-exit-triage.ps1 [-Minutes 60] + +param([int]$Minutes = 60) + +$since = (Get-Date).AddMinutes(-$Minutes) +Write-Host "== Application log events touching the client (last $Minutes min) ==" +Get-WinEvent -FilterHashtable @{ LogName = 'Application'; StartTime = $since } -ErrorAction SilentlyContinue | + Where-Object { $_.Message -match 'RecRoom|GameAssembly|UnityPlayer|breakpad' } | + Select-Object -First 12 | + ForEach-Object { + $first = ($_.Message -split [char]10 | Where-Object { $_ -and $_.Trim() }) | Select-Object -First 4 + Write-Host ("{0} id={1} {2}" -f $_.TimeCreated, $_.Id, $_.ProviderName) + $first | ForEach-Object { Write-Host (" " + $_.Trim()) } + } + +Write-Host "" +Write-Host "== WER reports for the client ==" +Get-ChildItem "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportArchive" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match 'RecRoom' -and $_.LastWriteTime -gt $since } | + Select-Object LastWriteTime, Name | + Format-Table -AutoSize + +Write-Host "" +Write-Host "== fresh minidumps ==" +@("$env:LOCALAPPDATA\Temp", "$env:LOCALAPPDATA\CrashDumps", "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportQueue") | + Where-Object { Test-Path $_ } | + ForEach-Object { + Get-ChildItem $_ -Recurse -Depth 2 -Include *.dmp, *.mdmp -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -gt $since } | + Select-Object LastWriteTime, Length, FullName + } | Format-Table -AutoSize diff --git a/Tools/diagnose-quest-tls.py b/Tools/diagnose-quest-tls.py new file mode 100644 index 0000000..e6d0692 --- /dev/null +++ b/Tools/diagnose-quest-tls.py @@ -0,0 +1,145 @@ +"""Build an unsigned, logging-only diagnostic APK for the exact Quest Unity binary. + +Preserves certificate decisions. Adds Android log probes after native X509 +verification and after the Unity verification callback. Never edits the input. +Run zipalign and sign with the installed test application's key afterward. +""" +import argparse +import hashlib +import struct +import zipfile +from pathlib import Path + +EXPECTED = 'd68dd33530dc9c6f963da17cad03d56fbaF51ac4bef297d05bf56684a44ee607'.lower() +CAVE = 0x125AE00 +LIMIT = 0x125B100 +LOG_PRINT = 0xBA9F0 + +def word(v): + return struct.pack('> 2) & 0x3FFFFFF)) + +def adr(pc, target, reg): + delta = target - pc + assert -(1 << 20) <= delta < (1 << 20) + return word(0x10000000 | ((delta & 3) << 29) | (((delta >> 2) & 0x7FFFF) << 5) | reg) + +def mov(dst, src, wide=True): + return word((0xAA0003E0 if wide else 0x2A0003E0) | (src << 16) | dst) + +def pair(a, b, offset, load=False): + return word((0xA9400000 if load else 0xA9000000) | ((offset // 8) << 15) | (b << 10) | (31 << 5) | a) + +def patch(binary, radicade_system_trust=False): + assert hashlib.sha256(binary).hexdigest() == EXPECTED, 'Unknown Unity binary' + out = bytearray(binary) + assert not any(out[CAVE:LIMIT]), 'Diagnostic space is occupied' + # Existing RX PT_LOAD ends before alignment padding; extend only into that + # verified empty padding, staying before the following RW PT_LOAD file range. + ph = struct.unpack_from(' +[CmdletBinding()] +param( + [string]$GameDir, + [Parameter(Mandatory = $true)][string]$OutDir, + [switch]$Zip, + [switch]$Force +) + +$ErrorActionPreference = 'Stop' + +# Reference values: the install that logs in and loads rooms. +$expected = @{ + AssemblySize = 167390208 + AssemblyMd5 = '11e14eb35671e2725bd0c78a6a91bb48' + MetadataSize = 38654794 + MetadataMd5 = 'cc9c32014b5fc3dd1a2a8fb12376be15' + InteropSize = 38654794 + InteropMd5 = '4a1a28078f957af470f3b31951b32648' + BootSize = 173 + BootMd5 = 'dc2e5f7af7d5df736a4cc3477b4b1a1f' + WinhttpSize = 26112 + WinhttpMd5 = 'b2a4d0cb04bf8f5a27ccab237ecc2586' + LoaderSize = 60928 + LoaderMd5 = '9430bb84349d52b753a181788a86c5e2' + PluginSize = 62464 + PluginMd5 = '8718a45dd42d7cb72b3e46e04b744d91' + InteropCount = 335 +} + +function Get-Md5 { + param([string]$Path) + return (Get-FileHash -LiteralPath $Path -Algorithm MD5).Hash.ToLower() +} + +function Assert-Piece { + param( + [string]$Label, + [string]$Path, + [long]$Size, + [string]$Md5 + ) + if (-not (Test-Path -LiteralPath $Path)) { + throw "$Label is missing: $Path" + } + $info = Get-Item -LiteralPath $Path + $actual = Get-Md5 $Path + if ($info.Length -ne $Size -or $actual -ne $Md5) { + $detail = "$Label does not match the reference: {0} b md5 {1} (expected {2} b md5 {3})" -f $info.Length, $actual, $Size, $Md5 + if ($Force) { Write-Host ("WARNING: " + $detail) } + else { throw ($detail + ' - fix it or pass -Force if you know this build is fine.') } + } else { + Write-Host (" ok {0}" -f $Label) + } +} + +# --- locate the source install ------------------------------------------------ + +if (-not $GameDir) { + $candidates = @( + (Join-Path $env:USERPROFILE 'Downloads\2023-08-29T06-53-33Z') + ) + $downloads = Join-Path $env:USERPROFILE 'Downloads' + if (Test-Path -LiteralPath $downloads) { + $candidates += (Get-ChildItem -LiteralPath $downloads -Directory -ErrorAction SilentlyContinue | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'RecRoom.exe') } | + ForEach-Object { $_.FullName }) + } + foreach ($c in $candidates) { + if (Test-Path -LiteralPath (Join-Path $c 'BepInEx\plugins\RadicadeAugustClient.dll')) { $GameDir = $c; break } + } +} +if (-not $GameDir) { $GameDir = Read-Host 'Path to the patched Rec Room folder (the one that works)' } +$GameDir = $GameDir.TrimEnd('\') + +$metadataDir = Join-Path $GameDir 'Recroom_Data\il2cpp_data\Metadata' +$bepInEx = Join-Path $GameDir 'BepInEx' + +Write-Host 'Verifying the source install against the reference hashes...' +Assert-Piece 'GameAssembly.dll (build identity)' (Join-Path $GameDir 'GameAssembly.dll') $expected.AssemblySize $expected.AssemblyMd5 +Assert-Piece 'global-metadata.dat (host literals patched)' (Join-Path $metadataDir 'global-metadata.dat') $expected.MetadataSize $expected.MetadataMd5 +Assert-Piece 'global-metadata.interop.dat' (Join-Path $metadataDir 'global-metadata.interop.dat') $expected.InteropSize $expected.InteropMd5 +Assert-Piece 'Recroom_Data\boot.config' (Join-Path $GameDir 'Recroom_Data\boot.config') $expected.BootSize $expected.BootMd5 +Assert-Piece 'winhttp.dll (Doorstop 4.3.0)' (Join-Path $GameDir 'winhttp.dll') $expected.WinhttpSize $expected.WinhttpMd5 +Assert-Piece 'BepInEx.Unity.IL2CPP.dll (patched loader)' (Join-Path $bepInEx 'core\BepInEx.Unity.IL2CPP.dll') $expected.LoaderSize $expected.LoaderMd5 +Assert-Piece 'RadicadeAugustClient.dll' (Join-Path $bepInEx 'plugins\RadicadeAugustClient.dll') $expected.PluginSize $expected.PluginMd5 + +if (-not (Test-Path -LiteralPath (Join-Path $GameDir 'doorstop_config.ini'))) { throw 'doorstop_config.ini is missing' } +$interopCount = (Get-ChildItem -LiteralPath (Join-Path $bepInEx 'interop') -File -ErrorAction SilentlyContinue).Count +if ($interopCount -lt $expected.InteropCount) { + throw ("BepInEx\interop holds {0} assemblies, expected at least {1} - interop generation was incomplete" -f $interopCount, $expected.InteropCount) +} +Write-Host (" ok BepInEx\interop ({0} assemblies)" -f $interopCount) +Write-Host 'Source install verified.' +Write-Host '' + +# --- assemble the bundle ------------------------------------------------------ + +$bundleRoot = Join-Path $OutDir 'RadicadeAugustClient' +if (Test-Path -LiteralPath $bundleRoot) { + if ($Force) { Remove-Item -LiteralPath $bundleRoot -Recurse -Force } + else { throw "$bundleRoot already exists - delete it or pass -Force to replace it." } +} +New-Item -ItemType Directory -Path $bundleRoot -Force | Out-Null + +Write-Host ("Copying into {0} ..." -f $bundleRoot) + +# Root-level loader pieces and the patched data files. +Copy-Item -LiteralPath (Join-Path $GameDir 'winhttp.dll') -Destination $bundleRoot +Copy-Item -LiteralPath (Join-Path $GameDir 'doorstop_config.ini') -Destination $bundleRoot +foreach ($name in @('.doorstop_version', 'changelog.txt')) { + $p = Join-Path $GameDir $name + if (Test-Path -LiteralPath $p) { Copy-Item -LiteralPath $p -Destination $bundleRoot } +} + +$dataTarget = Join-Path $bundleRoot 'Recroom_Data' +New-Item -ItemType Directory -Path $dataTarget -Force | Out-Null +Copy-Item -LiteralPath (Join-Path $GameDir 'Recroom_Data\boot.config') -Destination $dataTarget +foreach ($name in @('radicade-patch.json')) { + $p = Join-Path $GameDir ('Recroom_Data\' + $name) + if (Test-Path -LiteralPath $p) { Copy-Item -LiteralPath $p -Destination $dataTarget } +} +$metaTarget = Join-Path $dataTarget 'il2cpp_data\Metadata' +New-Item -ItemType Directory -Path $metaTarget -Force | Out-Null +foreach ($name in @('global-metadata.dat', 'global-metadata.interop.dat')) { + Copy-Item -LiteralPath (Join-Path $metadataDir $name) -Destination $metaTarget +} + +# The whole BepInEx tree, minus logs, sidecar backups and caches. +$bepTarget = Join-Path $bundleRoot 'BepInEx' +# Excluded: logs, sidecar backups of previous builds, and the diagnostic RVA map the +# plugin writes (18 MB of local crash-analysis output). +robocopy $bepInEx $bepTarget /E /NFL /NDL /NJH /NJS /NP ` + /XF 'LogOutput.log' '*.log' '*.before-*' '*.tracer-*' '*.march-backup' '*.baseline-disabled' ` + 'radicade-addr-map.txt' | Out-Null +if ($LASTEXITCODE -ge 8) { throw ("robocopy failed with exit code {0}" -f $LASTEXITCODE) } +$global:LASTEXITCODE = 0 + +# The triage script travels with the bundle so the friend can produce a report. +$triage = Join-Path $PSScriptRoot 'august-client-triage.ps1' +if (Test-Path -LiteralPath $triage) { Copy-Item -LiteralPath $triage -Destination $bundleRoot } + +$assemblyInfo = Get-Item -LiteralPath (Join-Path $GameDir 'GameAssembly.dll') +$readme = @() +$readme += 'Radicade August 2023 client patch' +$readme += '' +$readme += 'This bundle carries the client half of a Rec Room 2023-08-29 (game version 20230825,' +$readme += 'Unity 2020.3.33f1) setup pointed at a Radicade server on YOUR OWN machine.' +$readme += '' +$readme += 'It only works on the identical game build. Check yours before copying anything:' +$readme += (" GameAssembly.dll must be {0} bytes, md5 {1}" -f $assemblyInfo.Length, $expected.AssemblyMd5) +$readme += '' +$readme += 'Install, into the folder that holds RecRoom.exe:' +$readme += ' 1. copy winhttp.dll, doorstop_config.ini and .doorstop_version into the game root' +$readme += ' 2. copy BepInEx\ next to them' +$readme += ' 3. copy Recroom_Data\boot.config and Recroom_Data\il2cpp_data\Metadata\*.dat over the' +$readme += ' existing files (keep copies of the originals; global-metadata.dat.orig is the' +$readme += ' patcher''s own backup and must stay untouched)' +$readme += ' 4. rename any winhttp.dll.baseline-disabled you already have out of the way' +$readme += ' 5. start your Radicade server first - https://localhost:3030 plus http://127.0.0.1:3031,' +$readme += ' ASPNETCORE_ENVIRONMENT=Development - and only then launch the game' +$readme += '' +$readme += 'Why the server has to be local: the patched global-metadata.dat rewrites the client''s' +$readme += 'bootstrap literals to https://localhost:3030, so a remote server cannot answer them.' +$readme += '' +$readme += 'Then check the install with the bundled triage script:' +$readme += ' powershell -ExecutionPolicy Bypass -File august-client-triage.ps1' +$readme += 'It prints one PASS/MISSING line per required piece and writes august-client-report.txt.' +$readme += '' +$readme += 'The line "Failed to load Type RecRoom.Analytics.Statsig.StatsigDebugOverrides" in' +$readme += 'Player.log is NORMAL and appears in a completely unpatched launch of this build too.' +$readme += 'It is never the reason a patched install fails.' +$readme += '' +$readme += 'Pieces and their reference hashes (the triage script checks these for you):' +$readme += ' GameAssembly.dll 167390208 b 11e14eb35671e2725bd0c78a6a91bb48' +$readme += ' Recroom_Data/il2cpp_data/Metadata/global-metadata.dat' +$readme += ' 38654794 b cc9c32014b5fc3dd1a2a8fb12376be15' +$readme += ' Recroom_Data/il2cpp_data/Metadata/global-metadata.interop.dat' +$readme += ' 38654794 b 4a1a28078f957af470f3b31951b32648' +$readme += ' Recroom_Data/boot.config 173 b dc2e5f7af7d5df736a4cc3477b4b1a1f' +$readme += ' winhttp.dll 26112 b b2a4d0cb04bf8f5a27ccab237ecc2586' +$readme += ' BepInEx/core/BepInEx.Unity.IL2CPP.dll' +$readme += ' 60928 b 9430bb84349d52b753a181788a86c5e2' +$readme += ' BepInEx/plugins/RadicadeAugustClient.dll' +$readme += ' 62464 b 8718a45dd42d7cb72b3e46e04b744d91' +$readme += ' BepInEx/interop/ 335 assemblies' +$readme += '' +Set-Content -LiteralPath (Join-Path $bundleRoot 'README-FRIEND.txt') -Value ($readme -join "`r`n") -Encoding UTF8 + +$size = (Get-ChildItem -LiteralPath $bundleRoot -Recurse -File | Measure-Object -Property Length -Sum).Sum +Write-Host ('Bundle assembled: {0} ({1:N1} MB)' -f $bundleRoot, ($size / 1MB)) + +if ($Zip) { + $zipPath = $bundleRoot.TrimEnd('\') + '.zip' + if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force } + Write-Host ('Compressing to {0} (this takes a while)...' -f $zipPath) + Compress-Archive -Path (Join-Path $bundleRoot '*') -DestinationPath $zipPath -CompressionLevel Optimal + Write-Host ('Zip written: {0} ({1:N1} MB)' -f $zipPath, ((Get-Item -LiteralPath $zipPath).Length / 1MB)) +} + +Write-Host '' +Write-Host 'Remind the friend that he still needs the game build above and his own Radicade server.' diff --git a/Tools/package-quest-obb.py b/Tools/package-quest-obb.py new file mode 100644 index 0000000..783656f --- /dev/null +++ b/Tools/package-quest-obb.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Rebuild an Android expansion OBB from the matching Apktool-decoded tree.""" +from __future__ import annotations + +import argparse +import os +import sys +import tempfile +import zipfile +from pathlib import Path + + +def read_no_compress(config: Path) -> tuple[str, list[str]]: + lines = config.read_text(encoding="utf-8").splitlines() + name = next((line.split(":", 1)[1].strip().strip("'\"") + for line in lines if line.startswith("apkFileName:")), None) + if not name: + raise ValueError(f"{config} has no apkFileName") + patterns: list[str] = [] + in_list = False + for line in lines: + if line == "doNotCompress:": + in_list = True + continue + if in_list and line.startswith("- "): + patterns.append(line[2:].strip().strip("'\"").replace("\\", "/")) + elif in_list and line and not line.startswith(" "): + break + if not patterns: + raise ValueError(f"{config} has no doNotCompress list") + return name, patterns + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("decoded_obb", help="decoded OBB root containing apktool.yml and assets/") + parser.add_argument("output", help="path for the rebuilt .obb") + parser.add_argument("--check-only", action="store_true", help="validate inputs and print a packing summary") + args = parser.parse_args() + + root = Path(args.decoded_obb).resolve() + config = root / "apktool.yml" + assets = root / "assets" + if not config.is_file() or not assets.is_dir(): + print("decoded OBB must contain apktool.yml and assets/", file=sys.stderr) + return 2 + output = Path(args.output).resolve() + if output.exists() and not args.check_only: + print(f"refusing to overwrite existing output: {output}", file=sys.stderr) + return 2 + + try: + obb_name, patterns = read_no_compress(config) + files = sorted((path for path in assets.rglob("*") if path.is_file()), + key=lambda path: path.relative_to(root).as_posix()) + names = {path.relative_to(root).as_posix(): path for path in files} + exact = {pattern for pattern in patterns if pattern in names} + extension_rules = {pattern.lower().lstrip(".") for pattern in patterns if "/" not in pattern and pattern not in names} + unknown = [pattern for pattern in patterns if pattern not in names and pattern not in extension_rules] + if unknown: + raise ValueError(f"unmatched doNotCompress entries: {unknown[:8]}") + stored = {name for name in names if name in exact or + Path(name).suffix.lower().lstrip(".") in extension_rules} + matched_extensions = {Path(name).suffix.lower().lstrip(".") for name in names + if Path(name).suffix.lower().lstrip(".") in extension_rules} + missing_extensions = sorted(extension_rules - matched_extensions) + if missing_extensions: + raise ValueError(f"doNotCompress extensions have no matching file: {missing_extensions}") + uncompressed_bytes = sum(path.stat().st_size for name, path in names.items() if name in stored) + total_bytes = sum(path.stat().st_size for path in files) + if output.name != obb_name: + raise ValueError(f"output filename must be {obb_name!r}, not {output.name!r}") + except (OSError, ValueError) as exc: + print(f"cannot package decoded OBB: {exc}", file=sys.stderr) + return 2 + + print(f"source: {root}") + print(f"output: {output}") + print(f"entries: {len(files):,}; uncompressed bytes: {total_bytes:,}") + print(f"stored entries: {len(stored):,}; stored bytes: {uncompressed_bytes:,}; deflated entries: {len(files) - len(stored):,}") + if args.check_only: + return 0 + + output.parent.mkdir(parents=True, exist_ok=True) + fd, temporary_name = tempfile.mkstemp(prefix=output.name + ".", suffix=".tmp", dir=output.parent) + os.close(fd) + temporary = Path(temporary_name) + try: + with zipfile.ZipFile(temporary, "w", allowZip64=True, compression=zipfile.ZIP_DEFLATED, + compresslevel=6) as archive: + for path in files: + name = path.relative_to(root).as_posix() + archive.write(path, arcname=name, + compress_type=zipfile.ZIP_STORED if name in stored else zipfile.ZIP_DEFLATED, + compresslevel=None if name in stored else 6) + # Validate every CRC and path before publishing the archive at its final name. + with zipfile.ZipFile(temporary, "r") as archive: + bad_entry = archive.testzip() + if bad_entry is not None: + raise ValueError(f"archive CRC check failed at {bad_entry}") + if set(archive.namelist()) != set(names): + raise ValueError("archive entry list differs from the decoded assets tree") + os.replace(temporary, output) + except Exception: + temporary.unlink(missing_ok=True) + raise + + print(f"created {output} ({output.stat().st_size:,} bytes); CRC and entry-list checks passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Tools/patch-0831-client-metadata.py b/Tools/patch-0831-client-metadata.py new file mode 100644 index 0000000..30ccc9e --- /dev/null +++ b/Tools/patch-0831-client-metadata.py @@ -0,0 +1,785 @@ +#!/usr/bin/env python3 +"""Point a Rec Room 20230831 client at a local Radicade server. + +Rewrites the rec.net bootstrap URLs that the client hardcodes as IL2CPP string +literals, so every service base it reads from GET /ns on the nameserver lands on +the local server. Same technique the 20230825 Radicade patch used: the rebuilt +literal blob is relaid at EOF and only the four header fields that describe it are +repointed, so every other byte of the metadata stays identical. + +The shuffled-header field positions differ per release and are derived +structurally (see .tmp-newbuild/find_layout.py, chain.py); for 20230831 they are +literal table offset 0x74 / size 0x24, literal data offset 0xA0 / size 0x3C. + +Usage: + python tools/patch-0831-client-metadata.py [--server https://ns.radicade.app] [--apply] + +Dry-run by default. Originals are kept as global-metadata.dat.orig on first patch. + +An install already patched for the previous Radicade address is re-pointed +directly: that host sits in the string table verbatim, so the rules below rewrite +it and no pristine copy is needed. Anything else - an install patched by a +different tool, or with the address baked in some other way - still needs the +pristine copy FIRST, because the replacement is applied to the file as it stands +and would otherwise find nothing to rewrite and silently keep its old bootstrap +host: + + cp global-metadata.dat.orig global-metadata.dat + python tools/patch-0831-client-metadata.py --server --apply + +--bake writes the one file worth handing out. Relocating leaves the original +literal blob in the file (dead once the header is repointed) and appends a +verbatim copy of it, so the stock Rec Room hosts survive inside the metadata twice +and the file grows by the size of the blob. Baked mode rebuilds the blob inside +its own region instead: every record gets a fresh (length, index) pair into a +repacked blob holding only the final strings, the stock hosts are gone from the +file, and the file keeps its exact original size. The whole region is literal +data and nothing else - 99.9999% of its bytes are covered by the records' own +ranges - which is what makes rewriting it in place safe; the run refuses if that +coverage check fails. The records keep their table order, because the client's own +plugins find the literal table by that shape rather than by any header field, and +the bake is checked by running their detector (2023Patch/RadicadeEacBypass/ +ImageSignatureRequest.cs) over the result. It reads the pristine copy when one is +present, so a single +invocation turns either a stock install or an already-patched one into the same +canonical file: + + python tools/patch-0831-client-metadata.py --bake --apply + +The file that comes out is the whole fix, so it is also what to hand to someone +else - same 20230831 build, drop it in place of their global-metadata.dat. Use +--out to write it somewhere other than the install (which is also how to bake +while the game is running, since a running Radicade holds the metadata open): + + python tools/patch-0831-client-metadata.py --bake --apply --out out/global-metadata.dat +""" +import argparse +import hashlib +import json +import os +import re +import shutil +import sys + +HEADER_FIELDS = { + "stringLiteral": (0x74, 0x24), + "stringLiteralData": (0xA0, 0x3C), +} + +KEEP = [ + "Upload to rec.net", + "rec.net/terms", + "rec.net/privacy", + "rec.net/rules", + "rec.net/parents", + "help.rec.net", + "api.steampowered.com", + "store.steampowered.com", + "firebase-settings.crashlytics.com", + "app-measurement.com", +] + +# Static, server-independent replacements for the client's own branding/support +# strings and links. The 20230831 build keeps these in the same il2cpp string +# table as the service hosts (a contiguous block of legal/community URLs around +# "rec.net"/"recroom.com", plus one support-host sentence spread across ~34 +# literals). Longest source wins, so the /home URL is retargeted before the +# bare-host rule rewrites the rest. +TEXT_RULES = [ + ("https://recroom.zendesk.com/home", "https://support.radicade.app/"), + ("recroom.zendesk.com", "support.radicade.app"), + ("https://www.recroom.com/privacy-policy/", "https://radicade.app/privacy-policy/"), + ("https://recroom.com/terms-of-service", "https://radicade.app/terms-of-service/"), + ("https://www.recroom.com/community/", "https://radicade.app/community/"), + # The rest of Rec Room's public pages and both of its support mailboxes: the + # Radicade site serves them at the same path, so these two catch-alls finish + # any page not named above. Rules run longest source first, so the named ones + # win and keep their exact destinations. + ("https://www.recroom.com/", "https://radicade.app/"), + ("https://recroom.com/", "https://radicade.app/"), + ("tokens@recroom.com", "tokens@radicade.app"), + ("support@recroom.com", "support@radicade.app"), +] + + +def build_rules(server: str): + b = server.rstrip("/") + return [ + ("https://ns.rec.net", b), + ("https://auth.rec.net", b), + ("https://api.rec.net", b), + ("https://accounts.rec.net", b), + ("https://cdn.rec.net", b), + ("https://notifications.rec.net", b), + ("https://www.rec.net", b), + ("http://cdn.rec.net", b), + ("http://img.rec.net/", f"{b}/images/img/"), + ("https://img.rec.net", f"{b}/images/img"), + ("https://images.rec.net", f"{b}/images/img"), + ("https://rec.net/shop/subscriptions", f"{b}/shop/subscriptions"), + ("https://rec.net/shop", f"{b}/shop"), + ("https://rec.net/{0}", f"{b}/{{0}}"), + ("https://rec.net", b), + ("rec.net/creator", f"{b.split('://')[-1]}/creator"), + (".rec.net", ".radicade.internal"), + ("rec.net", b), + # The previous Radicade address, so an install patched for it is re-pointed in + # one pass instead of needing the pristine copy. Longest source wins, exactly as + # above, and every destination matches what the rec.net rules above produce for + # the same literal - so an install patched from the pristine copy and one patched + # from the previous address converge on the same string table. + ("https://radicade.example.com/images/img", f"{b}/images/img"), + ("https://radicade.example.com/shop/subscriptions", f"{b}/shop/subscriptions"), + ("https://radicade.example.com/shop", f"{b}/shop"), + ("https://radicade.example.com/{0}", f"{b}/{{0}}"), + ("https://radicade.example.com", b), + ("http://radicade.example.com", b), + ("radicade.example.com/creator", f"{b.split('://')[-1]}/creator"), + ("radicade.example.com", b), + ] + + +# Substring sharing makes a repacked blob comfortably smaller than the original +# dense one (1,567,053 of 1,657,800 bytes), which is the headroom that lets the +# rebuild land inside the same region. Overlaps past this length are not worth +# probing for; the longest one that matters here is a shared https:// prefix. +SHARE_LIMIT = 1024 + + +def read_u32(buf, off: int) -> int: + return int.from_bytes(buf[off:off + 4], "little") + + +def read_i32(buf, off: int) -> int: + return int.from_bytes(buf[off:off + 4], "little", signed=True) + + +def resolve_metadata(game_root: str): + """-> (data_dir, path to the live global-metadata.dat)""" + data_dir = next( + (name for name in ("RecRoom_Data", "Radicade_Data") + if os.path.isdir(os.path.join(game_root, name))), + "RecRoom_Data", + ) + return data_dir, os.path.join(game_root, data_dir, "il2cpp_data", "Metadata", "global-metadata.dat") + + +def place_literals(buf, off, literals): + """Add literals to buf (in the order given), reusing any that already appears + in it, and record where each one landed in off. Mutates and returns both. + + A record's (length, index) pair is self-contained, so a literal may be placed + anywhere, including the middle of another one - that is what makes this pick + up the prefix sharing (https://ns.radicade.app inside .../shop) that IL2CPP's + own, suffix-only packer leaves on the table. + """ + for lit in literals: + if not lit or lit in off: + continue + found = buf.find(lit) + if found >= 0: + off[lit] = found + continue + used = len(buf) + # longest suffix of the buffer that is a prefix of the literal + lo, hi, tail = 1, min(len(lit), used, SHARE_LIMIT), 0 + while lo <= hi: + mid = (lo + hi) // 2 + if bytes(buf[used - mid:]) == lit[:mid]: + tail = mid + lo = mid + 1 + else: + hi = mid - 1 + if tail: + off[lit] = used - tail + buf += lit[tail:] + else: + off[lit] = used + buf += lit + return buf, off + + +def layout_blob(final, data_size): + """Lay the literals out in the shape the game's own build writes them in. + + IL2CPP writes this blob in table order, each record starting exactly where the + previous one ended, and the client finds the table again by that shape: + RadicadeEacBypass/ImageSignatureRequest.cs probes the first 8 and the first + 1024 records and wants >=99% of them to chain before it will read the image key + id (KEY:RSA:p1...) out of the ".rec.net"/".radicade.internal" literal it finds + there. A blob packed purely for size - every literal placed wherever it shared + most - stops looking like a literal table at all, so the key id comes back + empty, image requests lose their ?rcsig, and the client is killed on the next + scene load. So records that fit stay in table order and only the tail that does + not fit is packed for size. + + -> (blob, offset of every record, how many records stayed in table order) + """ + count = len(final) + ordered = [b or b"" for b in final] + tail = 0 + while True: + keep = count - tail + blob = bytearray() + placements = [0] * count + off = {} + for i in range(keep): + placements[i] = len(blob) + if ordered[i]: + blob += ordered[i] + off.setdefault(ordered[i], placements[i]) + place_literals(blob, off, sorted( + {ordered[i] for i in range(keep, count) if ordered[i]}, + key=lambda b: (-len(b), b), + )) + for i in range(keep, count): + if ordered[i]: + placements[i] = off[ordered[i]] + if len(blob) <= data_size or tail >= count: + return bytes(blob), placements, keep + tail = 64 if tail == 0 else tail * 2 + + +def final_bytes_per_record(data, table_off, count, data_off, data_size, rules): + """-> (bytes each record should expose, indices of the records we preserved). + + None marks a record that exposes nothing (the build's one empty literal), and + a bail-out message comes back instead when a record cannot be resolved: a + record whose range is broken would have to keep an index into a blob we are + about to overwrite, so those fail the bake rather than get a guess. + """ + final = [] + preserved = [] + for i in range(count): + pos = table_off + i * 8 + length = read_u32(data, pos) + index = read_i32(data, pos + 4) + if length == 0: + final.append(None) + continue + if index < 0 or index + length > data_size: + return None, None, f"literal #{i} has an unusable range ({length}, {index})" + raw = bytes(data[data_off + index: data_off + index + length]) + try: + text = raw.decode("utf-8") + except UnicodeDecodeError: + return None, None, f"literal #{i} is not UTF-8; it cannot be repacked" + if any(k.lower() in text.lower() for k in KEEP): + final.append(raw) + preserved.append(i) + continue + new = text + for frm, to in rules: + if frm in new: + new = new.replace(frm, to) + final.append(new.encode("utf-8")) + return final, preserved, None + + +def bake_metadata(data, table_off, table_size, data_off, data_size, rules): + """Rebuild the literal blob inside its own region. + + -> (out, report, error) where the report carries the per-record bytes, the + ranges we deliberately preserved, the packed blob size and how many literals + the rules actually rewrote. + """ + count = table_size // 8 + final, preserved, err = final_bytes_per_record(data, table_off, count, data_off, data_size, rules) + if err: + return None, None, err + + # Table order first, and never set iteration order for what is packed: an + # unstable order would let PYTHONHASHSEED change the layout, so two runs on the + # same input would produce different bytes and no one could check a copy by hash. + blob, placements, contiguous = layout_blob(final, data_size) + if len(blob) > data_size: + return None, None, ( + f"repacked blob needs {len(blob):,} bytes but the region holds {data_size:,}" + ) + if contiguous < min(count, 1024): + return None, None, ( + f"only {contiguous:,} of {count:,} literals could stay in table order; the " + "client finds the literal table by that shape, so this file would not load" + ) + + out = bytearray(data) + out[data_off:data_off + len(blob)] = blob + out[data_off + len(blob):data_off + data_size] = b"\x00" * (data_size - len(blob)) + for i, b in enumerate(final): + pos = table_off + i * 8 + if b is None: + out[pos:pos + 4] = (0).to_bytes(4, "little") + out[pos + 4:pos + 8] = (0).to_bytes(4, "little", signed=True) + continue + out[pos:pos + 4] = len(b).to_bytes(4, "little") + out[pos + 4:pos + 8] = placements[i].to_bytes(4, "little", signed=True) + rewritten = sum( + 1 for i, b in enumerate(final) + if b is not None + and bytes(data[data_off + read_i32(data, table_off + i * 8 + 4): + data_off + read_i32(data, table_off + i * 8 + 4) + read_u32(data, table_off + i * 8)]) != b + ) + report = { + "final": final, + # file offsets, so they line up with the positions the host scan reports + "kept_ranges": [(data_off + placements[i], len(final[i])) for i in preserved], + "blob_size": len(blob), + "contiguous": contiguous, + "rewritten": rewritten, + } + return out, report, None + + +# --------------------------------------------------------------------------- +# The client's own table detection, ported from +# 2023Patch/RadicadeEacBypass/ImageSignatureRequest.cs. +# +# This metadata has no magic/version at 0/4, so the client takes the header-scan +# path: it builds candidates out of every plausible (offset, size) pair in the +# first 0x100 header bytes, keeps the ones whose records chain end-to-end for the +# first 8 and the first 1024 records, and reads the image key id (KEY:RSA:p1...) out +# of the ".rec.net"/".radicade.internal" literal it finds there. Running this after +# a bake is the point: it is the check that a repacked blob can pass every one of +# our own and still fail, and the failure is silent - no key id, no ?rcsig on image +# requests, and the client is killed on the next scene load. +# --------------------------------------------------------------------------- +DETECTOR_HEADER_SCAN_LIMIT = 0x100 +DETECTOR_MINIMUM_RECORDS = 32 +DETECTOR_PREFIX_PROBE = 8 +DETECTOR_PACKING_PROBE = 1024 +DETECTOR_MAX_LITERAL_LENGTH = 1 << 20 + + +def detector_record(data, table_off, record): + pos = table_off + 8 * record + return read_u32(data, pos), read_i32(data, pos + 4) + + +def looks_like_literal_table(data, table_off, count, data_off, data_size, probe): + examined = min(count, probe) + contiguous = 0 + running = 0 + for record in range(examined): + length, index = detector_record(data, table_off, record) + if length < 0 or length > DETECTOR_MAX_LITERAL_LENGTH or index < 0 \ + or index + length > data_size: + return False + if index == running: + contiguous += 1 + running = index + length + return contiguous >= examined - max(1, examined // 100) + + +def sample_decodes_as_text(data, table_off, count, data_off, data_size): + step = max(1, count // 48) + for record in range(0, count, step): + length, index = detector_record(data, table_off, record) + if length < 0 or index < 0 or index + length > data_size: + return False + if length == 0: + continue + try: + bytes(data[data_off + index:data_off + index + length]).decode("utf-8") + except UnicodeDecodeError: + return False + return True + + +def scan_key_tails(data, table_off, count, data_off, data_size): + """The one literal the client turns into its image key id.""" + key = None + for record in range(count): + length, index = detector_record(data, table_off, record) + if length < 0 or index < 0 or index + length > data_size: + return None + if length != 8 and length != 18: + continue + literal = bytes(data[data_off + index:data_off + index + length]).decode("utf-8", "replace") + if literal != ".rec.net" and literal != ".radicade.internal": + continue + candidate = "KEY:RSA:p1" + literal + if key is not None and key != candidate: + return None + key = candidate + return key + + +def detect_key_id_from_game_metadata(data): + """-> the image key id the client's plugin derives from this metadata, or None.""" + if len(data) < 24: + return None + if read_u32(data, 0) == 0xFAB11BAF and read_i32(data, 4) == 27: + table, size = read_i32(data, 8), read_i32(data, 12) + data_off, data_size = read_i32(data, 16), read_i32(data, 20) + if (table < 24 or size < 0 or size % 8 != 0 or table + size > len(data) + or data_off < 24 or data_size < 0 or data_off + data_size > len(data)): + return None + return scan_key_tails(data, table, size // 8, data_off, data_size) + + fields = [] + for off in range(0, DETECTOR_HEADER_SCAN_LIMIT, 4): + value = read_i32(data, off) + if 0 < value <= len(data) and value not in fields: + fields.append(value) + tables = sorted( + ((t, s // 8) for t in fields for s in fields + if s % 8 == 0 and s // 8 >= DETECTOR_MINIMUM_RECORDS + and t >= DETECTOR_HEADER_SCAN_LIMIT and t + s <= len(data)), + key=lambda t: -t[1], + ) + slices = [(d, s) for d in fields for s in fields + if s > 0 and d >= DETECTOR_HEADER_SCAN_LIMIT and d + s <= len(data)] + for table_off, count in tables: + for data_off, data_size in slices: + if not looks_like_literal_table(data, table_off, count, data_off, data_size, + DETECTOR_PREFIX_PROBE): + continue + if not sample_decodes_as_text(data, table_off, count, data_off, data_size): + continue + if not looks_like_literal_table(data, table_off, count, data_off, data_size, + DETECTOR_PACKING_PROBE): + continue + key = scan_key_tails(data, table_off, count, data_off, data_size) + if key is not None: + return key + return None + + +def verify_baked(data, out, table_off, table_size, data_off, data_size, final, header_fields): + """Read the baked file back the way the loader will. -> list of failures.""" + count = table_size // 8 + bad = [] + for i in range(count): + pos = table_off + i * 8 + length = read_u32(out, pos) + text = bytes(out[data_off + read_i32(out, pos + 4): data_off + read_i32(out, pos + 4) + length]) + want = final[i] or b"" + if text != want: + bad.append("literal #%d does not read back" % i) + _, data_size_f = HEADER_FIELDS["stringLiteralData"] + # everything outside the literal region, its table and the resized size field + # must be byte-identical: the bake moves no other structure. + def blanked(buf): + copy = bytearray(buf) + copy[data_off:data_off + data_size] = b"\x00" * data_size + copy[table_off:table_off + table_size] = b"\x00" * table_size + copy[data_size_f:data_size_f + 4] = b"\x00" * 4 + return copy + + want, got = blanked(data), blanked(out) + for off in range(min(len(want), len(got))): + if want[off] != got[off]: + bad.append("byte 0x%X outside the literal data and its table changed" % off) + break + if len(out) != len(data): + bad.append("file size changed (%d -> %d)" % (len(data), len(out))) + return bad + + +def scan_hosts(out, pattern, kept_ranges, region): + """Hits of a bytes pattern in the baked file, split by the literal-data region. + + -> (inside the literal data, elsewhere). A hit inside a literal we deliberately + preserve (KEEP) is not a leftover, so those ranges are exempted. + """ + inside, outside = [], [] + for match in re.finditer(pattern, out): + if any(start <= match.start() and match.end() <= start + length + for start, length in kept_ranges): + continue + (inside if match.start() in region else outside).append( + (match.start(), bytes(match.group(0)))) + return inside, outside + + +def bake_main(args, data_dir, metadata) -> int: + rules = sorted(build_rules(args.server) + TEXT_RULES, key=lambda r: -len(r[0])) + pristine = metadata + ".orig" + source = pristine if os.path.isfile(pristine) else metadata + note = "" if source == pristine else " (no pristine copy beside the live file)" + print(f"source : {source}{note}") + data = bytearray(open(source, "rb").read()) + + table_off, table_size = read_u32(data, HEADER_FIELDS["stringLiteral"][0]), read_u32(data, HEADER_FIELDS["stringLiteral"][1]) + data_off, data_size = read_u32(data, HEADER_FIELDS["stringLiteralData"][0]), read_u32(data, HEADER_FIELDS["stringLiteralData"][1]) + count = table_size // 8 + print(f"size : {len(data):,}") + print(f"literal table: 0x{table_off:X} ({table_off:,}) records {count:,}") + print(f"literal data : 0x{data_off:X} ({data_off:,}) size {data_size:,}") + + # The region is only safe to overwrite if the records account for all of it: + # that is what proves it holds literal data and nothing else. + coverage = bytearray(data_size) + for i in range(count): + pos = table_off + i * 8 + length, index = read_u32(data, pos), read_i32(data, pos + 4) + if length == 0 or index < 0 or index + length > data_size: + continue + coverage[index:index + length] = b"\x01" * length + covered = coverage.count(1) + print(f"region coverage: {covered:,}/{data_size:,} bytes ({100.0 * covered / data_size:.4f}%)") + if covered < data_size * 0.999: + print("refusing to bake: the literal data region is not accounted for by the records", file=sys.stderr) + return 1 + + out, report, err = bake_metadata(data, table_off, table_size, data_off, data_size, rules) + if err: + print(f"bake failed: {err}", file=sys.stderr) + return 1 + final, kept_ranges = report["final"], report["kept_ranges"] + blob_size = report["blob_size"] + print(f"literals : {report['rewritten']:,} rewritten by the rules, {len(final):,} repacked into the region") + print(f"repacked blob: {blob_size:,} bytes ({data_size - blob_size:,} reclaimed, tail zeroed)") + print(f"table shape : {report['contiguous']:,}/{count:,} literals kept in table order, " + f"{count - report['contiguous']:,} shared at the end of the blob") + + region = range(data_off, data_off + data_size) + bad = verify_baked(data, out, table_off, table_size, data_off, data_size, final, HEADER_FIELDS) + sources = b"|".join(re.escape(frm.encode("utf-8")) for frm, _ in rules) + inside, outside = scan_hosts(out, sources, kept_ranges, region) + # Independently of the rule list: every Rec Room service host lives under + # rec.net, and in stock metadata lowercase rec.net only ever appears as one of + # those hosts, so anything still matching here is a bootstrap address the rules + # do not know about. + missed, _ = scan_hosts(out, rb"rec\.net", kept_ranges, region) + inside += missed + if inside: + bad.append("%d stock host reference(s) survived in the literal data, first at 0x%X: %r" + % (len(inside), inside[0][0], inside[0][1])) + + key_id = detect_key_id_from_game_metadata(bytes(out)) + if key_id is None: + bad.append("the client's own key-id detection finds nothing in this file " + "(ImageSignatureRequest.cs), so image requests would lose their ?rcsig") + else: + was = detect_key_id_from_game_metadata(bytes(data)) + moved = f" (was {was})" if was and was != key_id else "" + print(f"image key id : {key_id}{moved}, derived the way the client derives it") + if bad: + for line in bad[:5]: + print(f"verification failed: {line}", file=sys.stderr) + return 1 + print(f"verification : {count:,} literals read back; header, other tables and size untouched") + print(f"service hosts: none left in the literal data ({len(rules)} rule patterns + any *.rec.net)") + if outside: + kinds = sorted({text.decode("utf-8", "replace") for _, text in outside}) + print(f"note : {len(outside)} reference(s) to {', '.join(kinds)} remain outside the") + print(" literal data, in the client's UI string table. They are display text") + print(" and do not decide which server the client contacts, so they are left") + print(" byte-for-byte alone.") + pages = sorted({b.decode("utf-8") for b in final if b and re.search(rb"(?i)recroom\.com", b)}) + if pages: + print(f"note : {len(pages)} literal(s) still name a recroom.com page or address. Those") + print(" are browser links and support emails, not hosts the client connects to,") + print(" and there is no Radicade equivalent recorded for them:") + for text in pages[:8]: + print(f" {text}") + if len(pages) > 8: + print(f" ... and {len(pages) - 8} more") + + if not args.apply: + print("\ndry run - pass --apply to write") + return 0 + + if args.out: + open(args.out, "wb").write(bytes(out)) + print(f"written: {args.out}") + print("drop it in place of global-metadata.dat; the existing radicade-patch.json still") + print("names this server, so the client's own check stays satisfied") + return 0 + + backup = metadata + ".bak-" + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M%S") + shutil.copy2(metadata, backup) + try: + with open(metadata, "wb") as handle: + handle.write(bytes(out)) + except OSError as ex: + print(f"\ncannot write {metadata}: {ex}", file=sys.stderr) + print("the game keeps the metadata open while it runs - close Radicade and re-run,", file=sys.stderr) + print("or use --out to produce the file somewhere else and drop it in", file=sys.stderr) + return 1 + if source == metadata and not os.path.exists(pristine): + shutil.copy2(backup, pristine) + print(f"pristine kept: {pristine}") + marker = os.path.join(args.game_root, data_dir, "radicade-patch.json") + json.dump({ + "patchedAtUtc": __import__("datetime").datetime.utcnow().isoformat() + "Z", + "gameVersion": "20230831", + "serverUrl": args.server, + "mode": "baked", + "metadata": { + "changedLiterals": report["rewritten"], + "repackedLiterals": sum(1 for b in final if b), + "oldDataSize": data_size, + "newDataSize": blob_size, + "headerSha256": hashlib.sha256(bytes(out[:256])).hexdigest(), + "imageKeyId": key_id, + "literalOrder": report["contiguous"], + "literalTableField": f"0x{HEADER_FIELDS['stringLiteral'][0]:02x}", + "literalDataField": f"0x{HEADER_FIELDS['stringLiteralData'][0]:02x}", + }, + }, open(marker, "w"), indent=2) + print(f"written: {metadata}\nbackup : {backup}\nmarker : {marker}") + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("game_root") + ap.add_argument("--server", default="https://ns.radicade.app") + ap.add_argument("--bake", action="store_true", + help="rebuild the literal blob in place: one canonical file with no stock hosts left in it") + ap.add_argument("--out", default=None, + help="write the baked metadata here instead of into the install, to hand the file out") + ap.add_argument("--apply", action="store_true") + args = ap.parse_args() + + data_dir, metadata = resolve_metadata(args.game_root) + if not os.path.isfile(metadata): + print(f"not a Rec Room install: {metadata} missing", file=sys.stderr) + return 1 + if args.bake: + return bake_main(args, data_dir, metadata) + + data = bytearray(open(metadata, "rb").read()) + n = len(data) + table_off_f, table_size_f = HEADER_FIELDS["stringLiteral"] + data_off_f, data_size_f = HEADER_FIELDS["stringLiteralData"] + + def u32(byte: int) -> int: + return int.from_bytes(data[byte:byte + 4], "little") + + table_off, table_size = u32(table_off_f), u32(table_size_f) + data_off, data_size = u32(data_off_f), u32(data_size_f) + count = table_size // 8 + print(f"metadata : {metadata}") + print(f"size : {n:,}") + print(f"literal table: 0x{table_off:X} ({table_off:,}) records {count:,}") + print(f"literal data : 0x{data_off:X} ({data_off:,}) size {data_size:,}") + + fp = hashlib.sha256(bytes(data[:256])).hexdigest() + print(f"header sha256: {fp}") + + rules = build_rules(args.server) + TEXT_RULES + targets = {to for _, to in rules} + + records = [] + changes = [] + saw_target = False + for i in range(count): + pos = table_off + i * 8 + length = u32(pos) + index = int.from_bytes(data[pos + 4:pos + 8], "little", signed=True) + if length == 0 or index < 0 or index + length > data_size: + records.append((length, index, b"", False)) + continue + raw = bytes(data[data_off + index: data_off + index + length]) + try: + text = raw.decode("utf-8") + except UnicodeDecodeError: + records.append((length, index, raw, False)) + continue + if any(k.lower() in text.lower() for k in KEEP): + records.append((length, index, raw, False)) + continue + new = text + for frm, to in sorted(rules, key=lambda r: -len(r[0])): + if frm in new: + new = new.replace(frm, to) + if any(t and t in new for t in targets): + saw_target = True + if new != text: + changes.append((i, text, new)) + records.append((len(new.encode()), index, new.encode(), True)) + else: + records.append((length, index, raw, False)) + + if not changes: + print("nothing to rewrite" + (" (already patched)" if saw_target else "")) + return 0 + + # rebuild the blob: old bytes verbatim, changed literals appended after them + appended = bytearray() + new_index = {} + for i, old, new in changes: + new_index[i] = data_size + len(appended) + appended += new.encode() + new_data_size = data_size + len(appended) + + out = bytearray(data) + out += bytearray(data[data_off: data_off + data_size]) + out += appended + out[data_off_f:data_off_f + 4] = n.to_bytes(4, "little") + out[data_size_f:data_size_f + 4] = new_data_size.to_bytes(4, "little") + for i, old, new in changes: + pos = table_off + i * 8 + out[pos:pos + 4] = len(new.encode()).to_bytes(4, "little") + out[pos + 4:pos + 8] = new_index[i].to_bytes(4, "little", signed=True) + + print(f"\n{len(changes)} literal(s) to rewrite:") + for i, old, new in changes: + print(f" [{i}] {old!r}\n -> {new!r}") + print(f"blob: {data_size:,} -> {new_data_size:,} bytes, file: {n:,} -> {len(out):,}") + + # --- verification ------------------------------------------------------- + changed_positions = set() + for i, _, _ in changes: + pos = table_off + i * 8 + changed_positions.update(range(pos, pos + 8)) + for off in range(256): + if off in range(data_off_f, data_off_f + 4) or off in range(data_size_f, data_size_f + 4): + continue + if out[off] != data[off]: + print(f"verification failed: header byte 0x{off:X} changed", file=sys.stderr) + return 1 + for off in range(256, n): + if off in changed_positions: + continue + if out[off] != data[off]: + print(f"verification failed: payload byte 0x{off:X} changed", file=sys.stderr) + return 1 + if bytes(out[n:n + data_size]) != bytes(data[data_off:data_off + data_size]): + print("verification failed: relocated blob is not a verbatim copy", file=sys.stderr) + return 1 + changed_map = {i: new for i, _, new in changes} + for i, (length, index, raw, _) in enumerate(records): + pos = table_off + i * 8 + new_len = int.from_bytes(out[pos:pos + 4], "little") + new_idx = int.from_bytes(out[pos + 4:pos + 8], "little", signed=True) + text = bytes(out[new_data_size and n + new_idx: n + new_idx + new_len]) + want = changed_map[i].encode() if i in changed_map else raw + if text != want: + print(f"verification failed: literal #{i} does not read back", file=sys.stderr) + return 1 + print(f"verification: {count:,} literals read back, header and payload otherwise byte-identical") + + if not args.apply: + print("\ndry run - pass --apply to write") + return 0 + + backup = metadata + ".orig" + if not os.path.exists(backup): + shutil.copy2(metadata, backup) + print(f"backup written: {backup}") + open(metadata, "wb").write(bytes(out)) + + marker = os.path.join(args.game_root, data_dir, "radicade-patch.json") + json.dump({ + "patchedAtUtc": __import__("datetime").datetime.utcnow().isoformat() + "Z", + "gameVersion": "20230831", + "serverUrl": args.server, + "metadata": { + "changedLiterals": len(changes), + "oldDataSize": data_size, + "newDataSize": new_data_size, + "headerSha256": fp, + "literalTableField": f"0x{table_off_f:02x}", + "literalDataField": f"0x{data_off_f:02x}", + }, + }, open(marker, "w"), indent=2) + print(f"written: {metadata}\nmarker : {marker}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Tools/patch-quest-client-metadata.py b/Tools/patch-quest-client-metadata.py new file mode 100644 index 0000000..91a8137 --- /dev/null +++ b/Tools/patch-quest-client-metadata.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Point the unpacked Quest Rec Room 20230907 client at a Radicade server. + +Accepts either the raw apktool-decoded APK directory (containing +assets/bin/Data/Managed/Metadata/global-metadata.dat) or a wrapper containing +apk/ and obb/ subdirectories like the provided download. It changes only exact +IL2CPP literal-table rows, not broad byte substrings. + +Dry-run by default. Use --apply to back up the metadata and write the patch. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import struct +import sys +import tempfile +from pathlib import Path +from urllib.parse import urlsplit + +# The Quest release's header fields were mapped structurally from its unique +# metadata section chain. These contain the v27 string-literal table/data pairs. +FIELDS = { + "literal_table_offset": 0x44, + "literal_table_size": 0x6C, + "literal_data_offset": 0x28, + "literal_data_size": 0xEC, +} +GAME_VERSION = "20230907" +KNOWN_LITERALS = { + 2011: ".rec.net", + 2516: "20230907", + 41319: "http://cdn.rec.net/avatar/", + 41405: "https://auth.rec.net", + 41411: "https://ns.rec.net", + 41418: "https://rec.net", + 41419: "https://rec.net/shop", + 41420: "https://rec.net/shop/subscriptions", + 41421: "https://rec.net/{0}", +} + + +def u32(data: bytes | bytearray, offset: int) -> int: + return struct.unpack_from(" str: + parsed = urlsplit(value) + if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password: + raise ValueError("server must be an https:// URL without user info") + if parsed.query or parsed.fragment: + raise ValueError("server URL cannot contain a query or fragment") + return value.rstrip("/") + + +def load_literals(blob: bytes) -> tuple[int, int, int, int, list[tuple[int, int]]]: + if len(blob) < 256: + raise ValueError("metadata is shorter than its 256-byte header") + table_offset = u32(blob, FIELDS["literal_table_offset"]) + table_size = u32(blob, FIELDS["literal_table_size"]) + data_offset = u32(blob, FIELDS["literal_data_offset"]) + data_size = u32(blob, FIELDS["literal_data_size"]) + if table_size % 8 or table_offset < 256 or table_offset + table_size > len(blob): + raise ValueError("Quest metadata literal-table header fields are invalid") + if data_offset < 256 or data_offset + data_size > len(blob): + raise ValueError("Quest metadata literal-data header fields are invalid") + + literals = [] + for index in range(table_size // 8): + length, string_index = struct.unpack_from(" data_size: + raise ValueError(f"literal #{index} points outside its data segment") + try: + blob[data_offset + string_index:data_offset + string_index + length].decode("utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"literal #{index} is not UTF-8") from exc + literals.append((length, string_index)) + return table_offset, table_size, data_offset, data_size, literals + + +def build_patch(blob: bytes, server: str) -> tuple[bytearray, list[tuple[int, str, str]], int, int]: + table_offset, table_size, data_offset, data_size, literals = load_literals(blob) + # Refuse to patch a different client or a normalized/tooling metadata file. + for index, expected in KNOWN_LITERALS.items(): + if index >= len(literals): + raise ValueError(f"expected Quest 20230907 literal #{index} is missing") + length, string_index = literals[index] + actual = blob[data_offset + string_index:data_offset + string_index + length].decode("utf-8") + if actual != expected: + raise ValueError(f"not the expected Quest 20230907 metadata: literal #{index} is {actual!r}, expected {expected!r}") + + rules = { + "https://ns.rec.net": server, + "https://auth.rec.net": server, + "https://rec.net/shop/subscriptions": server + "/shop/subscriptions", + "https://rec.net/shop": server + "/shop", + "https://rec.net/{0}": server + "/{0}", + "https://rec.net": server, + # The server's signed image endpoint is /images/img/{name}. + "http://cdn.rec.net/avatar/": server + "/images/img/", + ".rec.net": ".radicade.internal", + } + rows = [] + for index, (length, string_index) in enumerate(literals): + if not length: + continue + old = blob[data_offset + string_index:data_offset + string_index + length].decode("utf-8") + # Retain the server host tail used by the runtime's image signature key id. + if index == 2011: + replacement = ".radicade.internal" + new = replacement if old == ".rec.net" else old + else: + new = old + # Longest-first prevents the root literal from consuming the shop forms. + for source, target in sorted(rules.items(), key=lambda item: -len(item[0])): + if source in new: + new = new.replace(source, target) + if new != old: + rows.append((index, old, new)) + + if not rows: + raise ValueError("no target literals found; this file may already be patched or not match the supported Quest build") + out = bytearray(blob) + appended = bytearray() + new_data_offsets: dict[int, int] = {} + for index, _old, new in rows: + encoded = new.encode("utf-8") + new_data_offsets[index] = data_size + len(appended) + appended.extend(encoded) + # As on the desktop patch, keep the old literal bytes intact and append the + # replacement strings, then update only the matching table rows and fields. + out.extend(blob[data_offset:data_offset + data_size]) + out.extend(appended) + new_data_size = data_size + len(appended) + struct.pack_into(" int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("apktool_dir", help="apktool-decoded directory containing apk/assets/bin/Data") + parser.add_argument("--server", default="https://ns.radicade.app") + parser.add_argument("--apply", action="store_true", help="write files (dry-run is the default)") + args = parser.parse_args() + try: + server = validate_server(args.server) + except ValueError as exc: + print(f"invalid server URL: {exc}", file=sys.stderr) + return 2 + + root = Path(args.apktool_dir).resolve() + candidates = [ + root / "assets" / "bin" / "Data" / "Managed" / "Metadata" / "global-metadata.dat", + root / "apk" / "assets" / "bin" / "Data" / "Managed" / "Metadata" / "global-metadata.dat", + ] + metadata = next((candidate for candidate in candidates if candidate.is_file()), candidates[-1]) + if not metadata.is_file(): + print(f"decoded Quest project missing {metadata}", file=sys.stderr) + return 1 + + try: + source = metadata.read_bytes() + patched, rows, old_size, new_size = build_patch(source, server) + except (OSError, ValueError, struct.error) as exc: + print(f"cannot patch Quest metadata: {exc}", file=sys.stderr) + return 1 + + print(f"metadata: {metadata}") + print(f"source sha256: {hashlib.sha256(source).hexdigest()}") + print(f"metadata size: {len(source):,} -> {len(patched):,}; literal bytes: {old_size:,} -> {new_size:,}") + print(f"server: {server}") + print(f"{len(rows)} live literal(s) will be redirected:") + for index, before, after in rows: + print(f" #{index}: {before!r}\n -> {after!r}") + print("verification: all literal rows read back; the patch changes only the table rows and its two data-segment header fields") + if not args.apply: + print("dry-run only; pass --apply to write the metadata backup and marker") + return 0 + + backup = Path(str(metadata) + ".orig") + if backup.exists() and backup.read_bytes() != source: + print(f"refusing to overwrite existing backup with different bytes: {backup}", file=sys.stderr) + return 1 + if not backup.exists(): + shutil.copy2(metadata, backup) + # Atomic replacement prevents a partially written 30 MB metadata file. + fd, temp_name = tempfile.mkstemp(prefix=metadata.name + ".", dir=metadata.parent) + try: + with os.fdopen(fd, "wb") as handle: + handle.write(patched) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temp_name, metadata) + except Exception: + try: + os.unlink(temp_name) + except OSError: + pass + raise + + marker = metadata.parents[2] / "radicade-quest-patch.json" + marker.write_text(json.dumps({ + "gameVersion": GAME_VERSION, + "serverUrl": server, + "sourceSha256": hashlib.sha256(source).hexdigest(), + "patchedSha256": hashlib.sha256(patched).hexdigest(), + "changedLiterals": len(rows), + }, indent=2) + "\n", encoding="utf-8") + print(f"backup: {backup}\nmarker: {marker}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Tools/patch-quest-standalone-login.py b/Tools/patch-quest-standalone-login.py new file mode 100644 index 0000000..ca97001 --- /dev/null +++ b/Tools/patch-quest-standalone-login.py @@ -0,0 +1,91 @@ +"""Experimental Quest 20230907 independent login; requires server platform-7 guard. + +Preserves Quest VR setup before switching account initialization to standalone. +Redirects auth parameters, platform enum, and title-screen error-page navigation. +Produces an UNSIGNED APK. Does not assert that login/gameplay is supported. +The v1 full-initializer replacement failed with error 3 and no usable hands. +This v3 experiment restores SetVRDisplayMode, the password page, and Quest input. +Error navigation currently applies to all title-screen errors; not release-ready. +""" +import argparse +import hashlib +import struct +import zipfile +from pathlib import Path + +EXPECTED = 'd9a72f2906ecee6cd292fa094fa57df8904b125dee161a04887603a3d0d3449e' +LIB = 'lib/arm64-v8a/libil2cpp.so' +REDIRECTS = ( + ('GetAuthParams', 0x3d0a930, 0x396ad48), + ('get_CurrentPlatform', 0x3d096fc, 0x43d6eb8), + ('Title-screen error to password page', 0x39c0f40, 0x39bbd80), + ('Saved-account auto-login error to password page', 0x39c67e0, 0x39bbd80), +) + +def patch(data): + if hashlib.sha256(data).hexdigest() != EXPECTED: + raise ValueError('Unknown IL2CPP binary; refusing to patch') + result = bytearray(data) + phoff = struct.unpack_from(' 0x{dst:x}') + # Preserve the original prologue and Quest-specific VR display setup through + # 0x3d09f14. Then run independent initialization and restore the original frame. + start = 0x3d09f18 + words = [0xaa1303e0, 0xaa1f03e1, + 0x94000000 | (((0x43d6f0c - (start + 8)) // 4) & 0x3ffffff)] + for addr in (0x3d0a124, 0x3d0a12c, 0x3d0a130, 0x3d0a138): + words.append(struct.unpack_from('.radicade-orig) is kept, so -Restore is always possible. + + The backup deliberately lives OUTSIDE the package folder, under + \radicade-backups\..., because Unity treats everything under + Library\PackageCache as an immutable package folder and logs + "asset ... has no meta file, but it's in an immutable folder" for anything + there it cannot match to a package manifest. + + The target project is probed for writability first: a Rec Room Studio project sitting + in a OneDrive folder (or covered by Controlled folder access) rejects every create and + overwrite, and Unity cannot run from such a tree at all. Copy the project somewhere + writable and keep working there. + +.PARAMETER ProjectRoot + Candidate project roots, tried in order; the first one that exists, holds the studio + package cache and actually accepts writes is used. + +.PARAMETER Url + Nameserver base URL the editor should use. + +.PARAMETER DllPath + Patch this DLL directly instead of probing ProjectRoot. + +.PARAMETER Restore + Put the backed-up DLL back and exit. + +.EXAMPLE + pwsh -File tools/patch-rrs-nameserver.ps1 + pwsh -File tools/patch-rrs-nameserver.ps1 -Url http://127.0.0.1:3031/nameserver + pwsh -File tools/patch-rrs-nameserver.ps1 -Restore +#> +[CmdletBinding()] +param( + [string[]] $ProjectRoot = @( + 'C:\RecRoom\Documents\EggFiles\ProjectTemplate', + 'C:\RecRoom\OneDrive\Documents\EggFiles\ProjectTemplate' + ), + [string] $Url = 'https://localhost:3030/nameserver', + [string] $DllPath, + [switch] $Restore +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +# Latin-1 gives a 1:1 byte<->char mapping, so IndexOf() returns byte offsets and the +# search stays native-fast instead of looping over ~330 KB in PowerShell. +$latin1 = [System.Text.Encoding]::GetEncoding(28591) + +# Known shipped values of the literal. The first one is what this template ships; the +# list keeps the tool working if a future build bakes a different host. +$knownUrls = @( + 'https://deluxe-backend-test.oldrecroom.com/nameserver' +) + +function Write-Status { + param([string] $Level, [string] $Message) + Write-Host ("[{0}] {1}" -f $Level, $Message) +} + +function Find-EditorCoreDll { + param([string] $Root, [switch] $Quiet) + + $pkgRoot = Join-Path $Root 'Library\PackageCache' + if (-not (Test-Path -LiteralPath $pkgRoot)) { + if ($Quiet) { return $null } + throw "No PackageCache under '$Root' - open the project in Unity once so packages get extracted." + } + + $candidates = Get-ChildItem -LiteralPath $pkgRoot -Directory -Filter 'com.recroom.studio.build@*' -ErrorAction SilentlyContinue | + ForEach-Object { Join-Path $_.FullName 'Editor\RecRoom.Studio.Editor.Core.dll' } | + Where-Object { Test-Path -LiteralPath $_ } + + if (-not $candidates) { + if ($Quiet) { return $null } + throw "RecRoom.Studio.Editor.Core.dll not found under $pkgRoot (looked in com.recroom.studio.build@*)." + } + if (@($candidates).Count -gt 1) { + Write-Status 'WARN' "multiple studio.build packages present, using the first: $($candidates[0])" + } + return @($candidates)[0] +} + +function Get-BackupPath { + <# Maps \Library\PackageCache\\...\X.dll to + \radicade-backups\\...\X.dll.radicade-orig, so the backup stays a + real project asset tree instead of polluting the immutable package folder. #> + param([string] $DllPath) + + $marker = '\Library\PackageCache\' + $idx = $DllPath.IndexOf($marker, [System.StringComparison]::OrdinalIgnoreCase) + if ($idx -lt 0) { + # Not inside a package cache (a -DllPath into Assets, say): keep it as a sibling file. + return "$DllPath.radicade-orig" + } + + $root = $DllPath.Substring(0, $idx) + $relative = $DllPath.Substring($idx + $marker.Length) + return (Join-Path $root (Join-Path 'radicade-backups' "$relative.radicade-orig")) +} + +function Test-Writable { + param([string] $Directory) + + $probe = Join-Path $Directory ('radicade-write-probe-' + [Guid]::NewGuid().ToString('N') + '.tmp') + try { + [System.IO.File]::WriteAllText($probe, 'probe') + Remove-Item -LiteralPath $probe -Force + return $true + } catch { + return $false + } +} + +function Resolve-Project { + param([string[]] $Candidates) + + $notes = New-Object System.Collections.Generic.List[string] + + foreach ($candidate in $Candidates) { + if (-not (Test-Path -LiteralPath $candidate)) { + $notes.Add("$candidate - does not exist") + continue + } + + $dll = Find-EditorCoreDll -Root $candidate -Quiet + if (-not $dll) { + $notes.Add("$candidate - no com.recroom.studio.build package cache inside") + continue + } + + if (-not (Test-Writable -Directory $candidate)) { + $notes.Add("$candidate - present, but the tree rejects writes") + continue + } + + return $dll + } + + $details = ($notes | ForEach-Object { " $_" }) -join [Environment]::NewLine + throw @" +No writable Rec Room Studio project found. Tried: +$details + +A tree that rejects writes cannot hold this project: Unity has to create and overwrite +files in Library\ and Temp\ just to open it. Copy the project somewhere writable first +(always exclude the OneDrive copy from your Unity Hub list afterwards), for example: + + robocopy "C:\RecRoom\OneDrive\Documents\EggFiles\ProjectTemplate" "C:\RecRoom\Documents\EggFiles\ProjectTemplate" /E /MT:16 /R:1 /W:1 + +then pass that root with -ProjectRoot, or just re-run this script. +"@ +} + +function Read-CompressedUInt { + param([byte[]] $Bytes, [int] $Offset) + $b0 = $Bytes[$Offset] + if (($b0 -band 0x80) -eq 0) { return @{ Value = [int]$b0; Size = 1 } } + if (($b0 -band 0xC0) -eq 0x80) { + return @{ Value = ((([int]$b0 -band 0x3F) -shl 8) -bor [int]$Bytes[$Offset + 1]); Size = 2 } + } + $v = (([int]$b0 -band 0x1F) -shl 24) -bor ([int]$Bytes[$Offset + 1] -shl 16) -bor ` + ([int]$Bytes[$Offset + 2] -shl 8) -bor [int]$Bytes[$Offset + 3] + return @{ Value = $v; Size = 4 } +} + +function Write-CompressedUInt { + param([byte[]] $Bytes, [int] $Offset, [int] $Value) + if ($Value -lt 0x80) { + $Bytes[$Offset] = [byte]$Value + return 1 + } + if ($Value -lt 0x4000) { + $Bytes[$Offset] = [byte](0x80 -bor ($Value -shr 8)) + $Bytes[$Offset + 1] = [byte]($Value -band 0xFF) + return 2 + } + $Bytes[$Offset] = [byte](0xC0 -bor ($Value -shr 24)) + $Bytes[$Offset + 1] = [byte](($Value -shr 16) -band 0xFF) + $Bytes[$Offset + 2] = [byte](($Value -shr 8) -band 0xFF) + $Bytes[$Offset + 3] = [byte]($Value -band 0xFF) + return 4 +} + +function Get-NameserverEntry { + <# Locates the #US string heap entry whose payload is one of $Known or $Target. + Returns offset/prefix/payload geometry, or $null when the literal is absent. #> + param([string] $Path, [string[]] $Known, [string] $Target) + + $bytes = [System.IO.File]::ReadAllBytes($Path) + $hay = $latin1.GetString($bytes) + + foreach ($candidate in @($Known + @($Target))) { + $payload = [System.Text.Encoding]::Unicode.GetBytes($candidate) + $at = $hay.IndexOf($latin1.GetString($payload), [System.StringComparison]::Ordinal) + if ($at -lt 1) { continue } + + for ($width = 1; $width -le 4; $width++) { + $prefixAt = $at - $width + if ($prefixAt -lt 0) { continue } + $prefix = Read-CompressedUInt -Bytes $bytes -Offset $prefixAt + if ($prefix.Size -ne $width) { continue } + if ($prefix.Value -ne (2 * $candidate.Length + 1)) { continue } + return [pscustomobject]@{ + Bytes = $bytes + PayloadOffset = $at + PrefixOffset = $prefixAt + PrefixSize = $prefix.Size + PrefixValue = $prefix.Value + Original = $candidate + Footprint = $prefix.Size + 2 * $candidate.Length + 1 + TerminalByte = $bytes[$at + 2 * $candidate.Length] + } + } + } + return $null +} + +# ---- main ------------------------------------------------------------------------- + +Write-Host '' + +if (-not $DllPath) { + $DllPath = Resolve-Project -Candidates $ProjectRoot + Write-Status 'INFO' 'project : writable, using the studio package cache below' +} +$backup = Get-BackupPath -DllPath $DllPath + +Write-Status 'INFO' "studio editor core: $DllPath" +Write-Status 'INFO' "backup location : $backup" + +if ($Restore) { + if (-not (Test-Path -LiteralPath $backup)) { + # Older runs kept the backup in the package folder; still honour one if it is all we have. + $legacy = "$DllPath.radicade-orig" + if (Test-Path -LiteralPath $legacy) { + Write-Status 'WARN' "using legacy in-package backup at $legacy" + $backup = $legacy + } else { + throw "No backup at '$backup' - nothing to restore." + } + } + try { + Copy-Item -LiteralPath $backup -Destination $DllPath -Force + } catch { + throw "Could not write to '$DllPath' - the project tree rejects writes. Copy the project somewhere writable and re-run with -ProjectRoot." + } + Write-Status 'PASS' 'restored the original DLL from backup' + exit 0 +} + +$entry = Get-NameserverEntry -Path $DllPath -Known $knownUrls -Target $Url +if (-not $entry) { throw "Nameserver literal not found in '$DllPath' (unexpected layout)." } + +if ($entry.Original -eq $Url) { + Write-Status 'PASS' "already points at $Url" + exit 0 +} + +$newPayload = [System.Text.Encoding]::Unicode.GetBytes($Url) +$newValue = 2 * $Url.Length + 1 +$free = $entry.Footprint - (1 + $newPayload.Length + 1) +if ($free -lt 0) { + throw "Replacement '$Url' ($($Url.Length) chars) does not fit in the original slot ($($entry.Original.Length) chars)." +} + +try { + if (-not (Test-Path -LiteralPath $backup)) { + $backupDir = Split-Path -Parent $backup + if (-not (Test-Path -LiteralPath $backupDir)) { + New-Item -ItemType Directory -Path $backupDir -Force | Out-Null + } + Copy-Item -LiteralPath $DllPath -Destination $backup + Write-Status 'INFO' "backed up original to $backup" + } +} catch { + throw "Could not write to '$backup' - the project tree rejects writes. Copy the project somewhere writable and re-run with -ProjectRoot." +} + +$bytes = [byte[]]::new($entry.Bytes.Length) +[Array]::Copy($entry.Bytes, $bytes, $entry.Bytes.Length) + +# Rewrite prefix + payload + terminal byte, then zero the unused tail of the slot. +$cursor = $entry.PrefixOffset +$cursor += Write-CompressedUInt -Bytes $bytes -Offset $cursor -Value $newValue +[Array]::Copy($newPayload, 0, $bytes, $cursor, $newPayload.Length) +$cursor += $newPayload.Length +$bytes[$cursor] = $entry.TerminalByte +$cursor++ +for ($i = $cursor; $i -lt ($entry.PrefixOffset + $entry.Footprint); $i++) { $bytes[$i] = 0 } + +try { + [System.IO.File]::WriteAllBytes($DllPath, $bytes) +} catch { + throw "Could not write to '$DllPath' - the project tree rejects writes. Copy the project somewhere writable and re-run with -ProjectRoot." +} + +# ---- verify ----------------------------------------------------------------------- +$check = Get-NameserverEntry -Path $DllPath -Known $knownUrls -Target $Url +if (-not $check -or $check.Original -ne $Url) { + throw "Verification failed: heap entry does not decode to '$Url' after patching." +} +if ([System.IO.File]::ReadAllBytes($DllPath).Length -ne $entry.Bytes.Length) { + throw 'Verification failed: file length changed.' +} + +# Every other heap entry must keep its offset, so compare everything outside the slot. +$after = [System.IO.File]::ReadAllBytes($DllPath) +$slotEnd = $entry.PrefixOffset + $entry.Footprint +$diffOutsideSlot = 0 +for ($i = 0; $i -lt $after.Length; $i++) { + if ($i -ge $entry.PrefixOffset -and $i -lt $slotEnd) { continue } + if ($after[$i] -ne $entry.Bytes[$i]) { $diffOutsideSlot++ } +} +if ($diffOutsideSlot -ne 0) { + throw "Verification failed: $diffOutsideSlot bytes changed outside the string-heap slot." +} + +Write-Status 'PASS' "nameserver: $($entry.Original) -> $Url" +Write-Status 'INFO' "slot $($entry.PrefixOffset), $($entry.Footprint) bytes, $free bytes of padding; file size unchanged" +Write-Status 'INFO' "open this copy from Unity Hub: $(Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $DllPath)))))" +Write-Status 'INFO' "undo with: pwsh -File tools/patch-rrs-nameserver.ps1 -Restore -DllPath `"$DllPath`"" diff --git a/Tools/player-log-window.py b/Tools/player-log-window.py new file mode 100644 index 0000000..12dd81d --- /dev/null +++ b/Tools/player-log-window.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Slice the client's Player.log by wall-clock window and summarise it. + +The Unity log is one flat file per launch whose lines start with +`[HH:MM:SS] [frame] [seconds] [Level] [Tag] ...`; anything that matters after a +test is usually a handful of error lines inside a one-minute window. + +Usage: + python tools/player-log-window.py --from 14:21 --to 14:23 [--errors] + python tools/player-log-window.py --grep "studio|rrs" --from 14:22:30 + python tools/player-log-window.py --log --from ... --to ... --show + +Options: + --errors dedupe and count Error/Warning/Exception lines instead of printing all + --show print every matching line (default when a --grep matches) + --tail N additionally print the last N raw lines of the file +""" + +import argparse +import os +import re +import sys + +DEFAULT_LOG = os.path.join( + os.environ.get("LOCALAPPDATA", ""), "..", "LocalLow", + "Against Gravity", "Rec Room", "Player.log") + +LINE_RE = re.compile(r"^\[(\d\d:\d\d:\d\d(?:\.\d+)?)\]") + + +def pad(value, tail): + """`14:22` means the whole minute, `14:22:30` the whole second.""" + if not value: + return None + return value if value.count(":") == 2 else value + tail + + +def in_window(stamp, start, stop): + if start and stamp < start: + return False + if stop and stamp > stop: + return False + return True + + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument("--log", default=os.path.normpath(DEFAULT_LOG)) + parser.add_argument("--from", dest="start") + parser.add_argument("--to", dest="stop") + parser.add_argument("--grep") + parser.add_argument("--errors", action="store_true") + parser.add_argument("--show", action="store_true") + parser.add_argument("--tail", type=int, default=0) + args = parser.parse_args(argv[1:]) + + with open(args.log, "rb") as handle: + blob = handle.read().decode("utf-8", "replace") + lines = blob.split("\n") + print(f"{args.log}: {len(lines)} lines, {len(blob)} bytes") + + pattern = re.compile(args.grep, re.I) if args.grep else None + selected = [] + for text in lines: + match = LINE_RE.match(text) + if not match: + continue + if not in_window(match.group(1), pad(args.start, ":00"), pad(args.stop, ":59")): + continue + if pattern and not pattern.search(text): + continue + if args.errors and not re.search(r"\[(Error|Warning)\]|Exception:|Error:", text): + continue + selected.append(text.rstrip()) + + if args.errors or not (args.show or pattern): + counts = {} + order = [] + for text in selected: + key = re.sub(r"\d+", "#", text[:220]) + if key not in counts: + counts[key] = 0 + order.append(key) + counts[key] += 1 + for key in order: + print(f" x{counts[key]:<4} {key}") + else: + for text in selected: + print(" " + text[:300]) + print(f"--- {len(selected)} selected line(s)") + + if args.tail: + print(f"--- last {args.tail} lines ---") + for text in [t for t in lines if t.strip()][-args.tail:]: + print(" " + text[:300]) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/Tools/quest-disassemble.py b/Tools/quest-disassemble.py new file mode 100644 index 0000000..a67e753 --- /dev/null +++ b/Tools/quest-disassemble.py @@ -0,0 +1,60 @@ +"""Read-only ARM64 inspection with optional on-device method-map annotations.""" +import argparse +import struct +import re +from pathlib import Path +from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN + +p = argparse.ArgumentParser(description=__doc__) +p.add_argument('library', type=Path) +p.add_argument('address', type=lambda s: int(s, 0)) +p.add_argument('size', type=lambda s: int(s, 0)) +p.add_argument('--map', type=Path) +p.add_argument('--metadata', type=Path, help='Exact Quest metadata for literal annotations') +p.add_argument('--august', action='store_true', help='Use August 31 metadata header fields') +a = p.parse_args() +b = a.library.read_bytes() +ph = struct.unpack_from('> 29 == 5: + index = (token & 0x1fffffff) >> 1 + table = struct.unpack_from(' +#include +#include +#include +#include +#include +#include +#include +#include +#include "quest-image-placeholder.h" +#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "RadicadeHTTP", __VA_ARGS__) +static uintptr_t base; +static void (*original)(void *, void *, void *); +static void *(*current_uri)(void *, void *); +static void *(*absolute_path)(void *, void *); +static void (*original_quit)(int, void *, void *); +static void *(*original_send)(void *, void *); +static int (*original_image_verify)(void *, void *); +static int verify_image(void *data, void *signature) { + // This exact-build entry point is image-only, after key-id/header parsing. + // Recognize the private server's legacy placeholder, never arbitrary signatures. + if (data && signature && *(uintptr_t *)((char *)signature + 0x18) == sizeof(image_placeholder) + && !memcmp((char *)signature + 0x20, image_placeholder, sizeof(image_placeholder))) { + LOG("Radicade legacy image placeholder accepted in image-only verifier"); + return 1; + } + return original_image_verify(data, signature); +} +static void *(*object_class)(void *); +static void *(*object_new)(void *); +static void *(*string_new)(const char *); +static void *(*method_named)(void *, const char *, int); +static void *(*invoke)(void *, void *, void **, void **); +static void (*write_barrier)(void *, void **, void *); +static void *inspect_send(void *request, void *method) { + if (!request) return original_send(request, method); + void *uri = *(void **)((char *)request + 0x10); + if (!uri) return original_send(request, method); + void *klass = object_class(uri), *exc = 0; + void *getter = method_named(klass, "get_AbsoluteUri", 0); + if (!getter) return original_send(request, method); + void *str = invoke(getter, uri, 0, &exc); + if (!str || exc) return original_send(request, method); + int len = *(int *)((char *)str + 0x10); + if (len < 0 || len > 1700) return original_send(request, method); + const uint16_t *chars = (const uint16_t *)((char *)str + 0x14); + char url[2048]; + for (int i = 0; i < len; ++i) { + if (chars[i] > 127) return original_send(request, method); + url[i] = (char)chars[i]; + } + url[len] = 0; + const char *origin = "https://ns.radicade.app/"; + if (!strncmp(url, origin, strlen(origin))) { + void *setter = method_named(object_class(request), "set_IsKeepAlive", 1); + if (setter) { + unsigned char enabled = 0; + void *ka_args[] = {&enabled}; + invoke(setter, request, ka_args, &exc); + if (exc) return original_send(request, method); + } + } + const char *prefix = "https://ns.radicade.app/images/"; + if (strncmp(url, prefix, strlen(prefix)) || strstr(url, "rcsig=") || strchr(url, '#')) + return original_send(request, method); + strcat(url, strchr(url, '?') ? "&" : "?"); + strcat(url, "rcsig=KEY%3ARSA%3Ap1.radicade.internal"); + void *ctor = method_named(klass, ".ctor", 1); + void *newuri = object_new(klass), *newstr = string_new(url); + if (!ctor || !newuri || !newstr) return original_send(request, method); + void *args[] = {newstr}; + invoke(ctor, newuri, args, &exc); + if (!exc) { + write_barrier(request, (void **)((char *)request + 0x10), newuri); + LOG("Image request key-id aligned for Radicade HTTPS origin"); + } + return original_send(request, method); +} +static void inspect_quit(int code, void *reason, void *method) { + LOG("fatal quit code=%d caller_rva=%lx reason_chars=%d", code, + (unsigned long)((uintptr_t)__builtin_return_address(0) - base), + reason ? *(int *)((char *)reason + 0x10) : 0); + original_quit(code, reason, method); +} +static void path_for(void *response, char *out, size_t size) { + strcpy(out, ""); + void *request = *(void **)((char *)response + 0x78); + if (!request) return; + void *uri = current_uri(request, 0); + if (!uri) return; + void *str = absolute_path(uri, 0); + if (!str) return; + int len = *(int *)((char *)str + 0x10); + const uint16_t *chars = (const uint16_t *)((char *)str + 0x14); + if (len < 0) return; + size_t i = 0; + for (; i < (size_t)len && i + 1 < size; ++i) { + // Never include query/fragment data even if a malformed URI reaches us. + if (chars[i] == '?' || chars[i] == '#') break; + out[i] = chars[i] >= 32 && chars[i] < 127 ? (char)chars[i] : '_'; + } + out[i] = 0; +} +static void inspect_chunk(void *response, void *stream, void *method) { + char path[192]; + path_for(response, path, sizeof(path)); + int status = *(int *)((char *)response + 0x18); + LOG("chunk begin response=%p status=%d path=%s", response, status, path); + original(response, stream, method); + LOG("chunk end response=%p", response); +} +static void jump(unsigned char *dst, uintptr_t target) { + uint32_t code[] = {0x58000050, 0xd61f0200}; // ldr x16,+8; br x16 + memcpy(dst, code, 8); + memcpy(dst + 8, &target, 8); +} +static void *install(void *unused) { + (void)unused; + sleep(30); + void *lib = dlopen("libil2cpp.so", RTLD_NOW | RTLD_NOLOAD); + if (!lib) { LOG("IL2CPP unavailable; no hook installed"); return 0; } + Dl_info info; + if (!dladdr(dlsym(lib, "il2cpp_domain_get"), &info)) return 0; + base = (uintptr_t)info.dli_fbase; + // ReadChunkLength's unused receiver is optimized away at call sites. + // Hook ReadChunked instead, which actually consumes its response receiver. + unsigned char *target = (unsigned char *)(base + 0x6d941e0); + // Position-independent original prologue; refuse any other code. + uint32_t expected[] = {0xd10203ff, 0xa9026ffc, 0xa90367fa, 0xa9045ff8}; + if (memcmp(target, expected, sizeof(expected))) { LOG("Prologue mismatch; no hook installed"); return 0; } + long page = sysconf(_SC_PAGESIZE); + if (page <= 0) return 0; + unsigned char *trampoline = mmap(0, (size_t)page, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (trampoline == MAP_FAILED) return 0; + memcpy(trampoline, target, 16); + jump(trampoline + 16, (uintptr_t)target + 16); + if (mprotect(trampoline, (size_t)page, PROT_READ | PROT_EXEC)) return 0; + __builtin___clear_cache((char *)trampoline, (char *)trampoline + 32); + original = (void (*)(void *, void *, void *))trampoline; + current_uri = (void *(*)(void *, void *))(base + 0x6d8d328); + absolute_path = (void *(*)(void *, void *))(base + 0xa0a956c); + void *start = (void *)((uintptr_t)target & ~((uintptr_t)page - 1)); + if (mprotect(start, (size_t)page, PROT_READ | PROT_WRITE | PROT_EXEC)) return 0; + jump(target, (uintptr_t)inspect_chunk); + __builtin___clear_cache((char *)target, (char *)target + 16); + mprotect(start, (size_t)page, PROT_READ | PROT_EXEC); + LOG("Chunk diagnostic active; path/status only"); + unsigned char *quit = (unsigned char *)(base + 0x3a1c0b4); + uint32_t quit_expected[] = {0xf81d0ff5, 0xa9014ff4, 0xa9027bfd, 0x910083fd}; + if (memcmp(quit, quit_expected, 16)) { LOG("Quit prologue mismatch"); return 0; } + unsigned char *qt = mmap(0, (size_t)page, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (qt == MAP_FAILED) return 0; + memcpy(qt, quit, 16); + jump(qt + 16, (uintptr_t)quit + 16); + if (mprotect(qt, (size_t)page, PROT_READ | PROT_EXEC)) return 0; + __builtin___clear_cache((char *)qt, (char *)qt + 32); + original_quit = (void (*)(int, void *, void *))qt; + void *qp = (void *)((uintptr_t)quit & ~((uintptr_t)page - 1)); + if (mprotect(qp, (size_t)page, PROT_READ | PROT_WRITE | PROT_EXEC)) return 0; + jump(quit, (uintptr_t)inspect_quit); + __builtin___clear_cache((char *)quit, (char *)quit + 16); + mprotect(qp, (size_t)page, PROT_READ | PROT_EXEC); + LOG("Quit diagnostic active; code/caller only, original behavior preserved"); + object_class = dlsym(lib, "il2cpp_object_get_class"); + object_new = dlsym(lib, "il2cpp_object_new"); + string_new = dlsym(lib, "il2cpp_string_new"); + method_named = dlsym(lib, "il2cpp_class_get_method_from_name"); + invoke = dlsym(lib, "il2cpp_runtime_invoke"); + write_barrier = dlsym(lib, "il2cpp_gc_wbarrier_set_field"); + if (!object_class || !object_new || !string_new || !method_named || !invoke || !write_barrier) + return 0; + unsigned char *send = (unsigned char *)(base + 0x688e200); + uint32_t send_expected[] = {0xd10103ff, 0xa90157f6, 0xa9024ff4, 0xa9037bfd}; + if (memcmp(send, send_expected, 16)) { LOG("Send prologue mismatch"); return 0; } + unsigned char *st = mmap(0, (size_t)page, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (st == MAP_FAILED) return 0; + memcpy(st, send, 16); + jump(st + 16, (uintptr_t)send + 16); + if (mprotect(st, (size_t)page, PROT_READ | PROT_EXEC)) return 0; + __builtin___clear_cache((char *)st, (char *)st + 32); + original_send = (void *(*)(void *, void *))st; + void *spage = (void *)((uintptr_t)send & ~((uintptr_t)page - 1)); + if (mprotect(spage, (size_t)page, PROT_READ | PROT_WRITE | PROT_EXEC)) return 0; + jump(send, (uintptr_t)inspect_send); + __builtin___clear_cache((char *)send, (char *)send + 16); + mprotect(spage, (size_t)page, PROT_READ | PROT_EXEC); + LOG("Radicade image key-id compatibility active; verification unchanged"); + LOG("Radicade fresh-connection experiment active"); + unsigned char *iv = (unsigned char *)(base + 0x6deb934); + uint32_t iv_expected[] = {0xd10343ff, 0xf9004bf7, 0xa90a57f6, 0xa90b4ff4}; + if (memcmp(iv, iv_expected, 16)) { LOG("Image verifier prologue mismatch"); return 0; } + unsigned char *it = mmap(0, (size_t)page, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (it == MAP_FAILED) return 0; + memcpy(it, iv, 16); + jump(it + 16, (uintptr_t)iv + 16); + if (mprotect(it, (size_t)page, PROT_READ | PROT_EXEC)) return 0; + __builtin___clear_cache((char *)it, (char *)it + 32); + original_image_verify = (int (*)(void *, void *))it; + void *ip = (void *)((uintptr_t)iv & ~((uintptr_t)page - 1)); + if (mprotect(ip, (size_t)page, PROT_READ | PROT_WRITE | PROT_EXEC)) return 0; + jump(iv, (uintptr_t)verify_image); + __builtin___clear_cache((char *)iv, (char *)iv + 16); + mprotect(ip, (size_t)page, PROT_READ | PROT_EXEC); + LOG("Legacy image placeholder compatibility active; TLS unchanged"); + return 0; +} +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + (void)vm; (void)reserved; + pthread_t thread; + if (!pthread_create(&thread, 0, install, 0)) pthread_detach(thread); + return JNI_VERSION_1_6; +} diff --git a/Tools/quest-image-placeholder.h b/Tools/quest-image-placeholder.h new file mode 100644 index 0000000..e7ac6c9 --- /dev/null +++ b/Tools/quest-image-placeholder.h @@ -0,0 +1,19 @@ +// Exact legacy Radicade image placeholder; not a cryptographic signature. +static const unsigned char image_placeholder[256] = { +0x21,0x6c,0x1e,0xfe,0x96,0x79,0xbd,0x65,0xaa,0x35,0x29,0x12,0x33,0xfe,0x78,0x8a, +0xc8,0x03,0xc6,0x56,0x64,0x76,0xb3,0xf4,0xb1,0x4a,0xe9,0xa4,0xa0,0x9b,0x36,0x4b, +0x4e,0xdb,0x20,0x8e,0x4e,0x3a,0xbb,0xe3,0xac,0x56,0x8a,0x22,0xec,0xb9,0xe1,0xae, +0x55,0xc0,0x06,0x42,0xa9,0x23,0x79,0x14,0xe2,0x9b,0x1a,0x25,0x1d,0xc9,0x12,0xfd, +0x85,0xd2,0x62,0x41,0x09,0xc6,0xd8,0x82,0x5d,0xb2,0xe5,0xb1,0x18,0x9a,0xd8,0x3c, +0x80,0xaa,0xd5,0x95,0x90,0x65,0xac,0x7d,0xcc,0xf3,0x16,0x1a,0x7f,0xe7,0xa5,0x3a, +0xe8,0x55,0x73,0xa2,0x54,0xec,0xa4,0xf0,0xaf,0x8d,0x27,0x0e,0x56,0x51,0x8e,0xed, +0xd0,0xc8,0x2b,0xbb,0x09,0xfa,0x77,0x5c,0x45,0xab,0x96,0x8e,0xc4,0x2c,0x0b,0xa8, +0x37,0x7b,0xba,0xd4,0x73,0xab,0xb0,0x63,0x4f,0x7d,0x6b,0xa8,0x32,0xee,0xcc,0x3a, +0xc6,0xbe,0x41,0x15,0x94,0x12,0xcf,0xcc,0x20,0xf9,0xfa,0xb2,0x1d,0x80,0xff,0x49, +0xce,0x27,0x41,0x05,0x4e,0x4c,0x56,0x4d,0x34,0x04,0xaa,0x6c,0x96,0x21,0xa4,0xd8, +0x44,0x31,0x74,0x67,0x69,0xf0,0x8c,0xb8,0xb9,0x7c,0x6b,0x79,0xa4,0x1b,0x0b,0xf2, +0xde,0xde,0x2a,0xd8,0x3f,0xfb,0x48,0xce,0x3c,0xc6,0xb1,0x99,0x8c,0x45,0x78,0xee, +0xa3,0x71,0xd4,0x0f,0x75,0x28,0x27,0xb6,0xf1,0x75,0x53,0xa0,0x58,0x78,0x07,0x3c, +0x04,0x51,0x4f,0x27,0x67,0x47,0x7e,0x2b,0x05,0x7b,0xf4,0xc1,0x9a,0xac,0xba,0x35, +0x71,0x8f,0x22,0x81,0xcf,0x05,0x72,0x84,0xe1,0x29,0xca,0x2f,0x23,0x80,0xa5,0x3a +}; diff --git a/Tools/quest-platform-inspect.c b/Tools/quest-platform-inspect.c new file mode 100644 index 0000000..020a333 --- /dev/null +++ b/Tools/quest-platform-inspect.c @@ -0,0 +1,124 @@ +// Read-only runtime metadata inspection; no account values or method calls. +#include +#include +#include +#include +#include +#include +#include +#include +#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "RadicadePlatform", __VA_ARGS__) +#define API(name, ret, args) ret (*name) args = (ret (*) args)dlsym(lib, #name); if (!name) { LOG("Missing API: %s", #name); return 0; } +static void *inspect(void *unused) { + (void)unused; + // Let Unity finish its single-threaded runtime initialization first. + sleep(30); + void *lib = 0; + for (int i = 0; i < 120 && !lib; ++i) { + lib = dlopen("libil2cpp.so", RTLD_NOW | RTLD_NOLOAD); + if (!lib) sleep(1); + } + if (!lib) { LOG("IL2CPP not loaded within 120 seconds"); return 0; } + API(il2cpp_domain_get, void *, (void)); + API(il2cpp_domain_get_assemblies, const void **, (void *, size_t *)); + API(il2cpp_thread_attach, void *, (void *)); + API(il2cpp_thread_detach, void, (void *)); + API(il2cpp_assembly_get_image, void *, (const void *)); + API(il2cpp_image_get_name, const char *, (void *)); + API(il2cpp_image_get_class_count, size_t, (void *)); + API(il2cpp_image_get_class, void *, (void *, size_t)); + API(il2cpp_class_get_name, const char *, (void *)); + API(il2cpp_class_get_namespace, const char *, (void *)); + API(il2cpp_class_from_name, void *, (void *, const char *, const char *)); + API(il2cpp_class_get_methods, const void *, (void *, void **)); + API(il2cpp_method_get_name, const char *, (const void *)); + API(il2cpp_method_get_param_count, uint32_t, (const void *)); + API(il2cpp_class_get_fields, void *, (void *, void **)); + API(il2cpp_field_get_name, const char *, (void *)); + API(il2cpp_field_get_offset, size_t, (void *)); + API(il2cpp_field_get_type, void *, (void *)); + API(il2cpp_type_get_name, char *, (void *)); + API(il2cpp_free, void, (void *)); + void *domain = 0; + size_t count = 0; + const void **assemblies = 0; + for (int i = 0; i < 120; ++i) { + domain = il2cpp_domain_get(); + if (domain) assemblies = il2cpp_domain_get_assemblies(domain, &count); + if (count) break; + sleep(1); + } + if (!domain || !count) { LOG("IL2CPP domain unavailable"); return 0; } + void *thread = il2cpp_thread_attach(domain); + if (!thread) { LOG("Thread attach failed"); return 0; } + char fields_path[192], map_path[192]; + unsigned user = (unsigned)getuid() / 100000; + snprintf(fields_path, sizeof(fields_path), "/data/user/%u/com.radicade.questtls/files/quest-recnet-fields.tsv", user); + snprintf(map_path, sizeof(map_path), "/data/user/%u/com.radicade.questtls/files/quest-method-map.tsv", user); + FILE *fields = fopen(fields_path, "w"); + FILE *map = fopen(map_path, "w"); + if (map) { + for (size_t a = 0; a < count; ++a) { + void *img = il2cpp_assembly_get_image(assemblies[a]); + size_t classes = il2cpp_image_get_class_count(img); + for (size_t c = 0; c < classes; ++c) { + void *klass = il2cpp_image_get_class(img, c); + if (!klass) continue; + if (fields && !strncmp(il2cpp_image_get_name(img), "RecNet.", 7)) { + void *fi = 0, *f; + while ((f = il2cpp_class_get_fields(klass, &fi))) { + char *tn = il2cpp_type_get_name(il2cpp_field_get_type(f)); + fprintf(fields, "%s\t%s\t%zx\t%s\n", il2cpp_class_get_name(klass), il2cpp_field_get_name(f), il2cpp_field_get_offset(f), tn ? tn : "?"); + if (tn) il2cpp_free(tn); + } + } + void *iter = 0; + const void *method; + while ((method = il2cpp_class_get_methods(klass, &iter))) { + void *code = *(void *const *)method; + Dl_info info = {0}; + if (code && dladdr(code, &info)) + fprintf(map, "%lx\t%s\t%s\t%s\t%s\t%u\n", + (unsigned long)((uintptr_t)code - (uintptr_t)info.dli_fbase), + il2cpp_image_get_name(img), il2cpp_class_get_namespace(klass), + il2cpp_class_get_name(klass), il2cpp_method_get_name(method), + il2cpp_method_get_param_count(method)); + } + } + } + fclose(map); + LOG("Full method map saved (metadata only)"); + } else LOG("Cannot open method map output"); + const char *names[] = {"PlatformManager", "PlatformManagerBootstrapper", "OculusPlatformManager", "StandalonePlatformManager", "GooglePlayPlatformManager", "BootSequence"}; + for (size_t a = 0; a < count; ++a) { + void *img = il2cpp_assembly_get_image(assemblies[a]); + if (strcmp(il2cpp_image_get_name(img), "Assembly-CSharp.dll")) continue; + for (size_t n = 0; n < sizeof(names)/sizeof(names[0]); ++n) { + void *klass = il2cpp_class_from_name(img, "", names[n]); + LOG("Class %s found=%d", names[n], klass != 0); + if (!klass) continue; + void *iter = 0; + const void *method; + while ((method = il2cpp_class_get_methods(klass, &iter))) { + void *code = *(void *const *)method; + Dl_info info = {0}; + uintptr_t rva = dladdr(code, &info) ? (uintptr_t)code - (uintptr_t)info.dli_fbase : 0; + LOG("Method %s.%s argc=%u rva=0x%lx", names[n], il2cpp_method_get_name(method), il2cpp_method_get_param_count(method), (unsigned long)rva); + } + iter = 0; + void *field; + while ((field = il2cpp_class_get_fields(klass, &iter))) + LOG("Field %s.%s offset=0x%lx", names[n], il2cpp_field_get_name(field), (unsigned long)il2cpp_field_get_offset(field)); + } + } + if (fields) fclose(fields); + il2cpp_thread_detach(thread); + LOG("Inspection complete"); + return 0; +} +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + (void)vm; (void)reserved; + pthread_t thread; + if (!pthread_create(&thread, 0, inspect, 0)) pthread_detach(thread); + return JNI_VERSION_1_6; +} diff --git a/Tools/recroom-mirror/download.mjs b/Tools/recroom-mirror/download.mjs new file mode 100644 index 0000000..081baa2 --- /dev/null +++ b/Tools/recroom-mirror/download.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../artifacts/recroom-network-mirror'); +const origin = 'https://recroom.network'; +const allowed = new Set(['recroom.network', 'cdn.recroom.network']); +const queue = ['/', '/rooms', '/events', '/news', '/creator', '/shop', '/download'].map(p => origin + p); +const seen = new Set(); +const manifest = []; +await fs.mkdir(root, { recursive: true }); +function localPath(url) { + return (url.hostname === 'cdn.recroom.network' ? '/mirror-cdn' : '') + url.pathname; +} +function rewrite(text) { + return text.replaceAll('https://cdn.recroom.network', '/mirror-cdn') + .replaceAll('https://recroom.network', ''); +} +function discover(text, base) { + const add = raw => { + try { + const value = raw.replaceAll('&', '&').replaceAll('\\/', '/'); + const url = new URL(value, base); + if (!allowed.has(url.hostname) || url.protocol !== 'https:') return; + if (!/\.(?:js|css|woff2?|ttf|png|jpe?g|webp|svg|ico|avif)(?:$|\?)/i.test(url.pathname + url.search)) return; + url.hash = ''; if (!seen.has(url.href)) queue.push(url.href); + } catch {} + }; + for (const m of text.matchAll(/(?:src|href|poster)=["']([^"']+)["']/g)) add(m[1]); + for (const m of text.matchAll(/url\(["']?([^\s)'";]+)["']?\)/g)) add(m[1]); + for (const m of text.matchAll(/["'`](https:\/\/[^"'`\s<>]+|\/(?:_next|static|logos|images)\/[^"'`\s<>]+)["'`]/g)) add(m[1]); + for (const m of text.matchAll(/["'](static\/(?:chunks|media)\/[^"']+)["']/g)) add('/_next/' + m[1]); +} +while (queue.length && seen.size < 350) { + const source = queue.shift(); if (seen.has(source)) continue; seen.add(source); + try { + const response = await fetch(source, { signal: AbortSignal.timeout(25000) }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const type = response.headers.get('content-type') || ''; + const url = new URL(source); + let output = localPath(url); + if (type.includes('text/html')) output = output.replace(/\/$/, '') + '/index.html'; + const filename = path.resolve(root, '.' + output); + if (!filename.startsWith(root + path.sep)) throw new Error('Invalid output path'); + const bytes = Buffer.from(await response.arrayBuffer()); + await fs.mkdir(path.dirname(filename), { recursive: true }); + if (/javascript|text\/css|text\/html/.test(type)) { + const sourceText = bytes.toString('utf8'); discover(sourceText, source); + await fs.writeFile(filename, rewrite(sourceText)); + } else await fs.writeFile(filename, bytes); + manifest.push({ source, file: output, type, bytes: bytes.length }); + console.log(`${manifest.length}: ${output} (${bytes.length})`); + } catch (error) { manifest.push({ source, error: error.message }); console.log(`FAILED ${source}: ${error.message}`); } +} +await fs.writeFile(path.join(root, 'manifest.json'), JSON.stringify({ capturedAt: new Date().toISOString(), source: origin, files: manifest }, null, 2)); +console.log(`Saved ${manifest.filter(x => !x.error).length} files; ${manifest.filter(x => x.error).length} failed; ${queue.length} queued. ${root}`); diff --git a/Tools/recroom-mirror/public-cache.mjs b/Tools/recroom-mirror/public-cache.mjs new file mode 100644 index 0000000..b8e7f1e --- /dev/null +++ b/Tools/recroom-mirror/public-cache.mjs @@ -0,0 +1,75 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { createHash } from 'node:crypto'; + +export function upstreamFor(url) { + const routes = { + '/_public/rooms/featuredrooms/current': 'https://rooms.recroom.network/featuredrooms/current', + '/_public/rooms/rooms/bulk': 'https://rooms.recroom.network/rooms/bulk', + '/_public/accounts/account/bulk': 'https://accounts.recroom.network/account/bulk', + '/_public/apim/apis/api/images/v3/feed/global': 'https://apim.recroom.network/apis/api/images/v3/feed/global', + }; + if (routes[url.pathname]) { + const target = new URL(routes[url.pathname]); + for (const [key,value] of url.searchParams) { + if (!['id','ids','skip','take','include'].includes(key) || !/^[0-9,]{1,2000}$/.test(value)) return null; + target.searchParams.append(key,value); + } + return target; + } + const image = url.pathname.match(/^\/_public\/img\/([a-zA-Z0-9_.%-]{1,180})$/); + if (image) { + return new URL('https://img.recroom.network/' + image[1]); + } + return null; +} + +export async function servePublicCache(url, req, res, root) { + if (!url.pathname.startsWith('/_public/')) return false; + const upstream = upstreamFor(url); + if (!upstream) { res.writeHead(404); res.end(); return true; } + const key = createHash('sha256').update(upstream.href).digest('hex'); + const dir = path.join(root, 'public-cache'); + try { + let bytes, type; + try { + bytes = await fs.readFile(path.join(dir,key)); + type = await fs.readFile(path.join(dir,key+'.type'),'utf8'); + } catch { + const response = await fetch(upstream, {redirect:'error', signal:AbortSignal.timeout(20000)}); + if (response.status === 404 && upstream.hostname === 'img.recroom.network') { + res.writeHead(200, {'Content-Type':'image/svg+xml','X-Mirror-Placeholder':'source-image-missing'}); + res.end('Image unavailable in source archive'); + return true; + } + if (!response.ok) throw new Error(`Public source returned ${response.status}`); + type = response.headers.get('content-type') || 'application/octet-stream'; + bytes = Buffer.from(await response.arrayBuffer()); + if (bytes.length > 15 * 1024 * 1024) throw new Error('Response too large'); + await fs.mkdir(dir,{recursive:true}); + await fs.writeFile(path.join(dir,key),bytes); + await fs.writeFile(path.join(dir,key+'.type'),type); + } + res.writeHead(200, {'Content-Type':type,'Cache-Control':'public, max-age=3600'}); + res.end(req.method === 'HEAD' ? undefined : bytes); + } catch(error) { + console.error('Public cache:', upstream.href, error.message); + res.writeHead(502, {'Content-Type':'application/json'}); + res.end(JSON.stringify({error:'public_snapshot_unavailable'})); + } + return true; +} + +export const bridgeScript = ``; diff --git a/Tools/recroom-mirror/radicade-adapter.mjs b/Tools/recroom-mirror/radicade-adapter.mjs new file mode 100644 index 0000000..7675c47 --- /dev/null +++ b/Tools/recroom-mirror/radicade-adapter.mjs @@ -0,0 +1,80 @@ +const server = new URL(process.env.RADICADE_WEB_SERVER || 'https://ns.radicade.app'); +if (!['https:', 'http:'].includes(server.protocol) || server.username || server.password || server.search || server.hash || server.pathname !== '/') throw new Error('RADICADE_WEB_SERVER must be a server origin'); +export const backendOrigin = server.origin; +const galleryIds = (process.env.RADICADE_GALLERY_IDS || '2').split(',').filter(x => /^\d+$/.test(x)); +const memory = new Map(); +const inflight = new Map(); +async function json(route) { + if (memory.get(route)?.until > Date.now()) return memory.get(route).data; + if (inflight.has(route)) return inflight.get(route); + const pending = (async () => { + const r = await fetch(backendOrigin + route, {redirect:'error',signal:AbortSignal.timeout(20000)}); + if (!r.ok) throw new Error(`Radicade ${route.split('?')[0]} returned ${r.status}`); + const data = await r.json(); memory.set(route,{data,until:Date.now()+15000}); return data; + })(); + inflight.set(route,pending); + try { return await pending; } finally { inflight.delete(route); } +} +export function publicRooms(rows) { return rows.filter(x => x.Accessibility === 1 && x.State === 0 && !x.IsDorm); } +export function roomCard(x) { + return {RoomId:String(x.RoomId),Name:x.Name,Description:x.Description ?? null,ImageName:x.ImageName || 'default', + CreatorAccountId:x.CreatorAccountId == null ? null : String(x.CreatorAccountId),Accessibility:1,IsRRO:!!x.IsRRO,IsDorm:false, + Stats:{CheerCount:x.Stats?.CheerCount ?? 0,VisitCount:x.Stats?.VisitCount ?? 0}}; +} +export function accountCard(x) { + return {accountId:String(x.accountId),username:x.username,displayName:x.displayName,profileImage:x.profileImage || 'default', + isJunior:!!x.isJunior,platforms:x.platforms ?? 0,createdAt:iso(x.createdAt),identityFlags:x.identityFlags ?? 0, + personalPronouns:x.personalPronouns ?? 0,isMetaPlatformBlocked:false}; +} +function iso(date) { return /(?:Z|[+-]\d\d:\d\d)$/.test(date) ? date : date+'Z'; } +export function photoCard(x) { + return {...x,Id:String(x.Id),PlayerId:String(x.PlayerId),RoomId:x.RoomId == null ? null : String(x.RoomId), + PlayerEventId:x.PlayerEventId == null ? null : String(x.PlayerEventId),Description:x.Description ?? null, + CreatedAt:iso(x.CreatedAt),TaggedPlayerIds:(x.TaggedPlayerIds || []).map(String)}; +} +async function rooms() { const data=await json('/rooms/rooms/hot'); return publicRooms(data.Results || []); } +function ids(url) { + const values=url.searchParams.getAll('id'); + if (values.length>100 || values.some(x=>!/^\d{1,18}$/.test(x))) throw new Error('Invalid IDs'); + return values; +} +function idQuery(values) { return new URLSearchParams(values.map(x=>['id',String(x)])).toString(); } +export async function serveRadicade(url,req,res) { + if (!url.pathname.startsWith('/_public/') && url.pathname !== '/_adapter/status') return false; + try { + let data; + if (url.pathname === '/_adapter/status') data={backend:backendOrigin,mode:'radicade',photoFeed:'public galleries of room creators and configured accounts',galleryIds}; + else if (url.pathname === '/_public/rooms/featuredrooms/current') { + const list=await rooms(); list.sort((a,b)=>(b.Stats?.VisitCount||0)-(a.Stats?.VisitCount||0)); + data={Rooms:list.slice(0,6).map(x=>({RoomId:String(x.RoomId)}))}; + } else if (url.pathname === '/_public/rooms/rooms/bulk') { + const wanted=new Set(ids(url)); data=(await rooms()).filter(x=>wanted.has(String(x.RoomId))).map(roomCard); + } else if (url.pathname === '/_public/accounts/account/bulk') { + const wanted=ids(url); data=wanted.length ? (await json('/accounts/account/bulk?'+idQuery(wanted))).map(accountCard) : []; + } else if (url.pathname === '/_public/apim/apis/api/images/v3/feed/global') { + const skip=Number(url.searchParams.get('skip') || 0),take=Number(url.searchParams.get('take') || 10); + if (!Number.isInteger(skip)||!Number.isInteger(take)||skip<0||skip>10000||take<1||take>100) throw new Error('Invalid pagination'); + const creators=[...new Set([...galleryIds,...(await rooms()).map(x=>String(x.CreatorAccountId))])].slice(0,50); + const images=[]; + for(const id of creators) images.push(...await json('/api/images/v5/player/'+id)); + const unique=[...new Map(images.filter(x=>x.Accessibility===1 && x.Type===1).map(x=>[String(x.Id),x])).values()]; + unique.sort((a,b)=>new Date(b.CreatedAt)-new Date(a.CreatedAt)); + data=unique.slice(skip,skip+take).map(photoCard); + } else if (/^\/_public\/img\/[a-zA-Z0-9_.%-]{1,180}$/.test(url.pathname)) { + const name=url.pathname.split('/').pop(); + if (decodeURIComponent(name).includes('/') || decodeURIComponent(name).includes('\\') || name === '..') throw new Error('Invalid image'); + const r=await fetch(backendOrigin+'/images/img/'+name,{redirect:'error',signal:AbortSignal.timeout(20000)}); + if (!r.ok) throw new Error('Radicade image returned '+r.status); + const type=r.headers.get('content-type') || ''; + if (!type.startsWith('image/')) throw new Error('Unexpected image type'); + res.writeHead(200,{'Content-Type':type,'Cache-Control':'public, max-age=60'}); + res.end(req.method==='HEAD'?undefined:Buffer.from(await r.arrayBuffer())); return true; + } else {res.writeHead(404,{'Content-Type':'application/json'});res.end('{"error":"adapter_route_not_connected"}');return true;} + res.writeHead(200,{'Content-Type':'application/json','Cache-Control':'no-store','X-Radicade-Backend':backendOrigin}); + res.end(req.method==='HEAD'?undefined:JSON.stringify(data)); + } catch(error) { + console.error('Radicade adapter:',error.message); + res.writeHead(502,{'Content-Type':'application/json'});res.end(JSON.stringify({error:'radicade_unavailable',message:error.message})); + } + return true; +} diff --git a/Tools/recroom-mirror/serve.mjs b/Tools/recroom-mirror/serve.mjs new file mode 100644 index 0000000..10afb4a --- /dev/null +++ b/Tools/recroom-mirror/serve.mjs @@ -0,0 +1,46 @@ +import http from 'node:http'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { servePublicCache, bridgeScript } from './public-cache.mjs'; +import { serveRadicade, backendOrigin } from './radicade-adapter.mjs'; +const archiveMode = process.env.RECNET_DATA_MODE === 'archive'; +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../artifacts/recroom-network-mirror'); +const types = {'.html':'text/html; charset=utf-8','.js':'text/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.avif':'image/avif','.ico':'image/x-icon','.woff':'font/woff','.woff2':'font/woff2','.ttf':'font/ttf'}; +http.createServer(async (req, res) => { + res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self' blob:; form-action 'none'; frame-src 'none'; object-src 'none'; base-uri 'self'"); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Cache-Control', 'no-store'); + if (!['GET','HEAD'].includes(req.method)) { res.writeHead(501, {'Content-Type':'application/json'}); res.end('{"error":"offline_preview","message":"Login and server actions are not connected in this local copy."}'); return; } + try { + const url = new URL(req.url, 'http://127.0.0.1'); + if (!archiveMode && url.pathname.startsWith('/_radicade/v2/')) { + url.pathname = url.pathname.replace('/_radicade/v2/', '/_public/'); + } + if (archiveMode ? await servePublicCache(url, req, res, root) : await serveRadicade(url, req, res)) return; + if (url.pathname === '/static/img/NoSearchResult.png') { + res.writeHead(200, {'Content-Type':'image/svg+xml'}); + res.end('[ = )'); + return; + } + const filename = path.resolve(root, '.' + url.pathname); + if (filename !== root && !filename.startsWith(root + path.sep)) throw new Error('Invalid path'); + let actual = filename; + const stat = await fs.stat(actual); + if (stat.isDirectory()) actual = path.join(actual, 'index.html'); + let content = await fs.readFile(actual); + const ext = path.extname(actual); + const dataPrefix = archiveMode ? '/_public/' : '/_radicade/v2/'; + if (ext === '.js' || ext === '.html') content = Buffer.from(content.toString() + .replaceAll('https://img.recroom.network/', dataPrefix + 'img/') + .replaceAll('/_public/img/', dataPrefix + 'img/')); + if (ext === '.html') { + const previewScript = ``; + const sourceLabel=archiveMode?'Cached recroom.network data':'Radicade public data · '+backendOrigin.replace(/[&<>"']/g,''); + const activeBridge = bridgeScript.replaceAll('/_public/', dataPrefix); + content = Buffer.from(content.toString().replace('', '' + activeBridge + previewScript).replace('', '
Local copy · '+sourceLabel+' · Login not connected
')); + } + res.writeHead(200, {'Content-Type':types[ext] || 'application/octet-stream'}); + res.end(req.method === 'HEAD' ? undefined : content); + } catch { res.writeHead(404, {'Content-Type':'application/json'}); res.end('{"error":"not_archived","message":"This file or API is not available in the local copy."}'); } +}).listen(3040, '127.0.0.1', () => console.log('Local website copy: http://127.0.0.1:3040')); diff --git a/Tools/reorganize-server-features.sh b/Tools/reorganize-server-features.sh new file mode 100644 index 0000000..e55866f --- /dev/null +++ b/Tools/reorganize-server-features.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# Groups the server into feature folders. Run with --plan to print, --apply to move. +# +# Only files move. Namespaces stay exactly as they are, because a namespace rewrite would touch +# every using in the repo for no functional gain - C# does not require a file's folder to match +# its namespace - and this way the whole change is a rename that `git log --follow` tracks. +# The project is SDK-style (globbed), so moved sources compile without editing Server.csproj, +# except for the two JSON resources the csproj names by path (starter avatar items, webhooks). +set -u + +mode="${1:---plan}" +root="$(cd "$(dirname "$0")/.." && pwd)" +cd "$root" + +moved=0 +skipped=0 + +move_file() { + local src="$1" dest="$2" + [ -e "$src" ] || { return 0; } + local name; name="$(basename "$src")" + if [ -e "$dest/$name" ]; then + echo "SKIP (exists): $src -> $dest/$name" + skipped=$((skipped + 1)) + return 0 + fi + echo "$src -> $dest/$name" + moved=$((moved + 1)) + if [ "$mode" = "--apply" ]; then + mkdir -p "$dest" + git mv "$src" "$dest/$name" + fi +} + +# Every *.cs under a directory moves to a feature folder. +move_dir() { + local src="$1" dest="$2" + [ -d "$src" ] || { return 0; } + while IFS= read -r file; do + move_file "$file" "$dest" + done < <(find "$src" -name '*.cs' -not -path '*/obj/*' -not -path '*/bin/*' | sort) +} + +move_glob() { + local pattern="$1" dest="$2" + for file in $pattern; do + [ -e "$file" ] || continue + move_file "$file" "$dest" + done +} + +# ---------------------------------------------------------------- players, accounts, auth +move_dir "Server/API/Accounts" "Server/Players" +move_dir "Server/API/Players" "Server/Players" +move_dir "Server/API/Link" "Server/Players" +move_glob "Server/API/Public/PublicPlayerAPI.cs" "Server/Players" +move_dir "Server/RecNet/Accounts" "Server/Players" +move_glob "Server/Utils/PlayerUtils.cs Server/Utils/UsernameRules.cs Server/Utils/RegistrationGuard.cs Server/Utils/AuthSecurityMonitor.cs Server/Utils/FirstTimeUtils.cs" "Server/Players" +move_dir "Server/Auth" "Server/Auth" +move_dir "Server/RecNet/Auth" "Server/Auth" +move_glob "Server/Utils/Attributes/RecAuth.cs" "Server/Auth" + +# ---------------------------------------------------------------- avatars, custom items, locker +move_glob "Server/API/Avatars/CustomAvatarItemsAPI.cs" "Server/CustomAvatarItems" +move_glob "Server/RecNet/Avatar/CustomAvatarItemDB.cs" "Server/CustomAvatarItems" +move_dir "Server/API/Avatars" "Server/Avatars" +move_glob "Server/RecNet/Avatar/*.cs" "Server/Avatars" +move_glob "Server/RecNet/Avatar/*.json" "Server/Avatars" + +# ---------------------------------------------------------------- gifts and reward boxes +move_glob "Server/RecNet/Avatar/Gift.cs Server/RecNet/CardBoxWord.cs" "Server/Gift" +move_glob "Server/Utils/FriendotronService.cs Server/Utils/GameRewardRedemption.cs Server/Utils/RroRewardPools.cs Server/Utils/RoomJoinGiftUtils.cs Server/Utils/QuestRewardDelivery.cs" "Server/Gift" +move_dir "Server/Radicade/Gifts" "Server/Gift" + +# ---------------------------------------------------------------- images +move_glob "Server/API/ImagesAPI.cs Server/API/Public/PublicPhotosAPI.cs" "Server/Images" +move_dir "Server/Images" "Server/Images" +move_dir "Server/RecNet/Images" "Server/Images" +move_glob "Server/Radicade/Public/PublicPhotoData.cs" "Server/Images" + +# ---------------------------------------------------------------- moderation family +move_glob "Server/API/Moderation/AppealAPI.cs" "Server/BanAppeal" +move_glob "Server/API/Moderation/PlayerReportingAPI.cs Server/API/Moderation/ContentReportAPI.cs Server/API/Moderation/HileAPI.cs" "Server/PlayerReporting" +move_glob "Server/RecNet/Moderation/Hile.cs Server/RecNet/Moderation/BlockDetails.cs Server/RecNet/Moderation/SanitizeStringRequest.cs Server/RecNet/Moderation/IsPureResponse.cs" "Server/PlayerReporting" +move_glob "Server/API/Moderation/ReputationAPI.cs Server/RecNet/Moderation/Reputation.cs" "Server/PlayerReputation" +move_glob "Server/RecNet/Moderation/CheerPlayerRequest.cs" "Server/PlayerCheer" +move_glob "Server/RecNet/Moderation/PlayerWarning.cs" "Server/PlayerWarnings" +move_glob "Server/RecNet/Moderation/ModerationDB.cs Server/RecNet/Moderation/IpBan.cs Server/RecNet/Moderation/HardwareBan.cs Server/RecNet/Moderation/DeviceId.cs Server/RecNet/Moderation/GameBlock.cs Server/RecNet/Moderation/VoteKickLog.cs Server/RecNet/Moderation/VoteKickReason.cs Server/RecNet/Moderation/AccountCreationBlock.cs" "Server/PlayersBanned" +move_glob "Server/Utils/AccountLock.cs Server/Utils/BanStatusMonitor.cs Server/Utils/GameBlockMonitor.cs Server/Utils/TokenRevocation.cs" "Server/PlayersBanned" +move_glob "Server/RecNet/Moderation/VoiceChatRestriction.cs Server/Utils/ModerationPolicy.cs Server/API/SanitizeAPI.cs" "Server/Moderation" +move_glob "Server/RecNet/Moderation/OkResponse.cs" "Server/Moderation" + +# ---------------------------------------------------------------- events, challenges, progression +move_dir "Server/API/Events" "Server/PlayerEvents" +move_dir "Server/RecNet/Events" "Server/PlayerEvents" +move_dir "Server/Utils/Objectives" "Server/PlayerEvents" +move_dir "Server/API/Progression" "Server/ProgressionEvents" +move_dir "Server/API/GameRewards" "Server/ProgressionEvents" +move_dir "Server/RecNet/Progression" "Server/ProgressionEvents" +move_dir "Server/RecNet/Enums/Challenges" "Server/ProgressionEvents/Enums" +move_glob "Server/Utils/LevelRewardTable.cs Server/Utils/LevelUpGiftService.cs Server/Utils/LevelUpRewardPool.cs Server/Utils/ProgressionAwardService.cs Server/Utils/RewardDay.cs Server/Utils/RewardTrace.cs Server/Utils/WeeklyChallenges.cs" "Server/ProgressionEvents" + +# ---------------------------------------------------------------- chat, clubs, relationships +move_glob "Server/Chat/Thread.cs Server/Chat/ChatPhotoMessages.cs Server/Chat/RecentImageReferences.cs" "Server/Chat" +move_dir "Server/API/Messages" "Server/Messages" +move_dir "Server/RecNet/Messages" "Server/Messages" +move_dir "Server/Clubs" "Server/Clubs" +move_dir "Server/API/Relationships" "Server/Relationships" +move_dir "Server/RecNet/Relationships" "Server/Relationships" +move_glob "Server/API/AnnouncementAPI.cs" "Server/Clubs/Announcements" + +# ---------------------------------------------------------------- economy +move_dir "Server/API/CampusCard" "Server/Commerce" +move_dir "Server/RecNet/Store/CampusCard" "Server/Commerce" +move_glob "Server/Commerce/CommercePlaceholderAPI.cs" "Server/Commerce" +move_dir "Server/API/Storefront" "Server/Storefront" +move_dir "Server/RecNet/Storefront" "Server/Storefront" +move_glob "Server/RecNet/Store/*.cs" "Server/Storefront" +move_dir "Server/API/IncentivizedReferrals" "Server/Promotions" +move_dir "Server/API/InfluencerPartnerProgram" "Server/Promotions" +move_dir "Server/RecNet/Store/Referrals" "Server/Promotions" +move_dir "Server/API/Wishlists" "Server/Storefront" + +# ---------------------------------------------------------------- rooms and play +move_dir "Server/API/Rooms" "Server/Rooms" +move_dir "Server/RoomServer" "Server/Rooms" +move_dir "Server/RecNet/Rooms" "Server/Rooms" +move_glob "Server/Utils/RoomUtils.cs Server/Utils/RoomProgressionService.cs Server/Utils/DefaultRooms.cs" "Server/Rooms" +move_glob "Server/CDN/RoomsCDN.cs" "Server/Rooms" +move_glob "Server/Radicade/Public/PublicRoomData.cs" "Server/Rooms" +move_dir "Server/Lists" "Server/Rooms/Lists" +move_dir "Server/API/Matchmaking" "Server/Matchmaking" +move_dir "Server/RecNet/Matchmaking" "Server/Matchmaking" +move_glob "Server/Utils/MatchmakingUtils.cs Server/Utils/MatchmakingRuntimeState.cs" "Server/Matchmaking" +move_dir "Server/API/Activities" "Server/Activities" +move_dir "Server/API/Royale" "Server/Royale" +move_dir "Server/RecNet/Royale" "Server/Royale" +move_dir "Server/API/Board" "Server/Board" +move_dir "Server/RecNet/Board" "Server/Board" +move_dir "Server/RecNet/Enums/Board" "Server/Board/Enums" +move_dir "Server/API/Keepsakes" "Server/Keepsakes" +move_dir "Server/RecNet/Keepsakes" "Server/Keepsakes" +move_dir "Server/API/Inventions" "Server/Inventions" +move_dir "Server/RecNet/Inventions" "Server/Inventions" +move_dir "Server/RecNet/Enums/Inventions" "Server/Inventions/Enums" +move_glob "Server/CDN/InventionCDN.cs" "Server/Inventions" + +# ---------------------------------------------------------------- platform, tooling, publishing +move_dir "Server/API/Notify" "Server/Notifications" +move_dir "Server/API/Notifications" "Server/Notifications" +move_dir "Server/RecNet/Notifications" "Server/Notifications" +move_dir "Server/Leaderboard" "Server/Leaderboards" +move_dir "Server/RecNet/Leaderboards" "Server/Leaderboards" +move_glob "Server/CDN/*.cs" "Server/CDN" +move_dir "Server/RecNet/CDN" "Server/CDN" +move_dir "Server/Storage/StudioAssetStorage.cs" "Server/Studio" +move_glob "Server/Storage/StorageAPI.cs" "Server/Storage" +move_dir "Server/API/Studio" "Server/Studio" +move_glob "Server/CDN/StudioAssetsCDN.cs" "Server/Studio" +move_dir "Server/API/Launcher" "Server/Launcher" +move_glob "Server/API/VersionCheckAPI.cs" "Server/Launcher" +move_dir "Server/RecNet/VersionCheck" "Server/Launcher" +move_glob "Server/API/ServiceRootHealthAPI.cs" "Server/Health" +move_dir "Server/NS" "Server/NameServer" +move_glob "Server/RecNet/NameServerResponse.cs" "Server/NameServer" +move_dir "Server/API/Analytics" "Server/Analytics" +move_dir "Server/API/Logs" "Server/Analytics" +move_dir "Server/RecNet/GameSight" "Server/Analytics" +move_dir "Server/API/Config" "Server/Config" +move_dir "Server/RecNet/Config" "Server/Config" +move_glob "Server/Utils/GameConfigSeeder.cs" "Server/Config" +move_dir "Server/API/BugReporting" "Server/BugReporting" +move_dir "Server/Admin" "Server/Admin" +move_glob "Server/Utils/AdminRuntime.cs Server/Utils/AdminUtils.cs" "Server/Admin" +move_dir "Server/Discord" "Server/Discord" +move_dir "Server/Radicade/Discord" "Server/Discord" +move_glob "Server/Radicade/Mail/*.cs" "Server/Mail" +move_glob "Server/Utils/LoadingScreenImages.cs Server/RecNet/LoadingScreenTip.cs" "Server/LoadingScreens" +move_glob "Server/API/TestCaseManagement/*.cs" "Server/Tooling" +move_glob "Server/Debug/*.cs" "Server/Tooling" +move_dir "Server/Custom" "Server/Tooling/Custom" +move_dir "Server/Web" "Server/Web" +move_glob "Server/Utils/StringTools.cs Server/Utils/BuildInfo.cs" "Server/Utils" + +echo +echo "planned: $moved file(s), skipped: $skipped (mode $mode)" +if [ "$mode" != "--apply" ]; then + echo "re-run with --apply to move them" +fi diff --git a/Tools/repair-unity-script-compiler.ps1 b/Tools/repair-unity-script-compiler.ps1 new file mode 100644 index 0000000..b738025 --- /dev/null +++ b/Tools/repair-unity-script-compiler.ps1 @@ -0,0 +1,238 @@ +<# +.SYNOPSIS + Puts back the Roslyn script compiler that Windows Defender quarantines out of the Unity + package cache, so the editor can compile the project at all. + +.DESCRIPTION + Unity 2020.3 compiles C# by launching an *external* compiler, and because this project + installs `com.unity.roslyn` (a transitive dependency of `com.unity.entities`) that compiler + is the one shipped inside the package: + + \Library\PackageCache\com.unity.roslyn@\Compiler~\win64\csc.exe + + Defender quarantines unsigned executables out of Unity package folders on this machine - the + recorded casualties are `com.unity.burst\.Runtime\hostwin\llvm-lipo.exe` and `dsymutil.exe` + (heuristic PUA signatures, ThreatID 2147972435) and, for this project, that `csc.exe`. It has + to be deleted rather than absent from the package: the registry tarball does ship it + (`package/Compiler~/win64/csc.exe`). + + With it gone, Unity logs the same failure for every assembly it was about to build: + + Starting external compile of Unity.Jobs.CodeGen.dll: ...\Compiler~\win64\csc.exe ... + Exception when running external compiler: System.ComponentModel.Win32Exception + (0x80004005): Native error= The system cannot find the file specified. + + and then, because nothing compiled at all, reports the misleading + + Unable to resolve reference 'Unity.Mathematics' / 'Unity.Collections' / ... + + for every `com.recroom.*` assembly, while `Library\ScriptAssemblies` stays empty apart from + `BuiltinAssemblies.stamp`. So: an empty ScriptAssemblies folder plus a wall of unresolvable + references means "the compiler never ran", not "the packages are broken". + + This script restores the file from `-FromPath`, or from the Unity editor install's own + `Editor\Data\Tools\Roslyn\csc.exe` (a different build of the same 3.8 apphost, verified to + drive the package's newer `csc.dll`), and then proves it runs by asking it for its version. + + The Unity-install copy is 3.8.0-dev.20527.1 as well, so the compiler behaviour is unchanged; + only the ~170 KB launcher differs. Prefer `-FromPath` with the authentic file if you have the + package tarball to hand: + https://cdn.packages.unity.com/tarballs/com.unity.roslyn/com.unity.roslyn-0.2.2-preview/8b5b337db92cbdec37b54235ec8b826c92c6ddef.tgz + (`tar -xzf package/Compiler~/win64/csc.exe`, md5 8c4361cc37113f8467feb1d22b4e83da) + + This is a repair, not a cure: Defender will do it again on the next full scan, and a Unity + package re-resolve copies the damaged file back out of the shared cache. Add the exclusions + this script prints at the end, and re-run with -IncludeGlobalCache, to close both loops. + +.PARAMETER ProjectRoot + Candidate project roots, tried in order; the first one that exists and holds the roslyn + package cache is used. + +.PARAMETER FromPath + Use this csc.exe instead of the Unity editor install's copy. + +.PARAMETER IncludeGlobalCache + Also repair the shared package cache under %LOCALAPPDATA%\Unity\cache\packages, which is + where a re-resolve would otherwise copy the missing file back from. + +.PARAMETER Force + Overwrite csc.exe even when one is already present and working. + +.EXAMPLE + pwsh -File tools/repair-unity-script-compiler.ps1 + pwsh -File tools/repair-unity-script-compiler.ps1 -FromPath C:\tmp\csc.exe -IncludeGlobalCache -Force +#> +[CmdletBinding()] +param( + [string[]] $ProjectRoot = @( + 'C:\RecRoom\Documents\EggFiles\ProjectTemplate', + 'C:\RecRoom\OneDrive\Documents\EggFiles\ProjectTemplate' + ), + [string] $FromPath, + [switch] $IncludeGlobalCache, + [switch] $Force +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$expectedMd5 = '8c4361cc37113f8467feb1d22b4e83da' + +function Write-Status { + param([string] $Level, [string] $Message) + Write-Host ("[{0}] {1}" -f $Level, $Message) +} + +function Find-RoslynCompilerDir { + <# The roslyn package folder inside a project, or $null. #> + param([string] $Root) + + $pkgRoot = Join-Path $Root 'Library\PackageCache' + if (-not (Test-Path -LiteralPath $pkgRoot)) { return $null } + + $dirs = Get-ChildItem -LiteralPath $pkgRoot -Directory -Filter 'com.unity.roslyn@*' -ErrorAction SilentlyContinue | + ForEach-Object { Join-Path $_.FullName 'Compiler~\win64' } | + Where-Object { Test-Path -LiteralPath $_ } + + if (-not $dirs) { return $null } + return @($dirs)[0] +} + +function Find-UnityInstallCompiler { + <# Tools\Roslyn\csc.exe from the editor version this project is pinned to. #> + param([string] $Root) + + $versionFile = Join-Path $Root 'ProjectSettings\ProjectVersion.txt' + $version = $null + if (Test-Path -LiteralPath $versionFile) { + $line = Get-Content -LiteralPath $versionFile | Where-Object { $_ -match '^m_EditorVersion:' } | Select-Object -First 1 + if ($line) { $version = ($line -split ':', 2)[1].Trim() } + } + + $roots = @() + if ($version) { + $roots += "C:\Program Files\Unity $version" + $roots += "C:\Program Files\Unity\Hub\Editor\$version" + } + $roots += @(Get-ChildItem 'C:\Program Files\Unity*' -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) + + foreach ($r in $roots) { + $candidate = Join-Path $r 'Editor\Data\Tools\Roslyn\csc.exe' + if (Test-Path -LiteralPath $candidate) { return $candidate } + } + return $null +} + +function Test-Compiler { + <# Runs the compiler's own version probe: the launcher only works if it can reach its csc.dll. #> + param([string] $Path) + + try { + $output = & $Path -version 2>&1 + } catch { + return [pscustomobject]@{ Ok = $false; Detail = $_.Exception.Message } + } + $text = ($output | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or $text -notmatch 'Visual C# Compiler version|^\d+\.\d+') { + $detail = if ($text) { $text } else { "exited with code $LASTEXITCODE and no output" } + return [pscustomobject]@{ Ok = $false; Detail = $detail } + } + return [pscustomobject]@{ Ok = $true; Detail = $text } +} + +function Repair-CompilerDir { + param([string] $Dir, [string] $Source) + + $target = Join-Path $Dir 'csc.exe' + if ((Test-Path -LiteralPath $target) -and -not $Force) { + $probe = Test-Compiler -Path $target + if ($probe.Ok) { + Write-Status 'PASS' "already present and working: $target ($($probe.Detail))" + return $false + } + Write-Status 'WARN' "present but not usable ($($probe.Detail)) - replacing it" + } + + if (-not (Test-Path -LiteralPath $Dir)) { + New-Item -ItemType Directory -Path $Dir -Force | Out-Null + } + + Copy-Item -LiteralPath $Source -Destination $target -Force + $probe = Test-Compiler -Path $target + if (-not $probe.Ok) { + throw "Restored $target but it still does not run: $($probe.Detail)" + } + + $md5 = (Get-FileHash -LiteralPath $target -Algorithm MD5).Hash.ToLowerInvariant() + if ($md5 -eq $expectedMd5) { + Write-Status 'PASS' "restored the authentic $target ($md5)" + } else { + Write-Status 'PASS' "restored $target (md5 $md5; the Unity install build, not the package one)" + } + return $true +} + +# ---- main ------------------------------------------------------------------------- + +Write-Host '' + +$project = $null +foreach ($candidate in $ProjectRoot) { + if (-not (Test-Path -LiteralPath $candidate)) { continue } + if (Find-RoslynCompilerDir -Root $candidate) { $project = $candidate; break } +} +if (-not $project) { + throw "No project with an extracted com.unity.roslyn package found. Tried:`n $($ProjectRoot -join "`n ")" +} + +$compilerDir = Find-RoslynCompilerDir -Root $project +Write-Status 'INFO' "project : $project" +Write-Status 'INFO' "compiler : $compilerDir" + +$source = $FromPath +if ($source) { + if (-not (Test-Path -LiteralPath $source)) { throw "-FromPath '$source' does not exist." } + Write-Status 'INFO' "source : $source (-FromPath)" +} else { + $source = Find-UnityInstallCompiler -Root $project + if (-not $source) { + throw @" +No csc.exe available to restore from. Either install the editor version named in +ProjectSettings\ProjectVersion.txt, or pass -FromPath pointing at a copy of the package's +win64\csc.exe taken from the registry tarball. +"@ + } + Write-Status 'INFO' "source : $source (Unity editor install)" +} + +$repaired = Repair-CompilerDir -Dir $compilerDir -Source $source +if (-not $repaired) { + Write-Status 'INFO' 'nothing to do for this project' +} + +if ($IncludeGlobalCache) { + $globalRoot = Join-Path $env:LOCALAPPDATA 'Unity\cache\packages\packages.unity.com' + $globalDirs = Get-ChildItem -LiteralPath $globalRoot -Directory -Filter 'com.unity.roslyn@*' -ErrorAction SilentlyContinue | + ForEach-Object { Join-Path $_.FullName 'Compiler~\win64' } + if (-not $globalDirs) { + Write-Status 'WARN' "no com.unity.roslyn under $globalRoot" + } + foreach ($d in $globalDirs) { Repair-CompilerDir -Dir $d -Source $source | Out-Null } +} + +# Unity will not pick the compiler up until it recompiles; ScriptAssemblies should then fill in. +$scriptAssemblies = Join-Path $project 'Library\ScriptAssemblies' +if (Test-Path -LiteralPath $scriptAssemblies) { + $built = @(Get-ChildItem -LiteralPath $scriptAssemblies -Filter '*.dll' -ErrorAction SilentlyContinue).Count + if ($built -eq 0) { + Write-Status 'INFO' 'Library\ScriptAssemblies is still empty - reopen the project (Unity recompiles on focus)' + } else { + Write-Status 'INFO' "Library\ScriptAssemblies holds $built assemblies" + } +} + +Write-Host '' +Write-Status 'INFO' 'Windows Defender is what removes this file. In an ELEVATED PowerShell:' +Write-Host " Add-MpPreference -ExclusionPath '$project'" +Write-Host " Add-MpPreference -ExclusionPath `"$env:LOCALAPPDATA\Unity\cache`"" +Write-Status 'INFO' 'the same rule also emptied com.unity.burst\.Runtime\hostwin (llvm-lipo/dsymutil, needed for Apple targets only)' diff --git a/Tools/sep-literals.py b/Tools/sep-literals.py new file mode 100644 index 0000000..aab6a27 --- /dev/null +++ b/Tools/sep-literals.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Inspect the il2cpp string-literal table of the 20230831 client. + +global-metadata.dat ships lightly obfuscated (header magic is not 0xFAB11BAF), +but the interop copy BepInEx writes (global-metadata.interop.dat) is plain v27, +which is the file the plugin itself reads. String literals live at header +offsets 8..24: + + int32 stringLiteralOffset // table of (length, dataOffset) pairs + int32 stringLiteralCount // BYTES in that table (8 per literal) + int32 stringLiteralDataOffset // concatenated utf8 bytes + int32 stringLiteralDataCount // BYTES of literal data + +Usage: + python tools/sep-literals.py find [-i] [--limit N] + python tools/sep-literals.py around [radius] + python tools/sep-literals.py token + +Environment: + RADICADE_METADATA override the metadata path +""" + +import os +import re +import struct +import sys + +DEFAULT_METADATA = ( + r"C:\RecRoom\2023-09-06T06-53-47Z\RecRoom_Data\il2cpp_data" + r"\Metadata\global-metadata.interop.dat" +) + + +def load(path): + with open(path, "rb") as handle: + blob = handle.read() + magic, version = struct.unpack_from(" len(blob): + raise SystemExit(f"implausible literal table ({table}, {table_bytes})") + literals = [] + for index in range(count): + length, offset = struct.unpack_from(" data_bytes: + raise SystemExit(f"literal #{index} escapes the data segment") + literals.append(blob[data + offset : data + offset + length].decode("utf-8", "replace")) + return version, literals + + +def main(argv): + if len(argv) < 2: + raise SystemExit(__doc__) + path = os.environ.get("RADICADE_METADATA", DEFAULT_METADATA) + version, literals = load(path) + command, rest = argv[1], argv[2:] + + if command == "find": + if not rest: + raise SystemExit("find needs a needle") + ignore_case = "-i" in rest + limit = None + positional = [] + index = 0 + while index < len(rest): + if rest[index] == "--limit": + limit = int(rest[index + 1]) + index += 2 + continue + if rest[index] != "-i": + positional.append(rest[index]) + index += 1 + if not positional: + raise SystemExit("find needs a needle") + needle = positional[0] + target = needle.lower() if ignore_case else needle + hits = 0 + for index, text in enumerate(literals): + haystack = text.lower() if ignore_case else text + if target not in haystack: + continue + print(f"#{index}\t{text!r}") + hits += 1 + if limit and hits >= limit: + break + print(f"-- {hits} match(es); {len(literals)} literals; metadata v{version}") + + elif command == "around": + index = int(rest[0]) + radius = int(rest[1]) if len(rest) > 1 else 25 + for other in range(max(0, index - radius), min(len(literals), index + radius + 1)): + mark = ">>" if other == index else " " + print(f"{mark} #{other}\t{literals[other]!r}") + + elif command == "token": + for raw in rest: + index = int(raw) + print(f"#{index}\t0x{(5 << 29) | (index << 1):X}\t{literals[index]!r}") + + else: + raise SystemExit(__doc__) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/Tools/sep-scan.py b/Tools/sep-scan.py new file mode 100644 index 0000000..4c0d780 --- /dev/null +++ b/Tools/sep-scan.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Locate metadata-usage tokens / pointers inside an il2cpp binary. + +On metadata v27 the compiled code does not carry literal indices: a string +literal is reached through a metadata-usage value `(type << 29) | (index << 1) | 1` +(type 5 = string literal). That value shows up in the image either as an +immediate operand in code or as the pre-loader content of a usage slot, so a +plain byte scan is enough to find the referencing function - parse #29705's +token with tools/sep-literals.py, scan here, then read the enclosing method +from the runtime address map. + +Usage: + python tools/sep-scan.py --binary --token 0xA000E813[,..] + python tools/sep-scan.py --binary --pointer 0x1891B46E8[,..] + python tools/sep-scan.py --binary --map --token ... + +Options: + --section .text restrict the scan (repeatable; default: every section) + --context N extra bytes of hex context to print (default 16) +""" + +import argparse +import os +import struct +import sys + +METADATA_USAGE_STRING_LITERAL = 5 + + +def literal_token(index): + return (METADATA_USAGE_STRING_LITERAL << 29) | (index << 1) | 1 + + +def parse_number(text): + return int(text, 0) + + +def read_sections(blob): + pe_offset = struct.unpack_from(" {module} {method} +{delta:#x}" + print(line) + context = blob[max(offset - 4, start) : offset + len(pattern) + args.context] + print(f" ctx {context.hex(' ')}") + total += 1 + offset += 1 + print(f"{total} occurrence(s)") + + if entries: + print("map loaded: %d methods" % len(entries)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/Tools/sept-map.py b/Tools/sept-map.py new file mode 100644 index 0000000..7210968 --- /dev/null +++ b/Tools/sept-map.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +"""Map obfuscated Il2CppInterop names between two game builds. + +Il2CppInterop emits, for every il2cpp member, a stub line + + NativeMethodInfoPtr___0 = IL2CPP.GetIl2CppMethodByToken( + Il2CppClassPointerStore<>.NativeClassPtr, ); + +The token encodes the global method index (0x06000000 + index), which shifts by +only a few units between builds, and is a structural type spelling +(parameter kinds, arity, primitives) that survives renames -- only obfuscated +type identifiers inside it change. So: (normalized signature, token within a +window) identifies a member across builds without knowing the obfuscated names. + +Usage: + python tools/sept-map.py show [MemberFilter] + python tools/sept-map.py find + python tools/sept-map.py map [window] + python tools/sept-map.py shape + python tools/sept-map.py types +""" + +import os +import re +import sys +from collections import defaultdict + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ROOTS = { + "aug": [ + os.path.join(ROOT, ".external", "aug-decomp", "acs"), + os.path.join(ROOT, ".external", "aug-decomp", "rcn"), + os.path.join(ROOT, ".external", "aug-decomp", "pfdoldleiba.cs"), + os.path.join(ROOT, ".external", "aug-decomp", "aug-bootsequence.cs"), + os.path.join(ROOT, ".external", "interop-decomp"), + ], + "sep": [ + os.path.join(ROOT, ".external", "sep-decomp", "acs"), + os.path.join(ROOT, ".external", "sep-decomp", "rcn"), + os.path.join(ROOT, ".external", "sep-decomp", "rp"), + os.path.join(ROOT, ".external", "sep-decomp", "recr"), + os.path.join(ROOT, ".external", "sep-decomp", "easy"), + os.path.join(ROOT, ".external", "sep-decomp", "code"), + ], +} + +METHOD_RE = re.compile( + r"NativeMethodInfoPtr_([A-Za-z0-9_]+?)_(.*?)_0 = IL2CPP\.GetIl2CppMethodByToken\(" + r"Il2CppClassPointerStore<([A-Za-z0-9_]+)>\.NativeClassPtr, (\d+)\)" +) +FIELD_RE = re.compile(r"NativeFieldInfoPtr_([A-Za-z0-9_]+?);") +TYPE_RE = re.compile(r"^\t*(?:\[[^\]]*\]\s*)*(?:public|internal|private|protected)?\s*" + r"(?:sealed\s+|abstract\s+|static\s+|unsafe\s+|partial\s+)*" + r"(class|struct|enum)\s+([A-Za-z0-9_]+)") +IDENT_RE = re.compile(r"[A-Z]{8,17}") + + +def norm(sig): + """Blank out obfuscated identifiers, keep structural spelling.""" + out = IDENT_RE.sub("#", sig) + return out + + +class TypeInfo: + __slots__ = ("name", "file", "methods", "fields", "line") + + def __init__(self, name, file, line): + self.name = name + self.file = file + self.line = line + self.methods = [] # (memberName, sig, token, lineno) + self.fields = [] # (fieldName, lineno) + + def shape(self): + return tuple(norm(s) for _, s, _, _ in self.methods) + + +def load(root): + types = {} + by_token = defaultdict(list) + files_to_parse = [] + for base in ROOTS[root]: + if os.path.isfile(base) and base.endswith(".cs"): + files_to_parse.append(base) + continue + if not os.path.isdir(base): + continue + for dirpath, _dirs, files in os.walk(base): + for fn in files: + if fn.endswith(".cs"): + files_to_parse.append(os.path.join(dirpath, fn)) + if True: + for path in files_to_parse: + if True: + current = None + with open(path, "r", encoding="utf-8", errors="replace") as fh: + for lineno, line in enumerate(fh, 1): + if current is None or "\tclass " in line or "\tstruct " in line or \ + " enum " in line or line.startswith(("public ", "internal ")): + m = TYPE_RE.match(line) + if m: + name = m.group(2) + current = types.get(name) + if current is None: + current = TypeInfo(name, os.path.relpath(path, ROOT), lineno) + types[name] = current + m = METHOD_RE.search(line) + if m: + owner = m.group(3) + if owner not in types: + types[owner] = TypeInfo(owner, os.path.relpath(path, ROOT), 0) + entry = (m.group(1), m.group(2), int(m.group(4)), lineno) + types[owner].methods.append(entry) + by_token[int(m.group(4))].append((owner, entry)) + continue + m = FIELD_RE.search(line) + if m and current is not None: + current.fields.append((m.group(1), lineno)) + return types, by_token + + +def show(types, name, filt=""): + t = types.get(name) + if t is None: + print(f"type {name}: NOT FOUND") + return + print(f"=== {name} ({t.file}) methods={len(t.methods)} fields={len(t.fields)}") + for member, sig, token, line in t.methods: + if filt and filt not in member: + continue + print(f" {token} {token - 0x6000000:>7} {member} [{norm(sig)}] {sig}") + for field, line in t.fields: + if filt and filt not in field: + continue + print(f" field {field}") + + +def map_member(types, sep_tokens, aug_type, aug_member, window=60): + t = types.get(aug_type) + if t is None: + print(f"august type {aug_type} not found") + return + targets = [(m, s, k) for m, s, k, _ in t.methods if m == aug_member] + if not targets: + targets = [(m, s, k) for m, s, k, _ in t.methods if aug_member in m] + if not targets: + print(f"member {aug_member} not found on {aug_type}") + return + for member, sig, token in targets: + print(f"\nAUG {aug_type}.{member} token={token} (idx {token - 0x6000000}) sig=[{norm(sig)}]") + hits = [] + for tk in range(token - window, token + window + 1): + for owner, entry in sep_tokens.get(tk, []): + m2, s2, tok, line = entry + if norm(s2) == norm(sig): + hits.append((abs(tk - token), tk, owner, m2, s2)) + hits.sort() + for dist, tk, owner, m2, s2 in hits[:8]: + print(f" SEP d={dist:+3d} {tk} {owner}.{m2} [{norm(s2)}] {s2}") + if not hits: + print(" no signature match in window") + + +def shape(types, name, verbose=True): + t = types.get(name) + if t is None: + print(f"type {name}: NOT FOUND") + return + if verbose: + print(f"=== shape {name} ({t.file}) methods={len(t.methods)} fields={len(t.fields)}") + for member, sig, token, _ in t.methods: + print(f" {token - 0x6000000:>7} {member} [{norm(sig)}]") + for field, _ in t.fields: + print(f" field:{field}") + return t + + +def similarity(a, b): + """Match two ordered (sig,) sequences; return (score, token pairs).""" + import difflib + sa = [x[0] for x in a] + sb = [x[0] for x in b] + sm = difflib.SequenceMatcher(None, sa, sb, autojunk=False) + pairs = [] + for block in sm.get_matching_blocks(): + for i in range(block.size): + pairs.append((a[block.a + i][1], b[block.b + i][1])) + score = 2.0 * len(pairs) / max(1, len(sa) + len(sb)) + return score, pairs + + +def match_type(aug_types, sep_types, aug_name, top=6): + t = aug_types.get(aug_name) + if t is None: + print(f"august type {aug_name}: NOT FOUND") + return + a = [(norm(s), tk) for _, s, tk, _ in t.methods] + scored = [] + for name, st in sep_types.items(): + b = [(norm(s), tk) for _, s, tk, _ in st.methods] + if not b and not a: + continue + score, pairs = similarity(a, b) + if not pairs: + continue + deltas = [tk2 - tk1 for tk1, tk2 in pairs] + median = sorted(deltas)[len(deltas) // 2] + spread = max(deltas) - min(deltas) + scored.append((score, -abs(median), name, st, median, spread, len(pairs))) + scored.sort(key=lambda x: (-x[0], x[1])) + print(f"=== {aug_name} ({t.file}) {len(t.methods)} methods; {len(t.fields)} fields") + for score, _n, name, st, median, spread, pairs in scored[:top]: + print(f" {score:5.2f} {name:<16} d={median:+5d} spread={spread:<5d} pairs={pairs:<4d} " + f"m={len(st.methods)} f={len(st.fields)} {st.file}") + + +def main(): + if len(sys.argv) < 3: + print(__doc__) + return 1 + cmd = sys.argv[1] + if cmd == "map": + aug_types, _ = load("aug") + _sep_types, sep_tokens = _load_tokens("sep") + map_member(aug_types, sep_tokens, sys.argv[2], sys.argv[3], + int(sys.argv[4]) if len(sys.argv) > 4 else 60) + return 0 + if cmd in ("match", "verify"): + aug_types, _ = load("aug") + sep_types, _ = load("sep") + names = [sys.argv[2]] if cmd == "match" else sys.argv[2].split(",") + top = int(sys.argv[3]) if len(sys.argv) > 3 else 6 + for name in names: + match_type(aug_types, sep_types, name, top) + return 0 + root = sys.argv[2] + types, by_token = load(root) + if cmd == "show": + show(types, sys.argv[3], sys.argv[4] if len(sys.argv) > 4 else "") + elif cmd == "shape": + shape(types, sys.argv[3]) + elif cmd in ("find", "findidx", "window"): + token = int(sys.argv[3], 0) + if token < 0x6000000: + token += 0x6000000 + span = int(sys.argv[4]) if len(sys.argv) > 4 else 0 + for tk in range(token - span, token + span + 1): + for owner, entry in sorted(by_token.get(tk, [])): + member, sig, tok, line = entry + mark = "<<<" if tk == token else " " + print(f" {mark} {tk} {owner}.{member} [{norm(sig)}]") + elif cmd == "types": + rx = re.compile(sys.argv[3]) + for name in sorted(t for t in types if rx.search(t)): + print(f" {name} ({types[name].file})") + else: + print(__doc__) + return 1 + return 0 + + +_CACHE = {} + + +def _load_tokens(root): + if root not in _CACHE: + _CACHE[root] = load(root) + return _CACHE[root] + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/september-client-triage.ps1 b/Tools/september-client-triage.ps1 new file mode 100644 index 0000000..e065e19 --- /dev/null +++ b/Tools/september-client-triage.ps1 @@ -0,0 +1,608 @@ +<# + Read-only triage for a Radicade-patched September 2023 Rec Room install + (game build 20230831, folder 2023-09-06T06-53-47Z, Unity 2020.3.33f1). + + It also answers the two questions a boot that never reaches the login screen + raises on a machine somebody else cannot look at: + + * WHICH server does this client's own data point at? The metadata literal + table is read directly, because a byte search cannot tell the live host + from the dead copy the patch leaves behind. A client whose literals say + api.recflare.net is running another project's patch, whatever the plugin + config says. + * WHAT has this Windows user talked to before? BepInEx, config, caches and + the last log are read, because %LOCALAPPDATA%Low\Against Gravity\Rec Room + belongs to the user rather than to a game copy and is shared by every + install on the PC. + + Usage: + powershell -ExecutionPolicy Bypass -File september-client-triage.ps1 + powershell -ExecutionPolicy Bypass -File september-client-triage.ps1 -GameDir "C:\path\to\2023-09-06T06-53-47Z" + + It only reads files. It writes september-client-report.txt next to this + script (override with -OutFile). Send that file back verbatim; it names + exactly which piece of the stack is missing. + + The single failure this script exists for: + + [Error :InteropManager] Failed to generate Il2Cpp interop assemblies: + ... System.FormatException: Invalid or corrupt metadata (magic number check failed) + + Rec Room does not ship a standard IL2CPP metadata header. Its global-metadata.dat + begins with a per-release scrambled 256-byte block, so LibCpp2IL's magic check + (0xFAB11BAF) rejects it. The working install keeps a normalized copy + (global-metadata.interop.dat, magic af 1b b1 fa) and points BepInEx at that copy + instead - either through a patched loader (BepInEx 6.0.0-be.697) or through the + [IL2CPP] GlobalMetadataPath setting (newer BepInEx builds). + + A stock BepInEx core has neither, so interop generation dies, no interop + assemblies are written, and the chainloader then fails with + "Could not load file or assembly 'UnityEngine.CoreModule'". +#> +[CmdletBinding()] +param( + [string]$GameDir, + [string]$OutFile +) + +$ErrorActionPreference = 'Continue' +$script:Report = New-Object System.Collections.Generic.List[string] +$script:Failures = 0 +$script:Warnings = 0 + +function Add-Line { + param([string]$Text = '') + $script:Report.Add($Text) +} + +function Write-Both { + param([string]$Text = '') + Write-Host $Text + Add-Line $Text +} + +function Add-Verdict { + param( + [ValidateSet('OK', 'WARN', 'MISSING')][string]$State, + [string]$Text + ) + if ($State -eq 'MISSING') { $script:Failures++ } + if ($State -eq 'WARN') { $script:Warnings++ } + Add-Line ("[{0,-7}] {1}" -f $State, $Text) + if ($State -ne 'OK') { Write-Host ("[{0}] {1}" -f $State, $Text) } +} + +function Get-Md5 { + param([string]$Path) + try { return (Get-FileHash -LiteralPath $Path -Algorithm MD5).Hash.ToLower() } + catch { return 'unreadable' } +} + +function Get-Bytes { + param([string]$Path) + # The leading comma matters: without it PowerShell unrolls the array into the pipeline + # and the caller receives one object per byte. On the 33 MB global-metadata.dat that is + # 33 million boxed objects and the script appears to hang for minutes. + try { return ,([System.IO.File]::ReadAllBytes($Path)) } + catch { return $null } +} + +# True when the file holds the literal as UTF-16 or as UTF-8/ASCII. +function Test-Literal { + param([byte[]]$Bytes, [string]$Literal) + if (-not $Bytes) { return $false } + foreach ($needle in @([System.Text.Encoding]::Unicode.GetBytes($Literal), + [System.Text.Encoding]::UTF8.GetBytes($Literal))) { + $limit = $Bytes.Length - $needle.Length + for ($i = 0; $i -le $limit; $i++) { + if ($Bytes[$i] -ne $needle[0]) { continue } + $hit = $true + for ($j = 1; $j -lt $needle.Length; $j++) { + if ($Bytes[$i + $j] -ne $needle[$j]) { $hit = $false; break } + } + if ($hit) { return $true } + } + } + return $false +} + +function Get-FirstBytes { + param([string]$Path, [int]$Count = 4) + $bytes = Get-Bytes $Path + if (-not $bytes) { return 'unreadable' } + return (($bytes[0..([Math]::Min($Count, $bytes.Length) - 1)] | ForEach-Object { $_.ToString('x2') }) -join ' ') +} + +function Get-TextIfExists { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { return $null } + try { return (Get-Content -LiteralPath $Path -Raw -ErrorAction Stop) } catch { return $null } +} + +function Get-MatchCount { + param([string]$Text, [string]$Pattern) + if (-not $Text) { return 0 } + return ([regex]::Matches($Text, $Pattern)).Count +} + +# The patch keeps the original literal blob verbatim and appends the rewritten literals, so +# a byte search finds BOTH the dead and the live copy of every host (the reference file still +# contains "ns.rec.net" as dead bytes) and cannot tell them apart. Only the literal table +# says which string the runtime actually resolves, so read it: for 20230831 its four header +# fields sit at byte 0x74 (table offset), 0x24 (table size), 0xA0 (data offset), 0x3C (data +# size) - the same positions tools/patch-0831-client-metadata.py rewrites. +function Get-MetadataHostCounts { + param([string]$Path, [int]$Top = 25) + + $bytes = Get-Bytes $Path + if (-not $bytes -or $bytes.Length -lt 0x100) { return $null } + + $tableOff = [BitConverter]::ToUInt32($bytes, 0x74) + $tableSize = [BitConverter]::ToUInt32($bytes, 0x24) + $dataOff = [BitConverter]::ToUInt32($bytes, 0xA0) + $dataSize = [BitConverter]::ToUInt32($bytes, 0x3C) + if ($tableSize -eq 0 -or ($tableOff + $tableSize) -gt $bytes.Length) { return $null } + if (($dataOff + $dataSize) -gt $bytes.Length) { return $null } + + $counts = @{} + $records = [int]($tableSize / 8) + for ($i = 0; $i -lt $records; $i++) { + $pos = [int]$tableOff + ($i * 8) + $len = [BitConverter]::ToUInt32($bytes, $pos) + $idx = [BitConverter]::ToInt32($bytes, $pos + 4) + if ($len -eq 0 -or $len -gt 512 -or $idx -lt 0) { continue } + $start = [int]$dataOff + $idx + if (($start + $len) -gt $bytes.Length) { continue } + # Only http(s) literals are interesting, and UTF-8 will not start one with any + # other byte - skipping the rest keeps this loop off the slow path. + if ($bytes[$start] -ne 0x68) { continue } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, [int]$len) + $m = [regex]::Match($text, '^https?://(?[^/?#]+)') + if (-not $m.Success) { continue } + $hostName = $m.Groups['host'].Value.ToLowerInvariant() + if ($counts.ContainsKey($hostName)) { $counts[$hostName]++ } else { $counts[$hostName] = 1 } + } + + return @($counts.GetEnumerator() | + ForEach-Object { [pscustomobject]@{ Host = $_.Key; Count = $_.Value } } | + Sort-Object -Property Count, Host -Descending | + Select-Object -First $Top) +} + +# --- locate the install ------------------------------------------------------- + +if (-not $GameDir) { + $candidates = @( + (Join-Path $env:USERPROFILE 'Downloads\2023-09-06T06-53-47Z'), + (Join-Path $env:USERPROFILE 'Desktop\2023-09-06T06-53-47Z'), + 'C:\2023-09-06T06-53-47Z' + ) + $downloads = Join-Path $env:USERPROFILE 'Downloads' + if (Test-Path -LiteralPath $downloads) { + $candidates += (Get-ChildItem -LiteralPath $downloads -Directory -ErrorAction SilentlyContinue | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'GameAssembly.dll') } | + ForEach-Object { $_.FullName }) + } + foreach ($c in $candidates) { + if ($c -and (Test-Path -LiteralPath (Join-Path $c 'GameAssembly.dll'))) { $GameDir = $c; break } + } +} + +if (-not $GameDir) { $GameDir = Read-Host 'Path to the Rec Room folder that contains GameAssembly.dll' } +$GameDir = $GameDir.TrimEnd('\') + +if (-not $OutFile) { + if ($PSScriptRoot) { $OutFile = Join-Path $PSScriptRoot 'september-client-report.txt' } + else { $OutFile = Join-Path (Get-Location) 'september-client-report.txt' } +} + +# The data folder is renamed in some distributions (RecRoom_Data -> Radicade_Data), +# so find it instead of assuming. BepInEx resolves {GameDataPath} at runtime and does +# not care about the name. +$dataDir = $null +foreach ($name in @('RecRoom_Data', 'Radicade_Data', 'Recroom_Data')) { + $p = Join-Path $GameDir $name + if (Test-Path -LiteralPath (Join-Path $p 'il2cpp_data')) { $dataDir = $p; break } +} +if (-not $dataDir) { + $dataDir = Get-ChildItem -LiteralPath $GameDir -Directory -ErrorAction SilentlyContinue | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'il2cpp_data') } | + Select-Object -First 1 -ExpandProperty FullName +} + +$metadataDir = if ($dataDir) { Join-Path $dataDir 'il2cpp_data\Metadata' } else { $null } +$metadata = if ($metadataDir) { Join-Path $metadataDir 'global-metadata.dat' } else { $null } +$interopDat = if ($metadataDir) { Join-Path $metadataDir 'global-metadata.interop.dat' } else { $null } +$pristineDat = if ($metadataDir) { Join-Path $metadataDir 'global-metadata.dat.orig' } else { $null } + +$bepInEx = Join-Path $GameDir 'BepInEx' +$coreDir = Join-Path $bepInEx 'core' +$loader = Join-Path $coreDir 'BepInEx.Unity.IL2CPP.dll' +$configFile = Join-Path $bepInEx 'config\BepInEx.cfg' +$interopDir = Join-Path $bepInEx 'interop' +$unityLibs = Join-Path $bepInEx 'unity-libs' +$plugin = Join-Path $bepInEx 'plugins\RadicadePatch.dll' +$bepLog = Join-Path $bepInEx 'LogOutput.log' +$changelog = Join-Path $GameDir 'changelog.txt' +$assembly = Join-Path $GameDir 'GameAssembly.dll' +$winhttp = Join-Path $GameDir 'winhttp.dll' +$doorstop = Join-Path $GameDir 'doorstop_config.ini' +$markerFile = if ($dataDir) { Join-Path $dataDir 'radicade-patch.json' } else { $null } +# Per Windows user, NOT per game copy: every install on this PC shares it. +$profileDir = Join-Path $env:USERPROFILE 'AppData\LocalLow\Against Gravity\Rec Room' + +# Reference values from the install that logs in and loads rooms. +$expected = @{ + AssemblySize = 167873024 + AssemblyMd5 = '54f26d061797ab615688d62108195fa7' + MetadataSize = 33774004 + MetadataMd5 = 'ce5097af6072c4e474f02704f020e94f' + InteropSize = 32115816 + InteropMd5 = '4c6548745a2855fc9ab1c5fbaea639d2' + LoaderSize = 60928 + LoaderMd5 = '9430bb84349d52b753a181788a86c5e2' + WinhttpMd5 = 'b2a4d0cb04bf8f5a27ccab237ecc2586' + PluginSize = 96768 + PluginMd5 = '64e4045f09107f5525764b83c6f4ba2b' + InteropCount = 332 + InteropHash = 'f4651f741ef8c3f61cd261bfb1e02e72' + MagicScrambled = 'e0 eb 02 00' + MagicStandard = 'af 1b b1 fa' +} + +# --- header ------------------------------------------------------------------ + +Write-Both 'Radicade September 2023 client triage (game build 20230831)' +Write-Both ("generated : {0}" -f (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) +Write-Both ("game folder : {0}" -f $GameDir) +Write-Both ("data folder : {0}" -f $(if ($dataDir) { Split-Path $dataDir -Leaf } else { 'NOT FOUND' })) +Write-Both ("powershell : {0}" -f $PSVersionTable.PSVersion) +Write-Both '' + +# --- game build --------------------------------------------------------------- + +Write-Both '== game build ==' +if (Test-Path -LiteralPath $assembly) { + $info = Get-Item -LiteralPath $assembly + $md5 = Get-Md5 $assembly + Write-Both ("GameAssembly.dll : {0} b md5 {1}" -f $info.Length, $md5) + if ($info.Length -eq $expected.AssemblySize -and $md5 -eq $expected.AssemblyMd5) { + Add-Verdict 'OK' 'GameAssembly.dll is the reference September build' + } elseif ($info.Length -eq $expected.AssemblySize) { + Add-Verdict 'WARN' 'GameAssembly.dll has the reference size but a different hash - verify the build' + } else { + Add-Verdict 'MISSING' ("GameAssembly.dll is {0} b, the reference build is {1} b - this patch stack only works on that exact build" -f $info.Length, $expected.AssemblySize) + } +} else { + Add-Verdict 'MISSING' 'GameAssembly.dll - this is not a Rec Room install folder?' +} +Write-Both '' + +# --- IL2CPP metadata ---------------------------------------------------------- + +Write-Both '== IL2CPP metadata ==' +if ($metadata -and (Test-Path -LiteralPath $metadata)) { + $info = Get-Item -LiteralPath $metadata + $md5 = Get-Md5 $metadata + $magic = Get-FirstBytes $metadata + Write-Both ("global-metadata.dat: {0} b md5 {1} magic {2}" -f $info.Length, $md5, $magic) + if ($magic -ne $expected.MagicScrambled) { + Add-Verdict 'WARN' ("global-metadata.dat magic is {0}, expected the scrambled Rec Room header {1} - unusual for this build" -f $magic, $expected.MagicScrambled) + } + if ($info.Length -eq $expected.MetadataSize -and $md5 -eq $expected.MetadataMd5) { + Add-Verdict 'OK' 'global-metadata.dat matches the reference host-literal patch' + } else { + Add-Verdict 'WARN' 'global-metadata.dat is not the reference file - the live hosts below say what it is really patched for' + } + + # A byte search cannot answer "which server does this client use": the patch keeps the + # original blob verbatim and appends the rewritten literals, so the dead copy of every + # host is still in the file (the reference metadata still contains "ns.rec.net"). Read + # the literal table instead - that is what the runtime resolves. + $hosts = Get-MetadataHostCounts -Path $metadata + if ($hosts) { + Write-Both ' live service hosts this client resolves (from the literal table, dead copies ignored):' + foreach ($h in $hosts) { Write-Both (" {0,4}x {1}" -f $h.Count, $h.Host) } + + $foreign = @($hosts | Where-Object { $_.Host -match 'recflare|viverec|trollroom|meownet' }) + # Only a PORTED loopback host is the baked-server tell: this build carries a bare + # "localhost" literal of its own, so that one must not raise anything. + $loopback = @($hosts | Where-Object { $_.Host -match '^(localhost|127\.0\.0\.1):[0-9]+$' }) + $stock = @($hosts | Where-Object { $_.Host -match '(^|\.)rec\.net$' }) + + if ($foreign.Count -gt 0) { + $names = ($foreign | ForEach-Object { $_.Host }) -join ', ' + Add-Verdict 'MISSING' ("this client's own literals resolve {0} - that is another private server's patch. No plugin setting can redirect a host the client resolves itself, so install this bundle's Metadata folder over \il2cpp_data\Metadata." -f $names) + } elseif ($loopback.Count -gt 0) { + Add-Verdict 'WARN' 'the metadata is patched for a machine-local address: the server has to run on THIS machine, or take the Metadata folder from the bundle instead' + } elseif ($stock.Count -gt 0 -and $md5 -ne $expected.MetadataMd5) { + Add-Verdict 'MISSING' 'the client still resolves the official RecNet hosts: this metadata was never patched for a private server' + } + } else { + Add-Verdict 'WARN' 'could not read the metadata literal table - unexpected layout for this build' + } + + # The marker is what the patch tool wrote; the plugin reads the same file at boot. + if ($markerFile -and (Test-Path -LiteralPath $markerFile)) { + $markerText = Get-TextIfExists $markerFile + $patchedUrl = if ($markerText -match '"serverUrl"\s*:\s*"([^"]+)"') { $Matches[1] } else { '?' } + Write-Both ("radicade-patch.json: serverUrl {0}" -f $patchedUrl) + if ($patchedUrl -match 'localhost|127\.0\.0\.1') { + Add-Verdict 'MISSING' ("this install was metadata-patched for {0} (an earlier pack), so its bootstrap host is a machine-local address and nothing is listening there. Take global-metadata.dat and global-metadata.interop.dat from the bundle." -f $patchedUrl) + } + } else { + Add-Verdict 'WARN' 'no radicade-patch.json in the data folder: nothing records which server this install was patched for' + } +} else { + Add-Verdict 'MISSING' ("global-metadata.dat not found under {0}" -f $metadataDir) +} +if ($pristineDat -and (Test-Path -LiteralPath $pristineDat)) { + $info = Get-Item -LiteralPath $pristineDat + Write-Both ("global-metadata.dat.orig: {0} b magic {1}" -f $info.Length, (Get-FirstBytes $pristineDat)) +} + +# The normalized copy is what interop generation must be pointed at. +if ($interopDat -and (Test-Path -LiteralPath $interopDat)) { + $info = Get-Item -LiteralPath $interopDat + $md5 = Get-Md5 $interopDat + $magic = Get-FirstBytes $interopDat + Write-Both ("global-metadata.interop.dat: {0} b md5 {1} magic {2}" -f $info.Length, $md5, $magic) + if ($magic -eq $expected.MagicStandard) { + Add-Verdict 'OK' 'global-metadata.interop.dat carries the standard header LibCpp2IL requires' + } else { + Add-Verdict 'MISSING' ("global-metadata.interop.dat magic is {0}, expected {1} - interop generation cannot read this file" -f $magic, $expected.MagicStandard) + } + if ($info.Length -ne $expected.InteropSize -or $md5 -ne $expected.InteropMd5) { + Add-Verdict 'WARN' 'normalized metadata is not the reference file - harmless if interop assemblies are already built, otherwise regenerate it' + } +} else { + Add-Verdict 'MISSING' 'global-metadata.interop.dat is absent: nothing can normalize the header for interop generation' +} +Write-Both '' + +# --- BepInEx loader ----------------------------------------------------------- + +Write-Both '== BepInEx loader ==' +$beVersion = $null +foreach ($candidate in @($bepLog, $changelog)) { + $text = Get-TextIfExists $candidate + if ($text -and $text -match 'BepInEx (\d+\.\d+\.\d+-be\.\d+)') { $beVersion = $Matches[1]; break } +} +if ($beVersion) { + Write-Both ("BepInEx version : {0} (from {1})" -f $beVersion, (Split-Path $candidate -Leaf)) +} else { + Write-Both 'BepInEx version : unknown (no LogOutput.log or changelog.txt yet)' +} + +$loaderExists = Test-Path -LiteralPath $loader +$loaderPatched = $false +if ($loaderExists) { + $info = Get-Item -LiteralPath $loader + $md5 = Get-Md5 $loader + $bytes = Get-Bytes $loader + $loaderPatched = Test-Literal $bytes 'global-metadata.interop.dat' + Write-Both ("BepInEx.Unity.IL2CPP.dll: {0} b md5 {1} reads {2}" -f $info.Length, $md5, + $(if ($loaderPatched) { 'global-metadata.interop.dat (patched)' } else { 'global-metadata.dat (stock)' })) + if ($md5 -eq $expected.LoaderMd5) { + Add-Verdict 'OK' 'patched be.697 loader installed' + } elseif ($loaderPatched) { + Add-Verdict 'OK' 'loader reads the normalized metadata, but it is not the reference loader - a different BepInEx build' + } else { + Add-Verdict 'MISSING' 'loader reads global-metadata.dat: this is a stock BepInEx core and it cannot parse this game metadata' + } +} else { + Add-Verdict 'MISSING' 'BepInEx/core/BepInEx.Unity.IL2CPP.dll - BepInEx is not installed' +} + +# Newer BepInEx builds expose the same fix as a config setting instead of a patched binary. +$configText = Get-TextIfExists $configFile +$metadataOverride = $null +if ($configText -and $configText -match '(?m)^\s*GlobalMetadataPath\s*=\s*(.+?)\s*$') { $metadataOverride = $Matches[1] } +if ($metadataOverride) { + $overrideMatches = $metadataOverride -match 'interop\.dat' + Write-Both ("[IL2CPP] GlobalMetadataPath = {0}" -f $metadataOverride) + if ($metadataOverride -match '\{GameRootPath\}|\{GameDataPath\}') { + # BepInEx joins this onto the game root; only the {GameDataPath} form is right. + if ($overrideMatches) { + Add-Verdict 'OK' 'BepInEx is configured to normalize metadata through the config setting' + } else { + Add-Verdict 'MISSING' 'GlobalMetadataPath points at the un-normalized global-metadata.dat' + } + } else { + Add-Verdict 'WARN' 'GlobalMetadataPath is set without a {GameDataPath} placeholder - confirm the path resolves' + } +} else { + $needsIt = $loaderExists -and -not $loaderPatched + if ($needsIt) { + Add-Verdict 'MISSING' 'no [IL2CPP] GlobalMetadataPath override in BepInEx/config/BepInEx.cfg - this is the fix for a stock loader' + } elseif (-not $loaderExists) { + Add-Verdict 'WARN' 'BepInEx/config/BepInEx.cfg has no GlobalMetadataPath (fine on the patched be.697 loader)' + } else { + Write-Both '[IL2CPP] GlobalMetadataPath: not set (not required: the loader is patched)' + } +} +Write-Both '' + +# --- interop assemblies ------------------------------------------------------- + +Write-Both '== interop assemblies ==' +if (Test-Path -LiteralPath $interopDir) { + $dlls = Get-ChildItem -LiteralPath $interopDir -Filter '*.dll' -File -ErrorAction SilentlyContinue + $count = @($dlls).Count + $hashFile = Join-Path $interopDir 'assembly-hash.txt' + $hashText = Get-TextIfExists $hashFile + Write-Both ("BepInEx/interop : {0} .dll files" -f $count) + if ($hashText) { Write-Both ("assembly-hash.txt : {0}{1}" -f $hashText.Trim(), $(if ($hashText.Trim() -eq $expected.InteropHash) { ' (matches the reference install)' } else { ' (different from the reference install - interop will be regenerated)' })) } + else { Write-Both 'assembly-hash.txt : missing' } + if ($count -ge $expected.InteropCount) { + Add-Verdict 'OK' ("interop assemblies are present ({0} .dll >= {1})" -f $count, $expected.InteropCount) + } elseif ($count -gt 0) { + Add-Verdict 'MISSING' ("only {0} interop assemblies (expected {1}) - a failed generation left the folder half-built" -f $count, $expected.InteropCount) + } else { + Add-Verdict 'MISSING' 'BepInEx/interop is empty - interop generation never completed' + } +} else { + Add-Verdict 'MISSING' 'BepInEx/interop does not exist - on a fresh start BepInEx tries to generate it, which needs the normalized metadata' +} +if (Test-Path -LiteralPath $unityLibs) { + $libCount = @(Get-ChildItem -LiteralPath $unityLibs -Filter '*.dll' -File).Count + Write-Both ("BepInEx/unity-libs : {0} .dll files" -f $libCount) +} else { + Add-Verdict 'WARN' 'BepInEx/unity-libs is missing - BepInEx will download it (needs internet) before generating interop' +} +Write-Both '' + +# --- plugin and host pieces --------------------------------------------------- + +Write-Both '== Radicade pieces ==' +if (Test-Path -LiteralPath $plugin) { + $info = Get-Item -LiteralPath $plugin + $md5 = Get-Md5 $plugin + Write-Both ("RadicadePatch.dll: {0} b md5 {1}" -f $info.Length, $md5) + if ($md5 -eq $expected.PluginMd5) { + Add-Verdict 'OK' 'reference September client plugin' + } else { + Add-Verdict 'WARN' 'RadicadePatch.dll is a different build - fine if it is one you built yourself' + } +} else { + Add-Verdict 'MISSING' 'BepInEx/plugins/RadicadePatch.dll - nothing will patch the client' +} +if (Test-Path -LiteralPath $winhttp) { + $md5 = Get-Md5 $winhttp + if ($md5 -eq $expected.WinhttpMd5) { Add-Verdict 'OK' 'winhttp.dll is Doorstop 4.3.0 (reference)' } + else { Add-Verdict 'WARN' 'winhttp.dll is not the reference Doorstop - confirm nothing else owns the proxy dll' } +} else { + Add-Verdict 'MISSING' 'winhttp.dll - without it BepInEx never starts' +} +if (Test-Path -LiteralPath $doorstop) { + $text = Get-TextIfExists $doorstop + $target = if ($text -match '(?m)^\s*target_assembly\s*=\s*(.+?)\s*$') { $Matches[1] } else { '?' } + Write-Both ("doorstop target_assembly: {0}" -f $target) + if ($target -notmatch 'BepInEx\\core\\BepInEx\.Unity\.IL2CPP\.dll|BepInEx/core/BepInEx\.Unity\.IL2CPP\.dll') { + Add-Verdict 'MISSING' 'doorstop_config.ini does not target the IL2CPP chainloader' + } +} else { + Add-Verdict 'MISSING' 'doorstop_config.ini is missing' +} +Write-Both '' + +# --- per-user client data ----------------------------------------------------- + +# %LOCALAPPDATA%Low\Against Gravity\Rec Room belongs to the Windows user, not to a game +# copy: every install on the PC shares it. A nameserver answer or a config cached there by +# another private server therefore outlives the folder it came from, which is how a client +# whose own metadata says one server starts asking a completely different one for its +# gameconfigs. + +Write-Both '== per-user client data (shared by every install of this game) ==' +if (Test-Path -LiteralPath $profileDir) { + Write-Both (" folder: {0}" -f $profileDir) + foreach ($name in @('Library', 'appstate.raw', 'Player.log', 'Player-prev.log')) { + $p = Join-Path $profileDir $name + if (Test-Path -LiteralPath $p) { + $info = Get-Item -LiteralPath $p + Write-Both (" {0,-16} {1,12} b last write {2}" -f $name, $info.Length, $info.LastWriteTime.ToString('yyyy-MM-dd HH:mm')) + } + } + foreach ($name in @('HTTPCache', 'Cookies', 'AnalyticsCache')) { + $p = Join-Path $profileDir $name + if (Test-Path -LiteralPath $p) { + $n = @(Get-ChildItem -LiteralPath $p -Recurse -File -ErrorAction SilentlyContinue).Count + Write-Both (" {0,-16} {1} file(s)" -f $name, $n) + } + } + + $talkedTo = @{} + foreach ($name in @('Library', 'Player.log')) { + $text = Get-TextIfExists (Join-Path $profileDir $name) + if (-not $text) { continue } + foreach ($m in [regex]::Matches($text, 'https?://(?[a-z0-9.-]+)')) { + $h = $m.Groups['host'].Value.ToLowerInvariant() + if ($talkedTo.ContainsKey($h)) { $talkedTo[$h]++ } else { $talkedTo[$h] = 1 } + } + } + if ($talkedTo.Count -gt 0) { + Write-Both ' hosts this profile has seen (cached responses and the last run):' + foreach ($h in @($talkedTo.GetEnumerator() | + ForEach-Object { [pscustomobject]@{ Host = $_.Key; Count = $_.Value } } | + Sort-Object -Property Count, Host -Descending | + Select-Object -First 25)) { + Write-Both (" {0,4}x {1}" -f $h.Count, $h.Host) + } + $stale = @($talkedTo.Keys | Where-Object { $_ -match 'recflare|viverec|trollroom|meownet' } | Sort-Object) + if ($stale.Count -gt 0) { + Write-Both (" note: this profile's cache still names another private server ({0}). Harmless while the client dials Radicade, but rename this folder before a fresh attempt so nothing stale can be replayed." -f ($stale -join ', ')) + } + } +} else { + Write-Both (" {0} not found - nothing cached for this Windows user yet" -f $profileDir) +} +Write-Both '' + +# --- what the last run said --------------------------------------------------- + +Write-Both '== last BepInEx log ==' +$logText = Get-TextIfExists $bepLog +if ($logText) { + $markers = [ordered]@{ + 'magic number check failed' = 'LibCpp2IL rejected the metadata header' + 'Failed to generate Il2Cpp interop' = 'interop generation aborted' + 'UnityEngine.CoreModule' = 'chainloader could not load Unity assemblies' + 'Chainloader startup complete' = 'plugin loaded' + 'Done!' = 'Il2CppInteropGen finished' + 'TlsFatalAlert' = 'a TLS handshake was aborted (certificate refused)' + 'user_canceled' = 'the embedded verifyer refused the certificate - host not on TLS:AllowedHosts' + 'wantsToQuit answered false' = 'the anti-cheat shutdown was vetoed: the boot is stuck, it did not crash' + 'Exception: Aborted' = 'the anti-cheat shutdown aborted the session' + 'metadata was patched for' = 'the [Meta] warning fired (metadata and Connection:ServerUrl disagree)' + } + foreach ($k in $markers.Keys) { + $n = Get-MatchCount $logText ([regex]::Escape($k)) + if ($n -gt 0) { Write-Both ("{0,4}x {1,-34} ({2})" -f $n, $k, $markers[$k]) } + } + if ((Get-MatchCount $logText 'magic number check failed') -gt 0 -and + (Get-MatchCount $logText 'Chainloader startup complete') -eq 0) { + Add-Verdict 'MISSING' 'the last run died in interop generation: BepInEx is pointed at the un-normalized metadata' + } elseif ((Get-MatchCount $logText 'Chainloader startup complete') -gt 0) { + Add-Verdict 'OK' 'the last run completed the chainloader' + } + + # Reaching another private server is fatal, not cosmetic: nothing this client asks for + # exists there, so the boot never gets its configs and the anti-cheat path takes the + # session down. + $dialled = @() + foreach ($pat in @('recflare\.net', 'viverec\.com', 'trollroom', 'meownet')) { + if ((Get-MatchCount $logText $pat) -gt 0) { $dialled += ($pat -replace '\\', '') } + } + if ($dialled.Count -gt 0) { + Add-Verdict 'MISSING' ("the last run dialled another private server ({0}): this client's service bases are not Radicade's. Copy the bundle's Metadata folder over this install and rename %LOCALAPPDATA%Low\Against Gravity\Rec Room before the next launch." -f ($dialled -join ', ')) + } + if ((Get-MatchCount $logText 'metadata was patched for') -gt 0) { + Add-Verdict 'WARN' 'the metadata marker and Connection:ServerUrl disagree - see the [Meta] line in the log' + } + $versionLine = ($logText -split "`n" | Where-Object { $_ -match 'BepInEx \d+\.\d+\.\d+-be\.\d+' } | Select-Object -First 1) + if ($versionLine) { Write-Both ("header: {0}" -f $versionLine.Trim()) } +} else { + Add-Verdict 'WARN' 'BepInEx/LogOutput.log not found - launch the game once so the log exists' +} +Write-Both '' + +# --- verdict ------------------------------------------------------------------ + +Write-Both '== verdict ==' +Write-Both ("missing: {0} warnings: {1}" -f $script:Failures, $script:Warnings) +if ($script:Failures -eq 0) { + Write-Both 'Every required piece is in place.' +} else { + Write-Both 'The MISSING lines above are the whole problem; fix them in that order.' +} + +try { + Set-Content -LiteralPath $OutFile -Value ($script:Report -join "`r`n") -Encoding UTF8 + Write-Host '' + Write-Host ("Report written to {0}" -f $OutFile) +} catch { + Write-Host ("Could not write {0}: {1}" -f $OutFile, $_) +} + +exit $(if ($script:Failures -gt 0) { 1 } else { 0 }) diff --git a/VPS/fix-dorm-blob.sh b/VPS/fix-dorm-blob.sh new file mode 100644 index 0000000..a2a515f --- /dev/null +++ b/VPS/fix-dorm-blob.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# Fix the "joining room" / dorm-load crash on the VPS (ns.radicade.app). +# +# Root cause (proven from Player.log 15:59 session): +# Dorm Room 2 / SubRoom 2 references DataBlob ebb4bab8-7206-41b8-8458-1db103ed22c7, +# but the file is missing from the VPS CDN storage. The 2023 client 404s on +# GET /room/ebb4bab8-..., loops "Data Mismatch -> Redownloading room details", +# then a circuits-chain promise fires SessionManager.FatalApplicationQuit. +# +# Two independent fixes; EITHER ONE unblocks the join. +# +# MODE 1 (data, no code change) : upload a valid minimal empty room-save blob +# under the exact missing name via the admin API. +# Run from anywhere (your Windows Git Bash works): +# BOT_SECRET='' \ +# bash VPS/fix-dorm-blob.sh +# or: bash VPS/fix-dorm-blob.sh '' +# +# MODE 2 (code, durable) : deploy the RoomUtils.cs self-heal already in +# this repo (clears stale references on every room fetch) and restart +# the service. Run ON the VPS, in the repo checkout: +# bash VPS/fix-dorm-blob.sh --deploy +# --------------------------------------------------------------------------- +set -euo pipefail + +HOST="${HOST:-https://ns.radicade.app}" +BLOB="ebb4bab8-7206-41b8-8458-1db103ed22c7" +BLOB_DIR="${BLOB_DIR:-/tmp}" + +mode1() { + local secret="${1:-${BOT_SECRET:-}}" + if [ -z "$secret" ]; then + echo "ERROR: no secret given." + echo "Usage: BOT_SECRET='' bash $0" + echo " or: bash $0 ''" + exit 2 + fi + # Minimal valid empty room-save (same 4-byte protobuf the working template + # subrooms carry): field2 varint 2, field3 empty bytes. + printf '\x10\x02\x1a\x00' > "$BLOB_DIR/$BLOB" + echo "[1/2] Uploading $BLOB (4 bytes) via admin API..." + local code + code=$(curl -sS -m 30 -o /tmp/upl-resp.txt -w "%{http_code}" -X POST \ + "$HOST/admin/rooms/upload/datablob?key=$secret&name=$BLOB" \ + -F "file=@$BLOB_DIR/$BLOB") || { echo "Upload request failed (network) - is the VPS backend back up?"; exit 1; } + head -c 200 /tmp/upl-resp.txt 2>/dev/null; echo + if [ "$code" != "200" ]; then + echo "Upload failed (HTTP $code). 401 => wrong secret; 000/timeout => VPS backend still hung." + exit 1 + fi + echo "[2/2] Verifying public fetch..." + code=$(curl -sS -m 30 -o /dev/null -w "%{http_code}" "$HOST/room/$BLOB") + echo "GET /room/$BLOB -> $code" + if [ "$code" = "200" ]; then + echo "OK: dorm blob now resolvable. Relaunch RecRoom - joining rooms should work." + else + echo "Verification failed (HTTP $code)"; exit 1 + fi +} + +mode2() { + echo "Deploying RoomUtils self-heal (run on the VPS, in the repo checkout):" + git pull origin main + cd Server && dotnet publish -c Release -o publish + systemctl restart radicade-server 2>/dev/null \ + || echo "NOTE: restart the ASP.NET service however it is named on this host (systemctl list-units | grep -i radicade)" + sleep 5 + local code + code=$(curl -sS -m 30 -o /dev/null -w "%{http_code}" "$HOST/ns" || echo 000) + echo "GET $HOST/ns -> $code (expect 200)" + echo "On the first dorm fetch, the server log should show:" + echo " [RoomUtils] Cleared 1 missing DataBlob reference(s) in room 2 (files not in CDN storage)." +} + +case "${1:-data}" in + data) mode1 "${2:-}" ;; + deploy) mode2 ;; + *) echo "usage: $0 [data|deploy] (data mode: pass the BotSecret as \$2 or via BOT_SECRET env)"; exit 2 ;; +esac diff --git a/docs/2023-0831-CLIENT-SWITCH.md b/docs/2023-0831-CLIENT-SWITCH.md new file mode 100644 index 0000000..2b90dea --- /dev/null +++ b/docs/2023-0831-CLIENT-SWITCH.md @@ -0,0 +1,1680 @@ +# Switching the game client to 20230831 (September 2023) + +Target: `C:\RecRoom\2023-09-06T06-53-47Z` — a Steam depot download whose +*name* is only the download timestamp. The build's own version literal (inside +`RecRoom_Data/globalgamemanagers`) is **20230831**, Unity **2020.3.33f1**, Steam +manifest `471711_1957069142347218099`. + +Why this build: the RRS packages in `Documents/EggFiles/ProjectTemplate` are the +**2023-08-31** set (`com.recroom.versioning@3ec42268d40e`, whose +`APOBPPFCFHK()` returns enum 34 -> PersistenceVersion 64). The August client +(20230825) writes 61 and accepts up to 63, which is exactly why the studio's room +was refused with "You are playing on an older version of Rec Room than this room +was saved with". 20230831 is the release that owns the studio's revision, so it is +the client to run. + +## The release's metadata is shuffled differently + +Rec Room ships `global-metadata.dat` with a 256-byte header whose 31 +(offset,size) field *positions* are permuted per release, and the section tables +are laid out in a per-release order. The August map is known from +`tools/AugustInterop/normalize.cjs`; the September one was derived structurally in +`.tmp-newbuild/`: + +- `find_layout.py` — locates a string-literal table (records `{u4 length, i4 + dataIndex}`, monotonic indices) plus the data blob they point into + (printable UTF-8). On the August file it reproduces the documented fields + (table 0x2C/0xBC, data 0xC8/0xC0) and for September gives **table 0x74/0x24 + = 0x17BDD78, 52,309 records; data 0xA0/0x3C = 0xE11A10, 1,657,800 bytes**. +- `chain.py` — the tables are laid out back to back from 0x100 with *zero gaps* + (verified on August: every section starts where the previous one ends). Walking + that chain with the header's own numbers yields a **unique** 29-step layout that + ends exactly at EOF, with only two unused header values, which classifies every + slot as an offset, a size or unused. +- Section sizes then match August's within ~0.2-0.5%, and the resulting + standard-v27 header copy loads cleanly in LibCpp2IL: + +``` +dotnet run --project tools/AugustInterop -- artifacts/20230831-normalized-metadata.dat +Reading type definitions...42636 OK +Metadata parsed successfully. +imageDefinitions: 305 typeDefs: 42636 methodDefs: 283919 fieldDefs: 171027 +``` + +(August for comparison: 305 images, 42,537 types, 283,224 methods.) + +## Installed into the new client + +- `Recroom_Data/il2cpp_data/Metadata/global-metadata.dat` — host literals rewritten + to `https://localhost:3030` by `tools/patch-0831-client-metadata.py` (9 literals, + the same set the August patch produced: `ns.rec.net`, `auth.rec.net`, + `cdn.rec.net`, bare `rec.net`, the `/shop` and `/{0}` forms, the `.rec.net` + tail -> `.radicade.internal`). The rebuilt literal blob is relaid at EOF and only + the four header fields that describe it are repointed. Original kept as + `global-metadata.dat.orig`; marker in `Recroom_Data/radicade-patch.json`. +- `Recroom_Data/il2cpp_data/Metadata/global-metadata.interop.dat` — the normalized + tooling copy (`artifacts/20230831-normalized-metadata.dat`) with the 305 + synthetic `` rows repaired, for Cpp2IL/Il2CppInterop. This file must never + replace the game's `global-metadata.dat`. +- Doorstop (`winhttp.dll`, `doorstop_config.ini`, `.doorstop_version`) and + `BepInEx/{core,config,patchers,unity-libs}` from the August install, including the + patched loader (`BepInEx.Unity.IL2CPP.dll` md5 `9430bb84...`, reads + `global-metadata.interop.dat`). August's `BepInEx/interop` and its build-specific + plugin were deliberately **not** copied. +- `Recroom_Data/boot.config` — the desktop variant (`vr-enabled=0`, + `player-connection-mode=Listen`); the vanilla file is kept as `boot.config.orig`. + +Server: `20230831` added to `DBConfig.SupportedGameVersions`. + +## Launch results (2026-09-26) + +First launch crashed without any BepInEx log: Doorstop's runtime folder (`dotnet/`, +67 MB, `coreclr.dll` plus corlib; `doorstop_config.ini` points at it as +`coreclr_path = dotnet\coreclr.dll`) had not been copied from the August install. +With `dotnet/` in place BepInEx starts and writes `BepInEx/LogOutput.log`. + +Second launch reached Cpp2IL and failed inside interop generation: + +``` +Failed to generate Il2Cpp interop assemblies: System.IndexOutOfRangeException + at LibCpp2IL.Metadata.Il2CppTypeDefinition.get_NestedTypes() +``` + +Cause: two sections were swapped. LibCpp2IL treats `nestedTypes` as a flat array +of *type-definition* indices (`nestedTypeIndices.Skip(NestedTypesStart).Take(NestedTypeCount)` +then `typeDefs[idx]`), while `interfaces` indexes the binary's type table and its +values legitimately exceed the type-definition count. Of the two ~70 KB blocks, +**nestedTypes** is the 71,356-byte one at 0x1826FC8 (fields 0xA8/0xE0; every value +below 42,636) and **interfaces** is the 70,556-byte one at 0x1873AB8 (fields +0x68/0x08; values up to 134,062). With that fixed the copy re-validates through +`LibCpp2IlMain.LoadFromFile` against the new `GameAssembly.dll`: +`Binary + metadata initialization: True`, 283,919 method pointers mapped. + +Third launch: interop generation succeeded — 335 assemblies plus +`BepInEx/interop/assembly-hash.txt`, log ends with `Il2CppInteropGen Done!` and +`Chainloader startup complete`. The game then boots about 19 s and dies the way the +August client did before its plugin existed: + +``` +BestHTTP...BouncyCastle.Crypto.Tls.TlsFatalAlert: user_canceled(90) +RecNet name server query failed (error code: summer): user_canceled(90) +``` + +The literal patch is working — the client really is querying the local server — and +the remaining blocker is the client's embedded certificate validation, which is one +of the hooks the August plugin installed. + +## Remaining work + +1. ~~Launch once with no plugins to let Il2CppInterop generate this build's + interop assemblies.~~ Done: `BepInEx/interop` holds 335 assemblies. +2. ~~Port the client plugin.~~ Done - see the port section below. +3. **Verify**: sign-in plus loading the studio room, which is also the first hard + evidence of the schema ceiling this build accepts. Sign-in, session stability and + the Dorm load are now green; entering the studio-authored room itself is the part + still waiting on a click in-game. + +## Plugin port (`2023Patch/RadicadeSeptemberClient`) + +`RadicadeAugustClient.dll` is built against August interop and patches August's +obfuscated identifiers, so it cannot load in this client. The port is a source +rebuild (`RadicadeSeptemberClient`, plugin id `com.radicade.eacbypass`, version +3.0.0, version gate `Application.version != "20230831"`) sharing the same four +sources plus TLS/hub/websocket/address-map files from `RadicadeEacBypass`. + +Il2CppInterop's **method tokens are stable between builds** (deltas 0-4; only the +obfuscated identifiers rotate) and its type spellings keep their structure, so the +rename map was derived mechanically instead of by hand: + +- `tools/sept-map.py {show,find,map,match,verify,types}` indexes the decompiled + stubs in `.external/{aug,sep}-decomp/` and matches `(normalized signature, token + +/- window)`. Every ported symbol was confirmed by token **and** shape. +- Examples: launch validator `ADJOODNBCKG/NFPDGJNDFOF/GBAMNAKGEGD` -> + `IDLGLOLKLEB/MKGADIPJDLK/IDDOELEGIFB`; exit state `CCKNJCHHBLL` -> + `KJAMNFCDBIO` (setter `KALEFEDAHCC` -> `EHPDJDJLPKM`); cheat manager + `MJLAGDJLOBC`/`EFLCHFDDPJP` -> `EKGOMPDGNEK`/`FOKOIBODNAD`; promise type + `PFDOLDLEIBA` -> `NOKBHGDKGNF`. `EACManager.GenerateChallengeResponse`, + `Tool.ResetToDefaultCollisionLayer` and the `CheatManager` scan entry kept their + names. +- `.gitignore` was blocking the edit tools for the port tree (rule + `/2023Patch/*`); the new tree and its four shared sources are now un-ignored. + +Build is clean (0 errors/0 warnings) and the DLL replaces the August one in +`BepInEx/plugins`. Boot markers seen live: embedded-TLS acceptance, launch-validator +bypass, `[FileCheck] entry bypassed`, `[ExitVeto]`/`[QuitVeto]`, cheat-scan +suppression, `[AddrMap] mapped 254799 methods`, `[ToolGuard]`, and +`[CheatDrop] dorm drop (FOKOIBODNAD) held back`. + +## Image signature wall: the key id came from the wrong metadata file (fixed) + +The wall: the client requested images with `sig=p1`, every response carried +`Content-Signature`, and the client still filed an anti-cheat report +`Type=6 Message=Signature malformed (default.png?sig=p1)` - no image ever rendered +and the featured-rooms UI looped. Zero `[RsaVerify]` calls appeared in the log, so +the failure happened **before** any RSA verify. + +Which is exactly how this client reports a **key-id mismatch** - the conformance +notes in `ServerTests/RecNetShapeConformanceTests.cs` record that a wrong key id +makes the build drop the image as "malformed" before it reaches RSA - and the +mismatch was self-inflicted: + +- the metadata patch rewrites the literal `.rec.net` to `.radicade.internal`, so the + running game pins `KEY:RSA:p1.radicade.internal` (verified: record #2311 of + `global-metadata.dat` reads `.radicade.internal`); +- the plugin read its key id from `global-metadata.interop.dat`, the *normalized + tooling copy* made before the literal patch, which still says `.rec.net`; it + therefore stated `KEY:RSA:p1.rec.net` in `rcsig`, the server echoed the stock id + back, and the client rejected its own answer. + +Fix: `ImageSignatureRequest.DetectKeyIdFromGameMetadata` reads the metadata the +runtime actually loads. That file's 256-byte header is **shuffled per release**, so +the literal table is discovered instead of hardcoded: every 32-bit slot in the first +0x100 bytes is a candidate field value, a (table,data) layout is accepted only when +its records fit the data segment, sample as strict UTF-8, and are **packed in +order** (`index[n] == index[n-1] + length[n-1]`, the rule that makes the answer +unambiguous - a wrong table cannot pass it), and the key id itself must still be the +single tail literal the file carries. The plugin now reads the game metadata first +and keeps the interop copy as a fallback, logging +`[ImageSig] image key id: ... (game metadata: ..., interop fallback: ...)`; image +requests log `[ImageSig] stated key id on `. + +Live result (2026-09-26 14:11 boot, `RADICADE_HILE_GATE=drop`): + +- `[ImageSig] image key id: KEY:RSA:p1.radicade.internal (game metadata: ...)`, + 52 image requests, each carrying `rcsig=KEY%3ARSA%3Ap1.radicade.internal`; +- **no** `PlayerReporting/v1/hile` report and no `Signature malformed` anywhere - + the key-id wall is gone and signatures are no longer the reason images fail; +- `Initial Room Load '@TestPlayer's Dorm' (25.32) [instance 20957]` completes, + `SpawnLocalPlayer` finishes at 11.1 s, and the process is still alive at 400 s+ + (the August client self-exited ~40 s in through EAC teardown). + +The periodic `Could not enable featured rooms display... no images are available` +warning is the known August noise (`featuredrooms/current` is not served), not a +signature failure. + +Tooling added while chasing this: `tools/sep-literals.py` (literal table inspector: +`find`, `around`, `token`) and `tools/sep-scan.py` (PE byte scan for metadata-usage +tokens/pointers, naming the enclosing method from the runtime address map). The scan +did **not** locate the verifier: at metadata v27 the code reaches a literal through +a `.data` table of 8-byte encoded usage tokens, and the bare 4-byte token pattern is +too common in 30 MB of code to be conclusive. + +`.tmp-newbuild/` holds the derivation scripts (`find_layout.py`, `chain.py`, +`make_interop_metadata.py`, `solve_map.py`, `pin_map.py`) and `map_20230831.json`. + +## 2026-09-26 15:05 - per-frame exception flood gated at the game's log handler + +Reported symptom: after a studio test the log fills with + + InvalidOperationException: You should not try to retrieve bindings from a disposed container! + NullReferenceException: Object reference not set to an instance of an object. + +Trigger (both sessions): the presence fallback cancels the in-progress load +(`Room load to cancelled` -> `Attempting to go to dorm`; the 15:04 boot also +logged `Local Player's presence room instance is invalid`). The room scope's UJect +container (`HOKFDAHMIPF` in `UJect.Runtime`, it has `Dispose`) is disposed while +systems built from it keep updating, so each of them resolves through the disposed +container every frame and the guard `LIFBDDODLCA.KILJNHIAFPI` throws: + +- `RecRoom.Core.Creation.CircuitsV1.CircuitVisibilityBehaviour.ShouldBeVisible` + via `HHLGMMPJKPK` -> `HOKFDAHMIPF.BBDOIOJLDPI` -> `LIFBDDODLCA.KILJNHIAFPI`; +- `RecRoom.CircuitsV1Manager.Update`; +- `RecRoom.Audio.AudioManager.Update` -> font-texture-rebuild callback chain; +- `RecRoomSceneManager.Update` -> `SceneMoodSettings.FPAMNPDBNJC` (the NRE the + same broken resolve produces once it fails instead of throwing). + +Unity logs every throw with its full managed stack: 8016 exception entries / 12.5 MB +Player.log (14:22 session), 7151 / 6 MB (14:45). None of it is actionable - the +systems are stale, the container is gone - and it buries everything else. + +Fix: `2023Patch/RadicadeSeptemberClient/LogFloodGate.cs` patches +`ODGGOALOLEG.LogException(Il2CppSystem.Exception, UnityEngine.Object)`, the game's +`ILogHandler` in RecRoom.Debugging.Runtime and therefore the single managed funnel +every `UnityEngine.Debug.LogException` goes through. The first occurrence of each +distinct exception (type + message + top stack frame) logs in full; identical +repeats collapse to one per 30 s window, with `[LogGate]` count reports. Different +exceptions are untouched, and `RADICADE_LOG_GATE=0` disables the gate entirely. + +`RADICADE_LOG_GATE_SELFTEST=1` proves the gate in one boot: the first exception that +reaches the game handler makes it fire five identical ones and log PASS/FAIL. (It +cannot run at plugin load - Unity is still on its own `DebugLogHandler` then, which is +why the first attempt reported FAIL with five uncollapsed lines in Player.log.) + +Verified (2026-09-26 15:04 boot, `RADICADE_HILE_GATE=drop`, +`RADICADE_LOG_GATE_SELFTEST=1`): + +- `[LogGate] armed on ODGGOALOLEG.LogException ...`; +- `[LogGate] self-test: fired 5 identical exceptions through the live game handler; + 4 were collapsed, 1 reached the log (PASS)` - Player.log has exactly one; +- the same boot collapsed real floods with the exact signatures the user pasted + (disposed container, `SceneMoodSettings` NRE) plus two others it exposed: + `Malformed Response: '{}'/'[]'` (`EFMPNEEHJFJ.CMKDNAPFCOI`) and + `Local Player's presence room instance is invalid` (`CPEIFKPBJND.ICPBBDFEHBO`); +- session log 2 exception lines / 764 KB, against 7151 / 6 MB the run before. + +The gate only skips the log write, so it cannot cause behaviour changes; the boot that +verified it still ended ~46 s in through the client's own +`shutdown request /matchmaking/player/logout`, a boot-scene restart +(`CheatManager.OnDestroy` -> `Awake` -> `Start`) and then silence - no dump, no WER, +no `[ExitVeto]` block. That is the pre-existing silent-exit class, and its trigger is +the same presence bounce the cancelled-load flood points at. + +## 2026-09-27 02:00 - chat: the conversation pane was blank because the message list was empty + +Reported symptom: the Chat screen shows `Party Chat - 0 party people!` on the left and +a right-hand conversation titled `Chat` with a completely empty body, even though +`POST /chat/thread/withmembers`, `GET /chat/thread/1?messageCount=50` and +`POST /chat/thread/1` all answered 200. + +Client-side truth, from Player.log plus the plugin's `[Net]` lines around 02:00:29: + + [Net] Post .../chat/thread/withmembers (creates thread 1 for accounts 2 and 3) + [Net] Get .../chat/thread/1?messageCount=50 + Exception: Index was out of range. Must be non-negative and less than the size of the collection. + at System.Collections.Generic.List`1[T].get_Item (System.Int32 index) + at MMBGIGIFEMJ.AJONACNDODH (ELKGEHBGLCO HEJJEIECPNK, System.Int32 FJLPCOJGEJP) + at RRUI.Data.ChatThreadModel.FetchOlderMessages (System.Int32 FJLPCOJGEJP) + at RRUI.Data.ChatThreadModelController+FetchOlderMessagesScrollRectImpl.IHJJMFLAKAE (System.Double) + at ... OSA`2.ChangeItemsCount -> OSA`2.Refresh -> RRUI.RecyclingListScrollRect.Clear + at RRUI.RecyclingListScrollRect.SpawnLoadingPlaceholders (GameObject) + [LogGate] repeats of this exception are now suppressed for 30s at a time + [Net] Post .../chat/thread/1 (the message the user then typed) + +The thread the client had just created had **zero** messages, and `FetchOlderMessages` +pages history from the OLDEST message the model already holds - `MMBGIGIFEMJ` is the +chat API client (`MessageJson{Type,Version,Data}`, `DMMNEMENNED{Latest,NewerThan, +OlderThan}`), and `ELKGEHBGLCO` is the client's thread with a `List` on it. +With that list empty the index is out of range. The throw comes out of the recycling +list's own refresh, so it fires once per frame, the list never completes a build, and +the pane stays blank forever - the user could still type, which is why a message did get +posted afterwards. + +Real threads never hit this because Rec Room opens every conversation with a system +notice from the pseudo-player `-5`. Captured real traffic (`chat.rec.net`, 2022, in +`.external/recflare-server/research/RecNet API.postman_collection.json`) shows it as the +oldest message of every thread that has one: + + "senderPlayerId": -5, + "contents": "{\"Type\":0,\"Version\":1,\"Data\":\"Player <@U29565301> started a chat\"}" + +The same capture fixes three more shapes we were guessing at, and it is what +`Server/Chat/Thread.cs` now serves: + +- `GET /chat/thread?maxCount=16&mode=0` is an ARRAY of threads carrying + `latestMessage` (not `lastMessage`/`messages`), `chatThreadId`, `playerIds`, + `lastReadMessageId`, `chatThreadName`, `snoozedUntil`, `isFavorited`. DMs carry + `chatThreadName: null` (we send `""`) - the client renders the other member's display + name itself, so a `"Chat"` placeholder here was wrong too. +- `GET /chat/thread/1?messageCount=50` is an OBJECT with a `messages` page (newest + first); `GET /chat/thread/1/message?MessageCount=16&Mode=0` is a BARE ARRAY of the same + messages. The second route did not exist on this server at all - it is the spelling + `FetchOlderMessages` calls, and the client rejects a thread object there. +- `POST /chat/thread/1` answers the whole thread with its messages, and a blank + `messageContents` reports `chatResult` 1 with the thread still attached. The client + posts exactly that blank call right after `/thread/withmembers` to "open" a thread, and + we were answering 400. + +Also fixed while in there: a thread with no messages was dropped from the thread list +(`if (messages.Count == 0) continue;`), which is why the sidebar showed nothing at all; +`lastMessage` was the OLDEST message of the page rather than the newest; and thread +membership was not checked on the send route. + +Verified: `dotnet test ServerTests/Server.Tests.csproj` - **608 passed** (9 new cases in +`ServerTests/ChatThreadTests.cs` pin the notice, the list/open/messages shapes, backwards +paging and the blank-contents result), and all three routes were replayed live with a +real client token against the rebuilt server. + +### Retest 02:16 - the routes the first pass never saw + +Rerunning with the client exposed two things the 02:00 evidence could not: + + [Error] PINDLLOAFFA: HTTP Error 404 + [Error] Failed to ReadMessage: An error occurred + ... + [Net] Get /chat/thread/1/message?messageCount=8&mode=2&referenceMessageId=1 + [Net] Post /chat/thread/1/message/2/read -> 404 (server-side) + +The 404 was `POST /chat/thread/{id}/message/{messageId}/read` - the client's "ReadMessage", +which it calls for the newest message once a conversation has rendered. The messages +paging call also spelled its anchor `referenceMessageId`, not `MessageId`, so the anchor was +being ignored: every "older than message 1" page was answering the NEWEST messages instead +of the ones before the anchor, and the client kept re-requesting (the query repeats in the +log even after the pane filled). + +Both fixed, plus the result shape the read route has to use. `MMBGIGIFEMJ`'s method table +shows what the client awaits, and the read family returns +`MFBKDKPGMNP` - a Task of the result enum, not of a thread or a wrapper: + + public enum BBPILCKAECN // RecNet, the client's ChatResult + { Success, InvalidArguments, ThreadNotFound, MembershipNotFound, PlayerAlreadyOnThread, + CannotMessagePlayer, InvalidCharacters, RecentlyLeftThread, ThreadTooLarge, + InsufficientPermission, TooManyAffiliationThreads, UnderModeration, MessageNotFound, + InvalidThreadJoinType } + +The body is therefore a BARE NUMBER. `ChatResult(int)` in `Server/Chat/Thread.cs` now returns +the integer itself rather than a `{ ChatResult, chatResult }` envelope, which is what the +read, leave and rename routes answer; an envelope deserializes into nothing and surfaces as +`Failed to ReadMessage` even at HTTP 200. A non-member on the read route is a 200 with +`MembershipNotFound` (3), not a 404 - the status is what the client turns into a transport +error, the code is what it reports. + +Verified: `dotnet test` - **610 passed** (2 more cases), and live: the read route answers +`200` + `0`, and `?messageCount=8&mode=2&referenceMessageId=1` now answers `[]` instead of +the whole thread. + +Still not done: `GET /chat/thread/party` is the `[]` stub from the August port, so the left +column keeps reading `0 party people!`; `favorite`, `snooze` and `chatPrivacySetting` have no +route at all (so the star on the conversation header 404s); `lastReadMessageId` is one value +per thread rather than per member, so unread badges are wrong once two accounts read the same +conversation at different times; and every message is served with the full ~40-key alias +shotgun, which is why one 4-message thread answers a `POST` with 80 KB. + +## 2026-09-27 02:35 - chat, the other half: the push id has to be a NAME, not the enum's number + +A message now persisted and rendered from a refetch, but the conversation still only updated +for whoever asked: the live push never reached a handler. That half is realtime, and it is now +settled from the client itself rather than inferred. + +`RealtimeDiagnostics` (shared source, compiled into the September plugin) grew a probe that +prints the notification ids this client actually accepts. It hooks the dispatcher's own +registration calls - every void method on it taking `(string|enum, callback)` plus the +one-argument channel calls - and logs the first argument as each registration happens. The +result, at login, is the contract in one place: + + [Realtime] watching 6 dispatcher method(s) on HCACKLIOLEM (5 registration, 2 generic + overload(s) skipped but funnel through the core, rest channels): every notification id + this client accepts will be printed as it registers. + [Realtime] the client registered a notification handler under "ChatMessageReceived" + (HCACKLIOLEM.LFOCBEMNPDP) - a notification whose Id EQUALS "ChatMessageReceived" is + handled, anything else is dropped. + [Realtime] chat push key check: the server sends Id="90" and the client registered NO + handler for "90"; the chat handler it registered is under "ChatMessageReceived". + Only an exact match is delivered. + +Two things had to be learned to get there, and both are in the file's comments now: + +- **Reading the handler map kills the client.** The handlers live in an IL2CPP static field, + which Il2CppInterop surfaces as a property whose getter is a native + `il2cpp_field_static_get_value` call. Invoking it through reflection terminated the process + with no managed exception at all - Player.log simply stopped at 2 KB and nothing reached the + error log - while the dispatcher's own registration methods are ordinary managed methods that + patch safely. The map dump survives only as a managed-fields-only fallback. +- **`Il2CppSystem.Action` is not `System.Delegate`.** The dispatcher's overloads take + `Il2CppSystem.Action` / `Il2CppSystem.Action` / its own delegate type, and those are IL2CPP + proxies: `Il2CppSystem.Object` is not `System.Object`, so `IsAssignableFrom(typeof(Delegate))` + matches none of them and the first run hooked zero methods while reporting the dispatcher + found. The check walks the base chain for a Delegate-shaped type by name instead. + +The server change is one line in `Server/Chat/Thread.cs`. `Notification.Id`'s getter converts +any enum to its numeric value, and an id the client has no handler for is dropped without a +trace - which is exactly what a new message used to do. It is now spelled out as +`nameof(NotificationType.ChatMessageReceived)`, the way `PresenceUpdate`, `AccountUpdate` and +`SelfAccountUpdate` already were. The per-type rule is real and worth keeping in mind: this +client registers `ModerationKick` by its NUMBER (`"22"`, see `ConnectionRecoveryTests`) and +`ChatMessageReceived` by the NAME, so neither form is universally right. + +Verified: `dotnet test` - **611 passed** (a new case drives the send route with a recording hub +client and asserts the pushed envelope carries `"Id":"ChatMessageReceived"` and not +`"Id":"90"`), and live against the running client: + + [NotifyHub] -> Ye_BABskiLsHFK5zFYwTEg: Id="ChatMessageReceived" + [Realtime] Notification #16 Id=ChatMessageReceived IdToken=String Msg={..."id":14...} + [Net] Post /chat/thread/1 (the message, right after the push) + [Net] Post /chat/thread/1/message/14/read -> 200 0 + [Net] Get /chat/thread/1?messageCount=50 -> 200 + +The client now reacts to its own and the other account's messages: it marks the newest message +read and refetches the thread, which it never did before because the frame landed on no +handler. Thread 1 answers `len=108749` for a 50-message page, all of it alias shotgun. + +## 2026-09-27 03:15 - pointing the client at the public server (ns.radicade.app) + +The client now talks to `https://ns.radicade.app` (a netcup VPS behind Caddy -> +Kestrel) and nothing to the dev box. Two independent things decide that address, and moving +only one of them produces a client that boots against one server and then lives on another: + +1. **`Connection:ServerUrl`** in `BepInEx/config/com.radicade.eacbypass.cfg`, whose code +default is now `https://ns.radicade.app` (`RadicadeSeptemberClient/Plugin.cs`). + Everything the plugin rewrites - every `*.rec.net` service host, the hub socket, and the + `ns.radicade.app` host itself - is redirected to this base. `TLS:AllowedHosts` + follows it: `AlignAllowedHostsWithServer()` appends the host and saves the config, so a new + domain needs no second edit. +2. **The bootstrap host baked into the client metadata** by + `tools/patch-0831-client-metadata.py` (rule `https://ns.rec.net` -> the server base, plus + `api`/`auth`/`images`/`cdn` literals). The client fetches its nameserver document from that + baked host and then uses whatever absolute URLs THAT server returns, so a stale baked host + silently governs the whole session. + +That second point is the trap. The tool rewrites the file as it stands and does not reseed, so +re-running it on an install already patched for another address finds nothing to rewrite and +reports success with zero changes. Restore first: + + cp RecRoom_Data/il2cpp_data/Metadata/global-metadata.dat.orig \ + RecRoom_Data/il2cpp_data/Metadata/global-metadata.dat + py -3 tools/patch-0831-client-metadata.py --server https://ns.radicade.app --apply + +The plugin warns about exactly this mismatch at boot (`[Meta] this install's metadata was +patched for 'X' but Connection:ServerUrl is 'Y'`), which is how the localhost-baked install was +caught: it had been reading `/ns` from `localhost:3030`, and that server's own +`Radicade:ServerUrl` (the dev box's `https://:3030`) is what the whole session then +dialled - 41 requests to the IP, none to the configured domain. + +Verified on the running client: 134 requests to `https://ns.radicade.app/`, **zero** to +`` or `localhost`; the notify hub socket upgraded and the handshake was accepted; +`[TLS] Accepting server certificate for allowed Radicade host 'ns.radicade.app'`; the +login token was captured and 27 notification frames arrived. + +Still localhost by default, and deliberately left alone for now: `RadicadeAugustClient` (the +20230803 build, not in use) and `RadicadePatch` (the older C# host patcher, whose README +documents a plain-HTTP local workflow). + +One more thing learned the hard way: **stopping the server kills the running client.** The +02:36:52 run ended the moment the server was killed for a rebuild - the last lines are repeated +`/notify/hub/v1/negotiate` attempts and `Exception: Connection refused`. Every rebuild+restart +therefore costs a client relaunch; plan the retest around that instead of reading the death as +the old silent-exit bug. + +## 2026-09-27 11:55 - a friend's install died in interop, and the metadata header is the whole reason + +A second machine (game folder the same `2023-09-06T06-53-47Z`, but data folder renamed +`Radicade_Data`, BepInEx reported as **6.0.0-be.788**) failed before it ever reached the plugin: + + [Error :InteropManager] Failed to generate Il2Cpp interop assemblies: + ---> System.FormatException: Invalid or corrupt metadata (magic number check failed) + [Fatal : BepInEx] Unable to execute IL2CPP chainloader, no plugins will be loaded + [Error : BepInEx] Could not load file or assembly 'UnityEngine.CoreModule' + +That is not a broken file. Rec Room's `global-metadata.dat` does not start with the standard +IL2CPP magic; it ships a per-release scrambled 256-byte header. Ours begins `e0 eb 02 00`, +the normalized copy begins `af 1b b1 fa` (0xFAB11BAF - the value `Il2CppMetadata.ReadFrom` +checks). The working install exists because of exactly this: a normalized copy +(`global-metadata.interop.dat`) plus a loader that reads the copy. The friend's install has +neither, and three details in the log say so: + +- The failing line names `.../il2cpp_data/Metadata/global-metadata.dat`. Our patched loader + prints `global-metadata.interop.dat` there - `tools/AugustInterop --patch-loader` rewrites + that single `ldstr` - so theirs is a stock loader. +- `be.788` with no `GlobalMetadataPath` in its config. That setting does not exist in be.697 + at all (our working `BepInEx.cfg` has no such key under `[IL2CPP]`), which is why the fix + was a binary patch here. Newer BepInEx builds expose the same redirect as configuration. +- No `Detected outdated interop assemblies, will regenerate them now` line, and no `Preloaded + N interop assemblies`. `CheckIfGenerationRequired()` returns true silently when + `BepInEx/interop` is **absent**, so the folder was never there: a plain BepInEx was installed + on top of this game. `Downloading unity base libraries` agrees - `unity-libs` was missing too. + +So the rule for a summer 2023 Rec Room install is: **the BepInEx layer travels as one unit**. +The be.697 core with the patched loader, `global-metadata.interop.dat`, the generated +`BepInEx/interop` (332 assemblies here) and `assembly-hash.txt` have to arrive together; any +one of them alone is worse than nothing, and a newer core replaces all four. + +Two ways back, in order of least pain: + +1. Move the friend's `BepInEx` folder aside and copy the working one in, together with + `winhttp.dll`, `doorstop_config.ini`, `boot.config` and both `*.dat` files. +2. Keep the newer core and add this to `BepInEx/config/BepInEx.cfg` (then let interop + regenerate once, which takes minutes and needs `unity-libs`): + + [IL2CPP] + GlobalMetadataPath = {GameDataPath}/il2cpp_data/Metadata/global-metadata.interop.dat + + That only works if `global-metadata.interop.dat` is present in the metadata folder - the + normalized file is the piece a stock install never has. + +Both are now scripted, because hand-assembling this a third time is how the be.788 install +happened in the first place: + +- `tools/september-client-triage.ps1` - read-only, finds the data folder whatever it is called, + and prints one `OK`/`WARN`/`MISSING` line per piece: build identity, both metadata files and + their magic bytes, loader patched-vs-stock, the `GlobalMetadataPath` override, interop count + and hash, plugin/winhttp/doorstop, then the markers in the last `LogOutput.log`. It writes + `september-client-report.txt` and exits non-zero when anything is missing. Confirmed against + this install (8 OK, 0 warnings) and against a fixture that reproduces the friend's log + (5 MISSING, including `the last run died in interop generation`). +- `tools/make-september-client-pack.ps1` - builds the handover bundle: verifies the source + against the reference hashes and the two header magics, then copies the whole BepInEx tree + (minus logs, `*.before-*` backups and the 18 MB `radicade-addr-map.txt`), the data-folder + pieces, and a `README-FRIEND.txt` whose first instruction is *move your existing BepInEx + aside, do not merge*. `-VerifyOnly` checks the source and copies nothing. Run here: all ten + pieces `ok`, bundle 294.8 MB, and the bundled triage script reports the source install clean. + (It carries eleven checked pieces since 2026-09-27: the marker `radicade-patch.json` was added + to the bundle, since the plugin reads it to confirm the patched server - see the last section.) + +One thing the reference hashes pin down: this is the public-server install, so the friend needs +no local server. The shipped plugin config already reads +`ServerUrl = https://ns.radicade.app`, matching the domain baked into `global-metadata.dat`. + +## 2026-09-27 13:20 - the friend's second log: the stack boots, and the client asks a *different* private server for its config + +The be.697 stack is fine now (`BepInEx 6.0.0-be.697`, `1 plugin to load`, `[Radicade September +Client 3.0.0]`, `Chainloader startup complete`, every notification handler registered, +`[Sept] Connection patches loaded`). What the log shows instead is that the *client data* in that +folder is not this bundle's: + + [Warning:Radicade September Client] [Meta] this install's metadata was patched for + 'https://localhost:3030' but Connection:ServerUrl is 'https://ns.radicade.app' + [Info :Radicade September Client] [Net] Get https://localhost:3030/ + [Info :Radicade September Client] [Net] Get https://api.recflare.net/api/gameconfigs/v1/all + [Info :Radicade September Client] [Net] Post https://datacollection.recflare.net/data/event + [Info :Radicade September Client] [TLS] NotifyServerCertificate: host=api.recflare.net, ... + [Error :Il2CppInterop] ... TlsFatalAlert: user_canceled(90) + [Warning:Radicade September Client] [QuitVeto] wantsToQuit answered false (block #1) + [Warning:Radicade September Client] [LogGate] repeats of this exception are now suppressed ... Exception: Aborted + +### `recflare` is another project, and this install has never contained the string + +`recflare.net` is RecFlare's public server - the reference implementation we studied lives in +`.external/recflare-server`, and its services are exactly `api.recflare.net`, +`datacollection.recflare.net` and friends. On this working install the string appears **zero** +times in every piece that could carry a host: + + global-metadata.dat / .orig / .interop.dat / GameAssembly.dll -> 0 x recflare + +So those URLs are in *his* client data, and two things put them there: a second patch in the same +tree (his `global-metadata.dat` still carries our `.radicade.internal` image-key tail, so a +Radicade patch did run on it - over a client that already had `api.recflare.net` baked in by +something else), and/or the per Windows user folder replaying a config an earlier RecFlare +session cached there. That folder is shared by every install on the PC: +`%LOCALAPPDATA%Low\Against Gravity\Rec Room` holds one `Library`/`HTTPCache`/`Player.log` for all +of them. This machine's own `Library` still lists `https://rr2019-images.viverec.com/...` and +`https://:3030/...` from servers it has talked to before - the same mechanism, milder. + +### Why a foreign host is fatal rather than cosmetic + +`NameserverRedirect` rewrites only the exact hosts it knows (`ns.rec.net`, `api.rec.net`, +`auth.rec.net`, `img.rec.net`, ...) plus the configured server host, and the embedded TLS patch +accepts a certificate only for hosts on `TLS:AllowedHosts` (`localhost, ns.radicade.app`). +A host the client resolves itself that is in neither list is neither redirected nor trusted, so +`LegacyTlsAuthentication.NotifyServerCertificate` throws `user_canceled(90)`, no gameconfigs ever +arrive, and the anti-cheat path takes the session down: `CheatManager` wants to quit, `QuitVeto` +refuses, `LogGate` collapses the `Exception: Aborted` repeats. The `QuitVeto`/`[EacStub]`/`[AddrMap]` +noise at the end is a symptom of a boot that never got its configs. + +Everything else in that log is expected: the HarmonyX `TypeLoadException` walls on `Il2Cppmscorlib` +and `Assembly-CSharp` (`IStream`, `TabsModel`/`ObscuredEnum` constraint errors are +assembly-shape facts of the decompiled types), Il2CppInterop's "field accessor, it can't be +patched", the `GetTypesFromAssembly` warnings, `[ModuleList] winhttp.dll`, and the +`www.google.com/generate_204` probe. + +### What the triage script grew, and the bug it uncovered + +A byte search cannot answer "which server does this client use": the patch keeps the original +literal blob verbatim and appends the rewritten strings, so the *dead* copy of every host is +still in the file - the reference metadata still contains `ns.rec.net` as dead bytes, which is +why the old `Test-Literal 'localhost:3030'` check could never be conclusive. The script now reads +the **literal table** itself (0x74 table offset, 0x24 table size, 0xA0 data offset, 0x3C data +size - the same four fields `tools/patch-0831-client-metadata.py` rewrites) and prints the hosts +the runtime actually resolves: + + == IL2CPP metadata == + global-metadata.dat: 33774004 b md5 ce5097af... magic e0 eb 02 00 + live service hosts this client resolves (from the literal table, dead copies ignored): + 40x www.root-motion.com + 22x www.w3.org + 9x www.youtube.com + 7x ns.radicade.app + 5x recroom.com + 4x recroom-dataplane.rudderstack.com + ... + radicade-patch.json: serverUrl https://ns.radicade.app + +New verdicts: a live host belonging to another project (`recflare|viverec|trollroom|meownet`) is +`MISSING` - *no plugin setting can redirect a host the client resolves itself*; a **ported** +loopback host means the install was baked for a local server (the bare `localhost` literal is this +build's own, so it must not raise anything); and the marker `RecRoom_Data/radicade-patch.json` - +the URL the patch tool wrote, the same file the plugin compares against `Connection:ServerUrl` at +boot - is printed. A third section lists the per-user folder: sizes, cache entry counts, every +host it has seen, and a note when it still names another private server. Log markers were added +for `TlsFatalAlert`, `user_canceled`, `wantsToQuit answered false`, `Exception: Aborted`, +`metadata was patched for`, plus a hard `MISSING` when the last run dialled another private server. + +Adding that parser uncovered a latent bug in the script's own `Get-Bytes` helper: `return +[IO.File]::ReadAllBytes($path)` unrolls the array into the pipeline, so the caller received one +boxed object per byte - on the 33 MB metadata that is 33 million objects. A full run now takes +**1.0 s** end to end; before the fix the same run took minutes, and the new metadata read hung +outright (no output after 300 s). + +Verified both ways: this install -> `missing: 0 warnings: 0` plus the two new informational +sections, and a fixture made from this install's own metadata with every `ns.radicade.app` +literal rebound to `api.recflare.zzzzzzzzz` (same length, so the table's length fields stay valid) +and a `radicade-patch.json` naming `https://localhost:3030` -> `missing: 10 warnings: 4`, +including `this client's own literals resolve api.recflare.zzzzzzzzz - that is another private +server's patch` and `the last run dialled another private server (recflare.net)`. Recipe for the +fixture (delete it afterwards): + + py -3 -c "import pathlib; s=pathlib.Path(r'\RecRoom_Data\il2cpp_data\Metadata\global-metadata.dat').read_bytes(); pathlib.Path(r'\RecRoom_Data\il2cpp_data\Metadata\global-metadata.dat').write_bytes(s.replace(b'ns.radicade.app', b'api.recflare.zzzzzzzzz'))" + +### What the friend has to do + +He is launching a copy nested inside a copy (`...\2023-09-06T06-53-47Z (2)\2023-09-06T06-53-47Z`), +which is how two patches end up in one tree - the bundle was unpacked next to the game instead of +into it. One clean install, then: + +1. move the existing `BepInEx` aside and copy this bundle's `BepInEx` in (never merge), plus + `winhttp.dll`, `doorstop_config.ini`, `.doorstop_version`; +2. copy this bundle's `RecRoom_Data\` contents over his data folder - `boot.config`, **both `.dat` + files** and now also `radicade-patch.json` (`il2cpp_data\Metadata\global-metadata.dat` is the + piece his log proves he does not have: it is the file whose literals decide the bootstrap host); +3. rename `%LOCALAPPDATA%Low\Against Gravity\Rec Room` so no cached config from another server can + be replayed; +4. launch and look for `[Meta] metadata patched for https://ns.radicade.app; matches + Connection:ServerUrl.` - then run the triage script and send `september-client-report.txt`` + +## 2026-09-30 17:50 - chat and account: the members the client reads POSITIONALLY + +Two client logs came back, and both are about the same failure class rather than about missing +features: an obfuscated DTO whose members the client binds by POSITION, so a response is rejected +whole (`EFMPNEEHJFJ.CMKDNAPFCOI[T]` -> `Exception: Malformed Response: ''`) whenever the +member order or an alias differs from the reference. Unknown members are ignored; wrong ones are +not. `docs/2023-AUGUST-BUILD.md` documents the reader; this is what it cost on the two screens the +player was actually looking at. + +### Chat: the thread list, and the shotgun that broke it + + [Warning] [LogGate] repeats of this exception are now suppressed for 30s at a time: + Exception: Malformed Response: '[ { "id": 5, "Id": 5, "chatThreadId": 5, + "ChatThreadId": 5, "chatThreadName": "", "Ch... + @ at EFMPNEEHJFJ.CMKDNAPFCOI[T] (System.String IFIGOIHCFPF) + +That body is this server's `GET /chat/thread?maxCount=50&mode=0` - the sidebar list - and the +`id`/`Id`/`chatThreadId`/`ChatThreadId`/`name` prefixes are the alias shotgun each response used to +carry ahead of the real members. Every member after the first was bound one position off, so the +list threw and the conversation pane never rendered. `Server/Chat/Thread.cs` now serves the +reference names in the reference order, and nothing else: + +- list item: `latestMessage`, `chatThreadId`, `playerIds`, `lastReadMessageId`, `chatThreadName`, + `chatThreadType`, `snoozedUntil`, `isFavorited`; +- opened thread: `chatThreadId`, `playerIds`, `lastReadMessageId`, `chatThreadName`, + `chatThreadType`, `snoozedUntil`, `isFavorited`, `messages`; +- message: `chatMessageId`, `chatThreadId`, `senderPlayerId`, `timeSent`, `contents`, + `moderationState`. The ~40-key alias shotgun is gone, so a 4-message thread no longer answers + `POST /chat/thread/1` with 80 KB of aliases. + +(Those two were still wrong on the first pass - `chatThreadName` as `null`, `chatThreadType` dropped +as a shotgun alias, `latestMessage` repeated on the opened thread. See the 18:14 retest below, which +is the shape that stands.) + +A new case pins the ORDER, the presence and the nullability of those members rather than their +values (`ChatThreadTests.WireShapes_BindPositionally_InTheReferenceOrder`): the next person to +"clean up" a key order there breaks the client, and that is now a failing test rather than a log +line. + +### Account: what two profiles were rejected for + +Same log, same reader, different screens. Both live answers were replayed against +`https://ns.radicade.app` before the fix, and both are exactly what the client threw on: + + $ curl .../econ/api/influencerpartnerprogram/influencer?accountId=2 + {"AccountId": 2, "Code": "COACH"} -> Malformed Response: '{ "AccountId": 2, "Code": "COACH" }' + $ curl .../econ/api/influencerpartnerprogram/myinfluencer + {} -> Malformed Response: '{}' + +The first is a shape we invented: the captured real answer to that route is a BARE NUMBER +(`28848275`, "Influencer Support" in `research/RecNet API.postman_collection.json`), it is served +as one now, and `404` still means "not an influencer". The second is the recurring lesson of this +file in its purest form - `{}` is not a valid "nothing here": the client's DTO for it has one int +member (`influencerAccountId`), so an empty object has nothing to bind and the route is refetched +in a loop (the log fetches `myinfluencer` and `customAvatarItems/v2/fromCreator/2` a dozen times +over, each round ending in another Malformed). It now answers `{"influencerAccountId": 0}` (or the +caller's own id when they are one of the configured test influencers), which is the same value +with the member present. + +### Account, the other half: a refused sign-up must not look like a login + +The second log is a machine with no account on this server at all +(`GET /auth/cachedlogin/forplatformid/0/76561198695373725` -> `[]`, verified live), and its boot +loops every ~20 s with NO request after the login: + + [Onboarding] exit get_IsInAccountCreationState => false + [Net] Get /auth/cachedlogin/forplatformid/0/76561198695373725 + [Net] Get /auth/eac/challenge + [Net] Post /auth/connect/token + [Realtime] Login token captured (value omitted) + ... nothing. +12.7s / +35.8s / +53.4s: CheatManager.OnDestroy -> Awake -> Start, and again + +`Login token captured` is printed by `HubSocketAuth.CaptureFromLoginResponse` when the +response is HTTP 200 and HAS an `access_token` property - and `AuthResponse.access_token` is a +plain `string`, so a REFUSAL (`new AuthResponse("Account creation failed.", ...)`, no token) +serializes as `"access_token": null` and satisfies both checks. Every refusal this server could +answer a sign-up with - the per-IP account-creation cap (15/hour), an IP or hardware ban, the +five-accounts-per-device limit, a missing device id - was therefore delivered as a 200 that the +client read as a finished login, leaving it holding no token and dying before its next request, +while the plugin's own trace called it a successful login. That is the loop in that log. + +`ConnectWithToken` now answers those two provisioning refusals (the `cached_login` grant that tried +to auto-provision, and the `create_account` grant) as HTTP **400** carrying `error` and +`error_description` - the same shape the unlinked-identity refusal already uses and the one the +client acts on - and logs the reason server-side. A refusal the player can see beats a login that +silently goes nowhere, and the next log from that machine will say which refusal it was: the body +is no longer a 200, so it can no longer be read as a captured token. + +The plugin half of that lie is fixed too: `HubSocketAuth.CaptureFromLoginResponse` now requires +`access_token` to be a real non-empty STRING before it reports a captured token, and a 200 without +one logs `"connect/token answered HTTP 200 without an access_token - that is a login REFUSAL"` +together with the body's `error`/`error_description`. That single line is what this whole +investigation had to reconstruct from server-side evidence; the next run on that machine will just +print the reason. + +Still open on that machine, and worth knowing before the next trip: the plugin's `[AuthHttp]` +status trace is armed and logged its arming line, then never fires a single `begin`/status line in +either log, so no onboarding HTTP status is on the record. `AuthRequestStatusTrace` patches +`HTTPManager.SendRequest(HTTPRequest)` and the `HTTPResponse` constructor, which this client's +request path evidently does not go through; patching the request's completion callback (the way +`HubSocketAuth.CaptureFromLoginResponse` does for `connect/token`) is the shape that works. + +Verified: `dotnet test ServerTests/Server.Tests.csproj` - **776 passed, 2 failed**, and the two +failures are the storefront/equipment capture counts (`StorefrontRewardConfigTests` +`EveryGameStorefrontHasACapture` 44 vs 45, `AdminGiftSkinTests` 9 vs 11) that track the unrelated +in-flight storefront catalog work on this branch - no chat, auth, account or influencer test +touches them. New cases: the order, presence and nullability of all three chat shapes (a DM's empty +name, `chatThreadType` as an int, an opened thread carrying `messages` and not `latestMessage`), the +bare-number influencer answer with the one-member `myinfluencer` object, and a refused sign-up being +a 400 and not a tokenless 200. + +Also still open, seen in the same log and not touched here: `GET /api/progressionEvents/active` +answers `[]` where the client wants an object (same class of rejection, one route over), and the +`/chat/thread/party` stub, the per-member `lastReadMessageId` and the missing favorite/snooze +routes from 2026-09-27 remain as they were. The reference also has two routes this server does not: +`GET`/`PUT /chat/thread/chatPrivacySetting` (enum by number on read, by NAME on write) and +`GET /chat/thread/checkCanSendDirectMessageWithPrivacySetting` (a bare ChatResult int, whose real +refusals are 15/16). + +### Retest 18:14 - the shapes above broke `GetChatBetweenPlayers`, and the reference says why + +Shipping them made the conversation itself fail, which the list fix had hidden: + + [Error] ArgumentException: JSON parse error: Invalid value. + [Error] [18:14:34] Failed to GetChatBetweenPlayers: An error occurred + +`GetChatBetweenPlayers` is `POST /chat/thread/withmembers` - the OPENED-thread object - and two of +the three members the 17:50 rewrite changed are load-bearing in ways the 2022 capture cannot show: + +- **`chatThreadName` is DEREFERENCED without a null check.** Serving an unnamed thread as `null` + (what the capture shows, and what 17:50 switched to) takes the whole call down; the client wants + `""` and falls back to naming the members itself. Unnamed is an empty STRING, never null. +- **`chatThreadType` cannot be dropped.** It deserializes into a NON-NULLABLE int, and the client + discards the entire response when the member is absent - so removing it as one more shotgun alias + was a regression, not a cleanup. Every thread carries `chatThreadType: 0`, the only type the + reference ever serves. +- `latestMessage` is NOT repeated on the opened thread: the client is sent `messages` or + `latestMessage`, never both. `latestMessage` is also the one NULLABLE member of the list item + ("null only for a thread with no messages yet") - `lastReadMessageId` is a non-nullable int too. + +All of that is recorded in the working reference, with the client's own throw text, in +`.external/recflare-server/apps/chat/src/thread-db.ts` - "The client deserializes this into a +non-nullable int and fails the whole response on a null (\"expected 'Number Token', actual +'null'\")" and "The client dereferences this name without a null check (a null +NullReferenceExceptions its way out of GetChatBetweenPlayers)". The final shapes, in the +reference's own serializer order: + + list item latestMessage, chatThreadId, playerIds, lastReadMessageId, chatThreadName, + chatThreadType, snoozedUntil, isFavorited + opened thread chatThreadId, playerIds, lastReadMessageId, chatThreadName, chatThreadType, + snoozedUntil, isFavorited, messages + message chatMessageId, chatThreadId, senderPlayerId, timeSent, contents, moderationState + +The lesson, for the second time in a day: a payload is not "the real server's" or "ours" - it is +the one THIS client build reads. The 2022 capture (which spells a DM's name `null`), the September +DTO and the reference implementation disagree, and where they do, the reference wins: it is the one +whose comments cite each throw the September build makes, route by route. + +### 2026-09-30 19:40 - a wiped database could not start the server at all (EF identity conflict) + +Dropping `RadicadeServer` and starting the server is meant to be the easy way back to a clean +world. On the live host it died on the way up, twelve lines into the seeding pass: + + Unhandled exception. System.InvalidOperationException: The instance of entity type 'ConfigVal' + cannot be tracked because another instance with the same key value for {'Key'} is already being + tracked. When attaching existing entities, ensure that only one entity instance with a given key + value is attached. + at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry) + at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.AddAsync[TEntity](TEntity entity, ...) + at Program.<
$>g__EnsureSplitTestHardOverrides|0_32(RecDBContext dbContext) in Program.cs:line 1162 + at Program.<
$>g__EnsureRequiredGameConfigs|0_31(RecDBContext dbContext) in Program.cs:line 1137 + at Program.
$(String[] args) in Program.cs:line 427 + +The mechanism is a single rule of EF Core being ignored in four places: **a row this DbContext has +added is not in the database yet, so a query for its key answers null.** + +1. `EnsureRequiredGameConfigs` upserts `splitTestHardOverrides` - `UpsertGameConfigValue` queries, + sees nothing (empty database), and *adds* a `ConfigVal`. Tracked, unsaved. +2. Two lines later `EnsureSplitTestHardOverrides` queries for the same key, again sees nothing + against the database, and adds a second instance of it. EF refuses the moment the duplicate is + tracked - and the process dies before the `SaveChangesAsync` at the end of the method that would + have made the first row visible. + +That last detail is what made it unrecoverable rather than a one-off: nothing was persisted, so +**every** subsequent start walked into the same conflict. Three more keys were armed the same way +behind it - `splitTestsAvailable` and `splitTestSegmentProbabilities` (`RepairSplitTestConfigs`, +line 430) and the `UGC.Room2*` batch (`EnableBetaCreativeToolsConfigs`). + +The fix is `Server/Utils/GameConfigSeeder.cs`: one lookup that checks `dbContext.GameConfig.Local` +(the tracked rows, including unsaved adds) before the database, plus the upsert built on it. All +four find-or-add sites go through it now, with the tracked-aware trick `EnsureReferenceGameConfigs` +already used for its own keys. `ServerTests/GameConfigSeederTests.cs` pins it, including the +conflict itself as a case: add a row, query for it, watch the query answer null, then watch the +second add throw. Nothing else was wrong - the wipe regenerated the schema, the Coach profile, the +config, the default rooms and the dorm, and only this seeding step was unpassable. + +Anyone stuck on a build without the fix can seed the three keys by hand and get up: + +```sql +USE RadicadeServer; +INSERT INTO GameConfig ([Key],[Value]) VALUES + ('splitTestHardOverrides',''), ('splitTestsAvailable',''), ('splitTestSegmentProbabilities',''); +``` + +Both lookups then find a row in the database and neither adds anything; the blank values are filled +in by the upsert and `RepairSplitTestConfigs` on the same start. + +## 2026-10-01 18:30 - the game-update alert has no notification to send, so the client plays it + +Sending a Game Update from the panel stored a row, pushed a frame, and made no sound. The hub log is +specific about how far it got: + +``` +[Realtime] Notification #39 Id=27 IdToken=String Msg={} <- arrived, parsed +[Realtime] 27 envelope: {"Id":"27","Msg":{}} <- the frame, echoed +[Realtime] the client called IPCPDNFCGPA("{...}") <- no handler registered under "27" +``` + +`IdToken=String` rules out the old parse failure (`expected:'String Begin Token'`), and the third line +is the client falling past every registered handler into its channel/lookup path. The id was never +going to match, for a reason worth writing down: **this client has no notification for a game update +at all.** Its enum, read out of `RecNet.Runtime.dll` metadata (40 members), runs +`24 ModerationRoomBan`, `25 ServerMaintenance`, `30 GiftPackageReceived` - there is no 26, no 27, and +`AnnouncementGameUpdate` appears nowhere in that assembly as a string, while `ServerMaintenance`, +`ModerationKick`, `ChatMessageReceived` and `PresenceHeartbeatResponse` all do. So neither spelling +can work: the number matches nothing, and the name has no handler to register. `AnnouncementText` (26) +is equally phantom, which is the standing explanation for why regular announcements only ever reach a +player through the REST fetch and never through a push. + +That matches how Rec Room's own alert works: the sound is a client-side asset with nothing behind it +on the wire, and the server can only ever *request* it. So the request is the frame the panel already +produces, and the playing is now the client's own job - `2023Patch/RadicadeSeptemberClient/AnnouncementSoundAlert.cs`: + +* it hooks `WebSocketTransport.OnMessage` (the same raw-frame hook `RealtimeDiagnostics` already + uses) and reads the record properly - `target == Notification`, the envelope out of `arguments[0]`, + then its `Id` - rather than by substring, so a payload that merely mentions an id cannot set it off; +* ids `"27"`, `"AnnounceNewUpdate"` and `"RadicadeAnnounceNewUpdate"` all mean "play it", so moving + the server to an explicit marker later needs no new client build; +* the sound is embedded in the DLL (`Assets/AnnounceNewUpdate.wav`, 16-bit stereo 44.1kHz, ~7.6s) and + played through a 2D `AudioSource` on a `DontDestroyOnLoad` object - 2D because the alert is not a + position in the world and must not be attenuated by wherever the player is standing; +* Unity audio is main-thread only and a notify frame can arrive on BestHTTP's receive thread, so the + play is handed to the captured main-thread synchronisation context and refuses to run (loudly) + anywhere else rather than asserting inside the game; +* `Alert.TestOnStartup` is **off by default** and exists only to prove the DLL can play a sound with + no server involved: turned on, it plays the alert once, by itself, a few seconds after the game's + audio is up. A client must never make a noise on its own when somebody just launches the game - + which is exactly what the first build did, three times over. The loop only checked *after* posting, + so ticks that queued while the game was still booting each played (log: `play #1`, `#2`, `#3` on + one launch). The play is now claimed by the first probe that finds an audio listener, so the extra + ticks do nothing. `Alert.SoundFile` swaps in another wav; `Alert.Volume` and `Alert.Enabled` do + what they say. +* the clip is held on the source (`AudioSource.clip`) as well as played with `PlayOneShot`, because an + AudioClip nothing references natively is unloaded with the next scene's assets - observed as + `alert clip ready` logging a second time (the clip being rebuilt mid-session) after the boot plays. + +What to look for on a launch: `[Alert] watching the notify hub for the game-update notification`, then +`[Alert] alert clip ready from ...` the first time the alert is about to play, then either +`[Alert] playing the new-update alert (the startup test (Alert.TestOnStartup))` if that switch is on, +or - in a normal install - `[Alert] the server pushed the game-update notification (Id "27")` +followed by a play line naming that id. A launch that plays nothing at all is the expected case. + +No server change was needed for any of this: the deployed server already sends `Id "27"` with an empty +payload, so the alert works as soon as the new client DLL is installed - no rebuild, no redeploy. The +comment at that branch in `AdminPanelAPI` now records that the frame is a cue rather than an +announcement, and that it has to stay id-only. + +## 2026-10-01 22:00 - the last three `Malformed Response` lines, and the envelope a message rides in + +The 21:37 `BepInEx/LogOutput.log` is the first run against a server carrying the club-route, filter +and carousel work, and it is down to **three** parse failures from the dozen-per-boot of the earlier +runs. `LogGate` prints each distinct one once, so three lines is three routes: + +| Body the client printed | Route | Why | +|---|---|---| +| `'[ { "latestMessage": … } ]'` | `GET /chat/thread?maxCount=50&mode=0` | the message's `contents` | +| `'{}'` | `GET /econ/api/subscriptionseasons/v1/seasons/current` | an empty object is not a season | +| `'[]'` | `GET /api/progressionEvents/active` | an array is not a `long?` | + +The routes are identifiable from the printed body and the request log around it: those two `{}` and +`[]` routes are the only ones in that window that answer those bodies (`subscriptionseasons` was the +branch's one remaining `return Ok(new { })`, and `progressionEvents/active` answered +`Array.Empty()`), and the array is the sidebar list's item shape and nothing else. + +### A message's `contents` is a JSON envelope, not a sentence + +This is the one that has been hiding behind the thread-shape work all along. The thread item we serve +is byte-for-byte the working reference's item (`latestMessage`, `chatThreadId`, `playerIds`, +`lastReadMessageId`, `chatThreadName`, `chatThreadType`, `snoozedUntil`, `isFavorited` - all eight +present, in that order, `chatThreadName` as `""`), and the reference's own integration test asserts +exactly that JSON - yet the client rejects ours. The difference is not the thread at all, it is the +message inside it: + + [Net] Get .../chat/thread?maxCount=50&mode=0 + -> Malformed Response: '[ { "latestMessage": { "chatMessageId": 1, … + "contents": "Player <@U2> started a chat", "moderationState": 0 } ]' + +The client's message model does not read `contents` as display text: it parses it, and the text lives +inside a small envelope. Real traffic spells it +`{"Type":0,"Version":1,"Data":"Player <@U29565301> started a chat"}` (2022 capture, quoted in this +file's 18:14 section), the working reference stores and serves precisely that - its chat test data is +`contents: '{"Type":0,"Version":1,"Data":"hi"}'` - and this server was writing bare sentences into +it for every thread notice it generates itself. That is also the *other* unexplained throw from the +18:14 retest: + + [Error] ArgumentException: JSON parse error: Invalid value. + [Error] Failed to GetChatBetweenPlayers: An error occurred + +`JSON parse error: Invalid value` is what a JSON reader says about a bare sentence, and the 18:14 +fix (empty `chatThreadName`, `chatThreadType` present) did not address it - the run after it never +opened a conversation, so nothing disproved it either. `ChatStartedNotice`, the leave notice and +`ToMessageResponse` now all go through `ThreadAPI.MessageEnvelope`, which + +* stores this server's own notices (`Player <@U4> started a chat`, `Player <@U4> left`) as the + envelope, and +* wraps anything else that is not already a JSON object on the way out, so a message row written + before this change (bare text, from an earlier build or a hand-written request) is still readable + instead of failing the thread it appears in. A value that already parses as an object is passed + through untouched - the client posts the envelope, and re-wrapping it would nest it. + +`<`/`>` are deliberately left unescaped (`UnsafeRelaxedJsonEscaping`): the capture spells the mention +as `Player <@U29565301> started a chat`, the client parses mentions out of `Data`, and the default +encoder's `\u003C` is not the same bytes as the traffic this shape was taken from. + +### If the thread list still misbehaves, the element order is the next lever + +The list item's element type is readable straight out of the client's interop assembly, and it is +worth writing down because the reference's member order is the 2022 capture's, not necessarily this +build's: `RecNet.Runtime.dll` declares a static API wrapper returning +`MFBKDKPGMNP>` (`NativeMethodInfoPtr_EKLFFAGHACF_Public_Static_…_List_1_ELKGEHBGLCO_0`) +- so the body is a bare array whose element is `ELKGEHBGLCO`, which the interop decompiles to +(`ilspycmd -t ELKGEHBGLCO BepInEx/interop/RecNet.Runtime.dll`) 12 backing fields in this order: + + long, OBLNNOGIKLC, long, List, ChatMessage, List, bool, long, string, + Nullable, bool, Nullable + +`OBLNNOGIKLC` is `enum { Player, Club, Party }` - the thread type, in slot 2 - and the key literals in +`global-metadata.interop.dat` (`chatThreadId`, `chatThreadType`, `lastReadMessageId`, `messages`, +`latestMessage`, `playerIds`, `isFavorited`, `chatThreadName`, `snoozedUntil`, `otherPlayerId`) all +exist as string literals (`tools/sep-literals.py find …`), which is where the slot names come from. +`otherPlayerId` is a `Nullable` and lands on slot 12 (`isMuted`/`lastMessageId`, by contrast, are +**not** literals in this build). The reference's order does not match that list at all, so if the +envelope fix above does not clear the list line, the next thing to serve is the twelve members in the +DTO's own order - and the payload the client prints is then the evidence either way. + +### `progressionEvents/active` and `seasons/current`: `null`, and 404 + +* `GET /api/progressionEvents/active` answers `long?` (`KIJDNIPGGDC() -> MFBKDKPGMNP`), and with + no keepsake event configured it now answers the JSON literal `null` at HTTP 200 - the one body that + type deserializes from as "no event". The empty array it used to answer is a parse failure + (`'[]'`, both the 17:54 and 21:37 runs), and a 404 is an empty body, which the client reports as a + failed call it retries. `Ok(null)` would have been a 204 with no body at all, so the `null` is + written explicitly (`Content("null", "application/json")`). +* `GET /econ/api/subscriptionseasons/v1/seasons/current` answered `{}`, and the client's DTO here has + six members (`Guid`, `string`, `string`, `DateTime`, `DateTime?`, `List<…>` - the 2023-09-12 + reference's `ALGDPMHOPGG`), so an empty object has nothing to bind. It answers `404` now: writing + six invented member names would put a season in front of the player that does not exist. + +Verified: `dotnet build Server/Server.csproj` 0 errors, `dotnet test ServerTests` **798 passed / 9 +failed**, the same nine catalog/storefront/equipment capture-count failures this branch has carried all +week. The chat and startup-compatibility cases were updated for the envelope and the `null` body +respectively. + +## 2026-10-02 17:00 - chat photos: the `` reference, and the bulk lookup that 404'd + +A photo message is not a text message with a special payload - it is the same envelope with `Type` 2 +and a `Data` that is one reference: + + {"Type":2,"Version":1,"Data":""} + +Both literals are in the client's own metadata, verbatim and adjacent +(`tools/sep-literals.py find "Image:"`): `""` (the parse) and `""` (the +compose). The capture is `(.+)`, not `(\d+)`, so the value is whatever `string.Format` was handed. + +Two facts pin down what N is meant to be: + +* The client's image vocabulary is built on `SavedImageId` - the picker helper's own promise type is + `Task` (`PKMFHFKFFAE.PGJDJGBGNFP(List, …) -> MFBKDKPGMNP`), and the metadata + DTO's first member is the id. +* The client's **only** by-id metadata route is `api/images/v5/bulk`, in its own route table: the + contiguous literals at `sep-literals.py around 39485` are `api/images/`, `…/v1/cheer`, + `…/v1/deletesaved`, `…/v1/listsaved`, `…/v1/modifyaccessibility`, `…/v1/sendlink`, `…/v1/slideshow`, + `…/v2/named`, `…/v4/uploadsaved`, `…/v5/bulk`, `…/v5/cheered/bulk`, `…/v6`. Every other photo + route the client uses is path-built off `api/images/`. + +What the 2026-10-02 16:16 session did, in order (client-side `[Net]` lines, which are a prefix on +`HTTPManager.SendRequest` and therefore log EVERY request, in send order): + + Get /chat/thread/1/message?messageCount=8&mode=2&referenceMessageId=1 + Get /api/images/v5/player/2?sort=0 -> the sender's own photo roll + Get /api/images/v5/bulk?ids=-1 -> 404: the route does not exist here + [ShareCamera](Clone) -> photo taken + Post /api/images/v4/uploadsaved -> {ImageName:f91f6f3b-….png} + Get /api/images/v6?name=f91f6f3b-….png -> the metadata, SavedImageId 17 + Get /chat/thread/1?messageCount=50 (x2) + Post /chat/thread/1 -> {"Type":2,"Version":1,"Data":""} + +So the client uploads the photo, asks for its metadata, and then posts a message whose reference is +`0` anyway - every photo message this server has ever received says `` (96 occurrences in +`Persistent/Logs/player-data-debug.log` are 8 stored messages read back through the log's field +aliases). `0` is the default of a `long`, not an id: it resolves to nothing, both clients paint the +missing-texture placeholder, and the v6 response that carries the real id is only used for the photo +roll's own UI. + +### The two repairs + +* `GET /api/images/v5/bulk?ids=…` now exists (`ImagesAPI.GetBulkImageMetadata`). `ids` is repeatable + and each value may be a comma-separated list, matching the reference server's spelling. An id with + no record, or one that is not public, is **absent** from the answer rather than an error, so the + list can be shorter than the request (the reference documents exactly this). The member order is + the reference's live capture (`Id`, `Type`, then the rest raw) with the alias spellings + (`SavedImageId`, `SavedImageType`, `ClubId`) APPENDED rather than interleaved: a positional reader + stops before them, a name-looking reader finds every name it asks for. With the route missing, + every such call was a 404 and a correct id could not be turned into the `ImageName` the photo is + fetched by. +* `ThreadAPI.AddMessageToThread` now reseats an unresolvable reference to the image the sender was + attaching (`ChatPhotoMessages` + `ResolvePhotoPlaceholder`). The rewrite happens only when the + envelope's type marker is 2 and the reference is `0`/negative/empty. A live id is copied through + byte for byte, a text message is never touched, and a placeholder with nothing attributed behind + it is stored as sent - a wrong photo would be worse than none. + +### 17:41 - a photo PICKED out of the roll was still grey, and why + +The first retest picked an existing photo instead of taking one. The stored contents were still +``, and the running local server already had everything above (`v5/bulk?ids=7` answered +200): the rewrite only looked at the sender's **newest upload inside 15 minutes**, and a picked +photo was uploaded on 2026-09-08. The picker path never uploads anything, so it had nothing to +resolve to - and it turns out the client posts `` on that path too. + +What the picker path leaves behind is a different trace, one request earlier in the log: + + Get /api/images/v5/player/2?sort=0 -> the roll + … thumbnails … + Get /api/images/v6?name=08f5465c-….png -> the PICKED image's metadata (SavedImageId 7) + Post /chat/thread/1 -> {"Type":2,…,"Data":""} + +That `v6`-by-name lookup is the last images request before the message on BOTH paths (a fresh +capture asks for the new name right after `uploadsaved`), and it names exactly the image being +attached. So the server now records it: `RecentImageReferences` keeps one `(account, image id, +timestamp)` per account, written by `v6` and by `uploadsaved`, read by the chat post, with a +10-minute window. The chat rewrite prefers that attribution and keeps the 15-minute upload window +only as a fallback for a session where the metadata call did not arrive. + +Both sides of the conversation read the same stored row, so the repair reaches the sender (whose +client re-reads the thread and the `ChatMessageReceived` push straight after posting) and the +receiver (who resolves the stored id through `v5/bulk` -> `ImageName` -> `/images/{name}`). Note +what the 17:41 log also shows: for `` the client asks for NOTHING - the placeholder is +painted without a request - so a stored `0` can never resolve, and the id has to be right in the +row. + +### Retest + +1. STOP the running server first - it holds `Server/bin/Debug/net9.0` open, so a rebuild cannot land + under it. Then `dotnet run --no-launch-profile -c Debug` from `Server/` (or build, then start the + new `Server.exe`). +2. Send a photo, either picked from the roll or freshly captured - both paths are covered now. +3. The console prints `[ChatPhoto] sender=2 posted a placeholder photo reference; resolved it to + saved image N from …`; the stored contents are ``. +4. The message should render the photo in the sender's client on its next thread refresh, and in the + other account's client when it opens the thread. +5. In the client log, a `Get /api/images/v5/bulk?ids=N` (or a by-id fetch) should answer 200 - it + 404'd before. If the photo still shows a placeholder, what the client requests for the stored id + is the next thing to read: `[Net]` lines carry the request, and a non-public image is absent from + the answer by design. + +Confirmed 2026-10-02 17:54, local server, photo picked out of the roll: the client asked +`v6?name=08f5465c-….png`, posted `{"Type":2,"Version":1,"Data":""}`, and the +`ChatMessageReceived` push it got back carried +`{"Type":2,"Version":1,"Data":""}` - `08f5465c-….png` is image 7. The photo rendered in +the sender's client. What has NOT been exercised yet is the receiving side: the sender's client can +paint from the thumbnail it already fetched, so only the other account proves the stored id resolves +end to end. + +Verified: `dotnet test ServerTests/Server.Tests.csproj -o artifacts/chat-image-run` **816 passed / 4 +failed, 820 total** - the four are the in-flight catalog/storefront/equipment capture-count failures +(`StorefrontRewardConfigTests`, `DorkNetCatalogTests` x2, `AdminGiftSkinTests`), none of them chat. +The photo repair is verified by hand rather than pinned in the suite: the 17:54 client run above is +the evidence, and no photo test class ships (there was never one that earned its place). The routes +were also curled against a live local server: `v5/bulk?ids=14` -> the metadata, `v5/bulk?ids=-1` -> +`[]` at 200, `v5/bulk?ids=14,13&ids=999` -> request order with the unknown omitted. + +Note the server has to be STOPPED before a rebuild: a running `Server.exe` holds +`Server/bin/Debug/net9.0` open and every rebuild after that silently keeps the old bytes there (the +2026-10-02 17:41 retest ran against a binary built at 17:35, which had `v5/bulk` but not the +attribution). + +## 2026-10-03 - invites: the spelling the client actually posts, and accepting them + +The game invite - pressing Invite on a friend, "come to where I am" - is in the client's own route +table, in the contiguous `matchmake/*` block (literals `#46558`-`#46568`): + + matchmake/chatinvite/{0}/{1} matchmake/club/{0} matchmake/code/{0}/{1} + matchmake/dorm matchmake/event/{0} matchmake/instance/{0} + matchmake/invite/{0} matchmake/none matchmake/player/{0} + matchmake/room/{0} matchmake/room/{0}/{1} + +Those are paths on the matchmaking host, which this server fakes as a `/matchmaking` prefix +(`/matchmake/dorm` is served at `/matchmaking/matchmake/dorm` - visible in every client log). + +**The spelling the client actually posts is the bare form one.** The 2026-10-02 client log +(`BepInEx/LogOutput.log`, lines 1356 and 1463 - one per Invite press) carries exactly: + + [Net] Post https://:3030/matchmaking/invite + +with a form body. No path-shaped send appears anywhere in the log, so the earlier guess that the +client posts `matchmake/invite/{playerId}` was wrong. + +That matters because the `matchmake/invite/{0}` in the table above is NOT the send spelling. It sits +in the middle of the contiguous JOIN family (`matchmake/instance/{0}` before it, +`matchmake/player/{0}` after it) and carries one path parameter like its neighbours, so it is the +accept-by-invite-id spelling: "join the instance the invite with this id names". Serving it as a +send would invite whatever account happened to hold the same number as an invite's message id - +someone unrelated. The send surface is therefore exactly: + + POST /matchmaking/invite (form; the one provably posted) + POST /matchmaking/invite/{playerId} (path form, for a build that carries the target there) + +`POST /matchmaking/matchmake/invite/{playerId}` was registered as a send in the previous session and +has been REMOVED: that path is now the accept route below. + +Two more things were fixed on the way, both from the reference's own documentation of the same +route: `roomInstanceId` is OPTIONAL there (its schema says "the caller's room instance... resolves +the invite's RoomId"), while this server's binding made a missing instance a refusal before the +invite was even attempted. It now falls back to the caller's OWN presence row - the same row +`InvitePlayer` already insists the inviter be live in, so the fallback can only name an instance the +caller is really standing in - and refuses by name only when the caller has no instance at all. The +fields are also read the way this codebase already reads club invites: path value, form field, JSON +body or query string, under `playerId` or `invitedPlayerId`, because the client's exact body is not +observable from the server. + +Every invite now prints one line naming exactly what arrived, and that line is the thing to read on +the next client run - it pins the spelling this build really uses: + + [Invite] POST /matchmaking/invite from account 5: target=2, instance=20790 + (roomInstanceId sent as ); fields read: . + +Two neighbouring routes from the same literal table were missing as well and are now served: + +* `settings/partyinvite` -> `GET /api/settings/partyinvite`, answered as the BARE single-key object + the reference serves (`{"InviteLinkLifetimeInMinutes":60}`) - nothing here stores invite links, so + the link lives in the client and this only reports its lifetime; +* `api/offlineinvite/v1/send` -> `POST /api/offlineinvite/v1/send`. An offline invite is the same + stored game invite: `Notify.SendMessage` stores the row either way and an offline target simply + gets no hub frame, reading the message when it next signs in. + +### Accepting: both join spellings are served now + +The client's accept does not reach the network yet (live finding below), but both spellings it could +use are registered and behave as joins authored by the invite: + +* `POST /matchmaking/matchmake/invite/{inviteId}` -> `AcceptInviteByMessageId`. `inviteId` is the + stored `GameInvite` message the hub pushed; the row must be addressed to the caller and its `Data` + must name a live instance. The sender must still be ONLINE in that exact instance - an invite is + not a blanket key - so: not addressed to the caller -> `RoomInviteExpired`, the caller's own row + -> `ChatMessageNotAnInvite`, the sender signed off -> `PlayerNotOnline`, the sender moved on -> + `RoomInviteExpired`, the instance is gone -> `RoomIsNotActive`. +* `POST /matchmaking/matchmake/chatinvite/{threadId}/{messageId}` -> `MatchmakeChatInvite`, the chat + party acceptance. A stored message in the caller's thread resolves the inviter from its sender + (the caller's own message -> `ChatMessageNotAnInvite`; older than `InviteLinkLifetimeInMinutes` = + 60 -> `RoomInviteExpired`). An id this server never stored falls back to the thread's single other + member. A thread the caller is not in, or one with no unique other member -> + `ChatPartyInviteNotFound`. + +Both funnel into `AcceptInviteIntoInstanceAsync`: bans outrank the invite (`Banned`, +`BannedFromRoom`), a pending-warning bounce is honoured, the caller's presence row is moved into the +inviter's instance and a `MatchResponse` with `errorCode: Success` and the room instance is +returned. Each step logs `[Invite] accept:` / `[Invite] accepted:` / `[Invite] accept refused:` so +the next client run shows exactly which spelling arrived and what it resolved to. + +### Live findings - the two invite surfaces block in different places + +After the invite above was pushed and rendered (Notification #111, `handler failures: 0`), the server +saw NO accept request of any spelling - no `matchmake/instance/20790`, no `matchmake/invite/...`, +nothing. The Unity log (`Player.log`) shows why: `ArgumentOutOfRangeException: Index was out of +range` thrown from `WatchNotificationsModel.KHNPLDJLMEA(HashSet\`1, CKPNHJPJAGB+KINNNKACEEA)` - the +watch notification LIST build (twice) - and `[Error] Failed to perform action. : Spam Alert!` with +the stack `WatchNotificationCardModel.OnPrimaryButtonClicked()` -> +`OHIKJCLKIID:JDJCONMMAHM(String)`. So the button press is refused inside the client's notification +model; the server routes above are ready for whenever a client run finally posts an accept. + +The 2026-10-03 20:15 run pinned down a second, different surface: the **chat party invite**. The +sender's client posted the chat message `{"Type":1,"Version":1,"Data":"SmartCheetah985... sent a +party invite!"}` (Type 1 = party invite), and the server forwarded it verbatim +(`Notification #25 Id=ChatMessageReceived`). The receiver's client then fetched +`GET /chat/settings/partyinvite` - TWICE, at 20:15:45 and 20:15:49 - and the server answered +**404**, because only the `/api/settings/partyinvite` prefix was registered. Right after that, +`PartyInviteChatMessageModel.HasPartyInviteExpired()` threw `An error occurred` in a task +continuation (`MMBGIGIFEMJ:DMNKANIOLMG`), every time the card model recomputed - so the party invite +card could not be acted on. The sender-side flow reads the same setting from the other prefix +(`GET /api/settings/partyinvite` -> 200 at 20:10:03), which is why only the receiver broke. + +That chat-host spelling is now served (`[HttpGet("~/chat/settings/partyinvite")]` beside the api +one, same bare single-key object), so the card's own expiry model has the lifetime it asks for; the +test pins both templates. In the same run the game invite itself was delivered and then +deleted/dismissed by the client (`POST /api/messages/v3/delete` 2 s later) with no join request, so +the watch path still needs a run where accept is actually pressed while the server console is +watched. + +The 2026-10-03 20:22-20:31 run (both accounts connected at once) added the next missing piece: the +OTHER account's client posted `POST /api/messages/v1/sendMultiple` while an invite was being sorted +out and read a **404** - the only non-200 that client hit all run. Account 2 sent both invites +(game invite message #10 and the Type-1 party invite chat message #5) and the server pushed each to +account 5 (`[Notify] message 10 (type GameInvite) pushed to account 5 on 1 connection(s)`, `Chat +notify: thread=2, message=5, sender=2, recipients=[2,5]`), but no accept request of any spelling +arrived from either client. `sendMultiple` is the reference's bulk message send - real JSON +(`{"ToPlayerIds":[...],"Type":n,"Data":"..."}`), one stored row and one `MessageReceived` frame per +unique recipient - and is now served (`MessagesFanoutTests` pins the template, the per-recipient +rows and the empty-id refusal). + +Client disassembly (`global-metadata.interop.dat` + GameAssembly through `tools/AugustInterop`, +which reads this build's metadata once pointed at it) shows the party-invite button is gated client +side in `RRUI.Data.PartyInviteChatMessageModel`: `CanAcceptPartyInvite` is +`!HasPartyInviteExpired()` AND a flag off the sender's `BaseAccountModel` presence, and +`get_CanJoinParty` additionally requires `BaseAccountModel::get_IsJoinableFromChatPartyInvite()` - +which compares the sender's presence entry against `Matchmaking::AGDPIAALDDJ()` (the local player's +own matchmaking value). Those read the account/presence models, not an invite route, so a missing or +mismatched presence value on the sender is enough to grey the card out before any request is made. + +Verified 2026-10-03 (server restarted on the new build; all requests over `https://127.0.0.1:3030`): + +| Request | Result | +| --- | --- | +| `POST /matchmaking/matchmake/invite/8` (no token) | **401** - the accept route exists | +| `POST /matchmaking/matchmake/chatinvite/1/2` (no token) | 401 | +| `POST /matchmaking/matchmake/notaroute` (no token) | **404** - unknown sibling, so the 401s mean registration | +| `POST /matchmaking/matchmake/instance/20800` (no token) | 401 - the instance join the accept is expected to reach | +| `POST /matchmaking/invite` / `invite/43` (no token) | 401 | +| `POST /api/offlineinvite/v1/send` (no token) | 401 | +| `GET /api/settings/partyinvite` | 200, `{"InviteLinkLifetimeInMinutes":60}` | +| `GET /chat/settings/partyinvite` | 200, same object - it 404'd in the 20:15 run and the receiver's card then threw | +| `POST /api/messages/v1/sendMultiple` (no token) | **401** - the route exists (a sibling spelling answers 404) | + +What the tests pin: `InviteFlowTests` (now 18 cases) covers every registered spelling by reflection +(the `matchmake/*` path asserted as ACCEPT, not send), the path-shaped invite resolving the caller's +own instance into a `GameInvite` message, the refusal when there is no instance anywhere, the +offline variant storing the same message for a target with no presence, the party-invite settings +shape, accept-by-message-id success moving the caller's presence into the inviter's instance, each +invite-by-id refusal by name (foreign, self-sent, sender offline, sender moved, instance gone), the +chat accept resolving the inviter from a stored message, its fallback to the thread's other member, +and the expired/non-member refusals. `MessagesFanoutTests` (3 cases) pins the bulk +`sendMultiple` route, its one-row-per-unique-recipient storage and its empty-id refusal. Whole +suite: **830 passed / 4 failed, 834 total** - the same four in-flight catalog/storefront/equipment +failures, none of them invites. + +### The invitee's client fetches the room the invite names - and the room answered 404 (the 21:04 run) + +The 21:04 run (the one with friend-side logs in `Downloads/friend-logs`) added the missing half of +the invite. Both accounts were connected; the friend (account 5) opened account 2's profile - its +client fetched `accounts/account/2/bio`, `rooms/rooms/ownedby/2`, `api/images/v5/player/2`, +`api/customAvatarItems/v2/fromCreator/2` and `accounts/accountprivacysettings/2` in one second - and +then posted the bare-spelling invite: + +``` +[Invite] POST /matchmaking/invite from account 5: target=2, instance=20800 (roomInstanceId sent as 20800); fields read: action=Invite; controller=MatchmakingAPI; playerId=2; roomInstanceId=20800; joinRoomInviteMode=None. +[Notify] message 12 (type GameInvite) pushed to account 2 on 1 connection(s). +``` + +The invitee's client reacted **80 ms** later, and what it asked for is the room the notification +names (`Msg.RoomId: 101`, `Msg.Data: "20800"`): + +| Request as account 2, right after the invite | Answer | +| --- | --- | +| `GET /rooms/rooms/101?include=5421&unityAssetTarget=0&unityAssetVersion=1` | **404** | +| `GET /api/inventions/v1/room?id=101` | 200 `[]` (correct - the dorm has no inventions) | +| `GET /rooms/rooms/bulk?id=101` | 200 **`[]`** | +| `GET /images/5cea039d-...png?width=256&cropSquare=1` (the inviter's avatar) | 200 | + +Room 101 is the friend's **private dorm** (`IsDorm`, `Accessibility = Private`, creator 5), and the +old rule showed a private room to its creator and its co-owners only - so the one player who had +just been invited into it could not fetch it, and the join prompt had no room card to build from. +Everything that client sent afterwards was heartbeats and carousel fetches, its only deliberate +call being `POST /api/messages/v3/delete` at 21:05:12; no accept of any spelling was posted - the +same dead end as every earlier run, now with a concrete reason for the prompt to have nothing to +act on. + +The rule now follows the invite: `RoomUtils.CanSeeRoom` also answers a private room to an account +holding a **live** game invite into it (`RoomUtils.HasLiveGameInvite` - a stored +`MessageType.GameInvite` message addressed to them whose sender is online in an instance of that +room), so `rooms/rooms/{id}` and `rooms/rooms/bulk?id=` both serve it to the invitee. A stranger's +answer is unchanged, and an inviter who has left stops granting it (nothing could be placed there +anyway - `AcceptInviteIntoInstanceAsync` refuses the same way). +`InviteFlowTests.InviteAlsoMakesThePrivateRoomFetchableForTheInviteeOnly` pins all three states +through the real handlers. + +The friend's side of the run carried two more server-shape failures, neither attributed to a route +yet: `Exception: Deserialization returned null` -> `Malformed Response: '{}'` at 21:05:01, and +`GAOFLKNLIAI: expected:'{', actual:'[', at offset:0` -> `Malformed Response: '[]'` at 21:05:09 while +that client was in its `DOWNLOADING_DETAILS` room-load phase (the `[]` bodies that run are the +empty list answers - inventions, inbox, carousel lists - and the `{}` bodies are the datacollection +acks). + +And the `ArgumentException: Error message cannot be null` the friend's client logged at 21:05:18 is +ours. It is an unobserved-task exception whose stack runs +`FPNLMMCEKLO+NHCIKGEPNNL.CPOPMFILHCF(Task)` -> `FIKNOBAKBJH.BEJNCICKNOM(T)` -> +`ANHPEPEMBNM+<>c__3.DHFNHECJGBH(HttpResult)` -> `LLDACAIIDPN.EGMGFIBBBOM(string)` -> +`FIKNOBAKBJH.NEPFPOCMJPJ(string)`: the client's response continuation for a request this server +answered **200**, taking its error branch and building the exception from a null message. Every +success envelope serialized `"error": null`; the reference spells `"error": ""`. `Success` now +defaults `error` to `string.Empty` (a refusal still carries its reason), pinned by +`RecNetShapeConformanceTests.Success_SpellsItsAbsentErrorAsAnEmptyString`, with the `RoomSaveTests` +envelope assertion moved to the reference shape. + +What an invite looks like client-side, from this build's metadata (`.external/sep-decomp`, the +interop stubs carry every name even where the bodies do not): the outer notification `Id` `"2"` is +`HCACKLIOLEM.PHKAIMJMIML.MessageReceived`; the inner `Msg.Type` is `MOHGFNBDKJI` (`GameInvite = 0`, +`FriendStatusOnline = 20` - exactly the server's `MessageType`); and the notification's action +dispatcher `ILFPMMHFHOE` has one payload struct per action of `DBANOMFCFBF` (`GoToRoom`, +`ShowRoomDetails`, `ShowAccountDetails`, `ShowMessage`, `AcceptGameInvite`, ...) with +`AcceptGameInviteData { FromAccountId }`. A game invite is therefore a button action whose payload +names the inviter, which is the next thing to check if a run with the room now visible still posts +no accept. + +### The invitee's client, in its own log: the invite arrives, the press posts nothing (the 22:21 run) + +The 22:21 run finally came with the **invitee's** client logs. `Player.log` under +`AppData/LocalLow/Against Gravity/Rec Room/` (mtime 22:22, engine path +`Downloads/2023-09-06T06-53-47Z/RecRoom_Data`, an RTX 4060 - `Initial Room Load +'@TestPlayer's Dorm' (99.109) [instance 20802]` at 22:21:08) is account 2's install, and so is the +`BepInEx/LogOutput.log` beside it. The two attached AMD-Radeon logs are the *other* install +(`Downloads/2023-09-06T06-53-47Z (2)`, the inviter): its `[Net]` lines carry both +`Post .../matchmaking/invite` calls, which is what the server logged as account 5 both times. + +Condensed, in order, everything the invitee's client sent around the invite: + +| Step | Request | +| --- | --- | +| card renders | `GET /images/5cea039d-....png?width=128` (the inviter's avatar), `POST /datacollection/data/heartbeat` | +| the room it names | `GET /api/inventions/v1/room?id=101` -> 200 `[]` | +| then | `GET .../images/5cea039d-....png?width=256&cropSquare=1`, `PUT /playersettings/playersettings` x2 | +| end of the attempt | `POST /api/messages/v3/delete` - the notification is dropped | + +No `rooms/rooms/101`, no `matchmake/*`, no accept: the press produced no request at all, and the +second invite (message 15, 22:22:02) repeated the same sequence. Both installs also log +`[Meta] this install's metadata was patched for 'https://localhost:3030' but Connection:ServerUrl is +'https://:3030' ...` at startup. That line is **cosmetic**: the plugin rewrites the +address before it leaves the client (`[Connection] Redirected nameserver request to the configured +Radicade server.`), and every `[Net]` URL in both logs reads `:3030`, so it is not the +reason a request is missing. + +### The crash behind the press is ours, and a null `error` was only half of it + +The invitee's Player.log logs `ArgumentException: Error message cannot be null` at 22:21:11 - after +this server had been restarted with the success-envelope fix - from +`ANHPEPEMBNM+<>c__3.DHFNHECJGBH(OAFAKLEKFGI)` -> `LLDACAIIDPN.EGMGFIBBBOM(string)` -> +`FIKNOBAKBJH.NEPFPOCMJPJ(string)`: its response continuation builds the exception from the body's +`error` field. A null `error` on a 200 was one way to feed it a null, but not the only one - a +response with **no body at all** hands the same path an empty string and gets the same crash, inside +the caller's task continuation. That is why the caller does not fail visibly: it stops. Every 4xx +this server served during that window was bodyless: + +| Request (from either client, 22:20-22:22) | Answer that run | +| --- | --- | +| `GET /clubs/club/home/me` (no home club, a deliberate 404) | 404, `len=0` | +| `GET /econ/api/subscriptionseasons/v1/seasons/current` (deliberate 404) | 404, `len=0` | +| `GET /rooms/rooms/101?include=5421...` (private dorm, answered to a caller with no live invite) | 404, `len=0` | +| `GET /rooms/rooms/99?include=5421...` (the same rule, the other dorm) | 404, `len=0` | + +The reference never answers a failure without a body: its route failures are +`{ success: false, error: 'No such room' }` and its global handlers spell the status +(`{ success: false, error: { message: 'not found' } }`). The 2023 client's `error` is a string - the +same DTO as the success envelope - so this server now answers with the shape this build can bind. + +### Every failure carries the envelope now (fix) + +`RecNet/Responses/Failure.cs` is the failure twin of `Success`: `success: false` plus a **non-null, +non-empty** `error` string (even `Failure("")` spells *request failed*). +`Middleware/FailureEnvelopeMiddleware.cs` fills it in for the whole pipeline: any 4xx/5xx that +reached the client with `Content-Length: 0` and no content type gets +`{"success":false,"error":""}` under its own status, with the wording spelled per status +(`404 -> "not found"`, `401 -> "unauthorized"`, `500 -> "internal server error"`, ...). Answers that +already describe themselves (a written body, a content type, a streamed response), redirects, the +101 upgrade the notify hub takes over and every non-error status are left exactly as they are. + +It is registered just before `UseAuthentication()`, so it wraps authentication, authorization and +every controller: a bare `NotFound()` from any handler, a `[Authorize]` 401 and a path no route +matches are all covered. It also sits *inside* the request logger, so the console line that used to +read `len=0` now prints the envelope it counted. Live, on the restarted server (PID 35348, log +`artifacts/server-failure-envelope-run.log`): + +``` +GET /econ/api/subscriptionseasons/v1/seasons/current -> 404 {"success":false,"error":"not found"} +GET /definitely/not/here -> 404 {"success":false,"error":"not found"} +POST /matchmaking/matchmake/instance/20800 -> 401 {"success":false,"error":"unauthorized"} +GET /api/settings/partyinvite -> 200 {"InviteLinkLifetimeInMinutes":60} +``` + +`FailureEnvelopeTests` (13 cases) pins the envelope, the per-status wording and the pass-through +rules (`AnAnswerThatAlreadyHasABodyIsLeftAlone`, `SuccessfulAndEmptyAnswersAreLeftAlone`, +`UpgradesAndRedirectsAreLeftAlone`). Whole suite after the change: **838 passed / 9 failed, 847 +total** - the invite and shape suites are green, and the nine failures are the in-flight +catalog/storefront/equipment counts being edited (`EquipmentInventoryTests` expects 87 and saw 57, +and its neighbours), none of them touching this pipeline. + +What this does not claim: the press may still post nothing. In this run the invitee's client never +even asked for the room (`rooms/rooms/101`), so if `[Invite] accept:` is still absent after the +envelope fix, the next lever is the client's own button path rather than another server shape - the +plugin already watches the notification channels, and the action dispatcher noted above +(`AcceptGameInviteData { FromAccountId }`) is where a press would have to appear. + +### Retest + +1. The local server was restarted on this build after the fixes above (PID 35348, log + `artifacts/server-failure-envelope-run.log`), so the routes are live; stop and start it again + after any further rebuild (a running `Server.exe` serves the old bytes). +2. A 4xx in that log now prints the answer the client received (`len=37` and a JSON body) where it + used to print `len=0`. If a 4xx there still reads `len=0`, a response escaped the pipeline - + that is the one to fix next, whatever the client then does. +3. In the client's watch, press Invite on a friend who is in another account's client. The server + console should print `[Invite] POST /matchmaking/invite from account ...` - the bare-spelling + proof, again. The invitee's client should follow with `GET /rooms/rooms/{roomId}?include=5421...` + answering **200** (it was 404 for a private room before), with `"error": ""` on its successes. +4. The friend's client shows the join prompt (watch) or the party-invite card (chat) and presses + it. Read the server console for `[Invite] accept:` - that line is the accept spelling this build + really posts. The party-invite card needs the `chat/settings/partyinvite` fix above; without it + its model throws on the missing lifetime and the press never becomes a request. +5. If no `[Invite] accept:` line appears, the block is client-side, and the logs now say which side + of it: the invitee's `Player.log` for a fresh `ArgumentException: Error message cannot be null` + (a bodyless 4xx that escaped step 2) or the client's own button path - as observed 2026-10-02 + (`Spam Alert!`, `WatchNotificationsModel` throw, and the 22:21 run where a press produced + `PUT /playersettings/playersettings` twice and `POST /api/messages/v3/delete`, nothing else). + That is the thing to chase, not a server 404. +6. If a line does appear, `[Invite] accepted:` or `[Invite] accept refused: ... (code)` is the whole + outcome: refusals come as client-readable JoinStatus codes, and a refusal naming an invite id the + caller does not own means the client's id and the stored message diverged (the `[Invite]` send + line and the notification push carry the ids to compare). +## 2026-10-08 19:50 - announcements ride the name the client registers, and the new-update sound hack is gone + +Two threads, one root cause. + +**"Sending an announcement does nothing."** The panel's announcement branch pushed the numeric enum +`AnnouncementText` (26). The client's dispatcher delivers a frame only when its Id EQUALS a registered +key, and the registration probe (this file's own dump, taken at login) shows the announcement handlers +registered under the literal names - `"AnnouncementUpdate"` and `"AnnouncementDelete"` - with no +numeric id for either. `AnnouncementText` (26) is the phantom id this doc already named for the game +update: the frame parses, matches nothing, and is dropped, so a saved announcement only ever appeared +on the next login through the `api/announcement/v1/get` fetch. The push now goes out under +`"AnnouncementUpdate"`, the way the community board (`"CommunityBoardUpdate"`) and the progression +frame already do. Server-side, `AnnouncementBroadcast.Update(...)` holds the literal and +`AdminPanelAPI.UpdateAnnouncements` calls it; `AnnouncementWireTests` pins both the id and the payload. + +**The new-update alert is removed from the client.** `AnnouncementSoundAlert.cs`, its +`Assets/AnnounceNewUpdate.wav` (1.3 MB, embedded) and the `` that shipped it are +gone, and the call in `Plugin.cs` with them - so the plugin no longer watches the hub for the +game-update frame or carries a sound at all. The DLL drops from ~1.7 MB to 140 KB. The panel still +sends the game-update frame id-only; nothing on the client answers it yet. That is the next piece of +work, and the removed hack was in the way of doing it properly - it played a stand-in sound off the +same frame rather than letting the game show its own update prompt. + +Retest: send a regular announcement and look for `[Realtime] Notification #N Id="AnnouncementUpdate"` +in the client log, followed by the announcement appearing without a relog. The client DLL in +`BepInEx/plugins` is rebuilt and deployed; the previous one is kept beside it as +`RadicadeSeptemberClient.dll.bak-announce-wav`. + +## 2026-10-08 20:15 - the game-update prompt is the version check, not a notification (21 is not it) + +The 2026-10-01 entry guessed the alert was a client-side sound the server could only ever *request*. +That was half right and the wrong half: the client does have a game-update path, it just is not a +notification. + +`ModerationUpdateRequired` (21) is **not** the id. The dispatcher probe's login dump lists every key +the client registers a handler under, and the numeric set is exactly + +``` +1 2 3 5 6 20 22 23 25 30 31 32 50 60 61 62 70 71 80 81 82 83 84 96 100 110 120 121 +``` + +- no 21 (and no 24, 26, 27, 40, 85, 90, 95). A frame under 21 parses, matches nothing and is dropped, +exactly like 26 and 27. The only game-update-relevant key in that table is the **string** name +`"AppVersionUpdate"`, and a string key is only ever matched by an identical string. + +What actually drives the prompt is REST. Every boot the client asks + +``` +[Net] Get https://ns.radicade.app/api/versioncheck/v4?v=20230831&p=0&pid=4 +``` + +and the answer carries `VersionStatus` plus `UpdateNotificationStage`. The server was answering +`ValidForPlay` / stage `0` unconditionally, which is why no build ever showed an update prompt - and +why a client-side sound was the only thing that could ever be made to happen. The client's own string +table has exactly the status and stage members this implies: + +``` +GFBJICOODHE.ValidForPlay.UpdateRequired.CHPBKJBGLIB.Warn.Prompt.Require.FHCAEIBGBHF +``` + +- `ValidForPlay` / `UpdateRequired` (and a third member, matching the three-value `VersionCheckStatus` + the server already models as `ValidForPlay = 0, UpdateRequired = 1, ValidForMenu = 2`), then the + escalation stage `Warn / Prompt / Require` and one more member beyond it. The client also carries + the string `Update Required` in its UI string table. + +So a Game Update is now the announcement row plus the answer it produces: + +* `GameUpdateVersionCheck` (`Server/RecNet/VersionCheck/`) reads the panel's `Announcement` config for + a live, unexpired `"Game Update"` row. Same expiry rule as the announcement list - a row with no + parsable `expires_at` never expires, so the prompt is taken down by deleting the announcement. +* `api/versioncheck/v4` answers `UpdateRequired` (+ stage `1`) while one is live, `ValidForPlay` + otherwise. The `ValidVersion`/key set is untouched - it stays exactly the five keys the client's + strict lookups need. Deliberately stage `1`, not `0`: `0` is what a healthy boot already gets, so + it is the one value that may also mean "nothing to show". +* The panel's Game Update send now pushes the id-only frame under `"AppVersionUpdate"` instead of + `AnnouncementGameUpdate` (27) - the re-check request for a client that is already connected, so an + announcement lands without a relog. + +Why 21 was a reasonable guess worth checking: `ModerationUpdateRequired` *sounds* like "the moderation +state changed, go re-read it", which is the same shape of request. It is still not registered - which +also means the two places the server pushes 21 today (`ReleaseModerationHoldAsync`, +`DeliverWarningAsync`) are reaching nothing. The warning prompt survives on the 22 that follows it. + +Still unknown, and the next launch answers it: what payload the `"AppVersionUpdate"` handler binds. +The probe now prints the bound type and its members for that key (it already did for progressions), so +one login log settles whether the empty object is enough or the frame has to carry the version. If the +prompt does not appear but the version check *is* answering `UpdateRequired`, that is the thing to +read. If it appears but is too soft or too harsh, the lever is `GameUpdateVersionCheck.UpdateStage` +(climb the stage) or `VersionStatus` (`ValidForMenu` instead of `UpdateRequired`). + +Verified: `dotnet test ServerTests/Server.Tests.csproj` on the affected filter (GameUpdate, +VersionCheck, Announcement, CommunityBoard, WatchWire) **54 passed / 0 failed**, including the new +`GameUpdateVersionCheckTests` and the untouched `VersionCheck_AllowsPlay_WhenNoGameUpdateIsPublished` +shape test. Client DLL rebuilt (140 KB, still no embedded sound) and deployed. + +## 2026-10-16 - the named services: one host each, and the rewrite that keeps their routes + +The nameserver no longer hands the client one host with a path per service +(`https://ns.radicade.app/auth`). Every service is now advertised as a host of its own - +`https://auth.radicade.app`, `https://images.radicade.app`, `https://thorn.radicade.app`, 38 in +all - and the host label is the route the server has always served that service under, so +`auth.radicade.app/` is exactly what `ns.radicade.app/auth/` was. +`DBConfig.ServiceDomain` + `DBConfig.ServiceUrl(name)` build the names; +`2023Patch/RadicadeEacBypass/NameserverRedirect.cs` (`ServicePrefixes`) turns each host back into +that prefix, in the funnel, before any socket is opened. + +Two names are deliberately not their own prefix. `api` stays the bare base, because the client +sends unprefixed calls to it (`api/versioncheck/v4`) and a segment would break every one. `cdn` +keeps `/cdn`, the segment its advertised base has always carried. The stock `cdn.rec.net` entry +sitting above them stays bare, and that is not an inconsistency: the *stock* client asks for bare +blob paths while this one appends to a base that already ended in `/cdn`. + +Everything else is unchanged on purpose. `ns.radicade.app` is still the bootstrap host and still +the base every rewritten request lands on - `Connection:ServerUrl`, the baked metadata bootstrap, +`TLS:AllowedHosts` - so the client never resolves or trusts a `*.radicade.app` service host at +all: the redirect happens in the funnel, before DNS. The URLs the server generates itself +(`DBConfig.ServerUrl` in `Admin*`, `StudioAuthAPI`, `CommunityBoardV2`) keep working for the same +reason. Of the three payload fields that *are* built from `DBConfig.NameServer`, `CDN` and `Images` +still land on the routes they named before - with one wrinkle worth knowing: the `{Images}/img/{name}` +URLs the server puts in photo and webhook payloads collapse to the bare `/images/{name}` route, +because the redirect's `/img/` de-doubling applies to every redirected host, not just `img.rec.net`. +That is the same picture either way: both routes serve the file, and both take the key id from the +`rcsig` the client states, so the signature the anti-cheat reads is unchanged. + +The third field, `WWW`, is the one endpoint here that a *browser* consumes rather than the client +(`ConfigAPI`/`FirstTimeUtils` hand it out as `ShareBaseUrl`, and the server serves those share pages +at `/www/{0}`). The funnel cannot rewrite a browser, so a share link built as +`https://www.radicade.app/{0}` opens the share page only if that host resolves to this server *and* +the server answers it at the root - `www.radicade.app` has to be routed by Host header, or that one +base has to keep the `/www` path it used to carry. Everything else in the list is reached only +through the rewritten client. + +What this costs is any client with no plugin to do the rewrite. The Quest build is +metadata-patched only (`tools/patch-quest-client-metadata.py`, no BepInEx/Harmony port), so it +will now resolve `auth.radicade.app` and the rest itself: it needs DNS for those names, a +certificate they are served under, and a server that routes by Host header before it works again. +Windows is unaffected, and this is the one thing to weigh before deploying. + +The names are one mapping in two places, so the test that guards it reads both. `dotnet test +ServerTests/Server.Tests.csproj`: 1597 tests, 8 failed - and all 8 fail on pristine `10c9035` as +well (catalog/storefront/challenge data shapes, none of them reaching the nameserver), while the +suite went 1577 -> 1597 with the 20 new cases +green. The new `EveryAdvertisedServiceHostLandsOnAServiceRootTheServerServes` takes the advertised +names, runs them through `NameserverRedirect` and checks the segment that comes out against the +server's own route templates, so a name misspelled on either side - `roomieintergrations`, say - +fails here instead of in a client that half-works. `HubRealtimeTests` now derives each advertised +service from its host label and asserts there is no path; `RecNetShapeConformanceTests` asserts +the same four bases it did before, by name. + +Not deployed: the running server keeps answering with the path form until this is pushed and +restarted. Ship the plugin first, then the server - the order is not symmetric. A client running +the new plugin against the *old* `/ns` is fine (its requests stay on `ns.radicade.app`, which the +redirect leaves alone), but an old plugin against the *new* `/ns` resolves `auth.radicade.app` +itself - the failure mode already written up above under "Why a foreign host is fatal": no +rewrite, no certificate on `TLS:AllowedHosts`, and the anti-cheat path takes the session down at +boot. +## 2026-10-16 - the notify service is advertised under its full name + +The notify service is no longer named `notify.radicade.app`. The nameserver advertises it as +`notifications.radicade.app`, and the *route* did not move: the client's redirect still lands it on +`/notify`, so the hub socket is still `wss://ns.radicade.app/notify/hub/v1?...`, the health root is +still `/notify`, and `NotifyHubRoutes` is unchanged. This renames the advertised name only, which is +why nothing in the request path moved. + +It is the first name in the set that is deliberately not its own route. Every other host label is +the segment the server serves that service under, and +`EveryAdvertisedServiceHostLandsOnAServiceRootTheServerServes` enforced that as an equality +(`Assert.Equal(label, segment)`); that assertion now reads `DBConfig.ServiceRoute(label)`, and +`DBConfig.ServiceRouteOverrides` holds the one entry - `notifications` -> `notify` - so the +divergence is a fact the server, the redirect and the test all read, rather than a hole in the +check. It is not weaker for it: pointing the redirect entry at `/notifications` instead of `/notify` +fails the test (verified by doing it), and so does advertising a label the server has no route for. + +`notify.radicade.app` was retired, not aliased - the redirect map has no entry for it any more, so a +request still naming that host is left exactly as it is, which `NameserverRedirectTests` asserts. +The consequence is worth stating plainly: a client carrying this plugin against a server that still +advertises the old name sends its hub socket to a host with no DNS record, which is the same stall +this service's plugin half was written for. Ship the server and the plugin together; there is no +window in which a new plugin and an old `/ns` both work. + +Nothing infra changed, and nothing needs to. `notify.radicade.app` never had a DNS record either - +public DNS answers only for `ns.radicade.app` - because the redirect happens in +the funnel, before DNS: the client never resolves a service host at all. So this rename needs no +Cloudflare record, no Caddy site and no certificate. `www` remains the one name that has to be +routed by Host header, because a browser consumes it. + +Server: `Server/DB/DBConfig.cs` (`Notifications = ServiceUrl("notifications")`, plus +`ServiceRouteOverrides`/`ServiceRoute`). Client: `2023Patch/RadicadeEacBypass/NameserverRedirect.cs`, +one map entry. Tests: `HubRealtimeTests` (both nameserver-set tests now compare through +`ServiceRoute`), `NameserverRedirectTests` (the socket case and the service-host case name +`notifications.radicade.app`, and the retired host is asserted to be left untouched). +`dotnet test ServerTests/Server.Tests.csproj`: 1599 tests, 8 failed - all 8 also fail on pristine +`10c9035` in a clean `git worktree`, and that set is a strict subset of the baseline's 10, the suite +being order-dependent in a couple of unrelated places. The affected classes alone are 190/190. diff --git a/docs/2023-AUGUST-BUILD.md b/docs/2023-AUGUST-BUILD.md new file mode 100644 index 0000000..6c7bc15 --- /dev/null +++ b/docs/2023-AUGUST-BUILD.md @@ -0,0 +1,3944 @@ +# August 2023 client migration — local work, not yet playable + +Target: `C:\RecRoom\2023-08-29T06-53-33Z`. +The version stored in this client's `globalgamemanagers` is **20230825**, +and its Unity version is 2020.3.33f1. + +## Implemented + +- Server default game version is 20230825 (earlier supported versions remain). +- The metadata patcher recognizes this release's shuffled 256-byte header using + a SHA-256 fingerprint, excluding only the two fields it intentionally updates. + Unknown headers still fail closed. +- Literal table offset/size fields are at 0x2C/0xBC; literal data offset/size + fields are at 0xC8/0xC0. All 52,189 literal records fit the original blob. +- Rewrite verification checks unchanged literal contents and all original bytes + outside the literal records and the two edited header fields. +- The patch marker records 20230825 rather than the old hardcoded March version. +- Local development exposes HTTPS on localhost:3030 and HTTP on 127.0.0.1:3031, + and advertises HTTPS service URLs. + +Original client metadata and boot configuration are preserved beside the files +as `.orig`. No March BepInEx files or generated interop assemblies were copied +into the August client. No Git push or remote server deployment was performed. + +## Verification and remaining blocker + +The patcher's real-file verification passes, including a second dry run with +zero changes. Server startup succeeds and the version endpoint returns +ValidVersion=0 / VersionStatus=0. The 38 targeted VersionCheck, Nameserver and +SplitTestCompatibility tests pass. + +Initial startup failures (before the August plugin): + +- HTTP: the client's request constructor throws `Exception: Invalid URI scheme`. +- Direct desktop launch with HTTPS: embedded BestHTTP/BouncyCastle rejects the + local certificate with `user_canceled(90)` / nameserver error `summer`. +- EAC bootstrapper launch: native crash near `Connecting to RecNet`. + +The new `2023Patch/RadicadeAugustClient` project builds against August interop +and replaces the March plugin locally. It installs embedded TLS, nameserver, +image signature key, hub authentication, and August EAC challenge hooks without +March-only UI patches. The old DLL is preserved as +`BepInEx/plugins/RadicadeEacBypass.dll.march-backup`; the old configuration is +preserved as `com.radicade.eacbypass.cfg.before-august`. + +Live verification: August Client 2.0.0 loads without a plugin-load exception, +TLS accepts the configured localhost endpoint, the game fetches the nameserver, +`/api/gameconfigs/v1/all` and `/api/config/v1/amplitude` successfully, and logs +the Steam profile. It then reports **Launch validation failed. Is Rec Room +installed correctly?** Login and room joining remain unverified. No broad +launch-validation or error-suppression patch was installed. + +## Launch-validation root cause and bypass + +Native analysis with the `tools/AugustInterop` disassembler traced the error: + +- `NDIIPBKGGIG::CBNCPLACNIE(15)` is the only call site producing the error + screen; value 15 is `JKBPKDAGEHN.LAUNCH_VALIDATION_FAILED`. +- Its sole caller chain is `NFPDGJNDFOF::GBLLEJBCFGI -> NFPDGJNDFOF::GBAMNAKGEGD`. +- `GBAMNAKGEGD` is the boot validator invoked from `BootSequence::DFCEILLEFOK` + via `ADJOODNBCKG::BEFDPLAELIH`. It resolves an obfuscated static-data file + reference, opens the referenced file, parses its PE headers, and computes a + SHA-256 over selected sections in 0x400-byte chunks on a background thread, + then hard-rejects (`CBNCPLACNIE(15)`) when the hash does not match the + expected value baked into `GameAssembly.dll`'s static data. +- The check is not satisfiable in a patched installation (BepInEx loader + metadata redirect and localhost server wiring), so the plugin now installs + `AugustLaunchValidatorPatch`, a prefix on `ADJOODNBCKG.NFPDGJNDFOF::GBAMNAKGEGD` + that resolves the boot completion promise (`PFDOLDLEIBA.OLKGFACPIPC`, with a + direct handler-list fallback) and skips the native body. It must be + registered via `harmony.PatchAll` in the plugin `Load` method — an earlier + attempt defined the patch class but never registered it, which is why the + error screen still appeared once. + +Rebuild and reinstall `RadicadeAugustClient.dll` after this change; the log +should show `Bypassing native launch validator` before boot proceeds. + +Build validation: August plugin builds with zero warnings/errors. The March +plugin cannot be compiled unchanged against August's renamed types. + +The final client target is `https://localhost:3030`. Start the server with +`dotnet run --project Server/Server.csproj`. For diagnostics, direct desktop +launch uses `RecRoom.exe +forcemode:screen`; it now passes TLS and encounters +the launch-validation blocker described above. + +## BepInEx interop generation fixed + +The installed BepInEx 6.0.0-be.697 initially failed Cpp2IL's magic-number check. +The original `.orig` metadata has the same shuffled header: this is the release's +format, not evidence of download corruption or a URL-patch error. + +`tools/AugustInterop/normalize.cjs` creates a **separate tooling copy** with a +standard v27 header. It also repairs the 305 synthetic `` records, whose +name indices and generic-container indices otherwise break assembly generation. +Only verified module records are repaired. This copy must NEVER replace the +game's `global-metadata.dat`; the native runtime expects the custom format. + +The installed loader reads `global-metadata.interop.dat` for Cpp2IL. Its original +is backed up at `BepInEx/core/BepInEx.Unity.IL2CPP.dll.before-august-interop`. +The loader modification changes only RunCpp2Il's metadata filename literal. + +Verified with the actual August GameAssembly.dll: 42,537 types and 283,224 +methods parsed, native registrations resolved, interop generation completed, +335 files written including the hash and caches, and the live loader logged +`Chainloader startup complete`. Interop generation was first verified with no +plugins; the August connection plugin was then built and installed separately. + +See `tools/AugustInterop/README.md` for reproduction and rollback. + +## August login "code: block" root cause and fix + +Symptom: title screen showed `Cached/Manual Login failed with: Unable to login +(code: block)` right after a `POST /matchmaking/player/login` log line whose +`[Net]` line prints when the request is queued, not when the response lands. + +Server-side truth (server-console.log + PlayerDataDebug): the login POST +actually returned **200** both times. The real 405 came from +`POST /api/PlayerReporting/v1/moderationBlockDetails`, which the server only +mapped as GET; Kestrel answered the POST with 405 Method Not Allowed. The +August login flow runs its moderation-block check inside the login promise, and +a failed check maps to `JKBPKDAGEHN.LOGIN_CODE_BLOCK` - hence "code: block", +which reads like a moderation block but was a route mismatch. + +Fix: added `[HttpPost("v1/moderationBlockDetails"), Authorize] GetBlockDetailsPost()` +in `Server/API/Moderation/PlayerReportingAPI.cs`, delegating to the existing GET +action so both verbs serve the identical body. Verified live: POST now returns +401 without a token (auth layer reached, no more 405) and the GET shape is +unchanged for older clients. Server rebuilt and restarted on 3030/3031. + +Remaining non-fatal August 404s (client continues without them): +`GET /matchmaking/player/qos`, `GET /matchmaking/player/connection-info`, +`GET /chat/thread/party`, `GET /cdn/room/`. RudderStack analytics cannot +resolve its host and self-disables after 15 failures. + +Verified in-game after the fix: `POST /matchmaking/player/login` -> 200, +`POST /matchmaking/player/exclusivelogin` -> 200, +`POST /api/PlayerReporting/v1/moderationBlockDetails` -> 200 (86-byte empty +block-details body), hub WebSocket connected, and the avatar spawned in the +Dorm. The August 2023 build boots fully into the Radicade server. The RSA +neutralizer correctly passes through verification calls signed with non-p1 +keys (observed on a `cdn/sigs/...` fetch). + +## Remaining August 404s fixed + +The four 404s the August client hit after login were implemented, using the +recflare reference server's response shapes where documented: + +- `GET /matchmaking/player/qos` - unauthenticated BARE ARRAY of QoS probe + regions (id + host:port address); the client pings these and reports back + through `PUT /matchmaking/player/photonregionpings` (already a no-op ack). +- `GET /matchmaking/player/connection-info` - authed `{ success, value, error }` + envelope carrying a freshly minted HS256 `photonAuthToken` (sub/rn.platid/ + rn.plat/rn.deviceclass/rn.env claims, 1h TTL, signed with the server JWT key), + the configured Photon app ids, `photonRegion`, and the `photonRoomId` of the + caller's current room instance (presence first, `roomInstanceId` query param + as fallback). The `experiments` block mirrors the reference verbatim; + `shouldUseGameServerNetworking` stays false. +- `GET /chat/thread/party` - empty object (recflare's documented stub: parses as + "no party" without failing the client's deserializer). +- `GET /platformnotifications/crm/me/config` - empty object (same stub + convention; the 404 here raised one console error per login). + +Photon app ids and region are configurable (`Radicade:Photon:RealtimeAppId`, +`VoiceAppId`, `ChatAppId`, `Region` in appsettings) and default to empty ids / +`us` region, exactly as recflare behaves with no Photon configured: the client +is told empty ids and connects to nothing. A deployment that runs a Photon +server sets the three ids and the client connects normally. + +Corrections after the first live run with those routes (all observed in +BepInEx LogOutput.log): + +- `chat/thread/party`: the August client deserializes this as a LIST - the + empty-object stub made it throw `expected:'[', actual:'{'`. Answers a bare + empty array now. +- `platformnotifications/crm/me/config`: the empty-object stub made the manager + throw `Deserialization returned null`. The client parses a counted envelope + here (same as `config/categories`), so it answers + `{ Results: [], TotalResults: 0 }` now. +- `connection-info` with empty Photon app ids is WORSE than a 404: the client's + GetRegions call gets `Empty application id` (ReturnCode -2) and Photon drops + the session with InvalidAuthentication. The ids must be the ones the client + actually uses - they live in the game's + `BepInEx/config/com.radicade.eacbypass.cfg` under [Photon] and are now set in + appsettings (`22dda1bb-...` realtime, `82766593-...` voice, `e8ea33f3-...` + chat), matching the AppIdOverride the March plugin was already forcing. + +### 2026-09-27: the same empty-app-id trap, hit again on a second host + +Moving the client to the public server reproduced the `connection-info` failure above, +from the other direction. The loader reported `GOING TO ^DormRoom 0% PREPARING..` and +sat there; the server looked healthy throughout (presence, matchmaking and the room +routes all 200), and the client's own log showed the real story: + + [Network.Photon.LoadBalancingClient] Queued Network action failed due to exception: NullReferenceException + [RoomLoad.RoomManager.PresenceUpdates] Error joining room instance RoomInstance - ID:21516 DORM_ROOM:25/32 + JOKAIBMCFDA::Failed to connect to region "us" + IKDHNNILPMJ::Unexpected Photon Disconnect due to cause: InvalidAuthentication + +with `Empty application id` in Player.log exactly once and `InvalidAuthentication` eight +times, retrying roughly every 7 s. The cause was config, not code: the ids were set in +`appsettings.Development.json` and **not** in `appsettings.Production.json`, so a host +running the Production environment kept the `DBConfig` defaults (`""`) and answered +`connection-info` with an empty `photonRealtimeAppId`. Photon's GetRegions then fails +with ReturnCode -2 and drops the session, which is the documented behaviour quoted +above - `connection-info` with empty ids is worse than a 404. + +Two changes so the next environment does not have to be diagnosed from a loading screen: + +- `Server/appsettings.Production.json` carries the `Radicade:Photon` block (same app as the +dev config and the plugin's `AppIdOverride`). +- `Server/Program.cs` prints either `Photon: realtime app id configured, region 'us' + (voice set, chat set).` or a full WARNING naming the four keys to set, at startup. The + warning path is verified by booting with `Radicade__Photon__RealtimeAppId=` set to empty. + +Note the failure is silent by design from the server's point of view: nothing 500s and no +route errors. Anything that shows up as "the room load hangs at 0%" should have the +client's Photon state checked first, because the REST surface will look perfect. + +## Run-3 hard crash root cause: RSA passthrough access violation (fixed) + +The fully-green-server run (20:25) still crashed the client on the loading +screen with the infamous `DILGBLCELMP: Exception of type 'DILGBLCELMP' was +thrown.` at `DLHKINDAIHK+MICFNEHOAMC.MoveNext()` right after +`GET /platformnotifications/crm/me/config` (200) and +`[ImageSig] RSA verify called #1 with non-p1 key - passing through`. + +That obfuscated exception was a red herring: the *real* fatal is in BepInEx +`ErrorLog.log`: + +``` +Fatal error. System.AccessViolationException + at Il2CppInterop.Runtime.IL2CPP.il2cpp_runtime_invoke(...) + at DynamicClass.DMD(...) +``` + +Chain of events: + +1. After `exclusivelogin`, the client downloads the file-integrity manifest + `GET /cdn/sigs/6fae9a40-edf5-44c5-a263-36d7b5b4a91f` (served since an earlier + blocker) and starts verifying the listed game files' signatures. +2. It verifies with the game's OWN embedded RSA key (not p1.rec.net), which + reached `RsaVerifyNeutralizerPatches.VerifyPrefix`'s deliberate + "non-p1 -> pass through to original" path. +3. Invoking the il2cpp original through the Harmony DMD re-implementation + access-violates (interop cannot safely re-enter that native method). Unity + surfaces the AV as the unmappable `DILGBLCELMP` il2cpp exception inside the + sigs-check async chain (`DLHKINDAIHK+MICFNEHOAMC` awaited the RecNet HTTP + task, then died during verify) - hence the crash *after* the crm 200 line + even though crm/party responses were parsed fine. + +Fix (client plugin, `RadicadeAugustClient.Plugin.cs`): the RSA verify prefix now +forces `true` and skips the original for EVERY intercepted verify, p1 key or +not - the passthrough path is never taken because it is un-invokable. Rebuilt +(`dotnet build 2023Patch/RadicadeAugustClient/... -c Release`, 0 errors) and +deployed to `BepInEx/plugins/RadicadeAugustClient.dll` (2023-09-23 21:07). + +Lesson: on this build, any Harmony-prefixed il2cpp method whose prefix returns +true (executes the original) risks the DMD AV. Neutralizer-style patches must +always skip the original when the original's failure is the thing being patched. + +## Run-4 follow-up: file check fails gracefully -> manifest parser bypassed (fixed) + +After the RSA-forcing fix, the client survived the sigs manifest (RSA verify +forced true, DILGBLCELMP now a caught/logged error) but the checker +(KKHDHILFOGO) threw while PARSING the placeholder manifest: +`IndexOutOfRangeException in KKHDHILFOGO.PFNKMGPPFGP` -> "File check failed +KKHDHILFOGO+LPIKGKOLBJH: Error validating file Failed to process verification +file due to internal exception" -> POST /api/PlayerReporting/v1/hile -> +PreparingToExit -> clean shutdown. Not a crash, but still an exit. + +The real manifest format encodes per-file hashes inside the RSA-signed blobs, +so no manifest we can serve without Rec Room's private key parses. + +Fix (client plugin): new `FileCheckManifestParserPatch` prefixes +`KKHDHILFOGO.PFNKMGPPFGP` and returns an empty file->hash dictionary - with no +files listed the per-file verify loop completes as success. Rebuilt and +deployed (2023-09-23 ~21:20). Look for `[FileCheck] manifest parser bypassed` +in the next BepInEx log. + +## Run-5: manifest parser patch is NOT viable -> sigs route reverted to 404 (final) + +The `FileCheckManifestParserPatch` attempt failed differently: HarmonyX's DMD +cannot compile the detour for `KKHDHILFOGO.PFNKMGPPFGP`'s out-byref generic +signature (`Dictionary>& out`) and throws +`InvalidProgramException: Common Language Runtime detected an invalid program` +at `(il2cpp -> managed) PFNKMGPPFGP` detour time - the checker then fails with +NullReferenceException and the same `hile` report + PreparingToExit follows. + +Empirical summary across runs (all 2023-09-23): + +| cdn/sigs response | client outcome | +|------------------------------|--------------------------------------------------| +| 404 (run 1) | reaches the Dorm (known-good) | +| 200 placeholder (run 4) | IndexOutOfRange in parser -> hile -> exit | +| 200 + parser patch (run 5) | InvalidProgramException (DMD) -> hile -> exit | + +Conclusion: the check TOLERATES a 404 and fails hard on any 200 we can produce. +The parser patch was removed from the plugin and `SigsCDN.cs` now deliberately +answers 404 (with the full analysis in its header comment). Plugin rebuilt and +redeployed; server rebuilt, restarted (PID 4768), verified: sigs 404, +gameconfigs/qos 200. The RSA verify neutralizer stays for p1-keyed image +signatures; the DMD passthrough AV lesson from run 3 still applies to any new +patch that would execute an il2cpp original. + +## Run-6 correction: 404 is NOT tolerated; final sigs/RSA combination + +Run 6 disproved the "client tolerates a 404" theory from run 5's writeup: +`OBHEMJEJNMK: HTTP Error 404` -> "File check failed ... Dowload failed" -> +hile report -> PreparingToExit. Reconciling ALL six runs gives the real +rules of the file check (KKHDHILFOGO): + +1. Missing manifest (404) -> fatal (run 6) +2. Manifest present, non-p1 verify PASSES through -> DMD access violation + to il2cpp original (run 3, flaky) +3. Manifest present, non-p1 verify forced TRUE -> parser decodes the + fake 16-byte blobs as a hash table -> IndexOutOfRange (run 4) +4. Manifest present, non-p1 verify forced FALSE -> parser skips blob + decoding, check completes -> known-good (run 1, Dorm reached) +5. A client-side parser prefix is impossible (DMD InvalidProgramException, + run 5) - the out-byref generic signature is un-detourable. + +FINAL combination (all live 2023-09-23 ~21:35): SigsCDN.cs serves the +placeholder manifest again (with byte-range support) AND +RsaVerifyNeutralizerPatches now answers `__result = isP1` - TRUE only for the +embedded p1.rec.net key (image signatures, boot-critical), FALSE for every +other key (the file check, which must fail cleanly to skip blob decoding). +Never passthrough (AV), never unconditional TRUE (IndexOutOfRange). +Plugin rebuilt/deployed, server rebuilt/restarted (PID 42708), verified: +sigs 200 placeholder, gameconfigs/qos 200. Watch for +`[ImageSig] RSA verify #N (game key) forced false` in BepInEx. + +## Run 11 outcome (22:36) + Run 12: exit-state veto (deployed 22:55) + +Run 11: suppression armed, file-check bypass fired, Dorm load started at +11.13s - then the process DIED mid-scene-load at 16.26s with no exception, +no hile POST, no exit sequence. The IJHOPKJFKBI prefix crashed the process +inside the DMD stub BEFORE the prefix body ran: the scanner thread is a +native, unattached thread (CallerCount(1), no managed callers), and il2cpp +detours on such threads access-violate - the run-3 VerifyHash AV class. +winhttp.dll cannot be dodged at the source: it is the EAC-bypass door DLL +(the only non-stock DLL in the game root) and loads BepInEx itself. + +Run 12 therefore ALLOWS the hile report to happen (HileAPI logs it at info) +and vetoes the MANAGED exit state machine instead: +`CCKNJCHHBLL.KALEFEDAHCC` (AppExitState setter, enum Running/PreparingToExit/ +ReadyForExit) is prefixed; transitions OUT of Running are blocked for 120s +after load, then pass through. The observed shutdown always begins with the +managed "AppExitState changed: Running => PreparingToExit" line, so this +cuts the shutdown off at its head from attached managed-thread territory +where detours are proven safe. Alt+F4 still works (native path). + +Run 12 markers: `[ExitVeto] blocked AppExitState -> PreparingToExit at ~16s` +in BepInEx, hile POST visible server-side but session SURVIVES, room load +completes, player spawns in the Dorm. If the game still exits: the scan +thread exits natively without the managed transition - next lever is +thread-freezing or attacking the scan itself. + +## Run 10 outcome (22:31): hile suppression DID NOT FIRE - registration bug, fixed + +Run 10 replayed run 9's ending exactly: file-check bypass fired, Dorm room +load ran 6.1s, hile POST at ~18.3s, PreparingToExit 160ms later, room load +cancelled. No `[Hile] suppressed` line, no `armed` startup line. Cause: +`HileReportSuppressionPatch` was added as a class but never registered - +this plugin patches per-class (`harmony.PatchAll(typeof(...))`) and the new +class was missing from the Awake chain. Fixed + startup log line +"[Hile] report suppression armed" added; deployed 22:34:43. + +Server-side wins confirmed in run 10: gameserverregionpings PUT no longer +404s ("Failed to upload cluster pings" gone); curatedlists 404 gone but +the client now logs non-fatal "Deserialization returned null" + "No such +list" for `GET /lists/curatedlists/bulk?id=17859340` - the `[]` shape is +tolerated but wrong; may want a `Results/TotalResults` envelope or a list +object later (NOT the current blocker). + +Run 11 markers: `[Hile] report suppression armed` at startup, then +`[Hile] suppressed report 0 (Obscured) message='winhttp.dll'` at ~18s, +NO `POST .../v1/hile`, no PreparingToExit, player spawns in the Dorm. + +## Run 9 outcome (22:16): file check DEAD, new blocker = native module scan (winhttp.dll) + +The both-entries bypass WORKED: `[FileCheck] entry bypassed` fired after +exclusivelogin, no `GET /cdn/sigs`, no game-key RSA verify, no +"Signatures don't match!". Boot went deeper than ever: matchmake/dorm -> +room 25/32 instance 20830 -> Photon connect -> LoadScene(dormroom2) -> +Initial Room Load '@TestPlayer's Dorm' completed its 5.3s flow - then the +session died: hile POST at 20.48s, PreparingToExit at 20.64s, room load +cancelled. + +The hile body decoded server-side: `Type=Obcured Message=[0:winhttp.dll]` +- the native module scanner flags our EAC-bypass proxy DLL (game root, +26KB, deployed 15:18) as an obscured module. The hile enum (MEHDFJHKLBK. +JBLJIENBBHK) decodes [0:...] = (Obscured, "winhttp.dll"). Same pattern in +the short 22:15:40 session. + +Also fixed (server, 22:29): GET /lists/curatedlists/bulk -> [] (was 404, +caused one "Deserialization returned null") and PUT +/matchmaking/player/gameserverregionpings -> {} (was 404, "Failed to +upload cluster pings"). + +## Run 10: hile funnel suppression (deployed 22:29) + +Client: `HileReportSuppressionPatch` - prefix on +`KDJDKJNPELI.IJHOPKJFKBI(MEHDFJHKLBK.JBLJIENBBHK, string)` (CallerCount(1) +funnel for EVERY hile report; enum Obscured=0/Time=1/Inject=2/.../ +AppData_Boot_InvalidSignature=310/...) returns false and logs the report; +finalizer on `ModerationManager.BJGDALDIOGI` is insurance for paths +skipping the static funnel. No hile report is legitimate in a Radicade +session. Server: HileAPI log downgraded warning -> info. + +Success markers: `[Hile] suppressed report 0 (Obscured) message='winhttp.dll'` +in BepInEx, no `POST .../v1/hile` in server log, no PreparingToExit, room +load 25/32 completes and the player spawns in the Dorm. If a DIFFERENT +hile type appears later, the suppression log will name it. + +## Run 9: entry patch v2 - both entries (deployed 22:08) + +Run 8 (21:54) failed exactly like run 7: no `[FileCheck] entry bypassed` line, +`GET /cdn/sigs/...` still ran, `RSA verify #1 (game key) forced false`, then +"Signatures don't match!". The public wrapper `GGIFLDPHCIP` is never invoked. +The decomp explains it: `GGIFLDPHCIP` is marked `CallerCount(0)` (dead public +wrapper) while the private twin `GOCCLPJDJIC` (same signature) is +`CallerCount(3)`; the no-arg `NPIILCCIPNF` is also `CallerCount(0)`, so it +needs no patch. `FileCheckEntryPatch` now uses `TargetMethods()` to prefix +BOTH entries and returns the already-resolved success promise +(`PFDOLDLEIBA.PLJINLOCHGM(KKHDHILFOGO.BJGODLEIDGE.NOMPJGOIAEB(...))`). +No download, no parsing, no RSA verify; the consumer sees a resolved +`MCFFIFIPNKE` exactly like a passing check produces. Success +markers for run 9: `[FileCheck] entry bypassed - resolved success promise +returned` in BepInEx, and NO `GET /cdn/sigs` / no RSA verify / no +`POST .../v1/hile`; boot should continue past the file check into the Dorm. + +## Run-7/8: forced-FALSE also fails; final bypass = resolved success promise (deployed) + +Run 7 (`__result = isP1`, non-p1 -> false, manifest 200) failed with an +explicit validation error: `File check failed KKHDHILFOGO+LPIKGKOLBJH: +Error validating file verification.sig: Signatures don't match!` at +`KKHDHILFOGO.AFHNPNGLCML` called from the parser. The complete matrix now +(six live client outcomes, one theory): + +- 404 manifest -> "Dowload failed" (run 6) +- verify false -> "Signatures don't match!" (run 7) +- verify true -> blob decode IndexOutOfRange (run 4) +- verify passthrough -> flaky DMD access violation (run 3) +- parser prefix -> impossible, DMD InvalidProgramException (run 5) +- run-1 "Dorm reached" -> actually a race: the flaky AV path returned + native false before the AV could trigger; the run-1 theory of "false skips + blob decoding" is dead. + +Conclusion: NO server response + verify-result combination passes this check. +`DILGBLCELMP` is the checker's own exception class (KGABAOLHEHL-derived, +string ctor), logged caught in every run - it is a symptom, never the cause. + +Run-8 hypothesis before the decomp check: patch the public entry +`KKHDHILFOGO.GGIFLDPHCIP(string, byte[], byte[], long, string)` (DMD-safe +parameter profile) and return an already-resolved success promise. Run 8 +disproved it (see Run 9 above); the fix now covers both entries. + +The RSA neutralizer keeps forcing TRUE only for the p1 key (image +signatures); with the entry bypass active the file check never reaches RSA +verify at all. + +## Run 12: AppExitState veto - process survives but zombie (deployed 22:55) + +Client: `ExitStateVetoPatch` - prefix on private setter +`CCKNJCHHBLL.KALEFEDAHCC` (nested enum CCFIKLBABNB), blocks any transition +out of Running for 120s after load, then allows. Safe to detour from managed +threads (fired dozens of times, no crash). HileReportSuppressionPatch fully +removed (run-11 lesson stands: NEVER detour KDJDKJNPELI.IJHOPKJFKBI). + +Outcome: the veto WORKS as a patch (`[ExitVeto] blocked ... PreparingToExit` +right after the hile POST at 17.6s) but is INSUFFICIENT. The shutdown path +does not run through AppExitState: immediately at hile time the client also +cancelled the room load ("Room load to 20833 cancelled" AFTER the 6s load +flow completed), tore down EAC ("[EAC] Tearing down and releasing runtime"), +POSTed logout + reportjoinresult, and threw OBHEMJEJNMK: Aborted. Then an +infinite ~1.35s loop: NRE + "An error occurred" + EAC teardown + +veto-blocked PreparingToExit AND ReadyForExit, from 17.6s to 65.6s+. +VoiceClient timed out after 30s; Backtrace "Cannot resolve destination +host". Result: alive-but-unusable zombie. The setter's CallerCount(3) +matches the three state drives visible in the loop. + +Conclusion: the exit has at least two arms - the AppExitState machine AND a +parallel shutdown initiator (EAC teardown / room-load cancel / Photon +disconnect). Vetoing the state machine alone wedges the process. + +## Run 13: server-side hile-response experiment (pending) + +Untested hypothesis: the native exit fires in response to the hile POST +RESPONSE (exit began ~160ms after the POST completed in runs 9/10/12). +HileAPI.PostHile is now env-selectable via `RADICADE_HILE_BODY`: +`ok-true` (default, unchanged shape) | `ok-false` | `nocontent` (204) | +`servererror` (500). Test pins ok-* to OkObjectResult+bool; 204/500 are +diagnostic-only. + +Procedure: kill Server.exe (PID 20224 at 23:00), `dotnet build`, start with +`RADICADE_HILE_BODY=ok-false`, relaunch client, compare: +- Exit GONE -> the native scanner waits on the server verdict; tune the + response (then try nocontent/servererror to map the contract). +- Exit STILL at ~17.6s -> trigger is client-local; next step is decomp of + the managed exit initiators (CCKNJCHHBLL.BEFDPLAELIH/GIEEBIOOFML/ + GBGMBMGDGGD, all run on attached threads) and prefixing them, or native + handling of the scanner thread (run-11 lesson: do NOT detour the funnel). + +## Run 13: hile-response experiment - exit is CLIENT-LOCAL (23:11) + +Server returned `Ok(false)` for the winhttp.dll hile report (verified in +server-console.log: "Hile experiment: returning Ok(false)"). Result: the +client STILL exited - same signature as run 12 (veto blocks, EAC teardown +loop, OBHEMJEJNMK Aborted, ~94s death). The server response does NOT drive +the exit. + +Two new facts: +1. The IMMEDIATE post-report exit was skipped. Run 12: exit began 160ms + after the hile POST and killed the room load. Run 13: room load + COMPLETED (FinishRoomLoad 21.5s), spawn completed ~23s, and the player + had ~30s of real gameplay before the first PreparingToExit attempt at + 52.5s. First-time NRE + "InvalidOperationException: Cannot error an + already completed promise" at 32s looked suspicious but did not kill. +2. The 52.5s exit is the same driver as the 17.6s exit (veto + EAC + Aborted + loop). Interpretation: the native scanner re-scans on its own schedule; + with Ok(false) the first post-report exit did not fire, but a later scan + still flagged the module and restarted the shutdown. + +Conclusion: stop DETECTION, not the shutdown. Next lever: make +winhttp.dll invisible to the module scan itself. + +## Run 14: PEB module hiding - hide winhttp.dll from the scanner (deployed 23:27) + +Client: `PebModuleHider.HideModule("winhttp.dll")` at plugin Load (before +any Harmony patching). Walks PEB->Ldr->InLoadOrderModuleList, matches +BaseDllName (x64 offset 0x58 from the InLoadOrder node; 0x60 was wrong - +FullDllName at 0x48 is a 16-byte UNICODE_STRING), unlinks the entry from +all three public lists (InLoadOrder / InMemoryOrder / +InInitializationOrder). Pure user-mode pointer writes on the main thread - +no detours, no scanner-thread contact, no DMD. Module ENUMERATION +(Toolhelp, EnumProcessModules, PEB-walks) no longer sees the DLL; module +RESOLUTION (LoadLibrary/GetModuleHandle) is untouched (loader hash tables), +so the BepInEx door keeps working. AllowUnsafeBlocks enabled. + +Server: hile restored to canonical Ok(true) (ok-true, default env) so run 14 +tests exactly one variable. Server PID 34420. + +Success markers: `[ModuleHide] winhttp.dll unlinked from PEB module lists` +in BepInEx at startup, NO `POST /api/PlayerReporting/v1/hile` in the whole +session (the scanner finds nothing to report), no PreparingToExit, room +load completes, spawn, stable gameplay past 2min. If hile STILL appears, +the scanner is not PEB-based (try hiding via handle-table wipe / ImagePath +alternate next). Keep ExitStateVetoPatch armed as belt-and-suspenders. + +## Run 14: PEB unlink - scanner does NOT use module lists (deployed 23:27, failed) + +`PebModuleHider.HideModule("winhttp.dll")` fired successfully +("[ModuleHide] winhttp.dll unlinked from PEB module lists"). BaseDllName +offset correction: 0x58 on x64, not 0x60 (FullDllName at 0x48 is 16 bytes). +Outcome: the hile POST STILL arrived at 16.3s with +Message=[0:winhttp.dll], immediate exit followed 150ms later (veto loop as +before). Server log confirms exactly one fresh report at 23:32:58. + +CONCLUSION: the scanner does not enumerate via PEB module lists. It walks +committed image regions (NtQueryVirtualMemory-style) and classifies each +region by parsing PE headers - unlinking the lists hides the NAME but the +region still self-identifies via its MZ/PE signatures. + +Note the response correlation across runs: ok-true -> immediate exit (12, +14); ok-false -> deferred exit (13). The exit is native local state, the +POST is only telemetry. + +## Run 15: PE signature wipe - declassify the mapped image (deployed 23:42, FAILED) + +`PeHeaderWiper.WipeSignatures("winhttp.dll")` at plugin Load (after the PEB +unlink, before Harmony). Zeroes the two structural signature tokens: +"MZ" at base+0 and "PE\0\0" at base+e_lfanew (6 bytes total, VirtualProtect +RW for the write then restore). A region-walking scanner that classifies by +parsing headers can no longer name this region; GetProcAddress is unaffected +(it follows e_lfanew into the intact optional/export headers without +validating the signature bytes) and winhttp IATs bound at process start. + +Success markers: both [ModuleHide] lines OK, NO POST /v1/hile in the whole +session, no PreparingToExit, load completes, spawn, stable past 2min. +Failure modes to distinguish in the log: +- hile still appears -> scanner hashes raw region bytes against the + module's on-disk image (need bytes-level rework of the mapped image or + targeted section wiping) -> next run. +- no hile but exit at ~16-18s -> an independent native watchdog (e.g. door + DLL fingerprint check) -> revisit native path. + +### Run 15 outcome (14:28 Sep 24, FAILED) + +Both hides active in the boot log ("[ModuleHide] winhttp.dll unlinked" AND +"PE signatures wiped"). The hile POST fired anyway at 19.3s session time +(client [Net] log: `Post .../api/PlayerReporting/v1/hile`), immediate exit +150ms later: veto blocked PreparingToExit, EAC teardown, room load +cancelled, `OBHEMJEJNMK: Aborted`, NRE/"An error occurred" loop ~1.3s - +identical to runs 12/14. + +Server-side note: server-console.log stopped receiving entries after +23:33 Sep 23 even though the server process survived (its console +redirection died with the previous agent session), so run-15 POSTs are +evidenced from the client log only. Server was alive and answering +gameconfigs 200 during the whole session. + +CONCLUSION after runs 14+15: the scanner neither enumerates PEB module +lists nor classifies regions by PE signature tokens. It still flags +winhttp.dll as `Type=Obcured` (note the misspelling - the flag is +"obscured", i.e. it is reporting our tampering itself, not a name it +got from the headers). Remaining hypotheses: (a) hashes raw mapped +bytes against the on-disk image, (b) fingerprints the door DLL through +the loader record / LoadLibrary bookkeeping, (c) an independent native +watchdog. + +DECISIVE observation for run 16: across ok-true runs the exit fires +~150ms after the POST, while run 13's ok-false (response that never +satisfied the handler) deferred the exit until the next scan. The exit +is triggered by the report's RESPONSE arriving, not by the scan itself. +So: drop the report request at the HTTP layer before it is ever sent. + +## Run 16: HileGate - drop the hile POST at the HTTP layer (deployed 14:50 Sep 24, FAILED - prefix never skipped the original) + +`HileGate` (in NameserverConnectionPatches.cs): a prefix check inside the +existing BestHTTP `HTTPManager.SendRequest(HTTPRequest)` patch - the same +managed, main-flow funnel the [Net] logger uses, proven detour-safe +(unlike the scanner-thread funnel KDJDKJNPELI.IJHOPKJFKBI, run 11 fatal). +When the request URI path ends with `/api/PlayerReporting/v1/hile`, the +prefix returns before any processing: the request is never queued, never +sent, and no completion callback can fire, so the response-gated exit +path never runs. The scanner cannot observe the drop - it only sees that +its report never completes. Opt-in via `HileGate.Enabled` (armed in the +August plugin Load; the March plugin source stays inert). The +ExitStateVetoPatch stays armed as a second layer. + +Success markers: `[HileGate] dropped anti-cheat report POST` around 16-20s, +NO hile POST reaching the server, NO PreparingToExit, load completes, +spawn, stable past 2 min. + +Failure modes to distinguish in the log: +- No [HileGate] line but exit at ~16-20s -> the scanner sends through a + different stack (native winhttp directly, not BestHTTP) -> shim the + door DLL's winhttp exports or block at the socket layer -> next run. +- [HileGate] line fires, no POST, still exit -> trigger independent of + report completion (native watchdog) -> revisit native exit initiators + (CCKNJCHHBLL.BEFDPLAELIH / GIEEBIOOFML, tokens 100667097/098). +- Periodic re-scan attempts every ~30s should each be dropped silently; + a rising [HileGate] count with stable gameplay is a WIN state. + +### Run 16 outcome (14:53 Sep 24, FAILED - implementation bug, model intact) + +The gate FIRED (`[HileGate] dropped anti-cheat report POST` at 16.9s) but +the POST went out anyway: server-console-run16.log line 12835 shows +`POST /api/PlayerReporting/v1/hile -> 200` at 14:53:55.431, exit machinery +started ~150ms later, veto loop as usual. ROOT CAUSE: the run-16 prefix was +VOID - in Harmony an early return from a void prefix does not skip the +original; only a bool prefix returning false does. The drop was a no-op, so +this run is evidence-wise just another ok-true repetition (12/14/15). + +FIX (deployed 14:58): the prefix is now `bool Prefix(HTTPRequest request, +ref HTTPRequest __result)`; on a hile report it logs, sets +`__result = request` (the overload's fluent return value - decomp confirms +SendRequest_Public_Static_HTTPRequest_HTTPRequest_0) and returns false so +the original never queues the request. Success markers unchanged; next run +must show the [HileGate] line AND no POST /v1/hile on the server, then no +PreparingToExit at all. + +### Run 16 RETRY (boot 14:59:23 Sep 24) - SUCCESS + +All success markers met for the first time in 16 runs: + +1. `[HileGate] dropped anti-cheat report POST` fired at 16.9s session time + (LogOutput.log line 174) - exactly where the scanner always reported. +2. ZERO `POST /api/PlayerReporting/v1/hile` reached the server for this + boot (server-console-run16.log: the only hile entry is the buggy 14:53 + run's). The drop now actually skips the original. +3. ZERO `[EAC] Tearing down`, ZERO `OBHEMJEJNMK: Aborted`, ZERO + NullReferenceException/"An error occurred" - the post-report zombie loop + never started. +4. Room load COMPLETED: joinresult 200 at 14:59:43 (instance 20845), + RunFor.Authority handlers finished, phases advanced through + DESERIALIZE_CONNECTABLES -> POST_DESERIALIZE_OBJECTS (attached log tail + at 20s), avatar/keepsake/invention fetches all flowed. +5. Session end (14:59:52 logout, 16 veto blocks 0-10s, process left + ~15:00:03) was a USER-INITIATED quit ~9s after spawn, NOT an + anti-cheat exit: no EAC teardown, no hile around the logout, and the + native quit path bypasses the veto by design (documented run-12 + behavior). The 120s veto window had not expired; the native path took + the process down cleanly. + +VERDICT: the anti-cheat kill chain is broken at its response gate. The +report is dropped client-side, the response never arrives, the exit +handler never fires, and the game survives into the Dorm. + +PENDING VERIFICATION (next soak run): the scanner's ~30s periodic re-scan. +Prediction: every subsequent report is dropped identically (rising +[HileGate] count, stable gameplay). If a later scan instead switches to a +different report channel (native winhttp direct), the first symptom is a +server-side hile POST - watch for it in a 5+ minute soak. + +Note: keep the ExitStateVetoPatch armed. With reports dropped it should +never fire again except for genuine user quits inside the 120s window; if +it starts blocking repeatedly WITHOUT a [HileGate] drop preceding it, a +second exit trigger exists and the native initiators +(CCKNJCHHBLL.BEFDPLAELIH / GIEEBIOOFML) become the next patch target. + +## Run 17: second exit trigger + veto removed by a parallel edit (Sep 24 +afternoon) + +Three more boots with the fixed gate (14:59, 15:07, 15:13) reshaped the +evidence: + +- 14:59 boot: gate dropped the report at 16.9s, room load COMPLETED + (joinresult 14:59:43), then 16 veto blocks (PreparingToExit x8, + ReadyForExit x8) at 14:59:52-15:00:03 - the exit re-request came ~9s + AFTER load finished, and the process left once the veto window ended. + No hile, no EAC teardown, no crash dump, no Windows fault. +- 15:07 boot: gate dropped reports TWICE (17s, room-change), survived 7 + minutes, then died through the managed shutdown cascade at 15:08:42 - + exactly when the run-12 veto (120s from 15:07:40) expired. Same clean + shape: the exit kept being re-requested and the veto kept absorbing it + until the window closed. +- 15:13 boot: a PARALLEL EDIT (Plugin.cs modified + rebuilt at 15:11, + not by this thread) REMOVED ExitStateVetoPatch entirely. That boot had + [HileGate] armed but no [ExitVeto] armed, died ~40s in with nothing + absorbing the exit. This confirms the veto was doing real work. + +MODEL REVISION: the exit is NOT purely response-gated. The scanner keeps +RE-REQUESTING exit on a cadence (runs show 10-40s intervals), plausibly a +report-completion TIMEOUT on the dropped report (run 13's ok-false exit +at ~30s fits: no satisfying response within the deadline = failure = +exit). Dropping the request cannot win alone; something must absorb the +re-requests forever. The veto did that until its window expired; a +parallel edit removed it. + +Run 17 change: ExitInitiatorTracerPatch prefixes the managed exit +initiators CCKNJCHHBLL.BEFDPLAELIH (100667097), GIEEBIOOFML (100667098) +and FNOEHNDLHHG (100667101), logs the FULL managed stack of whoever +calls them, and blocks the call. These are attached-thread, detour-safe +targets (run-14 prep). Deployed 15:42. Success markers: [ExitInit] +stack traces identifying the second trigger's caller, and sessions that +no longer die at 10-40s. If sessions still die with all three blocked, +the trigger is native-only (TerminateProcess-class) and the blockades +prove it by absence. + +Run 18: "crash on boot" was a disabled Doorstop proxy, not the tracer + +Symptom: the user reported a crash on boot immediately after the 15:42 +run-17 deploy. + +Evidence chain: + +- BepInEx/LogOutput.log still had mtime 15:14, i.e. no plugin log was + written for any boot after the 15:13 session. No boot has ever printed + [ExitInit] armed. +- The Unity log (AppData/LocalLow/Against Gravity/Rec Room/Player.log and + Player-prev.log, mtime 15:59:47 / 15:59:33) contains ZERO + BepInEx/Doorstop/Radicade lines: two VANILLA boots of ~14 KB each. + Player.log shows "Could not resolve host 'ns.rec.net'" at 3.6s, then + AppExitState Running => PreparingToExit at 3.9s, then + "[EAC] Tearing down and releasing runtime", process gone by ~7s. EAC + was live because the Doorstop/EAC-bypass door never loaded, and the + game was still pointed at the real nameservers. +- Cause: winhttp.dll (26,112 b, Doorstop 4.3.0 proxy, md5 b2a4d0cb...) had + been renamed to winhttp.dll.baseline-disabled at 15:17:15 by a parallel + action, which also created baseline-backup-20260924-151633/ containing a + copy of winhttp.dll, Player-before-baseline.log (the 15:13 session log), + the pristine 173 b RecRoom_Data/boot.config and the pristine 38,654,794 b + il2cpp_data/Metadata/global-metadata.dat. +- No new WER 1001 event and no new CrashDumps entry: the game exits + through its orderly quit path, it does not fault. That is why the crash + left no crash report and why only the Unity log has evidence. +- Fix: the snapshot's winhttp.dll was copied back to the game root (md5 + identical). The rest of the door chain was verified intact: + doorstop_config.ini enabled=true, .doorstop_version 4.3.0, + BepInEx/core/BepInEx.Unity.IL2CPP.dll, dotnet/coreclr.dll, 335 interop + assemblies, 64 unity-libs. + +Consequences: + +- Run 17 has NEVER booted. It was deployed at 15:42, 25 minutes after the + door was disabled, so ExitInitiatorTracerPatch has zero runtime data and + its block/trace behaviour is still unproven. +- The "baseline" experiment cannot work as designed. With the door removed + there is no patch stack at all, so the game dies at ns.rec.net DNS plus + EAC long before any comparison is possible. A vanilla control run needs + the door present and the patch groups switched off from inside the + plugin, never a removed proxy DLL. +- Parallel edits remain the biggest source of false signal: ExitStateVeto + Patch was deleted from the working tree at 15:11 and the door was + disabled at 15:17. Both were uncommitted, so neither the veto nor any + boot data exists in the run-17 build. + +The next boot is run 17's first real run. Expected markers: [ModuleHide] +x2, [HileGate] armed, [ExitInit] armed, a hile drop ~17s in, and [ExitInit] +stack traces naming the second exit trigger's caller. + +## Run 17's first real boot (16:10): the exit block broke the room load + +The door restore worked and the boot reached the Dorm, but the player saw a +BLACK SCREEN: the debug overlay showed `Room: 'DormRoom: Home', RoomId 2, +SubroomId 2883, Players 1 (2 TestPlayer, Loading)`, FPS 910 (nothing being +drawn). Run 17's own markers were all present: [ExitInit] armed, ONE blocked +initiator call, and four [HileGate] drops. + +The one block is the problem. It fired ~0.05s after `Chainloader startup +complete`, before any networking, and its caller trace is: + +``` + at System.Environment.get_StackTrace() + at ExitInitiatorTracerPatch.TraceAndBlock() + at DMD() + at (il2cpp -> managed) BEFDPLAELIH(Il2CppMethodInfo* ) +``` + +Read that trace correctly: in an IL2CPP build ALL game code is native, so +"no managed caller above the trampoline" does NOT mean an outside native +component. It only means the caller is another il2cpp method - ordinary game +code. A boot-time call to an exit initiator is therefore most likely part of +normal startup, and blocking it is not free. + +What followed, compared against the last good session (15:13, pre-run-17, +preserved as baseline-backup-20260924-151633/Player-before-baseline.log): + +- 15:13: load of 25/32 runs 14.08s -> COMPLETE at 19.77s (FinishRoomLoad). + FetchDeserializationDataTask.Run('25.32') finishes in 1ms: DownloadAsset + Bundle, then DownloadAndParse of MKMOLMPLENB, JGEHMKKALNE and OBHEFDFCAIC + all return in 2-7ms, then the phases march through DATA_DOWNLOADED, + INIT_OBJECTMODEL ... COMPLETE. +- 16:10 (run 17): the SAME room, the SAME sequence - and + `START: DownloadAndParse('OBHEFDFCAIC)')` never returns. The load sits in + DOWNLOADING_DATA until the client is closed 64s later. Everything after the + load never happens: no `rooms/25/experience/player`, no + `keepsakes/rooms/25`, no `roomkeys?roomId=25`, and no + `GET /rooms/photon_access_token`, so the dorm's Photon join is never even + attempted and the save can never arrive. That is the black screen. +- Server side is exonerated: `GET /rooms/rooms/25?include=5421...` returned + byte-identical payloads (len=2174) in every session including 16:10, and + the server log shows no [RoomUtils] self-heal firing. The only client-side + delta between the 15:13 build and the 16:10 build is run 17's tracer (the + veto was already gone at 15:13). Four consecutive pre-run-17 boots + (14:53, 14:59, 15:07, 15:13) all completed the dorm load. + +Run 18 change: `ExitInitiatorTracerPatch` still traces every call, but it now +ALLOWS the call (returns true) until `HileGate.DroppedCount > 0`, i.e. until +the scanner has actually filed a report, and only blocks from then on. The +exit that matters is the one the dropped report triggers; a call that arrives +before any report exists is startup traffic. HileGate grew the DroppedCount +counter (NameserverConnectionPatches.cs, shared into the August assembly), the +log line is now "[ExitInit] armed - ... traced; blocking switches on after the +first dropped hile report", and an allowed call logs +"[ExitInit] allowed managed exit initiator (no report filed yet)". + +Deployed 16:16 (34304 b). Success markers for the next boot: an `[ExitInit] +allowed ...` line during startup instead of a block, the dorm load reaching +COMPLETE / FinishRoomLoad, a visible Dorm, then the first hile drop and - from +that point - `[ExitInit] blocked ...` traces. + +## Run 18 gating result (16:17 boot): the hang is gone, the load still fails + +The gate did what it was built to do: + +- `[ExitInit] allowed managed exit initiator (no report filed yet)` at startup + (the boot-time BEFDPLAELIH call now passes through), +- four `[HileGate] dropped anti-cheat report POST` lines, and +- one `[ExitInit] blocked managed exit initiator ... at DMD()` after the drops - the gated block works. + +The infinite stall is gone too: `DownloadAndParse('OBHEFDFCAIC)')` returned +after 22.7s (16:17:44.4479 -> 16:18:07.1543) instead of never, and +`OperationContext.FinishRoomLoad` followed at 39.1551. + +But it returned a FAILURE, and the session died with it: + +``` +[16:18:07] [39.1509] AppExitState changed: Running => PreparingToExit +[16:18:07] [39.1566] [Error | RoomOperation] Error during operation + InitialRoomLoad-a820aca0-...: Error downloading save data! + Try again later or on a better connection. +[16:18:08] [40.2863] [Unity] [EAC] Tearing down and releasing runtime +[16:18:09] [41.6710] [Unity] PhotonHandler.OnDestroy +``` + +So the exit that ends these sessions is at least partly a CONSEQUENCE of the +failed room load - the documented DORM-BLOB-FIX chain - not purely the scanner. +Note also that the block was armed and STILL did not hold the process: a +`[ExitInit] blocked` line fired and the client left anyway, so the recurring +exit is not solely the managed initiators. + +WHAT IS MISSING THIS TIME: `GET /rooms/photon_access_token`. Every pre-run-17 +boot fetched it during the dorm load (14:59:39.570, 15:07:54.917, +15:13:41.243 and again at 15:13:52.159) and the good run shows +`[GetSubRoomDataSave()]: Total: [5397.72ms 2618frames]` - the subroom save +download succeeding after ~5.4s. Neither run-17-lineage boot (16:10, 16:17) +ever requested a Photon access token, so the Photon room join is never set up, +`GetSubRoomDataSave()` has nothing to resolve against, and the load fails with +"Error downloading save data!" instead - after a 22.7s timeout in 16:17, and +forever in 16:10. + +Everything server-side is byte-identical across good and bad sessions: +`/matchmaking/player/connection-info` ~1180 b, `/matchmaking/matchmake/dorm` +572 b, `/rooms/rooms/25?include=5421...` 2174 b, `/cdn/room/` 22 b. The tracer +is the only client-side delta, so the tracer is the A/B variable. + +Run 18a (deployed 16:19, 34304 b): the tracer registration is commented out +(the class stays in source with its findings recorded - a boot-time +BEFDPLAELIH call arriving from il2cpp with no managed frames, and the exit +leaving through FNOEHNDLHHG). Expected on the next boot: `[ExitInit] disabled +- tracer removed for the room-load A/B`, then `GET /rooms/photon_access_token` +in the client log and in the server log, `GetSubRoomDataSave()` resolving in +seconds, the phases running through to COMPLETE, and a visible Dorm. If all of +that happens, the tracer - or one of its three detours - is confirmed as the +room-load breaker and it comes back only one target at a time. + +## Run 18a result and the REAL regression: the baseline experiment restored the +## unpatched metadata (root cause of the black screen and the forced exit) + +Run 18a booted at 16:20 with `[ExitInit] disabled` (LogOutput.log line 33) and +the four `[HileGate] dropped` lines, and it failed EXACTLY like run 18 did: +no `GET /rooms/rooms/25/experience/player`, no `playerdata/me`, no +`keepsakes/rooms/25`, no `roomkeys?roomId=25`, no +`GET /rooms/photon_access_token`, then `Error downloading save data!`, +FailedToSpawnPlayer (Code 8715) and the forced exit. The exit tracer is +therefore NOT the room-load breaker - that hypothesis is dead, and the server +payloads were byte-identical the whole time. + +What the tracer-off boot did NOT rule out - and what actually broke every boot +after 15:16 - is the rest of the parallel "baseline" experiment. It did not +only rename winhttp.dll away at 15:17:15; it also put back two files from the +pristine (pre-Radicade) Sep 23 14:21-14:23 state, using mtime-preserving +copies, so nothing looked "new" afterwards: + +| file | good state (the 14:53-15:13 boots) | what every failing boot ran | +| --- | --- | --- | +| `RecRoom_Data/il2cpp_data/Metadata/global-metadata.dat` | 38,654,794 b, md5 `cc9c32014b5fc3dd1a2a8fb12376be15` | 32,039,456 b, md5 `825f34f1dd046a0ae764cd5a97dba5e6` | +| `RecRoom_Data/boot.config` | 173 b, md5 `dc2e5f7af7d5df736a4cc3477b4b1a1f` | 104 b, md5 `5694f1046c94cb6b9bbcf6854ed8eaae` | + +The 104 b boot.config is byte-identical to `RecRoom_Data/boot.config.orig` +(the patcher's own backup), which is the fingerprint of a restore, not of an +edit. Proof that the 38 MB metadata - and only it - carries the Radicade +rewrite: + +- `grep -a -c 'localhost:3030'`: 38 MB file = 3, live 32 MB file = 0. + `127.0.0.1` = 9 vs 1, `rec.net` = 20 vs 4. +- `RecRoom_Data/radicade-patch.json` records `patchedAtUtc + 2026-09-23T22:08:42Z` (15:08:42 local, `changedLiterals: 7`, serverUrl + `https://localhost:3030`), and that timestamp is exactly the mtime of the + 38 MB file captured in the snapshot: the file the baseline experiment + replaced was the Radicade-patched one that had been live for all four good + boots. +- Both files keep the release's custom shuffled header (magic `d4370000...`), + so the patched metadata is still the native format the runtime expects. The + standard-header `global-metadata.interop.dat` (magic `af1bb1fa`) is only the + tooling copy and was never touched. + +Mechanism: with the pristine metadata the client's hardcoded bootstrap +literals point at the official hosts again. The plugin's runtime redirects only +cover the nameserver host, the websocket host and TLS, so most traffic still +reaches the server through `GET https://ns.rec.net/ns` - which is why the boot +looks healthy, logs in, joins Photon and renders the Dorm - but the room-save +path is built from a literal that nothing rewrites at runtime, so +`GetSubRoomDataSave()` fails and the session is torn down by the room-load +failure handler. + +Fixed: both files copied back from +`baseline-backup-20260924-151633/RecRoom_Data/` (`cp -p`, provenance kept). +Live again now: metadata md5 `cc9c3201...` (38,654,794 b, 3x localhost:3030), +boot.config 173 b (`vr-enabled=0`, `player-connection-mode=Listen`, +`player-connection-debug=0`). winhttp.dll had already been restored, a scan for +any other 2026-09-24 write outside BepInEx found nothing, and the only disabled +sidecars are `winhttp.dll.baseline-disabled` (dormant) plus the BepInEx +config/loader backups - so the baseline undo is now complete. + +Caveat for future restores: the patcher's own input (38,654,604 b) no longer +exists anywhere, because the experiment also restored +`global-metadata.dat.orig` to the 32,039,456 b Sep 23 14:23 file. The canonical +restore sources are therefore the snapshot directory and +`global-metadata.interop.dat` - never `.orig`. + +Deployed build for the next boot stays run 18a (34304 b, md5 +`b5cdc49cf2da66e306ee20f4e60a515c`, tracer off), so the metadata fix is a clean +A/B. Success markers: `GET /rooms/rooms/25/experience/player` (404), then +`playerdata/me`, `keepsakes/rooms/25` and `roomkeys?roomId=25` in the server +log, `GET /rooms/photon_access_token`, `[GetSubRoomDataSave()]` resolving in +seconds, FinishRoomLoad COMPLETE and a visible Dorm. The gated +ExitInitiatorTracerPatch and a never-expiring exit veto come back only after +that, one target at a time, with the metadata state now treated as a +first-class tracked variable (md5 above) rather than an assumed constant. + +## Run 19 (16:54 boot): the Dorm LOADS - the next killer is the Play menu's +## missing list routes + +The metadata restore worked. The 16:54:57 boot completed the dorm load for the +first time since run 16: + +``` +16:55:10 phase DOWNLOADING_DATA, DownloadAndParse('OBHEFDFCAIC)') 16.2212 -> 16.2296 (8 ms, was 19-22 s or never) +16:55:11 GET /rooms/photon_access_token +16:55:13 [10976][19.4330] OperationContext.FinishRoomLoad(e4909cf1-...) +16:55:13 Moved to phase COMPLETE (SCENE_LOADED -> SETUP_ROOM_OBJECTS -> RUN_MIGRATIONS + -> DESERIALIZE_OBJECTS -> DESERIALIZE_CONNECTABLES -> COMPLETE, SpawnLocalPlayer at ~19.5s) +``` + +So `GetSubRoomDataSave()` was never the broken thing - the unpatched metadata +was. Player spawn works too (the only spawn-time error is a benign Toxmod +config warning). + +### The new failure: the Play menu's discovery rows 404 + +At 27.7 s the client opened the room list and its rows failed: + +``` +16:55:22 GET /lists/algorithmiclists/HotList?type=1 -> 404 + [Error] Error refreshing rooms from source Hot: An error occurred (RoomListModel) +16:56:05 GET /rooms/rooms/carousel/rising?skip=0&take=20 -> 404 + [Error] Error refreshing rooms from source CarouselEndpoint: An error occurred +16:56:05 [22050][70.8179] Error refreshing rooms from source Hot: An error occurred +16:56:05 [22051][70.8330] AppExitState changed: Running => PreparingToExit + NullReferenceException at RRUI.Data.RoomListModel.DOPOJKBDDEL() + <- UnityEngine.Object.Instantiate <- RRUI.StaggeredPrefabSpawner.EPABHKKCNIF() + [EAC] Tearing down; Uploading Crash Report +``` + +A failed row is not a skipped row: it NREs the model's prefab spawner and ends +the session. Same death in the 15:13 reference run (`Error refreshing rooms +from source Hot` at 15:13:59, exit at 15:14:06), which is why that run ended +seconds after the player reached the RecCenter. The curated-lists 404/`[]` +note from run 10 was never the whole story - `lists/algorithmiclists/*` and +`rooms/rooms/carousel/*` were simply never implemented. + +### Fix (server, 17:36) + +- NEW `Server/Lists/AlgorithmicListsAPI.cs`: + `GET /lists/algorithmiclists/{list}?type=1` -> `{ Type, Entities: [ { Id, Context } ] }` + (`Id` a string, `Context` null), the shape and field order the reference + server answers with, because the client's obfuscated DTOs are read + positionally. `type` is echoed (defaulting to 1 = Rooms). Every row key + answers 200: `HotList` and the other ranking rows are ordered by + `RoomStats.VisitCount` desc, `new` by `PublishedAt` desc, both over public + active non-dorm rooms, 20 per row. +- `Server/RoomServer/RoomServerAPI.cs`: `GET rooms/carousel/{*source}` (next to + the other carousel feeds) -> bare array of RoomDetail, name/tag filtered when + the source names something, falling back to every public room so a row is + never empty. Shape follows `rooms/hot_rooms` (see + docs/2023-WATCH-HOME-FIX.md); if a row still renders empty, the envelope + (RoomSearchResult) is the thing to change. +- Not changed: `lists/curatedlists/bulk` still answers `[]`. Its shape is a + guess either way (the client logs "Exception: No such list" for the empty + array), so it stays as the documented, non-fatal behaviour until the fatal + rows are confirmed fixed - changing a DTO the positional codec reads without + a verified field order is how a non-fatal error becomes a fatal one. + +Verified live over the plain-HTTP listener (server restarted 17:37, now +`server-console-run19.log`, `dotnet run --no-build` from `Server/`): + +``` +/lists/algorithmiclists/HotList?type=1 -> 200 {"Type":1,"Entities":[{"Id":"32",...},{"Id":"3"},...]} +/lists/algorithmiclists/new?type=1 -> 200 {"Type":1,"Entities":[{"Id":"35"},...]} +/rooms/rooms/carousel/rising?skip=0&take=20 -> 200 [ {RoomId:2,...}, ... ] (20289 b) +/lists/curatedlists/bulk?id=17859340 -> 200 [] (unchanged, see above) +``` + +Server build clean (0 errors, 389 pre-existing warnings). Client build unchanged +(run 18a, tracer still off), so the next boot A/Bs exactly one thing: the rows. +Expected markers: no "Error refreshing rooms from source Hot/CarouselEndpoint" +lines, no RoomListModel NullReferenceException, the Play menu carousels +populated with real rooms, and a session that survives the `hot` row refresh. + +## Run 20 (17:43 boot): the row fix holds - the room-list killer is gone + +The 17:43:25 boot ran against that rebuild, and every marker above landed: + +- `GET /lists/algorithmiclists/HotList?type=1` was requested twice and answered + 200 (1171 b). `Player.log` has **no** "Error refreshing rooms from source + Hot/CarouselEndpoint" and **no** `RRUI.Data.RoomListModel` NullReferenceException. +- The rows resolved into real rooms instead of failing: right after HotList the + client asked `rooms/rooms/bulk?id=2&id=4&id=7&...&id=19` and drew the carousels + (`rooms/hot?tag=rro` 200 / 27163 b, with RecRally/Crescendo/Park/DodgeballVR + images following). +- The Dorm was unaffected: `Moved to phase COMPLETE` at `[10504][20.13]`, + `OperationContext.FinishRoomLoad`, `SpawnLocalPlayer` total 10278 ms - the same + numbers as the 16:54 reference boot. The metadata precondition was re-checked + at boot: `global-metadata.dat` 38654794 b md5 `cc9c32014b5fc3dd1a2a8fb12376be15`, + `boot.config` 173 b md5 `dc2e5f7af7d5df736a4cc3477b4b1a1f`. + +The session still ended, but *differently* from every previous failure: the log +stops on a clean quit path (`POST /matchmaking/player/logout` -> `PreparingToExit +=> ReadyForExit` -> EAC teardown -> `PhotonHandler.OnDestroy`), with **no +exception in the 3.7 s before it** - where the 16:54 run had the RoomListModel NRE +40 ms before `PreparingToExit`. A silent 3.7 s gap plus a graceful logout is the +shape of a hand quit, not of the crash path, so run 20 cannot be called a crash +without knowing what the screen did. + +## Run 20 fix: an empty COMMUNITY carousel + +`GET /rooms/rooms/hot?tag=community&skip=0&take=20` answered 200 with +**43 bytes** - no room in `Rooms` carries the tag `community` (the column holds +`rro,2|quest,0` style pairs), so the Play menu's COMMUNITY carousel was empty. +`GetHotRooms`'s default branch now falls back to the whole public non-dorm +catalogue when the tag matches nothing, the same way `rooms/carousel/{*source}` +does; it answers 27163 b / 25 rooms now. + +Server rebuilt (0 errors, 389 pre-existing warnings) and restarted as +`server-console-run20.log`, PID 26088, both listeners up: + +``` +/rooms/rooms/hot?tag=community&skip=0&take=20 -> 200 27163 b 25 rooms +/rooms/rooms/hot?tag=rro&skip=0&take=20 -> 200 27163 b +/lists/algorithmiclists/HotList?type=1 -> 200 {"Type":1,"Entities":[...]} +/rooms/rooms/carousel/rising?skip=0&take=20 -> 200 20289 b +``` + +## Known non-fatal noise (do not chase these first) + +All of these are present in both the 16:54 run and the 15:13 reference run, so +they are background, not regressions: + +- `Failed to load Type "RecRoom.Analytics.Statsig.StatsigDebugOverrides"` - + `RuntimeInitializationHelper:OIDNKEGDPPF(Dictionary\`2)` -> `MBJKIOBDOAM()` -> + `SubsystemRegistration()`. It is **vanilla noise**, not a symptom: the + 15:13 baseline boot - loader renamed away, pristine 32,039,456 b metadata, + i.e. no patch stack at all - logs the same line + (`artifacts/baseline-20260924/Player-first-baseline.log`, line 24), and so + does every working boot since, including the bug-report upload at + `Server/RadicadeServer-Config/BugReports/20260925-044419-Player.log`. The + type is a debug-only Statsig helper the retail build does not carry; the + subsystem registration logs the miss and continues. The August plugin never + mentions the string (`grep -a -c StatsigDebugOverrides + RadicadeAugustClient.dll` = 0). Do not chase it. + +- `Malformed Response: '[]'` (2 in run 20, 5 in the 15:13 run) - `ODIGMKMGFCF: + expected:'Number Token', actual:'[', at offset:0`, i.e. the client deserialised a + bare array where it wanted an object. The two in run 20 land within ~1 s of + `api/inventions/v2/mine` and `api/inventions/v1/toptoday`, both of which answer + `[]` because the inventions tables are empty; the 15:13 run survived five of + them, so their shape is wrong but not fatal. +- `Could not enable featured rooms display... no images are available` - + `featuredrooms/current` deliberately 404s. +- 404s: `/rooms/rooms/25/experience/player`, `/rooms/rooms/25/experience`, + `/econ/econ/roomGiftDropShops/room/25`, `/data/commonQuery/userSegment`, + `/econ/api/influencerpartnerprogram/{influencer,myinfluencer}`. +- `Exception: No such list` / `Deserialization returned null` for + `lists/curatedlists/bulk` (documented above). + +## Run 21 setup: the tracer is re-armed (it was never the room-load regression) + +Run 20's exit was confirmed by the user: **it closed on its own, with the screen +looking fine** - no black screen, no error on screen, no crash dialog. That is a +silent self-exit through the graceful path, i.e. the same class of shutdown the +run-17 `ExitInitiatorTracerPatch` was built to name. + +The run-18a reasoning that parked the tracer is now known to be wrong. Its two +boots (16:10, 16:17) never fetched `/rooms/photon_access_token` and died with +"Error downloading save data!", so the tracer was blamed - but those boots were +running the *pristine* `global-metadata.dat` + `boot.config` restored by the +baseline experiment at 15:16, which is exactly what starves +`DownloadAndParse('OBHEFDFCAIC')`. With the patched metadata back, run 20 +completed the load in the usual 8ms and 10278ms and the tracer was off, so the +tracer is not implicated in the room-load regression. + +Re-armed in `Plugin.cs` (`harmony.PatchAll(typeof(ExitInitiatorTracerPatch))`), +rebuilt (`dotnet build 2023Patch/RadicadeAugustClient/RadicadeAugustClient.csproj +-c Release`, 0 errors) and deployed to `BepInEx/plugins/RadicadeAugustClient.dll` +(34304 b, md5 `7fd2f6c698f9568ebc6019bd8ca01e2f`; the previous run-18a build is +kept as `RadicadeAugustClient.dll.before-run20-tracer`, md5 +`b5cdc49cf2da66e306ee20f4e60a515c`). The old "disabled" log line is gone from the +binary, the arm message is present. + +What it does: prefixes `CCKNJCHHBLL.BEFDPLAELIH` / `GIEEBIOOFML` / `FNOEHNDLHHG` +(the managed funnel every shutdown passes through). While +`HileGate.DroppedCount == 0` the call is allowed and logged (that is normal boot +traffic); once a report has been dropped the call is **blocked** and the full +managed caller stack is logged as `[ExitInit] blocked managed exit initiator`. +The gate dropped a report at 17:43:38 in run 20 and the exit fired at 17:43:59, +so a repeat boot will be armed well before the exit. + +Expected in `BepInEx/LogOutput.log` on the next boot: one `[ExitInit] allowed +managed exit initiator` early (boot traffic, before the drop), then either an +`[ExitInit] blocked managed exit initiator` stack at ~37s - which names the +trigger and should also stop the shutdown - or **no** ExitInit line at all, which +would prove the trigger is native-only (TerminateProcess-class) rather than +managed. Either outcome is a result; the metadata/boot.config md5 pair above is +the precondition to re-check first. + +## Run 21 (19:34 boot, tracer armed): the trigger is NATIVE, and the tail is managed + +Both expected lines appeared, and they settle the model: + +``` +line 36 [ExitInit] allowed managed exit initiator -> DMD() -> (il2cpp -> managed) +line 183 [HileGate] dropped anti-cheat report POST +line 276 [ExitInit] blocked managed exit initiator -> DMD() -> (il2cpp -> managed) +``` + +Two things follow from the traces. First, **no managed caller ever appears**: the +frame above each initiator is `(il2cpp -> managed)`, i.e. native il2cpp code +invoking the managed method directly, so whatever decides to leave is not managed +code and no managed veto can see it. Second, the blocked initiator fires at the +**very end** of the shutdown, after `PreparingToExit` (19:34:36.77) and after +`ReadyForExit` (19:34:38.47) - the `[ExitInit]` block sits within the last ~9 log +lines, next to the `logout` / `PUT playersettings` / final analytics flush. So +`FNOEHNDLHHG` is the *last* step of a shutdown, not its trigger: blocking it held +the process ~13s longer than the previous boot but the native side still finished. + +What the boot looked like from the outside: + +- Dorm + spawn fine again: `Moved to phase COMPLETE` `[14020][26.5664]`, + `SpawnLocalPlayer` 10629.92 ms. +- Play menu fine: `HotList?type=1` 200 (1171 b) then `rooms/bulk?id=2&id=4&id=7...` + (21460 b) with the carousel art. +- Only three 404s, all long-known: `rooms/25/experience/player`, + `econ/econ/roomGiftDropShops/room/25`, `data/commonQuery/userSegment`. +- The client stayed **interactive until the last second**: + `Applied KEYBOARD_MOUSE control scheme LEGACY` (27.1s, automatic), `ProUX` + (32.42s), `LEGACY` (41.98s) - i.e. the settings menu was being used - then + `POST /matchmaking/player/logout` at 19:34:36.676, 100ms before + `AppExitState Running => PreparingToExit`. +- **No OS-level crash**: Windows Application log has no `Application Error` / + `Application Hang` entry for RecRoom.exe at 19:34; the game's own + `Crash Dump Report: New: 0 Previously Sent: 0`. This is a clean exit, not a + crash, however it looks on screen. + +So the open question is no longer *how* the exit happens (native, at the tail) but +*what decides it*, and the only always-present candidate left is the same one as +every prior run: the dropped anti-cheat report. The two runs with a known cause of +death now stand 37s (17:43, tracer off) and 50s (19:34, same shape) apart with no +other delta, so the next boot should be a **hands-off soak**: launch it, touch +nothing, and see whether it exits on its own at all. + +The user confirmed it was **not** a quit: "I was only toggling settings, never +pressed Quit". Ruled out in the same pass: no `[NotifyHub] -> ` send at +all in run 20 (the only hub lines are connect/register/disconnect, the last one at +the logout instant), so no server push ends the session; a 404 in the notify socket +is not it either; and the report's own response body is a dead end (`RADICADE_HILE_BODY` +tried `ok-true`/`ok-false`/`nocontent`/`servererror` back in run 13 - ok-false only +DEFERRED the exit). + +## Run 22: the exit veto is back (rebuilt), on the decision point this time + +The old `ExitStateVetoPatch` was the only mitigation that ever *absorbed* these +exits (16 blocks in runs 12/13/16) and it was deleted from the tree at 15:11 by a +parallel edit, never committed - the whole `2023Patch/RadicadeAugustClient/` +directory is untracked (`git status --porcelain` -> `??`), so there is no history to +recover it from. It has been rewritten against the decompilation instead of guesswork: +`.external/aug-decomp/acs/CCKNJCHHBLL.cs` gives +`enum CCFIKLBABNB { Running = 0, PreparingToExit = 1, ReadyForExit = 2 }`, +the private static setter `KALEFEDAHCC(CCFIKLBABNB)` (token 100667095, CallerCount +3) and its interop wrapper as the setter of property `CCKNJCHHBLL.GLNIKJNLCIB` +(getter `JABDIMNNBCC`, token 100667094). Every `AppExitState changed: Running => +PreparingToExit` line in `Player.log` is that setter. + +`ExitStateVetoPatch` now patches BOTH the wrapper setter and the private method +(resolved by name, whatever exists), allows only `Running`, and blocks every other +transition **with no expiry window** - run 17 showed the re-requests continue long +after the old 120s window closed and the process leaves the moment it opens. Blocks +log the first three, then every 25th. Escape hatch for a real quit: +`RADICADE_EXIT_VETO=0` before launch (with the veto armed, quitting from the menu is +refused too - end the process from Task Manager instead). Registration is wrapped in +try/catch so a veto that will not bind can never take the boot down with it. + +**A trap worth remembering:** the first build of this patch was a silent no-op. The +log line was `LogInfo(Enabled ? "armed ..." : "disabled ...")` over a +`static readonly bool Enabled`, and string-searching the built dll showed the +true-branch literal was *absent* while the false-branch one was present - i.e. the +compiler had decided the condition. `Enabled` is now a property +(`=> !_disabled`) and the log line is built at runtime, so the dll contains +`[ExitVeto] patches applied; blockExitState enabled=` and the value can be read +straight off the next boot instead of being assumed. Verify patches by searching +the SHIPPED dll for their strings; do not trust the arm message alone. + +Deployed `BepInEx/plugins/RadicadeAugustClient.dll`, 36352 b, md5 +`4a672a6b0ba59f73ca785cb318a6b824` (previous builds kept as +`RadicadeAugustClient.dll.tracer-run21`, md5 `34304 b / 19:32`, and +`RadicadeAugustClient.dll.before-run20-tracer`, md5 `b5cdc49cf2da66e306ee20f4e60a515c`). +Build clean (0 errors). Server untouched and up (`server-console-run20.log`, PID +26088). Expected on the next boot: `[ExitVeto] ... enabled=True`, the early +`[ExitInit] allowed` line, the `[HileGate] dropped` line, and then - instead of the +silent shutdown at 37-50s - `[ExitVeto] blocked AppExitState -> PreparingToExit` +while the session keeps running. If the session instead dies with no `[ExitVeto]` +line, the veto never bound and the `[ExitVeto] failed to apply` error text will say +why. + +## Run 23: the exit's chain is named, and the last wall is Unity's own quit callback + +Two questions were open after run 22: what *starts* the shutdown, and whether any +frame of it is still reachable from managed code. The first is now answered from +evidence that was already on disk; the second is patched. + +### The chain + +1. The client's own scanner finds a planted module and files the anti-cheat report. + That report body was decoded back in run 9 and is still in the server logs: + `Hile report received: Type=Obcured Message=[0:winhttp.dll]` - the BepInEx + Doorstop proxy `winhttp.dll` in the game root, classified `Obscured` (hile enum + `MEHDFJHKLBK.JBLJIENBBHK`, Obscured = 0). The classification is **inherent**: run 9 + logged the identical body at 22:16, hours before the run-14 PEB unlink (23:27) and + the run-15 PE-signature wipe (23:42) existed, so hiding never caused it and + un-hiding will not remove it. +2. It re-scans on a fixed schedule. `server-console.log` holds two reports at + `23:32:28.141` and `23:32:58.405` - **30.264s apart**. That schedule, not a boot + timer, is why the same build dies at 36.8s on one boot and 55.3s on the next: the + exit lands on the first tick after the boot completes. +3. The report is not the trigger. Dropping it at the HTTP layer (`HileGate`, run 16) + only defers the exit, and run 13 proved the response body is irrelevant. The + scanner's local verdict starts the shutdown; the report is a side effect. +4. Where it ends (run 22 `Player.log`): + `UnityEngine.Application:Internal_ApplicationWantsToQuit()` -> + `System.Func\`1[TResult]:Invoke()` -> `CCKNJCHHBLL:GBGMBMGDGGD(System.Boolean)` -> + `MGLDPDAEODB:EMDNIBPNEGH()` -> `PFDOLDLEIBA:CKIEGOCLGFD(Action\`1)`, and then + `[EAC] Tearing down and releasing runtime` three times in the last 3.3s. + +**The 15:13 reference run self-exits too.** `Player-before-baseline.log` - the boot +that loaded the Dorm and was taken as good - ends with +`[15:14:05] AppExitState changed: Running => PreparingToExit` at 41.81s, followed by +the same EAC teardown. Its `Could not enable featured rooms display` warnings start at +23.1s and repeat (run 22: 23.2s, 30.2s, 37.2s, 44.2s, 51.2s - every 7.0s), so they are +periodic RRUI noise and not the precursor they looked like. Nothing in the recent +client work causes this exit; it is inherent to running this build with a Doorstop +module in the game root. + +### Patched in this build + +- `ExitSequenceTracePatch` - a `Priority.First` prefix on + `BestHTTP.HTTPManager.SendRequest(HTTPRequest)` (the same managed funnel + `NameserverConnectionPatches` logs every request in) that dumps + `Environment.StackTrace` the first four times the **shutdown's own requests** go out: + `/matchmaking/player/logout` and `/playersettings/playersettings`. The shutdown + sequence's first observable network act is the logout POST (server log run 22: + `19:45:38.785`, right after the notify-hub reconnect). Whoever calls it is the owner + of the shutdown, and the stack names them - no inference needed. +- `ExitStateVetoPatch` now also dumps the stack on its **first** block, which names the + caller of the `Running => PreparingToExit` transition (run 22 blocked three times with + no caller recorded). +- `UnityQuitVetoPatch` - the last wall. Prefixes **both** `CCKNJCHHBLL.GBGMBMGDGGD` + overloads (decompilation tokens 100667099 / 100667100; the stack trace prints the + `bool` one, so both are bound rather than guessing) and forces `__result = false`, + which is "the application does not want to quit" and makes Unity cancel the quit. + It **skips** the original instead of running it and rewriting the result: run 22's + trace shows the original *throwing* (`NullReferenceException` out of + `StatsigUnity.EventLogger.StopCoroutine`), and a throwing original returns before any + postfix can override the value Unity reads - a postfix-only veto would be silent + exactly when it matters. Escape hatch: `RADICADE_QUIT_VETO=0`, and `Enabled` is a + property (`=> !_disabled`) with a runtime-built log line, the run-22 folding trap + avoided. + +Deployed `BepInEx/plugins/RadicadeAugustClient.dll`, 39424 b, md5 +`71df28734d88fd3f9eca70f202e517bb`; the run-22 build is kept as +`RadicadeAugustClient.dll.before-run23-quitveto` (36352 b, md5 +`4a672a6b0ba59f73ca785cb318a6b824`). Build clean (0 errors, 0 warnings). +Preconditions re-checked at deploy time: `global-metadata.dat` 38654794 b md5 +`cc9c32014b5fc3dd1a2a8fb12376be15`, `boot.config` 173 b md5 +`dc2e5f7af7d5df736a4cc3477b4b1a1f`. + +### What the next boot has to say + +- `[ExitTrace] shutdown-request stack dump armed; [QuitVeto] wantsToQuit veto enabled=True` + at load, then `[ExitVeto] ... enabled=True`, the early `[ExitInit] allowed`, and the + `[HileGate] dropped` line at the first 30s tick. +- At the exit: `[ExitTrace] shutdown request ...` with the stack that ordered it, and + `[QuitVeto] wantsToQuit answered false` when the quit is refused. +- **If the process is still alive minutes after that** the plant works and the remaining + problem is cosmetic (whatever state the half-run shutdown left the UI in). If it dies + anyway, the kill is outside Unity's quit path (`TerminateProcess`-class, or EAC's own + runtime) and the next lever is to remove the module the scanner objects to instead of + fighting the reaction - i.e. bootstrap BepInEx without a `winhttp.dll` proxy in the + game root, which means a loader-side injection (manual mapping or a launcher) rather + than Doorstop. + +## Run 23b: neutralise the EAC runtime itself (ported from the supplied patcher) + +`C:\RecRoom\Downloads\patch` is a decompiled copy of **`RecRoomil2cppPatcher`** +(WaeBoi69, loaded by Reloaded-II). Its `EacPatcher.cs` is the piece this project needed: +a proven recipe for neutralising EasyAntiCheat on Rec Room. Five hooks into +`EasyAntiCheat.Client.dll`: + +| Method | Replacement | +| --- | --- | +| `Hydra.Runtime:IsActive` | `return true` | +| `Hydra.Runtime:Initialize` | no-op | +| `Hydra.NativeClient:Initialize` | no-op | +| `Hydra.NativeClient:IsStartedWithEasyAntiCheat` | `return true` | +| `Hydra.Runtime:get_Initialized` | `return true` | + +The pattern is the point: the *initialisers* are stubbed while the *queries* lie and report +a healthy, active anti-cheat - which is what the game's own checks read. + +Our build exposes the same surface. `BepInEx/interop/EasyAntiCheat.Client.dll` contains +`EasyAntiCheat.Client.Hydra`, `.Hydra.Runtime`, `.Hydra.NativeClient`, `.Hydra.NetProtect` +and exactly those members (`ilspycmd -o ... EasyAntiCheat.Client.dll`; output kept in +`.external/interop-decomp/EasyAntiCheat.Client/EasyAntiCheat.Client.decompiled.cs`). +Why this is the root-cause lever and not another wall: the report is `Obscured` + +`winhttp.dll`, i.e. a *native module scan*, and `NativeClient.Initialize` is where that +native client is brought up. + +Deployed in our plugin (`RADICADE_EAC_NEUTRALIZE=0` disables; armed by default): + +- `EacNativeClientStubPatch` - every static `Initialize` on `Hydra.NativeClient` is + skipped, so EAC's native client (home of the module scanner) never starts. +- `EacHealthQueryLiePatch` - `Hydra.NativeClient.IsStartedWithEasyAntiCheat`, + `Hydra.Runtime.IsActive` and `Hydra.Runtime.get_Initialized` answer `true`. +- `Hydra.Runtime.Initialize(...)` is deliberately **left running**: it does the game-facing + bookkeeping (event wiring `RecRoom.AntiCheat.EACManager` subscribes to via `JMAPOMLPFMI` + / `HINJPEHPJGO` / `AOFHJPLDLBC`) and its native step is the stubbed + `NativeClient.Initialize`. Stubbing it too is stage B if this is not enough. +- Type discovery is name-based with a hard-reference fallback + (`typeof(EasyAntiCheat.Client.Hydra.Runtime).Assembly` in its own method, so the JIT only + resolves it if the soft lookups came up empty) - nothing else in the plugin touches that + assembly, so at Load time `AccessTools.TypeByName` can legitimately see nothing. + +The game's EAC readiness door is already propped open (`AugustChallengePatches` fakes +`EACManager.GenerateChallengeResponse` and forces `EACManager.MGEAHKJIIMO` true), so stopping +the runtime does not leave one of the game's checks waiting on it. + +**Success marker, and it is unambiguous:** the `[HileGate] dropped anti-cheat report POST` +line must no longer appear at the 30s tick. No report means the scanner never reached its +verdict, which means no shutdown. The run-23 exit walls stay armed as the backstop, and their +`[ExitTrace]` / `[QuitVeto]` / `[ExitVeto]` lines say whether they were needed. + +Deployed `BepInEx/plugins/RadicadeAugustClient.dll`, 43520 b, md5 +`434972bf54968f1287643d8be61de8f7` (the run-23 plain build is kept as +`RadicadeAugustClient.dll.before-run23b-eacstub`, 39424 b). Build clean (0 errors). + +### Verification method correction + +The run-22 "constant folding" conclusion came from a hand-rolled check - decode the shipped +dll as UTF-16 and `String.Contains` the literal - and that method is **not reliable**: +against this build it reports `EasyAntiCheat.Client.Hydra.NativeClient` and +`[EacStub] EasyAntiCheat Hydra runtime neutralisation enabled=` as MISSING, and the +decompiled IL shows both present. Do not use it. Use the decompiler on our own output: + +```bash +ilspycmd -o .external/selfcheck 2023Patch/RadicadeAugustClient/bin/Release/net6.0/RadicadeAugustClient.dll +grep -c "EacStub" .external/selfcheck/*.cs +``` + +The run-22 lesson's *shape* is kept anyway (`Enabled` is a property over a +`static readonly _disabled`, log lines built at runtime) - it costs nothing, and it is the +only version that cannot be folded if the compiler ever does decide to. + +### Run 23b outcome (20:44 boot): three negative results, each of them useful + +Session: 20:44:21 -> 20:45:11 (50s). Boot was healthy and identical to the best +boots - `Moved to phase COMPLETE` at 20.24s, `SpawnLocalPlayer` 10568.89ms. + +1. **EAC's managed runtime is not the scanner.** `[EacStub] ... enabled=True` and + `Hydra.Runtime.IsActive answered true` appear, but there is **no** + `skipped ... Hydra.NativeClient.Initialize` line anywhere in the boot: the game + never calls it, i.e. EAC's native client was never started in this setup - our + own `EACManager` patches had already seen to that. The `winhttp.dll` verdict + still arrived. **The scanner is RecRoom's own**, not Hydra's. Auena's recipe is + still worth having (it is what keeps a modded client's EAC surface quiet), but it + is not the fix for this. +2. **Unity's quit is not the kill.** `[QuitVeto] wantsToQuit answered false (block + #1)` fired and the game **kept running for ~18s afterwards** - it went on loading + images, PUTting settings and fetching avatar items - so the veto did its job on + the request it saw. But `Player.log` shows `Trying to send photon room updates to + server while application is quitting - this should not be happening` from 36.1s, + i.e. Unity was already in its quit state *before* the veto fired: the first quit + request took the native path (`Application.Quit()` from native code does not + consult `wantsToQuit`), and the request we blocked was a later one. The process + ended at 52.85s. Windows' Application log has no `Application Error`/`Hang` for + it, and this run's shutdown again begins with `[EAC] Tearing down and releasing + runtime` at 34.89s. +3. **The absence of `ExitVeto`/`ExitInit` lines is itself the result**: this run's + shutdown never touched the managed exit state machine *at all* (no + `AppExitState changed`, no blocked transition, no initiator call). So the veto + and the tracer are aimed at a path this build does not use when the anti-cheat + verdict starts the shutdown; the verdict path is native end to end. + +The stack dumps did name the boundary honestly: `[ExitTrace]` shows +`DMD` followed by +`(il2cpp -> managed) SendRequest(IntPtr, Il2CppMethodInfo*)` - the logout and the +settings PUT are issued by **native code calling into managed** BestHTTP, so no +managed caller exists to name. Same shape as run 21's finding for the initiators. + +## Run 23c: capture the verdict, stop obscuring the module, trace `CheatManager` + +Given the above, the only remaining fix is to stop the verdict instead of the +reaction. Three changes, all in one build, all observable in one launch: + +1. **The run-14/15 module hiding is now OFF by default** (`RADICADE_MODULE_HIDE=1` + re-arms it). Runs 14/15 proved the scanner cannot be blinded - unlink and wipe + both applied mechanically and the report fired anyway - while "a loaded module + that the loader lists do not know about, with its MZ/PE headers zeroed" is the + most literal description of an *obscured* module there is, and `Obscured` is the + exact verdict the client files against `winhttp.dll`. It bought nothing and may + have been feeding the classifier; the plugin now logs what a Toolhelp module walk + - the same view a scanner gets - actually sees (`[ModuleList] ...`). +2. **`HileGate` is now a capture gate**, not a drop gate: `ShouldDrop` dumps the + report's whole wire form (`request.MethodType`, the URI and `request.RawData` as + UTF-8, with `GetEntityBody()` as the fallback) and then lets it through, so both + the client log and the server log carry what the client believes it found. The + server's `HileAPI` keeps only `Type` and `Message`; the body may carry more. + `RADICADE_HILE_GATE=drop` restores run 16. +3. **`CheatManagerTracePatch`** prefixes every declared method of `CheatManager` + and its nested types and logs name + elapsed ms (first two calls of each, hard + line cap). `CheatManager` (Assembly-CSharp, global namespace, a + `SingletonMonoBehaviour`) is the one managed class in this build that plainly + does local anti-cheat work - `OnTimeCheatDetected`, + `OnObscuredTypeCheatDetected`, `OnAdvancedMovementCheatDetected`, + `IPEMGBLAFIJ(string)`, `BFNMCBEDOLF(string)`, `CheckForDUIDMismatch`, and two + nested helpers carrying `appPath`/`filenames`. Nothing is skipped: observing only, + because the one detour this project must never take is the scanner's own funnel + (run 11). Whatever runs a few hundred ms before the `[HileGate] captured` line is + the verdict, and its name is the next patch target. + +Deployed `BepInEx/plugins/RadicadeAugustClient.dll`, 48128 b, md5 +`018b991ab951196a76e4ed7f7f3eac4b` (run-23b build kept as +`RadicadeAugustClient.dll.before-run23c-capture`, 43520 b). Build clean (0 errors), +verified by decompiling the built dll (`ilspycmd -o .external/selfcheck`), not by +string search. The run-23 exit walls stay armed as the backstop. + +### Run 23c outcome (21:02 boot): the trace named the producer + +The `CheatManager` trace paid for itself on its first run: + +``` ++15113ms CheatManager.OnApplicationFocus ++15366ms CheatManager.MJLAGDJLOBC ++15367ms AALEGIEIKEL.LLPNCADGDNI ++15368ms AALEGIEIKEL.LLPNCADGDNI ++15368ms [HileGate] anti-cheat report captured (full wire form) +``` + +`CheatManager.MJLAGDJLOBC()` - a private no-argument method on the anti-cheat +MonoBehaviour - runs 2ms before the report, with `AALEGIEIKEL.LLPNCADGDNI(string)` +(a helper whose class carries an `appPath` field) called twice on the way. A pass +over files in the game directory is exactly what would decide that a proxy DLL in +the game root is `Obscured`, which is the verdict `[0:winhttp.dll]` reports. + +Two things the capture taught us about the report itself: `RawData` is **empty** and +the fields live in the entity body (36 bytes = `Type=Obcured&Message=[0:winhttp.dll]`, +matching run 9's decode exactly), and the body text was not printed by the first +version of the dumper - it printed the byte count only. Now it prints both. + +Also confirmed: `[ModuleList] winhttp.dll is enumerated by a Toolhelp module walk: +C:\RecRoom\2023-08-29T06-53-33Z\WINHTTP.dll` - run 14's PEB unlink and +run 15's PE wipe really are off now, and the module is back in the list a scanner reads. + +### What the exit walls actually buy, now measured on screen + +The 21:02 boot lost its session to the same shutdown, but this time the process +survived (the Unity quit veto answered `false` and the game kept running). What the +player sees in that state is the run-12 prediction, reproduced on demand: + +``` +Error joining room instance RoomInstance - ID:20871 DORM_ROOM:25/32 +NullReferenceException: Object reference not set to an instance of an object. +Attempting to go to dorm +Not logged in to matchmaking service +Not logged in to matchmaking! not letting me in no more +``` + +The server log agrees with the order: the report arrived for the first time in these +boots (`21:02:44.255 POST /api/PlayerReporting/v1/hile -> 200 body=true`, because the +gate is now in capture mode) and the client logged out 156ms later +(`21:02:44.411 POST /matchmaking/player/logout`). So the walls convert a silent exit +into a live-but-logged-out client: better for diagnosis, useless as a game. + +Server-side note for future probing: `localhost:3030` speaks **TLS**. A plain-HTTP +probe answers `000` in ~5ms and looks like a hung server; `curl -sk +https://127.0.0.1:3030/...` answers `200` in ~11ms. The server was healthy throughout. + +## Run 23d: suppress the scan instead of the reaction + +`CheatScanSuppressPatch` skips `CheatManager.MJLAGDJLOBC()` (env `RADICADE_CHEAT_SCAN=0` +leaves it alone). Given the trace, that method is the producer of the verdict, so +skipping it means no verdict, no report and no shutdown - the only stopping point that +leaves a playable client, since the reaction is native end to end and the walls only +trade an exit for a zombie. + +Supporting changes in the same build: + +- `CheatTrace` per-method limit 2 -> 4, so if the skip is not enough, the third and + fourth calls of every candidate are still on the record and a miss stays diagnosable. +- `[HileGate] armed` now states which mode it is in (capture vs `RADICADE_HILE_GATE=drop`) + instead of always claiming the drop. + +Deployed `BepInEx/plugins/RadicadeAugustClient.dll`, 50176 b, md5 +`b96b1b8926a1ec172e2b84ea0a85d179` (run-23c build kept as +`RadicadeAugustClient.dll.before-run23d-scan`, 48128 b). Build clean, verified by +decompiling the built dll. + +Expected on the next boot: the session stays logged in past the 15-40s window it has +never survived, with **no** `[HileGate] captured` line and a `[CheatScan] skipped +CheatManager.MJLAGDJLOBC() (#1)` where the verdict used to appear. If instead the +process dies exactly at the skip, that pass runs on a scanner thread and joins the +run-11 forbidden list (`KDJDKJNPELI.IJHOPKJFKBI`) - also a result, and the lever after +it is to remove the module the scan objects to rather than the scan itself. + +## Run 24 (2026-09-24 21:0x) - the first real crash of the project, and how to name it + +The skip worked. `[CheatScan] skipped CheatManager.MJLAGDJLOBC()` fired twice, `[HileGate] +captured` never appears, the server log has no `/api/PlayerReporting/v1/hile` POST for the +whole session, and there is no `matchmaking/player/logout` - the anti-cheat chain that ended +every previous run is genuinely gone. With it gone the client did something it had never done: +it **entered RecCenter**. + +One second later it died. This time it is not a graceful shutdown: + +``` +Application Error 09-24 21:11:58 + Faulting application: RecRoom.exe, 2020.3.33.23162 + Faulting module: GameAssembly.dll + Exception code: 0xc0000005 (access violation) + Fault offset: 0x00000000029a79ec +``` + +`Application Hang` has nothing, and the previous day's 18 RecRoom faults are a *different* +signature ({coreclr.dll +0x1d1fdd}, {GameAssembly.dll +0x7567f0 / 0x80000003 breakpoint}, +{unknown / 0xffffffffffffffff}); 09-24 had no RecRoom fault at all until this one. So this is +new code territory, reached only now that the room load is not being cut short at 15-40s. + +### Reading the dump without a debugger + +WER left `%LOCALAPPDATA%\CrashDumps\RecRoom.exe.8396.dmp` (61 MB). No cdb/windbg/dotnet-dump +is installed, so `tools/minidump_stack.py` reads the minidump directly: exception stream, +module list, thread context (amd64 Rsp/Rbp/Rip at 0x98/0xA0/0xF8) and the stack pages, then +prints every stack slot that points into a module as `module+0xrva`. `--map` resolves those +through the RVA table the client writes (below), so no debugger is needed at any point. + +It reproduces the WER record exactly, and adds the two things WER does not say: + +``` +fault: ACCESS_VIOLATION (0xc0000005) at 0x7ffc16f679ec + read of address 0x93 + in GameAssembly.dll+0x29a79ec + rsp 0x41fb78e6d0 rbp 0x24447df8250 rip 0x7ffc16f679ec +``` + +A read of **0x93** means a null object with a field at offset 0x93 was dereferenced, and +il2cpp left the check off - which is how pointer-arithmetic paths (`fixed`/`Unsafe`/native +arrays) fail, not ordinary managed field access (that raises NullReferenceException). The +stack around it (innermost first) is ~20 GameAssembly frames, with +`GameAssembly.dll+0x2881f73` the closest return address and `coreclr.dll+0x11f66a` +(BepInEx's .NET runtime) and `UnityPlayer.dll` frames behind them: + +``` +rsp+0x28 GameAssembly+0x2881f73 rsp+0x108 GameAssembly+0x11f2eae +rsp+0x98 GameAssembly+0x59adea8 rsp+0x1a8 GameAssembly+0x1201e67 +rsp+0xa8 GameAssembly+0x7764a5 rsp+0x218 coreclr+0x11f66a +rsp+0xd8 GameAssembly+0x49dcd3f rsp+0x2d8 GameAssembly+0x1202175 +``` + +### What the last requests were (the "last API was the heartbeat" question) + +``` +21:11:50.944 POST /matchmaking/player/heartbeat 200 14.5 ms len=631 +21:11:53-57 POST /datacollection/data/event 200 x3 +21:11:55.395 GET /clubs/club/home/me 200 +21:11:55.433 GET /data/commonQuery/userSegment 404 +21:11:55.648 GET /lists/algorithmiclists/HotList 200 1171 b +21:11:55.675 GET /rooms/rooms/bulk?id=2..19 200 21460 b +21:11:56.113 GET /rooms/rooms/3/interactionby/me 200 47 b +21:11:56.124 GET /rooms/rooms/3/experience 404 +21:11:56.170 GET /images/RecCenter?width=1024 200 71351 b +21:11:57.213 POST /datacollection/data/heartbeat 200 + 21:11:58, dump written 21:12, notify-hub SSE closes 21:12:04 +``` + +The request just before the crash is the data-collection heartbeat, and the matchmaking +heartbeat 7 s earlier is the only call in the session with an unusual duration (14.5 ms vs +sub-millisecond everywhere else, 631 b response), so both are on the list. But the crash is +1 s after the RecCenter art fetch and is a null dereference inside the game's own compiled +code, so the room-3 load is the stronger reading. `rooms/rooms/3/experience` returning 404 is +not obviously the cause: the reference server has no such route either. + +### The new instrument: rva -> method + +An offset from a module base is worthless until it is a method name, and IL2CPP will hand the +name over: the MethodInfo that `il2cpp_class_get_methods` returns begins with the compiled +method's native entry point. `2023Patch/RadicadeAugustClient/CrashAddressMap.cs` walks +`il2cpp_domain_get()` -> assemblies -> images -> classes -> methods (all raw entry points are +public in `Il2CppInterop.Runtime.IL2CPP`), writes `rvaimageClass::Method` to +`BepInEx/radicade-addr-map.txt`, and logs the methods bracketing the target RVA. It is a +background thread started 5 s into the boot, it never blocks the login, and it does not need +the crash reproduced - the table is static. `RADICADE_ADDR_MAP=0` disables it and +`RADICADE_ADDR_TARGET=0x...` re-aims it at the next WER offset, so no code change is needed +for the next crash. + +Deployed `BepInEx/plugins/RadicadeAugustClient.dll`, 55296 b, md5 +`129e9b5ef7e55820f05d8440b9d5b900` (run-23d build kept as +`RadicadeAugustClient.dll.before-run24-addrmap`, 50176 b). Build clean (0 warnings), and the +shipped IL was verified by decompiling the built dll (`CrashAddressMap.Start()` is called from +`Load`, and the enumeration + `.txt` path are present). + +Next: one boot, then `sort -k1 BepInEx/radicade-addr-map.txt` offline and resolve +`0x29a79ec` plus the stack RVAs above through it - which names the method that dereferenced +null, and the frames that led there. + +## Run 25 (2026-09-24 21:27) - RecCenter loads, then a second, different crash + +This is the run that made the room load irrelevant: **the room finished loading**. The +2023 build's own load profiler ends with `[Run Success Callbacks]`, `SpawnLocalPlayer` +total 9857 ms, `MoveToPhase(COMPLETE)`, and the server served everything in order +(`matchmaking/matchmake/room/3` -> instance 20876, room 3 metadata, photon token, +`reportjoinresult`, `verifyRole`). The anti-cheat chain stayed off (`CheatScan` skipped +`MJLAGDJLOBC` twice, no `[HileGate] captured`, no hile POST, no logout). + +Then, ~10 s after the spawn, the client died again - at a **different address**: + +``` +Application Error 09-24 21:28 RecRoom.exe / GameAssembly.dll + Exception code 0xc0000005 Fault offset 0x0000000003dc0c57 +``` + +`0x3dc0c57` is inside `Unity.Entities.ChunkDataUtility::GetComponentDataWithTypeRO` +(exact entry point match), and the faulting instruction is `mov rbx, [rdx+0x88]` with +`rdx = 0xFFFFFFFFFFFFFF77` - a read of address 0xFFFFFFFFFFFFFFFF. So the game handed an +ECS chunk accessor a garbage pointer, and il2cpp had no null check to catch it. + +Compared with the 21:11 crash, that is two different fault sites (`0x29a79ec` is a 26-byte +unnamed function; read of address 0x93) but the same shape: a pointer-arithmetic dereference +inside the game's own compiled code, roughly 1-3 s after a periodic RRUI +"Could not enable featured rooms display" warning. That warning is not the trigger - it also +fires every ~7 s in the dorm and nothing happens. Likewise the 35 `RecCenter_Chair ... has an +RigidbodyEx but no EmbodiedObject/RbexEntity/TransformEntity component` errors are pre-existing +noise: they appear 35 times in both the crashing run and in the 21:02 run, dorm included. + +### Tooling that made this readable + +- `BepInEx/radicade-addr-map.txt` - 254 171 methods from 305 images, written in **141 ms** + (18.5 MB, benign: a background thread, 5 s into boot). +- `tools/pe_unwind.py` - parses `.pdata` RUNTIME_FUNCTIONs and UNWIND_INFO (PUSH_NONVOL, + ALLOC_SMALL/LARGE, SET_FPREG, SAVE_NONVOL(_FAR), PUSH_MACHFRAME, chained info) and replays + a prologue to recover the caller - a real x64 unwinder. Two bugs found and fixed while + writing it: `ALLOC_SMALL` carries its size in the op-info nibble (a zero operand advanced + RSP by 8 instead of 0x28, which produced one bogus frame), and a past-the-end lookup used + to reuse the previous function's unwind info and manufacture frames - a leaf function with + no entry is now unwound from `[rsp]` instead, as a debugger would. +- `tools/minidump_stack.py --unwind` - walks the dump's faulting thread; resolves the WER + offset against the dump ("matches the WER fault offset") before using either. +- `tools/resolve_rva.py` - function boundaries from `.pdata`, names from the boot-time map, + and it says `UNNAMED` when the nearest name is not the function's own entry point. + +The verified chain for the 21:28 crash (innermost first, exact-name frames only): + +``` +Unity.Entities.ChunkDataUtility::GetComponentDataWithTypeRO <- crash + (unnamed generic-instantiation frames, no metadata entry point) +IHDIFFAKHOM::JEONIAELMNM + (unnamed) +EHEHGHCKFII::System.Collections.IEnumerable.GetEnumerator +Tool::CCFENLNCIOG -> Tool::LPAOCFPMJFD -> Tool::RpcOnReset + -> Tool::AuthorityResetToDefault -> ToolCleanup::BFFGEGKNGMM +RecRoom.Core.Updates.StaggeredUpdateManager::DGHHCHACHOM + -> (unnamed) -> UnityPlayer.dll main loop +``` + +Every frame's own `.pdata` entry was used (no leaf guessing except where flagged), and the +walk terminates in UnityPlayer's scheduler, so this is a call chain rather than a stack +picture. + +### Run 25b: the ObjectModel A/B (server-side, reversible) + +Both crashes are inside `Unity.Entities` code reached from a staggered update, and the dorm +has never crashed. `ObjectModel_EnableForRoom_2022_07_01` is exactly the split test that +gates Rec Room's ECS-backed object model for rooms, and this server shipped it as +`Allowed`, so the 2023 client turns that model on whenever it enters a room. It is now +`Disallowed`: + +- `Server/RecNet/Config/SplitTestConfigs.cs` - `HardOverrides` (fresh-DB seed), +- `Server/DB/DBConfig.cs` - `PlaceholderConfig` (same value, same reason), +- the live `GameConfig` row `splitTestHardOverrides`, updated in place with + `REPLACE(...,'ObjectModel_EnableForRoom_2022_07_01=Allowed','...=Disallowed')`. + +The third one is the only edit that actually reaches this client: `UpsertGameConfigValue` +and `EnsureSplitTestHardOverrides` only ever *add* what a row is missing, and +`RepairSplitTestConfigs` documents the stored row as admin-managed ("never clobber a value +that is already set"), so the DB row is the source of truth. Pinning the key in +`GameConfigAPI.Local2023Overrides` was tried first and reverted: it contradicts that design +*and* `RecNetShapeConformanceTests.GameConfigs_SuppressInvalid2023SplitTestConfigs`, which +asserts the DB row is filtered and passed through rather than replaced. + +Verified through the endpoint the client actually calls: + +``` +GET /api/gameconfigs/v1/all -> ObjectModel_EnableForRoom_2022_07_01=Disallowed +``` + +Server restarted (PID 11884, 3030/3031), build clean, `SplitTest`/`GameConfigs_` tests 6/6. + +Also eliminated while checking: room save data. Every subroom's `DataBlob` is empty - the +working dorm room 25 included - and `SubRoomHistory` is empty, so "RecCenter's data is +missing" is not a difference between a room that survives and one that does not. The client +never requests a room blob at all (`GET /cdn/room/` is only the CDN index, `{"status":"ok"}`, +22 b). The frames the map cannot name are generic instantiations: IL2CPP gives each one its +own code but they share a single metadata entry point, so `il2cpp_class_get_methods` never +sees them. Naming those needs the runtime instantiation table, not this map. + +### Run 25c: crash 3's own fault header, and the prop-binding difference + +Crash 3's `fault:` block had not been printed before (an earlier `sed` filter cut it off). It is +the same fault as crash 2, one instruction shape away: + +``` +fault: ACCESS_VIOLATION (0xc0000005) at 0x7ffc18cc728d + read of address 0xffffffffffffffff + in GameAssembly.dll+0x470728d (matches the WER fault offset) + rsp 0xc42b34ea20 rbp 0x261c24784a0 +``` + +resolved (crash 3, exact names only): + +``` +Unity.Entities.EntityComponentStore::GetComponentArraySize region <- crash, +0x140 = UNNAMED + (unnamed generic instantiations) +IHDIFFAKHOM::JEONIAELMNM + (unnamed) +EHEHGHCKFII::System.Collections.IEnumerable.GetEnumerator + (unnamed) -> Tool::CCFENLNCIOG -> Tool::ResetToDefaultCollisionLayer + -> Tool::LPAOCFPMJFD -> Tool::RpcOnReset -> Tool::AuthorityResetToDefault + -> (unnamed) -> ToolCleanup::BFFGEGKNGMM -> ToolCleanup::CJGILKPKDOL +RecRoom.Core.Updates.StaggeredUpdateManager::DGHHCHACHOM + -> (unnamed) -> (unnamed) -> UnityPlayer.dll scheduler +``` + +A read at `0xffffffffffffffff` is an index that came out as `-1`: some `TypeIndex`/component +lookup in the tool's component set has no answer, the code indexes with it anyway, and the +base pointer lands one byte past the end of the address space. + +**The `/similar` lead is dead.** The route does exist - `Server/RoomServer/V2_RoomsAPI.cs` +(`[Route("rooms/rooms/")]`, `GET {id:long}/similar`, our own local addition) - and it did +answer `200 len=10166` for room 3, followed 2.5 s later by +`Error refreshing rooms from source RoomSimilarity: An error occurred`. But run 24 (crash 2) +never requested `/similar` and never logged that error, and it died with the identical chain, +12 s after the same room reported COMPLETE. Two runs, same crash, one with the carousel and +one without: the carousel is a real but separate bug (our payload fails the client's +RoomSimilarity parse), not the cause of the AV. + +**Timing is deterministic.** Room load COMPLETE -> crash: + +| run | COMPLETE | crash | delta | props spawned | props -> crash | +|---|---|---|---|---|---| +| 24 (21:28) | 32.50 s | 21:28:33 | ~12 s | 28.41 s | ~16.6 s | +| 25 (21:44) | 100.50 s | 21:44:55 | ~13 s | 96.31 s | ~16.7 s | + +So it is a timer that fires ~12 s after the room finishes loading, not something the player +does: in run 24 the client sat in the room on heartbeats, in run 25 the watch menu was open. + +**The one thing that actually differs between the room that crashes and the room that does +not is prop binding.** RecCenter logs, once, at scene-setup time: + +``` +[RecCenter_Chair] / [Chair_White_Stationary] / [RecCenter_Couch] (N) + has an RigidbodyEx but no EmbodiedObject/RbexEntity/TransformEntity component +``` + +Exactly 35 in both crashing runs; **zero** anywhere in the dorm (which shares the same log). +Those objects exist in the Unity scene with a `RigidbodyEx` and are missing the ECS +companions the ObjectModel needs. `Tool::ResetToDefaultCollisionLayer` iterates exactly that +state - a tool's collision-layer component set - which is why it is the code that dies. + +**Run 25b's A/B aimed at the wrong key.** `ObjectModel_EnableForRoom_2022_07_01` never +reaches the client's ObjectModel path - it does not appear in `Player.log` at all. The gate +the client actually reads is the GameConfig family, served from the DB and logged at boot: + +``` +GameConfig 'UGC.ObjectModel.Allowed' = True (DB row: UGC.ObjectModel.Allowed = true) +GameConfig 'UGC.ObjectModel.AllowedInRoomV1' = True +GameConfig 'UGC.ObjectModel.ScalabilityAllowed' = true, .R2Allowed = true, .DormAllowed = false +``` + +and the load log shows the path being taken, for the dorm as well as for RecCenter: + +``` +[DATA_DOWNLOADED] Fetched Deserialization Data... +START: UnloadSceneToEnableObjectModelTask + Checking for Object Model Enabled flag... +[Moving to phase INIT_OBJECTMODEL...] -> [MoveToPhase(INIT_OBJECTMODEL)] +``` + +`Disallowed` therefore proved nothing about ObjectModel: the model ran anyway. Note the +seeded `UGC.ObjectModel.*` values (incl. `DormAllowed = false`) are the reference's, so +ObjectModel-on-for-rooms is the authentic August-2023 configuration, not drift of ours. + +**Where the room data should have come from.** The load's `[Downloading Data]` report for +RecCenter: + +``` +[AssetBundle_Download] 0.01ms | [SubRoom_Download] 0.01ms +[SuperRoom_Download] 0.00ms | [PlayerSave_Download] 4.00ms +``` + +`SubRoom_Download` is a no-op, consistent with every subroom `DataBlob` being empty. If the +prop -> ECS bindings normally arrive with the sub-room save, an empty save is precisely what +leaves scene props with a `RigidbodyEx` and no `EmbodiedObject`/`RbexEntity`/`TransformEntity`. + +Open, in order of expected value: + +1. Serve a real RecCenter sub-room save (or otherwise complete the prop bindings) and see + whether both the 35 bind errors and the AV disappear - this tests the cause directly. +2. A/B `UGC.ObjectModel.Allowed=false` (+ `AllowedInRoomV1`) in the DB row: cheap, + reversible, and it removes the whole ECS object path. Divergence from the reference, so + treat it as a probe, not a fix. +3. Stopgap only: make `Tool::ResetToDefaultCollisionLayer` (0x1201e32..0x1201e90) a no-op + from the client plugin. Removes the crash without fixing the corrupt entity, so the tool + simply never gets its collision layer restored. +4. Separate bug: fix our `/rooms/rooms/{id}/similar` payload shape so the RRUI carousel + stops failing with `Error refreshing rooms from source RoomSimilarity`. + +## Run 27 (2026-09-24 22:22, tool guard deployed): no crash - and the join NRE named + +Player.log 22:22:34 -> 22:36:23 (~832 s, no crash), `LogOutput.log` carries 9 `[ToolGuard]` +skips, all of them game-role nodes and shape containers (`[GameRoleNode]_INJHAOJMPFF_*`, +`[StandaloneShapeContainer](Clone)`) rather than the RecCenter chairs, and **zero** +`has an RigidbodyEx but no EmbodiedObject/RbexEntity/TransformEntity component` lines - every +run that dies has exactly 35. Three joins: + +| time | target | result | +|---|---|---| +| 22:22:42 | dorm 25/32, instance 20886 | COMPLETE | +| 22:29:51 | room 54/57, instance 20887 | COMPLETE | +| 22:30:17 | dorm 25/32, instance 20888 | **FailedToSpawnPlayer (8718)** | + +The third is the room bug the guard did not cover: the load walks every phase to +`SCENE_LOADED`, then dies inside `SETUP_ROOM_OBJECTS`. + +``` +[471.4358] Starting RunFor.All handler 729481d6-1503-47c8-b401-f98f736a13f2 (1 handlers) +[471.4486] [Error | RoomOperation] Error during operation InitialRoomLoad-a820aca0-...: + Object reference not set to an instance of an object. +[471.4830] Reporting result FailedToSpawnPlayer / (Code 8718) to RecNet Matchmaking ... +``` + +Stack (managed frame bottom of the throw): + +``` +EMKMMPEIGLD.FACCFIGFBIP(IEnumerable) +EMKMMPEIGLD.HLLNBBPLCAH(Tuple) +KOGBHMMNAJA1.EIAKBDPCFOD(Action1) <- broadcaster +EMKMMPEIGLD.KOIPGJONEBE(LPPEEGCNOPD, CancellationToken) +IGOEMHGCPBC2+DGLCCMDLGNI.Invoke(request, ct) +AGCMEKDMOEG+PAIKPNCMAPO.<b__2>d.MoveNext() +AGCMEKDMOEG.FFAFPFAGKDH(Guid, List, IEJHLHGIEFP, LPPEEGCNOPD, CancellationToken) +<b__0>d.MoveNext() +``` + +Two frames above the NRE: + +``` +[22:30:15] Operation SaveRoom sent. Waiting... +[22:30:15] Unobserved exception throw ... BDADBLBKLJN.OABJEHOGBIM(GKENAAINMOP) +[22:30:17] Error during operation RoomSave-6ad9514c-...: Object reference not set to an + instance of an object. +``` + +Both sites resolve by name without a crash dump: `grep -la EMKMMPEIGLD +BepInEx/interop/*.dll` answers **`RecRoom.Keepsakes.Runtime.dll`**, and `BDADBLBKLJN` is +`RecNet.Runtime.dll`. So the handler that faults a room load is the **keepsake runtime's**: + +* It runs in `SETUP_ROOM_OBJECTS`, again in `RUN_MIGRATIONS`, again in + `POST_DESTROY_OLD_OBJECTS` - one `729481d6` per keepsake pass - and the identical NRE is + thrown on the loads that *survive* (22:22:49.5, 22:29:59.0) as an unobserved task exception. + Chronic, and fatal only when it lands inside the phase run. +* Its fetches line up exactly: `GET /api/keepsakes/rooms/25` + `categories` + `globalconfig` at + 22:22:48.988, `GET /api/keepsakes/rooms/54` at 22:29:58.388. The failed load at 22:30 makes + **no keepsake request at all** - it dies before/inside that pass. + +What we were serving, and what the reference serves (`apps/api/src/routes/gameplay.ts`): + +| route | ours (run 27) | reference | +|---|---|---| +| `GET /api/keepsakes/rooms/{id}` | 200 `{FIOFPPHNMHM:[],EBKHAGMFFEK:[],OFNJMGKKKJP:[]}` (69 b) | **204, no body** - "the client treats a body here as data" | +| `GET /api/keepsakes/categories` | counted set with one fabricated keepsake (`IIILIMOMKDK`...) | `{Results:[],TotalResults:0}` | +| `GET /api/keepsakes/globalconfig` | `{NPCHGMEHAPA:true,NEFKKONMGJI:100,PJMJGIKKGJB:true}` | `{KeepsakeFeatureEnabled:true,KeepsakeRoomLimit:10,SocialXpBoostEnabled:false}` | + +The reference's note on `categories` is the same failure mode one layer down: the client parses +it as an object and "an array fails it outright - expected:'{', actual:'[', at offset:0, +logged as 'Failed to get keepsake categories' - **which takes the keepsake load down with +it**". Fabricated entries with guessed obfuscated field names are that same class of input: a +keepsake the client resolves that exists nowhere. + +**Fix (server, this run).** `Server/API/Keepsakes/KeepsakesAPI.cs` now answers exactly what the +reference does - 204 with no body for a room's keepsakes, an empty counted catalog, and the +client's own field names on the config. Tests pin all three shapes +(`StartupApiCompatibilityTests`: empty catalog, `NoContentResult`, config names). + +**Fix (server, same session).** The save that preceded the failure also faulted, and the +session's only subroom save (`POST /rooms/rooms/54/subrooms/57/data`, 200, 5718 b) is the call +it followed. The reference is explicit that this response is the one envelope the client reads +differently: + +> `value` is `{ room, subRoomDataSave }`, and `error` is NULL rather than `""`. The +> `subRoomDataSave` is camelCase with a different field set from the PascalCase `CurrentSave` +> embedded in the room ... Don't unify the two projections. + +We were serving the room's `CurrentSave` there - PascalCase, with none of `unityAsset`, +`unityAssetHash` or the echoed `dataBlobHash`. `RoomUtils.BuildSaveResponse` now serves the +documented projection (and `dataBlobHash` is read off the save body and echoed), while the +room's own `CurrentSave` is untouched. The other documented trap in this path is fixed too: a +subroom carries `CreatorAccountId`, null until its first save and the saver's id after - "the +client NREs on a null one" - which we did not send at all. + +Tests: 560 pass, including `SaveAcceptsTheJsonEnvelopeThe2023ClientPosts` (now asserting the +camelCase save + a non-null subroom creator), `SaveResponseEchoesTheBlobHashItWasGiven`, and +`AnUnsavedSubRoomReportsNoCreator`. + +Server relaunched (development environment, cwd `Server/`, `server-console-run28.log`), and the +new shapes verified live: `rooms/25 -> 204`, `categories -> {Results:[],TotalResults:0}`, +`globalconfig -> KeepsakeFeatureEnabled/RoomLimit 10/SocialXpBoost`. + +**How to read the next run.** The join is fixed if `729481d6` completes on the dorm load and the +post-load keepsake pass stops throwing: + +```bash +grep -c "Unobserved exception" Player.log +grep -n "729481d6" Player.log +curl -sk -o /dev/null -w '%{http_code}\n' https://localhost:3030/api/keepsakes/rooms/25 # 204 +``` + +Still open: the missing SignalR `UnsubscribeFrom` the client calls on every transition +(`autoloc.job`, `HubException: Method does not exist`), and real keepsake storage - the emptied +catalog means no keepsake can be placed until it exists. + +## Run 28 (2026-09-24 23:25, live): the watch page's missing buttons are two missing routes + +The watch's "This Room" tab makes exactly three requests as it opens, and the middle one was the +only failure: + +``` +[23:27:18.014] GET /rooms/rooms/25/interactionby/me -> 200 len=47 +[23:27:18.031] GET /rooms/rooms/25/experience -> 404 len=0 +[23:27:18.097] GET /images/default.png?width=1024 -> 200 len=80001 +``` + +The tab renders with SETTINGS alone - no LOAD, no SAVE ROOM, no ROOM STATS, no subroom (room +list) button. The client logs each failure as a crash-report line +(`OBHEMJEJNMK: HTTP Error 404` through `GFIBIELMKBM+<>c__27`1[TResponse].NOFENLOLAIH`), 10 of +them in the session against 12 `rooms/rooms/25/experience` 404s, and it keeps re-asking rather +than settling. + +Not permissions: the room payload already reports the viewer's role +(`Roles: [{"AccountId":2,"Role":255,...}]`, 255 = Creator) and `CreatorAccountId: 2` - which +is also how the subroom `CreatorAccountId` added in run 27 shows up on the wire. + +Neither `rooms/{id}/experience` nor `rooms/{id}/experience/player` existed on our server at all. +The reference's note on the player half is the cost of that: nothing tracks per-room progress, so +it serves an empty list - "**which the client reads as 'no progress in this room', where a 404 +would stall the room load**" - and the settings half is a bare two-key object, `Enabled: false` +with `DailyLimit: 1000`, "the cap that WOULD apply ... the client reads both keys whatever +`Enabled` says". Both added to `V2_RoomsAPI` (`GetRoomExperience`, `GetRoomExperienceForPlayer`). + +The other 404 on the same page is `econ/econ/roomGiftDropShops/room/{id}`, asked for on every +run (run 16 through run 28, 1-5 times each) and never once served. The reference answers an empty +list - "Empty stub so the client doesn't 404" - so `RoomGiftDropShopAPI` now does. It is the only +room-economy route this build asks for: `roomInventory`, `roomOffer`, `roomEconConfig` and +`ugcPurchasables` are never requested (checked across all six server logs), so no stubs for them +were added. + +Tests: 563 pass, adding `RoomExperienceAnswersTheTwoKeysTheClientReads`, +`RoomExperienceForPlayerAnswersAnEmptyList` and `RoomGiftDropShopsAnswerAnEmptyList`. Server +relaunched (`server-console-run29.log`, cwd `Server/`), and every route verified live: + +``` +rooms/rooms/25/experience -> 200 {"Enabled":false,"DailyLimit":1000} +rooms/rooms/25/experience/player -> 200 [] +econ/econ/roomGiftDropShops/room/25 -> 200 [] +api/keepsakes/rooms/25 -> 204 (empty) +``` + +## Run 29 (2026-09-24 23:50, live): MakeItToMidnight imported, and what the create route dropped + +`C:\RecRoom\Downloads\MakeItToMidnight` is a rec.net archive of the August 2023 RRO +MakeItToMidnight: `room_data.json` (the room, its one subroom "Home", and **100** revisions of that +subroom's save), `blobs/*.room` (137 MB, one scene blob per revision) and `image/*.png` (the +thumbnail). There is no Unity asset bundle in it and none is needed - a room's objects and prefabs +are shipped in the game build and a save only names them - so the subroom is imported with an empty +`UnityAssetId`. + +Two things decide which revisions may be imported, and both belong to the client that has to open +them: + +* `PersistenceVersion`. The revisions from this client's own build week (2023-08-21 through + 08-28) are all **61** - one v63 on 08-21 aside - which is the schema the build writes; the 67 + later ones climb 63, 64, 66, 68, 70, 72, 77, 87, 88, 103, 128 and 138 (the last from 2024-07) and + a newer save than the client can read is a room that opens broken. The taco importer's gate was + "38 or lower" because its archive was 2020-2022: the number is a property of the archive's era, + not a constant. +* `CreatedAt`, as the second net. **33 revisions pass** (2023-08-21T20:20 through 2023-08-28T21:00, + 44 970 347 bytes). + +The newest of those is the LIVE save and every other one is posted through the game's own save +route, so each lands in the subroom's history exactly as an ordinary save does - and each POST +pushes the revision it replaces into that history, which is what makes the whole list restorable. +The room is created on the OLDEST revision and walked forward, because whichever revision is posted +LAST is the one that stays live: creating it on the newest and then posting the older ones leaves +the room serving the second-newest save, which is what the first attempt did. + +`tools/import_room_dump.mjs` runs it in resumable phases (`plan|upload|register|verify|redate`), +recording what it uploaded, created and posted in `artifacts/room-dump-import/.json`. Blobs +keep the archive's own names (`5k1vmec9n5jf4dymy0zkjligj.room`), so a save in CDN storage stays +traceable to the dump it came from, and the `redate` phase re-stamps the revisions with the dates +the archive recorded - the save route stamps everything it records with the moment of the POST, so +the restore list would otherwise read "just now" 33 times and be useless to pick from. + +Result: **room 55 / subroom 58**, owner 2, Public, `MaxPlayers` 5, `WarningMask` Scary (1), tagged +`import`, thumbnail `798fe98e05b944999ab8c76181f45367.png`, scene +`a75f7547-79eb-47c6-8986-6767abcb4f92` (Maker Room), and 33 saves carrying the archive's dates. + +### What the import exposed in `admin/rooms/create/room` + +The first create answered `Created as 55!` and the row was wrong: `Accessibility` 0 (Private), +`WarningMask` 0, `Tags` empty. The route reads its body through `JsonExtension.Get`, which could +only read scalars: + +* An **enum** is not a type `JsonValue.GetValue()` knows, so it threw and the caller took its + fallback - every room created through this route came out Private with no warning mask, and + nothing in the response said a field had been dropped. +* A **collection** never got that far: `GetValue()` on a JsonArray throws before the scalar + read, so the tag list fell back to empty. The taco importer's `Tags` never landed either. +* The bool path ("1" and "true" both mean true) was already there, but read its node through + `GetValue()`, which hands back a `JsonElement` for a body parsed from JSON rather than an + `int` or a `string` - the check could not see either shape. + +`Get` now reads the node's own `JsonValueKind`, converts an enum from its number or its name, +deserializes a JsonArray/JsonObject whole, and keeps the bool spellings. Room 55's three fields were +corrected directly in SQL, because the running server could not be rebuilt mid-session: the live +client holds `Server/bin/Debug/net9.0/Server.dll`, so the verification build goes to +`.external/verify-build/bin/` (`-p:BaseOutputPath=...`) and the apphost exe is skipped entirely. + +A second, separate bug in the same block: `loadScreen` is `NOT NULL` with a CLR default of `[]`, and +a payload without `LoadScreens` wrote **null** over that default - the insert failed with a +required-property error, a 500 for any import that did not happen to send an empty list. + +Tests: **569 pass**, adding `AdminRoomCreateTests` - enum binding by number and by name, the bool +spellings, the tag list with its tag types, the defaults an absent field still gets (including +`loadScreen`), the subroom's scene and blob, and the Maker Room fallback for a scene the client does +not know. + +### Verified in the client + +The live session had already found the room by the time the import finished: + +``` +[23:57:23] GET /images/798fe98e05b944999ab8c76181f45367.png -> 200 +[23:57:25] GET /rooms/rooms/55?include=5421&unityAssetTarget=0&unityAssetVersion=1 -> 200 +[23:57:35] GET /cdn/room/5k1vmec9n5jf4dymy0zkjligj.room -> 200 len=1373747 +Player joined ^MakeItToMidnight in scene "Maker Room" with playerCount 1 +[RoomLoad.RoomManager] -- Initial Room Load '^MakeItToMidnight' (55.58) [instance 20907] -- +[23:57:49] Moved to phase COMPLETE +``` + +The load walks every phase through `SETUP_ROOM_OBJECTS`, `RUN_MIGRATIONS` and +`DestroyAndRebuildObjects`, with **0** occurrences of the +`no EmbodiedObject/RbexEntity/TransformEntity component` prop-bind error that ran at 35 for the +RecCenter, and 0 `FailedToSpawnPlayer`. The only errors after the join are the room's own circuits +(`CircuitsV2Errors`, "Studio Object is invalid!") alongside the guard's 25 `ToolGuard` skips. + +One note for the next restart: the fixed `Server.dll` is **not** in `Server/bin/Debug/net9.0/` yet, +because the running server holds it. A plain `dotnet build ServerTests/Server.Tests.csproj -c Debug` +once the client closes picks it up, and nothing about room 55 depends on it. + +## Run 30 (2026-09-25 00:00, live): the portfolio "crash" is the anti-cheat's dorm drop + +Clicking your own portfolio closed the game. It is not a crash: Windows recorded **no** +`RecRoom.exe` fault either side of it (the last one is 22:09:19, the pre-ToolGuard +`GameAssembly.dll+0x470728d`), and the process ran the game's own shutdown instead - +`[EAC] Tearing down and releasing runtime`, then `Trying to send photon room updates to server +while application is quitting`. What Player.log has at 00:01:05 is the answer: + +``` +[00:01:05] [Warning] [Default | Warning] Dropping to dormroom due to error [385120] +[00:01:06] [Log] [Unity] [EAC] Tearing down and releasing runtime +``` + +and `BepInEx/LogOutput.log` has the chain, one millisecond wide: + +``` +[CheatTrace] +244681ms CheatManager.OnObscuredTypeCheatDetected +[CheatTrace] +244682ms CheatManager.EFLCHFDDPJP +[HileGate] anti-cheat report captured (full wire form): body: Message=&Type=0 +[Net] Post https://localhost:3030/api/PlayerReporting/v1/hile -> 200 body=true +[QuitVeto] wantsToQuit answered false (block #1) - this is the anti-cheat shutdown +``` + +`CheatManager.EFLCHFDDPJP(string reason = "Dropping to dormroom due to error [385120]")` is the +whole of the message, and that string is the PARAMETER'S DEFAULT VALUE - `385120` is not an error +code, it names nothing, which is why every drop in every session reports the same number. Its +caller is the obscured-type handler, and what the page does to trip it is in the server log: the +profile renders the caller's own custom avatar items, and +`GET /api/customAvatarItems/v2/fromCreator/2` is fetched 7 times in 10 ms immediately before the +detection (`00:01:03.921`-`00:01:05.699`). + +The server side of that page is faithful and stays as it is: `/accounts/account/2/bio` 200, +`/api/customAvatarItems/v2/fromCreator/2` 200 with the reference's empty page, +`/accounts/accountprivacysettings/2` 200, `/rooms/rooms/ownedby/2` 200, and 404 with an empty +body on both influencer routes - which is the reference's own answer for "not an influencer", +not a gap. + +So the verdict is removed at its source, exactly as the file scan was in run 23d, and the result +is already deployed (`BepInEx/plugins/RadicadeAugustClient.dll`, 62464 b, md5 +`8718a45dd42d7cb72b3e46e04b744d91`, backup `.before-run30b-cheatdrop`): + +* `CheatDetectionSuppressPatch` - skips all three verdict handlers + (`OnObscuredTypeCheatDetected`, `OnTimeCheatDetected`, `OnAdvancedMovementCheatDetected`), not + just the one that fired. Skipping only the obscured one would leave the other two live for + whatever a clock-skewed or laggy machine trips next. A handler that does not resolve is logged + and skipped rather than thrown on - the throw would take the whole patch block (exit veto + included) down with it - and the startup line prints `verdict handlers guarded=3` (or names the + unguarded one). `RADICADE_CHEAT_TYPE=0` restores the detections. +* `CheatDormDropGuardPatch` - holds the drop itself back, narrowly: only when the reason contains + `385120`, the anti-cheat's own default. Any other reason still drops the player, so a genuine + room failure behaves as before. `RADICADE_CHEAT_DROP=0` lets every drop run. + +Both are registered in their own try/catch, so a resolution failure on a different build cannot +undo the exit veto, the exit tracer or the hile gate applied above them. + +## Run 30, second half: `rooms/rooms/{id}/similar` answered a bare array + +The "Similar Rooms" rail is a paged DTO in this client, and the route answered +`Array.Empty()` - `Malformed Response: '[]'` in Player.log, one of 9 in the session +(and 5 in the one before). A room that did not exist was a 404 on top of that, which stalls the +screen the rail sits on rather than emptying one row. `GetSimilarRooms` now returns +`RoomSearchResult` (scored by shared tags, same creator as the fallback for untagged rooms, +visibility checked per candidate, unknown room = empty page) and pages `skip`/`take`. +`ServerTests/SimilarRoomsTests.cs` pins the envelope, the empty page, the paging and the +visibility rule. **573 tests pass.** + +`/econ/econ/roomGiftDropShops/room/{id}` (`[]`), `/rooms/rooms/{id}/experience/player` (`[]`) and +`/econ/api/roomkeys/v1/room` (`[]`) were each checked against the reference while chasing the +remaining `Malformed Response: '[]'` lines: all three are documented there as empty lists, so the +chronic parse errors are not from them and are left alone for now - they are logged, not fatal. + +### The restart, and the one trap in it + +The server was rebuilt and relaunched for the `similar` fix (`server-console-run30.log`, cwd +`Server`, PID 33272). Launching the exe directly from `Server/` picks up **`appsettings.Production.json`** +- `Hosting environment: Production`, a different `JwtKey`, no HTTPS listener on 3030 and no Photon +app ids, which would break the client at the room join. The launch must carry the environment: + +``` +$env:ASPNETCORE_ENVIRONMENT='Development' +Start-Process ...\Server\bin\Debug\net9.0\Server.exe -WorkingDirectory ...\radicade-server\Server +``` + +and the log has to read `Now listening on: https://localhost:3030` **and** +`Now listening on: http://127.0.0.1:3031` **and** `Hosting environment: Development`. Live +after the restart: + +``` +rooms/rooms/55/similar -> 200 {"TotalResults":0,"Results":[]} +rooms/rooms/99999/similar -> 200 {"TotalResults":0,"Results":[]} (was 404) +rooms/rooms/ownedby/2 -> 200 len=5950 +api/customAvatarItems/v2/fromCreator/2 -> 200 {"Results":[],"TotalResults":0} +``` + +(`Authorization: Bearer` needs a token signed with the Development `JwtKey`; `?access_token=` is +only honoured on notify-hub paths, which is why a copied client token reads as 401 here.) + +## Run 31: the room's Unity asset bundles, uploaded and bound + +The August build's `MakeItToMidnight` import had no `UnityAssetId` because the room dump +(`C:\RecRoom\Downloads\MakeItToMidnight`) carries no bundle - the importer's note is right, and +`tools/import_room_dump.mjs` says the same thing: the save only *names* the content and the bytes +live elsewhere. The bytes turned up separately, in `C:\RecRoom\Downloads\AssetBundles`: 87 +`*.assetbundle` files plus one 546 KB `.room`, no manifest, no index. + +### What the archive actually is + +The files are named by their CDN blob name and carry the CDN's own timestamps, and those two facts +recover the whole structure. Group by upload time and each group is **one publish event**: Rec Room +uploads one bundle per target, seconds apart, and every bundle in a group carries the same +`container` path, which names the room it was built for. + +``` +2023-08-21 13:10 n=7 333.3 MB Assets/RoomSource/Assets/Rooms/MakeItToMidnight/MakeItToMidnight-Home.unity +2023-08-22 10:34 n=7 333.4 MB ... +2023-08-23 12:48 n=7 333.3 MB ... +2023-09-12 18:49 n=6 308.2 MB ... +``` + +Two rooms are in there (`MakeItToMidnight`, 12 batches, and `KOdByDawn`, 3), across Unity revisions +`2020.3.33f1` (the August 2023 build - the only one this client can load), `2021.3.29f1` and +`6000.0.27f1`. `SerializedFile.target_platform` separates the targets inside a batch, and the +texture formats confirm what the numbers mean: `9` and `13` carry ASTC (iOS and Android - twice, a +phone and a Quest build both report Android), while `5`, `31`, `43` and `44` carry BC/DXT +(`StandaloneWindows` and the consoles). Each publish costs ~333 MB across targets; `5` alone is +~85 MB. + +The bundle is not optional decoration. The 08-23 Android bundle alone holds 8080 objects - 1490 +GameObjects, 1366 meshes, 1255 MonoBehaviours, 291 textures, 171 AudioClips - and the names in it +(`Survivor 1`, `[PerkIcon]`, `PhotoBooth_Curtain*_LOD*`) are the names the room save builds its +object tree from. A subroom with an empty `UnityAssetId` loads the save against whatever the build +itself ships, which is exactly why the room opened as a Maker Room with its content missing. + +### Which bundle belongs to the live save + +The live revision is the newest v61 save, `5k1vmec9n5jf4dymy0zkjligj.room` (2023-08-28T21:00Z), and +the dump records the asset id it advertised: `32cafb11-c51b-485c-8f1f-73b73159feff`. The dump is the +only place that id appears, so the batch it names was pinned by time, and the pinning is exact once +the archive's clock is accounted for - its zip entry times are the CDN's **local** wall clock, +exactly 7 h behind the UTC `CreatedAt` the saves carry: + +``` +unityAssetId first saved publish batch delta +bd362d85-6cc5-4b6f-9d3b-33774e3938aa 2023-08-21T19:59:45 2023-08-21 13:10 +11 m +14c842e7-01f0-4b81-9d03-19f39b7393d0 2023-08-21T20:20:28 2023-08-21 13:10 -10 m +20e15a8f-faca-41dc-8025-dd7da5c3cd0c 2023-08-22T17:25:32 2023-08-22 10:34 +9 m +3fb814c1-271e-4e54-960e-d6298947baaa 2023-08-22T17:27:21 2023-08-22 10:34 +7 m +32cafb11-c51b-485c-8f1f-73b73159feff 2023-08-23T19:39:06 2023-08-23 12:48 +10 m +``` + +So `32cafb11` is the 2023-08-23 publish, and in that batch the desktop bundle is +`d7ftni8391e3nrdts2drs4qlb.assetbundle` - `StandaloneWindows`, `2020.3.33f1` (the client's own +revision), 85,232,592 bytes, md5 `31b4faa66491de6106ce748a457f9905`. Two saves share that id +because the later revisions (2023-08-24..28) changed the room without republishing content, so one +bundle serves them all - which is also why the earlier ids matter only for history restores. + +### Uploaded and bound + +The client resolves a bundle in two steps - `GET /rooms/unity_assets/{assetId}/{target}/{version}` +returns `{UnityAssetId, Target, Version, Filename}`, and the bytes come from `/data/{Filename}` - +and `UnityAssetsCDN.ResolveFilename` looks for a file named exactly the id (or the id plus +`.assetbundle`) in the CDN storage directory. So the file is stored under the id the save +advertises, and the room's subroom points at that id: + +``` +Server/RadicadeServer-Data/CDN/Storage/32cafb11-c51b-485c-8f1f-73b73159feff.assetbundle 85232592 b +Server/RadicadeServer-Data/CDN/Storage/32cafb11-c51b-485c-8f1f-73b73159feff hard link +UPDATE SubRooms SET UnityAssetId='32cafb11-c51b-485c-8f1f-73b73159feff' WHERE SubRoomId=58; +``` + +Both spellings are stored because the loader reaches for the bytes two ways: the metadata route +hands the client a `Filename` that it fetches from `/data/{Filename}`, and the CDN path for a +bundle is `/unity/{id}` - the id with no extension. `ResolveFilename` checks the bare id first, so +the extensionless name is what the metadata reports; the hard link costs no disk and covers the +route that asks by id. + +Verified against the running server (cwd `Server`, PID 33272 - the storage directory the server +reads is `Server/RadicadeServer-Data/CDN/Storage`, not the one at the repo root): + +``` +GET /rooms/unity_assets/32cafb11-.../0/1 -> 200 {"UnityAssetId":"32cafb11-...","Target":0, + "Version":1,"Filename":"32cafb11-..."} +GET /data/32cafb11-... -> 200 len=85232592 Accept-Ranges: bytes +GET /data/32cafb11-... (Range) -> 206 Content-Range: bytes 0-15/85232592 +GET /rooms/rooms/55?include=5421&unityAssetTarget=0&unityAssetVersion=1 + -> CurrentSave.unityAssetId = 32cafb11-c51b-485c-8f1f-73b73159feff +``` + +The `unityAssetTarget=0` the client sends is RecNet's own target list, not the SerializedFile +platform id the bundle carries: the server answers the id with one file, so the target in the query +does not have to be matched to the target in the bundle. + +### The tool, so the next room is one command + +`tools/import_room_bundles.py` does the three steps above and is what produced the tables in this +section: + +``` +python tools/import_room_bundles.py inventory --source ".../AssetBundles" --zip ".../AssetBundles.zip" +python tools/import_room_bundles.py plan --source ... --zip ... --dump ".../MakeItToMidnight/room_data.json" \ + --room MakeItToMidnight +python tools/import_room_bundles.py bind --asset-id 32cafb11-... --batch 2023-08-23 \ + --source ... --zip ... --subroom 58 --apply +``` + +`inventory` probes every bundle (platform, room container, Unity revision, object count) and groups +by publish batch - 2m37s for all 87, written to `artifacts/asset-bundles.json`. `plan` pairs a room +dump's asset ids with batches using the clock alignment above, and flags a pairing an hour or more +out as `DISTANT` rather than pretending to know it. `bind` copies the batch's `StandaloneWindows` +bundle into CDN storage under the id, points the subroom at it, and then runs the client's own +two-step resolution against the live server to prove it answers. The dump's own file name is +deliberately not the stored name - the id is what the client asks for - so the inventory JSON is the +record of which real blob each id was bound to. + +`KOdByDawn` is in the same archive with three batches and is bound the same way if it is ever +imported. The remaining older ids can be bound too, for history restores that keep their own +content: `14c842e7` and `bd362d85` are the 2023-08-21 batch +(`a4al16vq0235oom5aplev2omu.assetbundle`), and `20e15a8f` / `3fb814c1` are 2023-08-22 +(`esnrn95jqftokte3hmy8m1ffr.assetbundle`). + +## Run 32 - the beta tag on room 55, and a room that is developer-owned when the maker is a dev + +### Room 55 does not carry `beta` + +`Rooms.Tags` for room 55 is `import,0` - one entry, `import` at `TagType.General`. There is no +`beta` entry, so the room does not advertise itself as beta-enabled the way the imported Showdown +room does (`rro,2|games,0|beta,1`, `TagType.Auto`). + +Two things worth keeping, because they explain the value rather than just reporting it: + +- The importer wrote it. `tools/import_room_dump.mjs:228` stamps every imported room with a + hardcoded `[{ Tag: 'import', Type: 0 }]`, so whatever the real room carried was overwritten at + import time - this is a latent bug for every room imported that way, not a fact about + MakeItToMidnight. +- The archives cannot tell us what the original tags were. All 48 captured room JSONs in + `rooms.zip` (`more rr rooms/*/room_*.json`) carry **no room-level `Tags` field at all** - the + exporter drops it - and the `MakeItToMidnight/room_data.json` dump only has save-level + `Tags: []`, which is unrelated. + +Nothing server-side branches on the room-level tag: `grep '"beta"' Server/**.cs` finds only the +comment in `Program.cs:564`, and the tag is pure pass-through (`FullRoom.CopyTo` copies `Tags` +straight into the room payload) with the client acting on it. Beta creative-tools *creation* is +already enabled globally by `EnableBetaCreativeToolsConfigs` (`UGC.AllowBetaInvertedTubeCreation`, +`UGC.AllowNonBetaInvertedTubeCreation`, `UGC.EnableInventionsInBetaRooms`, `UGC.Room2Enabled`), so +the missing tag is not what keeps beta content from loading. To match the real room anyway: + +```sql +UPDATE Rooms SET Tags = 'import,0|beta,1' WHERE RoomId = 55; +``` + +Left unapplied - the user only asked whether it was there. + +### `IsDeveloperOwned` was never in the room payload + +The 2023 client's room parser has the field (it is in the August metadata dump, and in the captured +RecNet responses: `"IsDeveloperOwned": true` on a developer's room), but `FullRoom` did not declare +it at all, so **every** room on this server read as not-developer-owned and the client's Dev Rooms +row stayed empty however the rooms were made. + +Where a room is made matters: there is no create-room route. A player creates a room by +`POST /rooms/rooms/{id}/clone` (`RoomServerAPI.CloneRoom` -> `RoomUtils.CloneRoom`), which is the +same method `GetOrCreateDorm` uses to make a dorm - so the flag has to come from the maker's roles +and has to exclude dorms. + +What changed: + +- `RoomData.IsDeveloperOwned` (bool, default false) - a column on `Rooms`, `bit NOT NULL DEFAULT 0`. +- `Room.IsDeveloperOwned` - declared on the response base, so it is emitted for room lists and room + details alike, as `IsDeveloperOwned` (Program.cs pins the serializer to no naming policy, the + same PascalCase the real API answers with). +- `RoomUtils.CloneRoom` stamps it: `!room.IsDorm && await ctx.IsDeveloper(newCreator)`. +- `RoomUtils.ConvertToFull` and `FullRoom.CopyTo` carry it to the payload. +- `AdminRoomAPI.CreateRoom` (the import route) honours an explicit `IsDeveloperOwned` in the body - + a captured RRO room is developer-owned on the real service even when it is recreated here under a + local owner - and otherwise derives it from the owner it is being created for. +- `EnsureRoomDeveloperOwnedColumn` on boot, same self-heal pattern as `UnityAssetId` and the + join-gift columns: EF maps the property onto every `Rooms` query, so the column has to exist + before any startup step reads a room. + +Verified live against the restarted server (PID 38436, `server-console-run32.log`, Development, +3030 + 3031). The column is in place and the payload carries the field: + +``` +GET /rooms/rooms/55?include=5421... -> IsDeveloperOwned: false +GET /rooms/rooms/23 -> IsDeveloperOwned: false +GET /rooms/rooms/25 -> IsDorm: true, IsDeveloperOwned: false +``` + +Then both directions of the rule, through the client's own clone route, with tokens minted for the +two accounts on this server (2 `TestPlayer` = `developer,influencer`; 1 `Coach` = `influencer`): + +``` +POST /rooms/rooms/23/clone (name=DevOwnedProbe, account 2) -> RoomId 56, IsDeveloperOwned: true +POST /rooms/rooms/23/clone (name=PlainOwnedProbe, account 1) -> RoomId 57, IsDeveloperOwned: false +``` + +Both probe rooms were deleted again (`RoomRoles`, `RoomStats`, `SubRooms`, `Rooms`), leaving the 51 +rooms that were there before. `dotnet test ServerTests/Server.Tests.csproj` -> **581 passed** +(573 before, +8 in `ServerTests/RoomDeveloperOwnedTests.cs`: a dev's clone, a plain player's clone, +a dev's dorm, the flag on the room that is read back, the explicit false in the payload, and the +three admin-import cases). + +Every room that predates the column started false. The backfill was then applied, over the same +rule the create path uses (the maker's role, dorms excluded) - 5 rooms flagged, 46 left false: + +```sql +UPDATE Rooms SET IsDeveloperOwned = 1 +WHERE IsDorm = 0 AND CreatorAccountId IN (SELECT accountId FROM Profiles WHERE role LIKE '%developer%'); +``` + +``` +31 TestPlayers_MakerRoom_3528 52 studio-room-SdjyyctVdc 53 studio-room-NRyzI1_njb +54 TestPlayers_MakerRoom_1603 55 MakeItToMidnight (25 DormRoom - dorm, left false) +``` + +Room 55 is account 2's, so MakeItToMidnight became developer-owned along with the four the user +built - which is also what the real service says about that room. It is a data-only change, so the +running server picked it up without a restart; the detail route and the bulk list route the +client's own room lists read both answer it: + +``` +GET /rooms/rooms/55?include=5421... -> IsRRO: true, IsDeveloperOwned: true +GET /rooms/rooms/bulk?id=31..55 -> IsDeveloperOwned: true (x5) +GET /rooms/rooms/bulk?...&id=25 -> IsDorm: true, IsDeveloperOwned: false +``` + +## Run 33 - Showdown reimported, and the four-character bug that was skipping it + +The request was that Showdown was "a little broken", and to delete and reimport it from a fresh +export at `C:\RecRoom\Downloads\Showdown`. The import ran, but the interesting part is *why* +the previous one had not: the boot importer was looking in a directory that does not exist here. + +### What the export actually is + +The folder is a RecNet export, not a captured room dump: `room.json` plus 22 per-subroom +directories, each carrying `room_data.json` and `Saves//.room`. `Showdown.rar` +(46 MB, RAR5) holds the same tree. The export describes the *real* room 52815862 - `CreatorAccountId +1`, `Accessibility 1`, `IsRRO true`, `IsDeveloperOwned true`, `WarningMask 0`, `MaxPlayers 6`, +`CloningAllowed false`, `PersistenceVersion 150`, `CreatedAt 2022-03-03`, image +`65hs5cb56byqs05lu16kr3e8r.png`, and the description about Deputies and Bandits. 22 subrooms is +not 22 live subrooms: they are archive snapshots, all sharing the one scene +`a75f7547-79eb-47c6-8986-6767abcb4f92`, and they are named accordingly - `SD_Archive_001`, +`SD_Archive_082322`, `SD_Archive_11_29_2022_STABLE`, `SD_Archive_13_061824`, and so on. The single +subroom the server actually needs is the one every other room here has: one `Home` row pointing at +the scene and the save. + +### The root cause: `room-imports` vs `RoomImports` + +`EnsureShowdownRoom` looked for its files in `..\room-imports\Showdown` and `room-imports\Showdown` +- always lowercase. This checkout spells the directory `RoomImports`, and `Test-Path` answers +`False` for the lowercase spelling even though the tree is not case-sensitive as a whole. So the +lookup found nothing, the importer printed +`[Showdown] Skipped import - missing files ...` and returned on every single boot, and the room kept +whatever an older import had left behind. A silent skip is the worst kind of green: the log line is +there, but nothing above it looks like a failure. + +The fix keeps the spelled-correctly path first and the old lowercase spellings after it (so a tree +where they do resolve, or a deployment that still ships that name, keeps working): + +```csharp +var candidates = new[] +{ + Path.GetFullPath(Path.Combine("..", "RoomImports", "Showdown")), + Path.GetFullPath(Path.Combine("..", "room-imports", "Showdown")), + Path.GetFullPath(Path.Combine("RoomImports", "Showdown")), + Path.GetFullPath(Path.Combine("room-imports", "Showdown")), +}; +var sourceDir = candidates.FirstOrDefault(Directory.Exists) ?? candidates[0]; +``` + +### Which save to install, and why the newest one is the wrong one + +A room here is a bundle plus a save blob, and the two have to be the same revision or the client +loads a scene it has no data for. The bundle was already identified by hash: +`RoomImports/Showdown/Showdown_0_1.assetbundle` is 76,719,774 bytes, md5 +`220891bc78b852df1c7ba8e1af0e9d1b`, and matches 24 metadata entries - among them asset +`58784cb3-204c-47e0-9cf3-23a76b94ae90` as **Unity_2020_3_33f1, Target 0, Version 1**. That asset +revision is the one the August client can load, and the save that goes with it is +`8u07n4iwzt4xbqkww7zun0jam.room` (2023-06-02, 1,222,161 bytes), the newest save whose revision is +still 2020.3.33f1. + +The newest save in the export looks tempting and is a trap: the live `Showdown` subroom +(8785941127880372366) was last saved 2024-10-02 as `d39glazjteu2zztjuuulribr7.room` under asset +`d03c4c99-4814-412b-9594-f61851d32225`, and that asset exists only as 2021.3.29f1 / 6000.0.27f1 - +nothing a 2020.3.33f1 client can load. The previously installed blob was `e45vok0ppp89hda6n1xrm4z16.room` +(2022-10-18, the `SD_Archive_11_29_2022_STABLE` save), which is why the room was stale. + +### The blob really is Showdown + +Raw string overlap between the blob and the bundle is zero, so that proves nothing either way. But +the `.room` blob is protobuf, and walking its wire format for printable runs pulls out the room's +own content verbatim - `_SDLog_LocalPlayerSeesRedTeamValid`, +`_SDLog_LocalPlayerLooksForValidTeam`, `_Equipping gun index {0} to self`, +`_Not enough guns to give to self`, `_Tie Game!`, `Event Sender`/`Event Receiver`/`To Teams`. That is +Showdown's team-and-guns gameplay, and the `SD_` prefix on its own script log ids is the signature. +`artifacts/verify_showdown_blob.py` re-runs that walk against any blob. + +### The swap: room 32 out, room 58 in + +The old room was deleted (with its `RoomRoles`, `RoomStats`, `RoomInstances`, `RoomInteractions`, +`SubRoomHistory` and `SubRooms` rows - 51 rooms became 50), the era-matched save was put in place, +and the metadata was patched to match the export: + +| field | old room 32 | new room 58 | +| --- | --- | --- | +| `Description` | `Showdown` | the Deputies/Bandits text | +| `ImageName` | `default.png` | `65hs5cb56byqs05lu16kr3e8r.png` | +| `MaxPlayers` | 16 | 6 | +| `CloningAllowed` | 1 | 0 | +| `IsDeveloperOwned` | backfilled true | true | +| `CreatedAt` | 2026-09-09 | 2022-03-03 | + +Room 58's `Tags` were already `rro,2|games,0|beta,1` (the importer sets them) and its subroom 61 is +the one `Home` row: `UnitySceneId a75f7547-79eb-47c6-8986-6767abcb4f92`, `DataBlob Showdown.room`, +`UnityAssetId Showdown_0_1.assetbundle`, `MaxPlayers 6`, `Accessibility 1`. Nothing else in the +database still points at room 32 - every table that carries a `RoomId` was checked and all twelve +count zero rows for it. + +### It survives a restart, which is the part that could have bitten back + +The importer's `else` branch (room already exists) re-applies `IsRRO`, `Tags`, `subRoom.DataBlob`, +`subRoom.UnityAssetId` and `subRoom.UnitySceneId` on every boot, so those have to be checked against +what a restart would write. They are all already correct, and the scene is the subtle one: +`SceneLocations.MakerRoom` **is** `a75f7547-79eb-47c6-8986-6767abcb4f92`, the Showdown scene, so the +overwrite is a no-op. The fields that a restart would *not* touch - `MaxPlayers`, `Description`, +`ImageName`, `WarningMask`, `CloningAllowed`, `IsDeveloperOwned`, `CreatedAt` - are the ones the +patch above changed, so the importer's non-idempotent half does not undo any of it. + +### Verified live (server PID 36096, run 33, Development) + +``` +GET /rooms/rooms/58 -> 200 Name=Showdown MaxPlayers=6 Accessibility=1 + IsRRO=true IsDeveloperOwned=true IsDorm=false + CloningAllowed=false WarningMask=0 + ImageName=65hs5cb56byqs05lu16kr3e8r.png + CreatedAt=2022-03-03T16:41:31 + Tags=[rro,2][games,0][beta,1] SubRooms=1 +GET /cdn/data/Showdown.room -> 200 1,222,161 bytes (the 2023-06-02 save) +GET /data/Showdown.room -> 200 1,222,161 bytes (range-capable alias) +GET /cdn/data/Showdown_0_1.assetbundle -> 206 range request, 100 bytes of 76,719,774 +GET /rooms/unity_assets/Showdown_0_1.assetbundle/0/1 -> 200 + {"UnityAssetId":"Showdown_0_1.assetbundle","Target":0,"Version":1, + "Filename":"Showdown_0_1.assetbundle"} +GET /images/img/65hs5cb56byqs05lu16kr3e8r.png -> 200 2,777,168 bytes +``` + +The room thumbnail needs no `Images` table row: room art is served straight from +`DBConfig.CDNImages` by name (`/images/img/{name}`), which is why the built-in rooms' `ImageName` +values are bare names like `Dodgeball`. Both HTTPS (3030, the port the client talks to) and HTTP +(3031) answer for the room and the asset metadata. + +Backups kept: `RoomImports/Showdown/Showdown.room.before-run33` is the previous blob, and +`artifacts/showdown-room32-before-run33.txt` is the row-level snapshot of the deleted room. + +## Handing the client patch to another machine + +The August client patch is a **stack of six pieces**, not one DLL, and two of them are produced +by machine-local patching rather than shipped. A player who receives only +`BepInEx/plugins/RadicadeAugustClient.dll` gets a game that still talks to the official hosts, and +the only red line in his `Player.log` is the vanilla Statsig noise above - which is why that line is +the most common wrong lead. The reference install (`C:\RecRoom\2023-08-29T06-53-33Z`) +hashes as follows; anything that differs is the piece to replace: + +| piece | expected | +| --- | --- | +| `GameAssembly.dll` | 167,390,208 b, md5 `11e14eb35671e2725bd0c78a6a91bb48` - the build identity; every address map and the metadata patch are tied to it | +| `Recroom_Data/il2cpp_data/Metadata/global-metadata.dat` | 38,654,794 b, md5 `cc9c32014b5fc3dd1a2a8fb12376be15`, 28 x `localhost:3030`, 50 x `rec.net` | +| `Recroom_Data/il2cpp_data/Metadata/global-metadata.interop.dat` | 38,654,794 b, md5 `4a1a28078f957af470f3b31951b32648` (normalized tooling copy) | +| `Recroom_Data/boot.config` | 173 b, md5 `dc2e5f7af7d5df736a4cc3477b4b1a1f` (`vr-enabled=0`, `player-connection-mode=Listen`) | +| `Recroom_Data/radicade-patch.json` | provenance stub: `serverUrl https://localhost:3030`, 7 changed literals, `gameVersion 20230825` | +| `winhttp.dll` | 26,112 b, md5 `b2a4d0cb04bf8f5a27ccab237ecc2586` (Doorstop 4.3.0 proxy) | +| `doorstop_config.ini`, `.doorstop_version` | `enabled = true`, target `BepInEx\core\BepInEx.Unity.IL2CPP.dll`, version 4.3.0 | +| `BepInEx/core/BepInEx.Unity.IL2CPP.dll` | 60,928 b, md5 `9430bb84349d52b753a181788a86c5e2` - patched to read `global-metadata.interop.dat`; the original is kept as `.before-august-interop` (75,776 b, md5 `b8b3e4da1dbf171f3d405102d3c9d2e2`) | +| `BepInEx/interop/` | 335 assemblies | +| `BepInEx/plugins/RadicadeAugustClient.dll` | 62,464 b, md5 `8718a45dd42d7cb72b3e46e04b744d91` | +| Radicade server | `https://localhost:3030` + `http://127.0.0.1:3031`, `Hosting environment: Development`, **on the player's own machine** | + +The last row is the one no file copy can satisfy: the patched metadata rewrites the client's +bootstrap literals to `https://localhost:3030`, so the server has to run where the game runs (a +remote server needs a different metadata patch and a certificate the client accepts). + +The three failure modes seen so far, each with its own fingerprint: + +- **No loader.** `winhttp.dll` renamed to `winhttp.dll.baseline-disabled` (or `enabled = false`) - + `Player.log` dies at `Could not resolve host 'ns.rec.net'`, there is no `BepInEx/LogOutput.log` + for that boot. This is the 15:42 run-17 state. +- **Unpatched metadata.** `global-metadata.dat` is the 32,039,456 b / md5 + `825f34f1dd046a0ae764cd5a97dba5e6` original. The plugin loads and the boot looks healthy (login, + Photon, Dorm), then the room-save path fails: `GetSubRoomDataSave()` never resolves, the client + prints `Error downloading save data!` / FailedToSpawnPlayer `Code 8715` and tears the session + down. This is the 2026-09-24 15:16 baseline-experiment state. +- **No local server.** Everything else is in place but nothing listens on 3030 - the client's + requests fail and the screen shows the launch-validation/login failure. + +`tools/august-client-triage.ps1` reports all of the above in one pass (read-only; it also opens two +TCP sockets to 127.0.0.1:3030 and :3031 and writes `august-client-report.txt` next to itself): + +``` +powershell -ExecutionPolicy Bypass -File august-client-triage.ps1 +powershell -ExecutionPolicy Bypass -File august-client-triage.ps1 -GameDir "D:\Games\2023-08-29T06-53-33Z" +``` + +On the reference install with the server stopped it reports exactly one `[MISSING]` (port 3030) and +one `[WARN]` (port 3031); with the server up, none. Every `[MISSING]` line names a piece of the +table above that did not arrive, and the two log tails at the end of the report carry the rest. + +`tools/make-august-client-pack.ps1` assembles the handover itself: it verifies every piece against +the table (fail-closed, so it cannot ship an unpatched metadata or a foreign build), copies the +loader pieces, the patched data files and the whole `BepInEx` tree - minus logs, previous-build +sidecars and the 18 MB `radicade-addr-map.txt` diagnostic - into +`\RadicadeAugustClient`, then writes `README-FRIEND.txt` and drops the triage script in +alongside. The reference bundle is 302 MB in 448 files, of which `BepInEx/interop` (335 assemblies) +is the bulk: + +``` +powershell -ExecutionPolicy Bypass -File make-august-client-pack.ps1 -OutDir "D:\radicade-pack" -Zip +``` + +For the September build (20230831, folder `2023-09-06T06-53-47Z`) the same pair exists as +`tools/september-client-triage.ps1` and `tools/make-september-client-pack.ps1`, carrying that +build's reference hashes (332 interop assemblies, 294.8 MB bundle) and one extra check the August +scripts do not need: the metadata header magic, because a stock BepInEx core fails on this game's +scrambled `global-metadata.dat` with `Invalid or corrupt metadata (magic number check failed)`. +See `docs/2023-0831-CLIENT-SWITCH.md`. + +## Rec Room Studio on Radicade + +`C:\RecRoom\OneDrive\Documents\EggFiles\ProjectTemplate` is the Rec Room Studio template, and +it lines up with the client we already patch: Unity **2020.3.33f1** (the August build's engine) and +packages built from `features/rec-room-studio/releases/20230831-211740` (git `0c23610a`, recorded in +`RecRoomStudioPackages/VersionInfo.json`). Those packages carry the entire game runtime +(`com.recroom.studio`, `.studio.build`, `.studio.api`, `networking`, `circuitsv2`, `objectmodel`, ...), +which is what lets the studio run the real game code inside the editor. + +### Everything routes through one string + +A sweep of every `com.recroom.*` assembly for both ASCII **and** UTF-16 string heaps (plain `grep` +finds nothing - .NET literals live in the `#US` heap as UTF-16) turns up exactly one baked-in +service host: + +| Where | Literal | +| --- | --- | +| `Library/PackageCache/com.recroom.studio.build@*/Editor/RecRoom.Studio.Editor.Core.dll` | `https://deluxe-backend-test.oldrecroom.com/nameserver` | +| `.../com.recroom.studio/.../UI/RecRoomStudioWindow/SignInPanel.uxml` | `https://auth.rec.net/device` (label text only) | +| `.../RecRoom.Studio.Editor.Core.dll` | `https://events.statsigapi.net/v1/` (telemetry) | + +No config file, environment variable or command-line switch redirects it: the editor `GET`s that +nameserver URL and derives *every* other base (`Auth`, `API`, `CDN`, `Images`, `Studio`, ...) from the +JSON, so aiming the whole editor at a private server is a one-string patch. The `-environment` +argument belongs to the batchmode pipeline (`-roomId`, `-outputPath`, `-rebuildMode`, +`-RECNET_CLIENTSECRET`, ...), not to the window. + +### Sign-in is an RFC 8628 device flow + +`RecRoom.Studio.Editor.Core.dll` carries `connect/deviceauthorization` and `connect/studiotoken` +under the nameserver's `Auth` base, together with the matching model: `device_code`, `user_code`, +`verification_uri`, `verification_uri_complete`, `expires_in`, `interval`, `scope`, `access_token`, +`refresh_token`, `authorization_pending`, `tokenError`, and the grant URN +`urn:ietf:params:oauth:grant-type:device_code`. Nothing in the studio window works before that +succeeds - there is no offline path, and `"Play Local"` (which launches the game client with +`-RecRoomStudioPort:N` and drives it over `http://localhost:{port}/recroomstudio/checkavailability` +and `/loadroom`) stays disabled until a room is open, which itself comes from RecNet. + +### What the server now provides + +`Server/Auth/StudioAuthAPI.cs` implements both endpoints off the nameserver's `Auth` base +(`{ServerUrl}/auth/connect/...`). Real RecNet has the user approve the code at `auth.rec.net/device`; +a private server has no such page, so the code is auto-approved and the tokens are minted by +`AuthAPI.GenerateToken` - the same claims (`userId`, `tver`, roles, `rn.ver`) the game client gets, +which is what `[Authorize]` and token revocation actually check. The flow is refused for non-loopback +callers unless `Radicade:Studio:AllowRemoteDeviceAuth` is true, and the account is +`Radicade:Studio:AutoApproveAccountId`, else whichever account the caller names, else the lowest +account holding the `developer` role. Requests are read as form **or** JSON and logged in full (bar +secrets), because the exact wire shape is not documented anywhere - the first real sign-in attempt is +what will tell us. + +Verified live against the dev server: + +``` +POST /auth/connect/deviceauthorization -> device_code, user_code 8JXV-8W4D, expires_in 900, interval 2 +POST /auth/connect/studiotoken (device_code) -> access_token (JWT roles gameClient+developer+influencer, userId 2, tver 0) +GET /auth/account/me/haspassword + bearer -> true +POST /auth/connect/studiotoken replying the device_code -> invalid_grant (single use) +``` + +### The editor-side patch + +`tools/patch-rrs-nameserver.ps1` rewrites that one literal in place, as a `.NET` string-heap edit: the +entry keeps its 108-byte footprint (1-byte compressed length + 106 bytes of UTF-16 + terminator), the +freed tail is zero-filled, and because the heap is addressed by offset and the footprint is unchanged +no other string moves. One-time backup to `RecRoom.Studio.Editor.Core.dll.radicade-orig`, idempotent, +and after writing it re-decodes the entry and asserts that **nothing outside the slot changed** +(`-Restore` puts the original back). + +```powershell +powershell -ExecutionPolicy Bypass -File tools/patch-rrs-nameserver.ps1 # -> https://localhost:3030/nameserver +powershell -ExecutionPolicy Bypass -File tools/patch-rrs-nameserver.ps1 -Url http://127.0.0.1:3031/nameserver +powershell -ExecutionPolicy Bypass -File tools/patch-rrs-nameserver.ps1 -Restore +``` + +### Two blockers before any of this can be exercised + +1. **The project tree is write-protected.** Creating a file anywhere under + `C:\RecRoom\OneDrive\Documents\EggFiles\ProjectTemplate` fails (`ERROR_FILE_NOT_FOUND` for a + new file, `Access denied` when overwriting an existing one, and a hang via `cmd`) from Python, + PowerShell and .NET, while reads work and writes elsewhere on the same volume succeed - + `OneDrive\` itself is writable, `OneDrive\Documents` is not, and no `OneDrive` process is running. + Defender's Controlled Folder Access is enabled (`EnableControlledFolderAccess = 1`) and its + implicit protected "Documents" resolves to the OneDrive one. Unity cannot open a project it cannot + write `Library/` and `Temp/` into, so this has to be settled first. +2. **TLS.** The editor validates certificates normally - the game client does not, because our plugin + patches that. The certificate Kestrel serves on 3030 (`localhost-rsa-aes-devcert.pfx`, thumbprint + `D2275346BC059E9B62466F116F146F63CB89D5C4`, notAfter 2031-09-05) sits in `CurrentUser\My`, **not** in + `CurrentUser\Root`, so the editor's `https://localhost:3030/...` calls will be rejected until that + cert is trusted (or the editor is aimed at the plain-HTTP listener on `http://127.0.0.1:3031`, in + which case `Radicade:ServerUrl` has to match too, since the nameserver response is where every other + base URL comes from). + +After that the remaining surface is the studio's own room data: the window lists rooms and saves from +RecNet (`RoomTargetPanel`, `SaveListItem`) and uploads with +`git -c http.extraheader="AUTHORIZATION: bearer "` to a RecNet-hosted remote - none of which +exists here yet (`Server/API/Studio` is an empty placeholder directory). + +### The compile wall was Defender, not the packages (2026-09-26) + +The writable copy opened in Unity 2020.3.33f1 and immediately refused to compile anything: + +``` +Asset Packages/com.recroom.studio.build/Editor/RecRoom.Studio.Editor.Core.dll.radicade-orig has no + meta file, but it's in an immutable folder. The asset will be ignored. +NullReferenceException ... UnityEditor.Utils.Program.GetStandardOutput() (twice) +Assembly 'Packages/com.recroom.creation/Runtime/RecRoom.Creation.Runtime.dll' will not be loaded + due to errors: Unable to resolve reference 'Unity.Mathematics' +... the same for ~15 com.recroom.* assemblies, plus 'Unity.Collections', 'Unity.Settings.Editor', + 'Unity.RenderPipelines.Core.Editor', 'RecRoom.Studio.Common.Runtime' +``` + +with `Library/ScriptAssemblies` holding only `BuiltinAssemblies.stamp`. The first guess - that the +manifest's `file:../RecRoomStudioPackages/*.tgz` sibling folder had been left behind by the robocopy - +was **wrong**, and worth recording so it is not chased again: Unity resolves a `file:` path in +`Packages/manifest.json` relative to the **Packages folder**, so `../RecRoomStudioPackages` is +`\RecRoomStudioPackages`. That folder is inside `ProjectTemplate` and always was, holding all +55 tarballs plus `VersionInfo.json`. An integrity scan of the whole set (tar entry list vs extracted +files, for every tarball) found **zero** missing files. + +The actual failure is one line of `Editor.log`: + +``` +Starting external compile of Unity.Jobs.CodeGen.dll: + ...\Library\PackageCache\com.unity.roslyn@0.2.2-preview\Compiler~\win64\csc.exe /noconfig @Temp/... /shared +Exception when running external compiler on Unity.Jobs.CodeGen.dll: + System.ComponentModel.Win32Exception (0x80004005): ApplicationName='...\win64\csc.exe', ... + Native error= The system cannot find the file specified. +``` + +Unity 2020.3 compiles C# with an external compiler, and because this project installs +`com.unity.roslyn` (a transitive dependency of `com.unity.entities@0.51.1-preview.21`) that compiler +is the package's own Roslyn 3.8. **`csc.exe` was gone** - from `Library\PackageCache` *and* from the +shared `%LOCALAPPDATA%\Unity\cache\packages` copy - so the first compile died, nothing was written to +`ScriptAssemblies`, and every later reference resolution failed against an empty assembly set. The wall +of "Unable to resolve reference" is a symptom of "the compiler never ran". + +It was not shipped broken: the registry tarball +(`https://cdn.packages.unity.com/tarballs/com.unity.roslyn/com.unity.roslyn-0.2.2-preview/8b5b337db92cbdec37b54235ec8b826c92c6ddef.tgz`) +contains `package/Compiler~/win64/csc.exe` (md5 `8c4361cc37113f8467feb1d22b4e83da`), as it does for +`mac/csc` and `linux/csc`. Something deleted it locally. + +That something is **Windows Defender's PUA protection**, which has been quarantining unsigned +executables out of Unity package folders on this machine. `Get-MpThreatDetection` lists +`com.unity.burst@1.8.4\.Runtime\hostwin\llvm-lipo.exe` at 2026-09-26 08:35 (ThreatID 2147972435), +the older `dsymutil.exe` in burst 1.8.12/1.8.17/1.8.21 in the shared cache, and the same +`llvm-lipo.exe` in the OneDrive tree at 00:34 - `com.unity.burst\.Runtime\hostwin` now contains only +`vswhere.exe`. Burst's `bcl.exe` survived and the lost tools only matter for Apple targets, so the +one casualty that blocks everything is the script compiler. + +**Repair:** `tools/repair-unity-script-compiler.ps1` restores `Compiler~\win64\csc.exe` and proves it +works by running the compiler's own `-version` probe. Sources, in order: `-FromPath`, else the Unity +install's `Editor\Data\Tools\Roslyn\csc.exe`. The launcher is interchangeable - it is a stock .NET +Core apphost that loads the `csc.dll` beside it, and Unity's own copy drove the package's newer +`csc.dll` through a clean test compile (empty output, valid 3 KB assembly). Use `-FromPath` with the +extracted tarball member to get the authentic binary (it differs: md5 `5f0947da...` for Unity's +build). + +**Cure (needs an elevated shell):** + +```powershell +Add-MpPreference -ExclusionPath 'C:\RecRoom\Documents\EggFiles\ProjectTemplate' +Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\Unity\cache" +``` + +and re-run the repair with `-IncludeGlobalCache` so a package re-resolve cannot copy the damaged file +back out of the shared cache. + +Smallest reproducer for the whole thing, without Unity: run `csc.exe -version` in the package's +`Compiler~\win64`; if the file is missing you get the Win32Exception above, and a test compile with +`-noconfig -nostdlib -target:library -r:\System.Private.CoreLib.dll` produces a real assembly. + +### Verified: the compile wall is down (batch-mode run) + +The editor can be driven headlessly, which makes the whole state checkable without any clicking: + +```powershell +Unity.exe -batchmode -nographics -quit -projectPath -logFile +``` + +(it exits 1 on a purely cosmetic `Timeout after 300 seconds while waiting async operations to finish`). +Before and after the `csc.exe` repair, on the same project copy: + +| | before | after | +|---|---|---| +| `Starting external compile ...` | 3, then `Win32Exception ... cannot find the file` | 205, **0** failures | +| `Unable to resolve reference 'Unity.Mathematics'` / `will not be loaded due to errors` | ~15 assemblies | **0** | +| `Library/ScriptAssemblies/*.dll` | none, only `BuiltinAssemblies.stamp` | 105, incl. `Unity.Mathematics`, `Unity.Collections`, `Unity.Serialization`, `RecRoom.Studio.Common.Runtime` | + +The first repair attempt also showed why this has to be repaired in *both* caches: `csc.exe` compiled +189 assemblies and then vanished mid-run, which the Defender event log does **not** account for +(`Get-WinEvent ... /Operational` ids 1116/1117/1121 only ever name `burst\hostwin\llvm-lipo.exe`, and +never `csc`/`roslyn`). With the shared `%LOCALAPPDATA%\Unity\cache` copy repaired too, the next run +kept the file and logged no failure at all - and `com.unity.roslyn@0.2.2-preview`'s own directory +mtime stayed put, so this was not a whole-package re-extraction. Treat the project copy alone as +fragile; repair both. + +Two things the log still reports, neither of them blocking: + +* `Assets/**/*.cs` is empty, so there is correctly no `Assembly-CSharp` in the output - all of this + project's code lives in packages. Do not go looking for that assembly. +* The 11 remaining `error CS` lines are `[Script Updater]` advisories from + `com.unity.serialization@1.7.0-preview.1` against `Unity.Collections` 1.4.0 (`FixedString128` + renamed, `Rune` overloads). It still compiles, and nothing in the project references it. + +**Still eaten by the same Defender rule:** `com.unity.burst@1.8.4\.Runtime\burst-lld-14-hostwin`, the +LLD the Burst AOT job links with. It is missing from the project copy *and* from the shared cache, so +no re-extraction can bring it back - the batch log ends with +`Error while executing command: ...burst-lld-14-hostwin ... Native error: The system cannot find the +file specified`. `hostwin\llvm-lipo.exe` is quarantined on sight (twice on 2026-09-26 alone). Both are +Burst build/AOT tools: they break a player build, not the C# compilation the editor needs. Recover +them by re-downloading `com.unity.burst@1.8.4` from the registry - its `burst-lld-14-hostlin` and +`hostmac` siblings are intact, which is how we know the Windows one was deleted rather than never +shipped (1.8.17 in the same cache has three `hostwin` entries, 1.8.4 now has one). + +### Burst repaired, and the studio auth path re-verified (2026-09-26) + +Re-downloading `com.unity.burst@1.8.4` settled both the linker and its naming: the package ships +`package/.Runtime/burst-lld-14-hostwin.exe` **with** the extension (the log prints the extensionless +name because that is what `CreateProcess` is given, and Windows appends `.exe` itself), while +1.8.17/1.8.21 carry only `burst-lld-16/18-hostwin-arm64.exe`. A full file-set diff of that tarball +(480 files) against the extracted package found exactly two casualties and no extras: +`.Runtime/burst-lld-14-hostwin.exe` and `.Runtime/hostwin/dsymutil.exe` (40.8 MB). Both are restored +and the package now diffs clean, as does the whole `RecRoomStudioPackages` set. + +With the compiler, the certificate and the server all in one piece, the studio device flow was re-run +over **strict** TLS (the `localhost` dev cert is now in `CurrentUser\Root`, so `curl` without `-k` +succeeds): + +``` +POST /auth/connect/deviceauthorization -> device_code, user_code 9C3C-UXUF, + verification_uri https://localhost:3030/device +POST /auth/connect/studiotoken -> 400 grant_type is required (the full RFC 8628 + grant_type=urn:...:device_code urn:ietf:params:oauth:grant-type:device_code is NOT + accepted) +POST /auth/connect/studiotoken grant_type=device_code&device_code=... + -> access_token (JWT roles gameClient+developer+influencer, + userId 2, tver 0, rn.ver 20230825, expires_in 604800) +GET /auth/account/me/haspassword + bearer -> true +POST studiotoken replying the device_code -> invalid_grant (single use) +``` + +`grant_type=device_code` is the value the handler actually branches on; a request naming any other +grant type is answered with `unsupported_grant_type` and logged. What is left is unchanged: +rooms/saves, and the `git`-over-HTTPS upload remote - `Server/API/Studio` is still an empty +placeholder directory. + +### First real Sign in: two server-side gaps (2026-09-26) + +The first click of **Sign in** in the studio window opened the browser at +`https://localhost:3030/device?user_code=EYJ4-T9DF` and got a 404. The browser is not where the +authority on this is: `%LOCALAPPDATA%\Unity\Editor\Editor.log` records the editor's own verdicts, and +it had two of them. + +**1. `Device authorization failed: unsupported_grant_type`.** The editor polls `studiotoken` with the +full RFC 8628 grant type - `urn:ietf:params:oauth:grant-type:device_code` - while the handler only +branched on the bare `device_code`, so it answered `unsupported_grant_type` and the editor abandoned +the sign-in. Matching is now on the token rather than one exact string (dashes normalised, so any +spelling is accepted), and `refresh_token` got the same treatment. Re-verified against the live +server: that exact request now returns an `access_token`, and the short form still does. + +**2. There was no `/device` page.** `verification_uri` and `verification_uri_complete` were being +advertised while nothing served the path, so the browser step could only ever 404. New +`Server/Auth/DeviceApprovalAPI.cs` (`[Route("/")]` + `[HttpGet("device")]`, the same root-level +pattern `NameSever` uses, and the same HTML styling as `AccountsAPI.VerificationPage`) renders +"Approved - account 2" for a live code, and explains an unknown or missing one. It deliberately +approves nothing: the code is already issued approved, so the page's job is to tell the user what +they are holding. + +An earlier attempt had also logged +`Failed to initiate device authorization: Failed to obtain RecNet Service URIs: NullReferenceException` +inside `KJGMKNABDIJ.EMOFOGPOOLJ(String, Char)` - a parse of the nameserver response in *editor* code, +not a server fault. It stopped occurring once the client got past that stage, and it is worth knowing +because it looks like a server problem and is not. + +Unrelated noise now identified and safe to ignore: `Rec Room Studio unable to record last project +path` / `skipped checking for update` / `skipped checking for local update to install`, all +`FileNotFoundException` for `ProjectSettings\Packages\com.recroom.studio\BranchId.txt`, plus +`EditorUpdateCheck: Failed The requested URL returned error: 404`. Those only disable the studio's +self-update check. + +### The server now leaves a readable log + +PID 448 had no stdout redirect, so the editor's actual request bodies from that first attempt are +gone. It has been restarted (PID 39184) from `Server\` with `ASPNETCORE_ENVIRONMENT=Development` and +stdout captured to `.tmp-studio-auth.log` in the repo root. `ReadRequestFields` already logs the +method, path, client IP, content-type, user-agent and every field of every studio request, so the +next Sign in attempt is fully diagnosable: + +``` +Studio POST /auth/connect/studiotoken from ::1, content-type=application/x-www-form-urlencoded, + user-agent=curl/8.17.0: grant_type=urn:ietf:params:oauth:grant-type:device_code, device_code=..., client_id=recroom-studio +Studio token issued: account=2. +``` + +Restart pattern (the working directory matters - `DBConfig` resolves `RadicadeServer-Data/` and +`RadicadeServer-Config/` relative to it, so starting from anywhere else silently uses a different +account database): + +```powershell +Stop-Process -Id -Force # dotnet build cannot write bin/ while it runs +cd Server; $env:ASPNETCORE_ENVIRONMENT='Development' +Start-Process -FilePath "bin\Debug\net9.0\Server.exe" -WorkingDirectory (Get-Location) ` + -RedirectStandardOutput "..\.tmp-studio-auth.log" -RedirectStandardError "..\.tmp-studio-auth.err.log" +``` + +It takes 90-150 s to start listening on `https://localhost:3030` and `http://127.0.0.1:3031`, and the +log's `Content root path` line confirms which working directory actually won. (Starting it from a +script: do not capture the launcher's stdout - the server inherits the pipe and the shell blocks +forever. It starts fine, the wrapper just never returns.) + +### Sign-in works; the studio API surface (2026-09-26) + +Sign in now completes - the studio window shows the account, and a **Sign out** button. What it does +afterwards is visible because this server already logs every request: + +``` +[09:22:08] GET /studio/analytics/rudderstack-api-key -> 404 (x20) +[09:22:08] GET /rooms/rooms/ownedby/me -> 200 +[09:22:08] GET /rooms/rooms/25?include=1037 -> 200 +[09:22:13] GET /rooms/rooms/25/subrooms/32/saves?skip=0&take=30 -> 200 +[09:22:13] GET /studio/cloud-builds/for-room?roomId=25&subRoomId=32&skip=0&take=30 -> 404 +``` + +`cloud-builds/for-room` is the wall: with no route at all the panel showed +"Failed to open room - Could not download room details. 404 (Not Found)", and the editor retried it +on a loop (three `HttpRequestException: 404` in the console). + +The complete route list fell straight out of the same DLL that held the nameserver literal - the +client's UTF-16 #US heap keeps its route templates back to back, in +`Packages/com.recroom.studio.build/Editor/RecRoom.Studio.Editor.Core.dll` at offset ~296k: + +``` +cloud-builds +cloud-builds/for-room +cloud-builds/create (+ query keys autoPublish, buildBranch, cloudBuildId) +cloud-builds/custom-avatar-item/{0} +cloud-builds/custom-avatar-item/{0}/save/{1} (+ customAvatarItemSaveId) +cloud-builds/cancel +remote-run/push-to-device +collaboration/owners-in-room +account/{0} +``` + +and, in the same block, the auth client's own vocabulary - which closes the loop on the two fixes +above: + +``` +connect/studiotoken, client_id=recroom.studiopipeline, client_secret=54oc5C4BH7jpqp42KhChkNZOLP0U3yTx, + grant_type=client_credentials <- a second, non-interactive login path +recroom.studio, refresh_token <- and a refresh path +connect/deviceauthorization, urn:ietf:params:oauth:grant-type:device_code, device_code, + authorization_pending <- the editor DOES send the urn form, and it does poll for + authorization_pending; both are now handled +account/me, rrs.pushtostudio, RecRoom.Studio.SessionId +``` + +`Server/API/Studio/StudioAPI.cs` (the folder had been an empty placeholder since the studio work +started) now answers the three read-only ones: `cloud-builds/for-room` and +`collaboration/owners-in-room` with an empty list, `analytics/rudderstack-api-key` with an empty key +(that route is polled twenty-odd times a session and the 404s were noise). Verified 200 with the +exact query string the editor uses. + +Empty lists, not 404s, on purpose: this server has no cloud build to hand out. Producing one is the +other half of the feature - `cloud-builds/create` plus the git-over-HTTPS remote the studio pushes +the scene to - so the useful next state is the editor saying "this room has no builds" and showing +what it asks for after that, instead of failing on transport. + +Still unexplained but harmless: three `NullReferenceException`s in +`KJGMKNABDIJ.EMOFOGPOOLJ(System.String, System.Char)` fire a second after a successful sign-in. That +is the studio's own URI parsing helper (the same one behind the earlier +`Failed to obtain RecNet Service URIs`), so if the studio later complains about a missing service it +is the first place to look - the candidate is a nameserver key we do not emit that it expects to +split. + +> **Explained below** ("Opening a room"). Decompiling the class settles it: `EMOFOGPOOLJ` is +> `url.EndsWith('/') ? url : url + '/'`, it NREs on a **null** nameserver field, and the null one is +> `StudioCDN`. The three exceptions were the same missing key each session, not a cosmetic +> curiosity - every URI before it resolved, which is why the session carried on. + +### for-room wants a page, not an array (2026-09-26) + +Answering an empty list moved the failure from transport to **shape**, which is what makes the next +step legible - the studio window stopped reporting a 404 and started naming the type it wanted: + +``` +Could not download room details. +The JSON value could not be converted to + RecRoom.Core.Studio.RecNet.PagedResultsDTO`1[RecRoom.Core.Studio.RecNet.CloudBuildDTO]. +Path: $ | LineNumber: 0 | BytePositionInLine: 1. +``` + +Byte 1 of `[` means the studio's list DTO is an **object**. This repo had already learned that for +the game client - `V2_RoomsAPI.EmptyPage` carries the note "The empty page every paged list here +answers with `{ TotalResults, Results }` rather than a bare `[]`. The client's list DTOs are objects +(`PagedResultsDTO`)" - so `StudioAPI.CloudBuildsForRoom` now mirrors that shape: +`{ Results: [], TotalResults: 0, TotalCount: 0 }`. + +`collaboration/owners-in-room` is the exception and stays a bare array. The editor requested all +three studio routes within the same second (`09:23:55` in `.tmp-studio-auth.log`) and accepted `[]` +there without a deserialization error, so the two routes are not the same DTO family. Reading the +console to see *which* route threw, instead of assuming one uniform shape for every studio list, is +what separated them. + +> **Wrong, and corrected below.** The editor *did* reject `[]` on that route - it just logged the +> `Result>` deserialization error a second later, next to the room-open failure, where it +> read as part of that failure rather than as its own. "The two routes are not the same family" was +> the right conclusion for the wrong reason: `owners-in-room` is wrapped (`Result`), not paged. + +Opening a room still cannot succeed: `for-room` is only the lookup, and this server has no +`CloudBuildDTO` to return because nothing has ever been uploaded. The next milestone is producing +one - `cloud-builds/create` plus wherever the studio pushes the scene - not another shape fix here. + +### Two patcher details settled the same day + +* The one-time backup now lives **outside** the package folder, at + `\radicade-backups\\...\X.dll.radicade-orig`, because Unity treats everything + under `Library\PackageCache` as immutable and logs the `has no meta file, but it's in an immutable + folder` warning that opened this whole trail (`-Restore` still understands a legacy in-package + backup). Relocate an existing one with a plain `move`; restore -> patch -> re-run was verified + round-trip, leaving the slot at offset 296749 with 40 bytes of zero padding and the file size + unchanged. + +### Opening a room: the studio's DTOs, and why `""` is not `null` (2026-09-26) + +The screenshot that prompted this round: the room resolves (`TestPlayers_MakerRoom_3528/Home`), sign-in +holds, the panel offers **Build on Rec.net** and **Download** - and **Open Room** fails with two +stack traces that have nothing to do with transport, because the server log for that whole session +has no non-200 in it: + +``` +[09:33:33] NullReferenceException: Object reference not set to an instance of an object + KJGMKNABDIJ.EMOFOGPOOLJ(System.String, System.Char) +[09:33:34] ArgumentException: The string argument 'filename' cannot be empty. Parameter name: filename + at CDLPCLJLLLG.DHGPNOHKPNB(...) -> MAMHCJFJOGI+OMPAKMPBPNM.MoveNext() + OGINOCKCDO+ICFNAE5EAJ.MoveNext() <- "Could not download subroom data" +[09:33:34] JsonException: The JSON value could not be converted to + RecRoom.Core.Studio.RecNet.Result`1[System.Collections.Generic.List`1[System.Int32]]. + Path: $ | LineNumber: 0 | BytePositionInLine: 1. +``` + +The editor's own log keeps the full traces and names the methods, which is what makes the shapes +readable - `%LOCALAPPDATA%\Unity\Editor\Editor.log`, and the interesting lines are the ones between +the exception and the `UnityEngine.DebugLogHandler:Internal_LogException` marker. + +#### Decompiling the studio is one command, and it is already installed + +`ilspycmd` (8.2.0) is a global dotnet tool on this machine, and the editor's assemblies are ordinary +managed DLLs - the obfuscator renamed types and methods, not the DTOs, because the DTOs are the +wire contract: + +``` +ilspycmd -l "RecNet" ".../Library/PackageCache/com.recroom.studio.build@e443aa011f13/Editor/RecRoom.Studio.Editor.Core.dll" +ilspycmd -t 'RecRoom.Core.Studio.RecNet.Result`1' "" +ilspycmd -o .tmp-rrs-decomp "" # 7,725 lines, whole assembly +ilspycmd -o .tmp-rrs-authoring ".../com.recroom.studio@6492b87ce18e/Editor/Authoring/RecRoom.Studio.Editor.Authoring.dll" # 28,779 lines +``` + +Both dumps are kept in the repo working tree (gitignored, like the `.tmp-*.log` files) - the loader +lives in **Authoring** and the API client in **Core**, so a question about a route is usually a +grep across both. + +#### The reply shapes, verbatim + +```csharp +[Serializable] public class Result { public bool Success { get; set; } public string Error { get; set; } } +[Serializable] public class Result : Result { public T Value { get; set; } } +[Serializable] public class PagedResultsDTO { public List Results { get; set; } public long TotalResults { get; set; } } + +public class RoomDetailsDTO : RoomDTO { + public string DataBlob; public string DataBlobHash; + public IReadOnlyList SubRooms; public IReadOnlyList Tags; + public IReadOnlyList Roles; public IReadOnlyList CoOwnerAccountIds; } +public class SubRoomDTO { + public long SubRoomId; public string Name; public string UnitySceneId; public int MaxPlayers; + public SubRoomDataSaveDTO CurrentSave; } +public class SubRoomDataSaveDTO { + public long SubRoomDataSaveId; public long SubRoomId; public string UnityAssetId; public string UnityAsset; + public string UnityAssetHash; public string DataBlob; public string DataBlobHash; + public int PersistenceVersion; public byte OMVersion; // <- a BYTE, not an int + public int? SavedByAccountId; public PICFJDPCODO? SavedOnPlatform; + public ELMJICAIAAA? SavedOnDeviceClass; public string Description; public DateTime CreatedAt; } +``` + +`Result` is what catches the eye: every reply the studio *mutates* with is wrapped, and the wrapper's +`EEKDNHEOFFD()` ("assert result") is `Debug.Assert(Success == (Error == null))` followed by +`if (!Success) throw new Exception(Error)`. So a wrapper with `Success:false` is not a soft failure - +it throws out of whatever the editor was doing. `owners-in-room` is declared +`Task>` over `Result>`, which is exactly the type in the `JsonException`: a bare +`[]` was never accepted there, it just failed a second after the room-open error and read like part +of it. `StudioAPI.OwnersInRoom` now answers `{ Success: true, Error: null, Value: [] }` (the value is +the room's owner account ids, which the editor resolves one by one for its collaborator list). + +#### Every nameserver URL is required, and `StudioCDN` was missing + +`KJGMKNABDIJ` in the same DLL is the nameserver client: a `NameServerResponse` with 27 string fields, +a `IEBKFBLKOKM` that walks them one at a time into `CNHJLHELLLL[key] = new Uri(EMOFOGPOOLJ(url, '/'))`, +and `EMOFOGPOOLJ` is `url.EndsWith('/') ? url : url + '/'` - **it dereferences the string**, so a key +the response does not carry is a `NullReferenceException` mid-walk, wrapped as +`Failed to obtain RecNet Service URIs: Object reference not set to an instance of an object.` The +dictionary is assigned *before* the walk, so everything before the missing key stays usable: the +session looks healthy and three NREs are logged. + +``` +RecNetStatus Auth API Notifications Images CDN Commerce Matchmaking Storage Chat Leaderboard +Accounts Link RoomComments Clubs Rooms PlatformNotifications Moderation DataCollection +BugReporting Discovery PlayerSettings Studio GameLogs Strings WWW Econ StudioCDN +``` + +`GET /nameserver` on this server had all of them but **two**: `StudioCDN` (mapped, so this is the +null that NREs) and `RecNetStatus` (declared by the editor but never mapped, which is why it was +never the culprit). `RecNetStatus` stays unwritten on purpose - adding a URL that 404s to a field the +2023 client may also read is a worse trade than leaving a field the studio never touches absent - and +`StudioCDN` is now `{ServerUrl}/studiocdn`, the base for the editor's `roomunityasset/{filename}` +(source-project manifest) and `unityasset/{filename}` (the baked bundle). Neither route exists yet; +the point is that the editor can build a `Uri` and get an honest 404 instead of an NRE, and the fix +is one line in `DBConfig.NameServer` next to a comment saying all 27 are load-bearing. + +#### A blob name of `""` is fatal; `null` is the documented "nothing to download" + +The room loader (`OGINOCKCDO` in Authoring) does this, in order: + +1. room details, wrapped in `PFJIHBLICIE(..., "Could not download room details")`; +2. pick the save: `room.LNLADOGFALG?.IDNMEAFLKOF?.FirstOrDefault() ?? room.DJACJOEJMOD?.CurrentSave` - +the **saves list first** (the `/saves` page), then the room payload's `currentSave`; +3. download the subroom blob from `save.DataBlob` via +`BCEDBPKMONJ.OGLFKNHIMHG(name)` - failure text `"Could not download subroom data"`; +4. download the room-level blob from `RoomDetailsDTO.DataBlob` via +`BCEDBPKMONJ.FKDIJAIOAPJ(name)` - `"Could not download room data"`; +5. both go to `MAMHCJFJOGI.GMIJOHGNLFI(filename)`, which *first* calls +`CDLPCLJLLLG.DHGPNOHKPNB(filename, "filename")` - the empty-string guard from the trace - and then +`GET {CDN}/room/{filename}`; +6. and both are **skipped entirely when the name is null**, succeeding with an empty payload +(`new PLGEIAPHMDM(null)` / `new GDEHOACHGIF(null)`), which the validators then step over +(`if (payload != null && payload.Length != 0)`). + +So `""` is the one value that cannot be handled, and this server was emitting it in two places: + +* `RoomUtils.ConvertToFull` hardcoded `DataBlob = ""` - the room *level* field. Nothing on this server + has ever produced a room-level blob (a save carries `RoomData.Filename` **and** + `SubRoomData.Filename`; `RoomServerAPI.UploadRoomData` only ever stored the subroom one), so the + honest value is `null`. This is the failure the first attempt hit: the subroom download had + succeeded (the `/cdn/room/7dc6589a-...` 200 in the log) and the room blob killed it. +* `subRoom.CurrentSave = BuildCurrentSave(subRoom)` ran unconditionally, so a never-saved subroom + shipped a save object with `DataBlob: ""` instead of `currentSave: null`. That is the second + attempt: room 31 simply has no save, and the studio read the placeholder save and died on the guard. + +Both are fixed, and both fixes are safe for the game client - checked two ways. `grep -rli currentsave` +over the 2023 client's decompilation under `.external/aug-decomp` finds **nothing**: the client never +reads `currentSave` at all (it loads saves from the `/saves` route, whose blob-less entries this +server already omits). And the reference implementation under `.external/recflare-server` documents +the same contract - `SubRoomDto.CurrentSave: z.nullable().describe('Null until first save')`, no +room-level `DataBlob` field on its room DTO at all, and `if (input.roomDataFilename) sub.RoomDataBlob += input.roomDataFilename` on the save - so a null here is what the real backend does, not a +workaround. + +#### Verified against the running server + +Rebuilt, restarted, and read back with a token minted through the studio's own device flow (the +`client_credentials` path in `.tmp-studio-auth.log` works too, without a browser): + +``` +nameserver StudioCDN = 'https://localhost:3030/studiocdn' + +--- room 31 (TestPlayers_MakerRoom_3528) roomDataBlob=None + subRoom 34 'Home' flatDataBlob='' currentSave=null +--- room 54 (TestPlayers_MakerRoom_1603) roomDataBlob=None + subRoom 57 'Home' flatDataBlob='7dc6589a-f613-4575-afc2-21efaf9347ac' + currentSave={ "SubRoomDataSaveId": 1, "SubRoomId": 57, "DataBlob": "7dc6589a-...", ... } + +owners-in-room -> {"Success": true, "Error": null, "Value": []} +cloud-builds/for-room -> {"Results": [], "TotalResults": 0, "TotalCount": 0} +``` + +Room 31 now opens against an empty scene (nothing was ever saved) and room 54 against the save it has; +the room-level blob being absent means the scene opens without room-level data, which is what a room +with no room data is, and a studio save is what would create it. + +`dotnet build Server/Server.csproj` is clean, and **185 tests pass** across `RoomSave`, `SubRoom`, +`RoomRole`, `AdminRoom`, `RoomDeveloperOwned`, `PlayerWarning`, `CdnRange` and `RecNetShape` - the +last one is the conformance suite that compares payloads against the reference shapes, so the +`CurrentSave`/`DataBlob` change is covered in the direction that matters (a subroom that HAS a save +still reports it). `ServerTests` still cannot build as-is - its csproj compiles two +`2023Patch/RadicadeEacBypass/*.cs` files that live in the uncommitted stash - so this run used a +temporary project file beside it with those two `Compile Include`s dropped and the three test files +that need them (`HubRealtimeTests`, `ImageSignatureRequestTests`, `NameserverRedirectTests`) +removed. Do not "fix" the csproj: the missing sources are somebody's work in progress. + +#### What is left before a room can actually be built in Studio + +* `cloud-builds/create` (+ `buildBranch`, `autoPublish`, `cloudBuildId`, `cloud-builds/cancel`) - the + studio's `Build & Upload` button, and the only way a `CloudBuildDTO` will ever exist to list; +* `remote-run/push-to-device` (`{ SessionId, RoomId, SubRoomId, RoomData, SubRoomData, + BakedUnityAssets }` of `UploadFileResponseDTO`s); +* {StudioCDN}/`roomunityasset/{filename}` and `/unityasset/{filename}`, plus `{Storage}/upload` and + `/upload/unityassets` - where the studio puts the baked Unity assets it needs back; +* the **room**-level `DataBlob`: accept `RoomData.Filename` on the save route and store it, so a room + saved *from Studio* comes back with room data instead of `null`; +* `collaboration/owners-in-room` with the room's real owner ids, which needs the collaboration layer + this server does not have yet. + +### A room opens in Studio: the illegal-text check, and the realtime channels (2026-09-26) + +The three shape fixes above landed the room. `DormRoom/Home` opened with its real Maker Pen scene in +the viewport (Plants, Basketball, Welcome Mat, the whole dorm object list), the panel stopped saying +"Failed to open room", and **Build on Rec.net** filled in with the subroom's save history (`#1`, `#39`, +`#38`, `#37` - those numbers are the `SubRoomDataSaveId`s the `/saves` route mints). + +Two errors were left, and both were missing server surface rather than shape: + +``` +[10:52:53] HubException: Failed to invoke 'SubscribeTo' due to an error on the server. + HubException: Method does not exist +[10:54:41] NullReferenceException KOMDFENMFBG+EADNODIDNG.MoveNext() + -> dialog: "An exception occurred while checking for illegal text in components. + Trying again may fix the error. Exception: 'Object reference not set ...'" +``` + +#### The purity check: `POST /api/sanitize/v2/isAllowed/bulk` + +The studio runs every string in the scene through `KOMDFENMFBG.DOLJODEKDLI`, which calls +`Result`, i.e. `POST {API}/api/sanitize/v2/isAllowed/bulk` with `Values`, +and then does: + +```csharp +Result newStringAllowances = await NLMHGDMIMAP(toBeChecked); +for (int i = 0; i < toBeChecked.Count; i++) + HINAIGHAGOI.TryAdd(toBeChecked[i], newStringAllowances.Value.ArePure[i]); +``` + +`Value` is dereferenced and `ArePure` is indexed with the sender's own index, so the reply has to be +the `Result` wrapper with **one bool per string, in order** - which is the NRE, and the reason the +dialog says "trying again may fix the error" rather than naming the string it disliked. The route did +not exist at all (404, `len=0`), and the NRE is what the editor made of the empty body. + +One detail decides how the route reads its input. The editor's request builder +(`OCOOLHLEDHD.MFBDEFCEDGN`) appends its parameter list to the URL **only for GET**; for every other +verb it wraps the same list in `FormUrlEncodedContent`. So this arrives as a form body of repeated +`Values` fields, and the log line looks like the studio called it with no arguments at all - there is +nothing after `bulk`: + +``` +[09:54:59] POST /api/sanitize/v2/isAllowed/bulk -> 404 (0.2 ms) len=0 ct= body= +``` + +`SanitizeAPI.IsAllowedBulk` now answers `{ success: true, error: null, value: { ArePure: [...] } }` +from the same `StringTools.IsPure` every other route here applies to room names and inventions, with +the form spelling and the query spelling both accepted (so it can be exercised by hand). Verified: + +``` +$ curl -X POST https://localhost:3030/api/sanitize/v2/isAllowed/bulk -d 'Values=Plant (Dracaena)&Values=faggot&Values=' +{"success":true,"error":null,"value":{"ArePure":[true,false,true]}} # aligned, in order +``` + +(The `Result` names are lowercase because that is how `Success` declares them for the game client; +the editor binds them anyway - it is `JsonSerializerDefaults.Web`, i.e. case-insensitive. That is also +why the `owners-in-room` wrapper from the section above needed no second pass.) + +#### The realtime channels: `SubscribeTo` / `UnsubscribeFrom` on the hub + +The editor's realtime client (`LKDEHPKLIGJ`) connects to `{Notifications}/hub/v1` - `NotifyHub`, which +already serves the game client at that path - sends `{ Channels: [...] }` to those two methods, and +reads `Notification` frames back off the connection. The channel it subscribes to is +`RRS:`, which is exactly where a `rrs.pushtostudio` message would come from +(`remote-run/push-to-device`, still unimplemented). The hub had `SubscribeToPlayers` but not +`SubscribeTo`, and SignalR answers an unknown method with `HubException: Method does not exist` - an +error the editor logs and retries on every reconnect instead of ignoring. + +Both methods now exist, take the raw `object?` the way the neighbouring hub methods do, pull the +channel names out of the `JsonElement` by name (SignalR serializes camelCase, so the lookup is +case-insensitive), and log the subscription. Nothing publishes to them yet, which is stated in the +log line rather than silently swallowed: + +``` +[NotifyHub] U9-Bl4o4KZFIUWR5eMi9IA subscribed to RRS:test-session (nothing publishes to these yet). +[NotifyHub] U9-Bl4o4KZFIUWR5eMi9IA unsubscribed from RRS:test-session (nothing publishes to these yet). +``` + +Verified without a SignalR client package, by driving the hub's **long-polling** transport with curl - +worth keeping, because it is the only way this repo can poke the hub by hand: + +``` +TOK=$(curl -s -X POST 'https://localhost:3030/notify/hub/v1/negotiate?negotiateVersion=1' | jq -r .connectionToken) +curl -s -X POST "https://localhost:3030/notify/hub/v1?id=$TOK" -H 'Content-Type: text/plain;charset=UTF-8' \ + --data-binary $'{"protocol":"json","version":1}\x1e' # handshake +curl -s -X POST "https://localhost:3030/notify/hub/v1?id=$TOK" -H 'Content-Type: text/plain;charset=UTF-8' \ + --data-binary $'{"type":1,"target":"SubscribeTo","arguments":[{"channels":["RRS:test-session"]}],"invocationId":"1"}\x1e' +``` + +The `\x1e` record separator is part of the protocol, and the response body stays empty on the POST - +the server-side log line is the proof of dispatch (a missing method answers with a `HubException` in +the next poll instead). + +The panel's remaining warnings are the honest ones: "This scene has never been uploaded from Rec Room +Studio" and "Never save available on rec.net" are the empty `cloud-builds/for-room` page, and they go +away when `cloud-builds/create` and the build store exist. + +## Keepsakes: what the reference server holds, and the store we built + +There is a second reference in play now - a downloaded fork at +`rec-room-2023-server-main/rec-room-2023-server/asdfasdf` - so it is worth stating exactly what it +carries, because it is less than it looks. Five files mention keepsakes and only one of them holds +anything the client can use: + +| file | contents | +|---|---| +| `keepsakes_categories.json` | the **real** catalog: 5 categories in a counted result set | +| `APIs/API.cs:186,527,539` | the same three GETs we serve: `categories` (reads the JSON), `globalconfig`, `rooms/{roomId}` -> `Results.NoContent()` | +| `KeepsakesConfig.cs` | empty stub classes, no code | +| `APIs/Auth.cs:227` | `"keepsake"` in a role array - inert | +| `ProgressionEvent.cs:15` | `KeepsakeRoomLists`, never assigned | + +So the fork's keepsake endpoints are already byte-for-byte ours - the same 204 for a room, the same +config trio - **except that its catalog is populated and ours was empty.** That one file is the delta, +and it is trustworthy: each entry's field order matches the client's own +`KeepsakeCategoryConfigDTO(enum, string, int, int, string, string)` constructor, and its ids +(0 `explore`, 2 `presents`, 4/5/6 `science_core_*`) are the Science Core event this build's own +GameConfig text describes. + +### What the client itself says it wants + +Straight out of `global-metadata.dat` rather than inferred: the route literals are +`api/keepsakes`, `api/keepsakes/categories`, `api/keepsakes/events`, `api/keepsakes/globalconfig`; +the feature's error strings (`Failed to add keepsake`, `Failed to collect keepsake {0}`, +`Failed to delete keepsake {0}`, `Waiting for Keepsake Instance Data download...`, +`Cannot place additional keepsakes in this room!`) and the notification names +`KeepsakeInstanceAdded` / `KeepsakeInstanceRemoved` are all present. Our own log already showed the +three GETs firing on every room load (`rooms/25 -> 204`, `categories -> 200 len=43`, +`globalconfig -> 200 len=100`). + +The one write-side payload we can read comes from the recflare reference's notify package, which +documents the captured shape: + +```ts +/** `KeepsakeInstanceAdded` / `KeepsakeInstanceRemoved`. */ +interface KeepsakeInstancePayload { + KeepsakeInstanceId: string // the GUID the room save carries + KeepsakeCategoryConfigId: number + PlacedByAccountId: number + RoomId: number + SubRoomId: number | null +} +``` + +Two things follow from that, and they drive the design. The instance id is a **GUID the room save +already contains** - the client writes it when an author places a keepsake - so the server must store +the id it is told, never mint its own; and a room whose save holds a keepsake with no row behind it is +precisely the state the keepsake runtime null-references. + +### What shipped this session + +* `KeepsakeInstances` and `KeepsakeCollectionRecords` (hand-written guarded migration + `20260927120000_AddKeepsakes`, the same style as the neighbouring ones). +* `GET api/keepsakes/categories` now serves the five real categories (`RADICADE_KEEPSAKE_CATALOG=empty` + restores the empty set; `RADICADE_KEEPSAKE_ICON=` swaps both icon names, which is how we find + out which name the client actually reads). +* `GET api/keepsakes/rooms/{id}` is unchanged when nothing is placed (204) and serves + `{KeepsakeInstances, KeepsakeCollectionRecords, KeepsakeProgressionEventIds}` once something is - + the instances this caller has *not* collected, then their records. +* `POST api/keepsakes/rooms/{id}` places one, keeping the caller's GUID when it sends one; + `POST api/keepsakes/rooms/{id}/collect` records the pickup, once per player (a repeat keeps the + first timestamp). +* Anything else under `api/keepsakes` lands on a probe that names the call in the console and answers + **404, never 204**: a write that looks successful while the room save holds an id nothing backs is + the exact failure the empty catalog was protecting against. + +The two open unknowns are both answered by that probe rather than by more guesswork: the exact +spelling of the collect/delete routes (the literals only prove `categories`, `events`, `globalconfig`, +`api/keepsakes`) and the JSON key the client reads for a placed instance. The next in-game attempt +prints them. + +### The room that already has a keepsake in it + +`RoomImports/Showdown/Showdown.room` - imported as an RRO at boot - contains a `keepsake` component +with a GUID beside it: + +``` +offset 585176: ... "keepsake" ... $a75f7547-79eb-47c6-8986-6767abcb4f92 ... +offset 591174: ... keepsake ... (the room's component-type table) +``` + +Nothing registers it yet, and that is deliberate: registering a *guessed* id is exactly the kind of +invented keepsake that faults a load. The right way in is the probe - join Showdown, watch which ids +the client reports, and store those. + +### Keepsake rooms: how a room comes to be allowed to hold keepsakes + +The banner this leaves behind - **"This room does not have keepsake permissions."** - is not a room +role. The client's room-permission enum is `CAN_EDIT_ROOM_ROLES ... CAN_USE_MAKER_PEN ... +CAN_USE_PLAY_GIZMOS_TOGGLE` with no keepsake entry in it, the room DTO has no keepsake field either, +and the fork's `ProgressionEvent.KeepsakeRoomLists` is never assigned. The only per-room keepsake +payload the client asks for while a room loads is `GET /api/keepsakes/rooms/{id}`, and the keys that +response may carry - out of the client's own metadata - are `KeepsakeProgressionEventIds`, +`KeepsakeRoomId`, `KeepsakeRoomLimit`, `KeepsakeRoomListId`, `KeepsakeRoomLists`, `KeepsakeRooms`. +So the permission arrives with the room's keepsakes or not at all, which is why every room answered +NO CONTENT and every placement answered with that banner. + +A room is advertised as a keepsake room when it is listed in +`Server/RadicadeServer-Config/keepsake-rooms.json` (`{ "AllRooms": false, "RoomIds": [3, 25], +"RoomListId": 1, "RoomLimit": 100 }`) or when `RADICADE_KEEPSAKE_ROOMS` names it (ids, or `*` for +every room, with `RADICADE_KEEPSAKE_ROOM_LIST_ID` / `RADICADE_KEEPSAKE_ROOM_LIMIT` beside it). Those +rooms answer 200 with the keys above beside the three instance lists - and those lists stay honest, +so an empty keepsake room still carries nothing for the keepsake loader to resolve - and the server +logs `[Keepsakes] room {id} answered as a keepsake room: list ..., limit ..., N room(s) advertised, +M placed.` Every other room is unchanged: NO CONTENT, the old behaviour. **The config that ships +enables every room** (`AllRooms: true`): the banner is the symptom this exists to clear, so a +checkout that shipped it switched off would look exactly like the bug. Narrowing it back is an +operator edit - `AllRooms: false` plus the ids in `RoomIds`. + +The file is re-read on every room load, so an edit applies to the next room a player enters with no +restart. That is verifiable without the game: with the running server on 3030, `curl -sk +https://localhost:3030/api/keepsakes/rooms/25` answered `204 0b` while `AllRooms` was false and +`200 381b` with `KeepsakeRoomId`/`KeepsakeRoomLimit`/`KeepsakeRoomListId`/`KeepsakeRooms`/ +`KeepsakeRoomLists` the moment the file said `true` - same process, no relaunch. + +**Confirmed in game.** With `AllRooms: true`, walking into room 25 clears the banner: those five +room keys ARE the permission, and the client asks for them every room load (`[Net] Get +.../api/keepsakes/rooms/25` in the 21:09 log, the first run that got far enough to make the call). + +### Permission is not the whole gate: the keepsake comes from a progression event + +Permission cleared the banner but left the MakerPen's keepsake tile reading **`x0`** - a room can be +allowed to hold keepsakes and still offer nothing to place. The second half is the progression +event: + +* The room payload's `KeepsakeProgressionEventIds` names the event the room's keepsakes belong to + (the client's metadata spells the field run `EmptyKeepsakeProgressionEventIdsKeepsakeRoomId…`, + i.e. "empty" is its own state). +* `GET /api/progressionEvents/active` answers the active event's **ID** - a bare number, CONFIRMED: + with the id served, the client's log has no malformed line for this route at all, and the very + next calls are `GET /api/progressionEvents/event/1` and `GET /api/progressionEvents/record/1` + (the record asked four times in one session), i.e. `ProgressionEventDTO` and + `ProgressionEventRecordDTO` by id. Both were 404s here, so the id alone changed nothing on + screen; the record, its `KeepsakeRoomLists` and the palette's keepsake allowance all hang off + them.The client's progression-events client has exactly two no-argument calls, `KIJDNIPGGDC() -> + MFBKDKPGMNP` and a private one returning a record, and the id is what the model caches + first: `ABIEHIAHKJP(Nullable)` is that field's setter and `BJKPLIJHAJK(long, bool)` is the + id-taking record fetch. A nullable long deserializes from none of the shapes this endpoint + answered before (`[]`, `{}`, and a full record), each logged by the client as `Malformed + Response: ''`. + +Both by-id payloads carry an `_route` marker (`"event"` / `"record"`). An unknown member is ignored +by the deserializer, so it costs nothing, and it is what makes a `Malformed Response: ''` line +in the client's log name WHICH of the two the parser rejected - the log shows a body, never a URL. +* `RoomListModel.ApplyKeepsakeOverrides(ref eventRecord, RoomModel, long, int, long)` is the client + applying that record's `KeepsakeRoomLists` to a room, and the sibling members + `IsActiveEventKeepsakeEvent` / `GetKeepsakeRoomListIdFromRoomId` / `GetNextKeepsakeRoomUnlockTime` + are all event-shaped. + +The record's field names come from the 2023 fork's own `ProgressionEvent` model +(`Downloads/rec-room-2023-server-main/.../ProgressionEvent.cs`) and each one is **present in this +build's `global-metadata.dat`**: `ProgressionEventId, Name, IsEnabled, UsesBoost, BoostXpMultiplier` +(the fork spells it `BoosXpMultiplier` - a typo), `BoostDailyGameplayMinutesLimit, +ActiveExperiment, StartTime, EndTime, CollectionEndTime, EventInfoImageName, +ChallengesIconImageName, RewardsPipImageName, PurchasableXpBoostId, Rewards, KeepsakeRoomLists`. +Two fork fields are NOT in this build and are left out: `EventLeaderboardId` and +`InitialCurrencyGrant`. The event is windowed around the current time (start -30d, end +1y) rather +than pinned to a date, so a machine whose clock is in any year still sees it running. + +The event's `KeepsakeRoomLists` carry the same one-list entry the room payload carries (from +`KeepsakeRoomConfig.RoomListEntries`), because the client's copy of the event OVERRIDES the room's: +a room the event's list does not mention would be overridden straight back to no permission. Under +`AllRooms` the event's copy carries no member rooms - they cannot be enumerated - and the room's own +copy is what names it. + +**The event payloads were rejected, and the client's own log says so.** The game writes every +session to `BepInEx/LogOutput.log`, which the plugin's `[LogGate]` feeds: the room's keepsake answer +never appears in a `Malformed Response` line, but the event bodies do, verbatim - the whole record +(`Malformed Response: '{ "ProgressionEventId": 1, "Name": "Science Core", ...'`) and, after it was +narrowed to identity + window + keepsake lists, the narrowed one too (the log's truncation lands on +`"StartTime"` right after `"IsEnabled"`, which is how the two payloads are told apart). Both come +from `EFMPNEEHJFJ.CMKDNAPFCOI[T]`. Two things follow: + +* **Unknown members are ignored**, so a name that is merely absent costs nothing - which is why the + room's answer parses even though it carries six keys `KeepsakeRoomInstancesDTO` does not declare + (that is the answer that cleared the banner). +* A name whose TYPE is wrong fails the *whole* response, so the fix is to send FEWER names, not + more: every field that belongs to a neighbouring DTO rather than to this record is now out - the + XP-boost family (`BoostXpMultiplier`, `BoostDailyGameplayMinutesLimit`, `UsesBoost`, + `PurchasableXpBoostId`, all of which live on the boost DTOs) and the image-name fields (which may + be typed image references rather than strings). What is left is the event's identity + (`ProgressionEventId`, `Name`, `IsEnabled`), its window, `Rewards`, and `KeepsakeRoomLists`. + +Two knobs stay for the next narrow. `RADICADE_KEEPSAKE_EVENT_LISTS=empty` sends `KeepsakeRoomLists: +[]` in the record instead - an empty array deserializes for a list of anything, so if the malformed +line disappears with it set and returns without it, the entry shape is what the record rejects. +`RADICADE_PROGRESSION_ACTIVE=record` serves the record AT `/api/progressionEvents/active` instead of +the id, for the case where that endpoint turns out to be the client's other no-argument call. The +server console names which it served: `[ProgressionEvent] active -> 1` or `[ProgressionEvent] record +1 served (all keepsake list(s), list 1, limit 100).` + +**How to read the next run.** Walk into a room and place a keepsake; the server console names both +halves of the answer: + +```bash +grep -n "Keepsakes]" server-console-runNN.log # the room's answer, and any placement +``` + +A `POST /api/keepsakes/rooms/{id}` answered `accepted` means the tile had something to place. If the +tile still reads `x0`, the next lever is the event's contents rather than the room's permission - +and any keepsake call this server does not implement is still named by the `[Keepsakes] unclaimed +METHOD path body=…` probe (which answers 404 on purpose). `GET /api/keepsakes/events` is the one +keepsake route still stubbed to `[]`; the client's own route table (the literal run +`api/keepsakes api/keepsakes/categories api/keepsakes/events api/keepsakes/globalconfig` in +`global-metadata.dat`) shows those four are the whole keepsake surface, with the room route built +beside them. + +## Saving an outfit: the route the client posts to, and the 404 it met + +The client's `POST /econ/api/avatar/v4/saved/set` answered 404: this server had the saved-outfit +routes on **v3 only** (`GET /api/avatar/v3/saved`, `GET /api/avatar/v3/saved/{accountId}`, +`POST /api/avatar/v3/saved/set`), while the 2023 build reads its list from v3 and *writes* through +v4. The client's own route table in `global-metadata.dat` names the three avatar routes it knows: + +``` +api/avatar/v4/items api/avatar/v4/saved/name api/avatar/v4/saved/set +``` + +so the missing pair was `saved/set` and `saved/name` (`items` already existed). Only `saved/set` is +implemented here; nothing in this build has been seen calling `saved/name`, and a route invented +without knowing the shape it must answer is worse than a 404 that names itself. + +**v3 and v4 share one slot table.** Both post the same payload and both expect "save over this +outfit" to mean *re-save an existing slot*, and the client reads the result back through v3. Two +tables would let a player save an outfit that their own outfit list cannot find, so the slot +bookkeeping lives in one place (`RecNet/Avatar/SavedOutfits`) and each version is only a shape: +v3 echoes the outfit back, v4 answers the lean acknowledgement the client expects - `{ Success, +Slot }` - which is what the reference servers do too. + +Two things behind the 404 were wrong and had gone unnoticed because the route that used them was +never reached by this client: + +* `Outfit.Slot` was a **string**; the client posts an **integer** (`Slot: 0..n`). Deserializing the + client's body into the old DTO could not have bound, so even a v3-shaped route would have failed + on the body. It is `int?` now, and the payload's other two members (`OutfitSelectionsV2`, + `CustomAvatarItems`) are declared so they survive the round trip instead of being dropped on save. +* A body with **no slot** was accepted and stored as a literal `null` entry - the `[null]` an + earlier empty-body post left on account 2 is exactly that. A missing slot is now a 400, matching + the reference server: defaulting it would overwrite slot 0, the one outfit the player did not ask + to replace. Stored blobs are also filtered on read, so that legacy `[null]` is dropped rather than + handed to the client as an entry it cannot parse. + +Verified live against the running server: no token is a `401` (the route exists), a body without a +slot is a `400`, a real save is `200 {"Success": true, "Slot": 0}`, and +`GET /econ/api/avatar/v3/saved` serves that outfit back with its selections verbatim. The writes +were made on a throwaway slot and account 2's stored blob was restored afterwards. + +## Loading a room: what the second-per-second actually goes to + +A room join is slow - minutes for a Rec Room Studio room, and uncomfortably long even for a Dorm +whose save is 1.8 KB - and the log's own numbers say why. Measured against live from the client's +machine: + +| Measurement | Result | +| --- | --- | +| Fresh HTTPS request to the API | 0.57 - 0.86 s (`dns 0.28` + `tcp 0.19` + `tls 0.2` + server) | +| Second request on the same connection | **0.19 s** | +| Server-side work for a trivial route (`/api/version`) | the same 0.19 s as any other route | +| CDN asset bundle, one stream | 10 - 12 MB/s (85 MB ⇒ ~8 s) | +| TLS session resumption | already working (`Reused, TLSv1.3`) | + +0.19 s for *every* route including a no-op one is ~2 RTT to the VPS, which is 100 ms away - the +server is not spending that time, the wire is. So a room load costs what it costs because the client +makes dozens of calls and **opens a fresh TLS connection for each one** (the `[TLS] Accepting server +certificate ...` line is printed again before every single request in the log). Nothing server-side +removes that; two things were making it worse: + +* **`GET /econ/api/roomcurrencies/v1/getBalance` did not exist.** The client asks it immediately + after `getAllBalances` on every room join and retries an answer it cannot use; a 404 is an + exception plus another round trip per retry. Nothing here tracks room-currency balances, so it now + answers `200` with a zero balance that echoes the currency and account asked for + (`RoomCurrencyAPI.GetBalance`). +* **The CDN sent no `Cache-Control` at all.** `Accept-Ranges`, `Content-Length` and `Last-Modified` + were there, but nothing told the client it may reuse bytes it already has - and for a Studio room + the bytes in question are the entire asset bundle. Blob and asset routes now send + `public, max-age=31536000, immutable` for `.assetbundle` names (the id is minted per upload, so a + different bundle is a different URL) and `public, max-age=86400` for everything else, because an + admin repair can replace a room save under its existing name (`VPS/fix-dorm-blob.sh`). + +### Two things in the log that are not what they look like + +`Malformed Response: '[]'`, `'{}'` and `'[ {...rooms...} ]'` are thrown by the client's obfuscated +DTO reader, which **reads members positionally** - the same property `AlgorithmicListsAPI` documents +for `lists/algorithmiclists`. An empty body is therefore not a valid "nothing here" answer, and the +client retries instead of moving on. Several routes still answer that way by design (keepsakes off, +`progressionEvents/active`, `roomkeys/v1/room`); the retries are visible as 4x `photon_access_token` +and 5x `experience/player` per join. + +`Exception: Can't parse '' as a hex number!` fires ~112 times per session and is **not** +`PhotonAccessToken` being empty, despite that field being `""` in +`GET /rooms/photon_access_token`: the client fetches that route 7 times per session, and both +reference servers return `""` there too (`config/v1`'s `UseNewPhotonAuthToken` is `false`, so the +token is not used for the join). Whatever parses hex 112 times is elsewhere, and changing the token +to guess would risk the joins that currently work. + +### The failure at the end of a slow load + +`Unexpected Photon Disconnect due to cause: ServerTimeout` is what the client reports as *"Failed to +load room due to disconnect during load (Code 8706-3)"*: the load outlives the room's Photon +connection. The room-load stage timeouts are ours to set and are generous +(`RoomLoad.DownloadAssetBundleTimeoutSeconds = 900`, `PreDownloadTimeoutSeconds = 120`, +`PostDownloadTimeoutSeconds = 240`); the Photon peer timeout is not in that table, so the lever there +is client-side, not a config key. + +Verified locally against a dev server, then the server was stopped: + +``` +GET /econ/api/roomcurrencies/v1/getBalance?currencyId=...&accountId=2 -> 200 {"CurrencyId":...,"AccountId":2,"RoomId":0,"Balance":0} +GET /cdn/data/{blob}.room -> 200 Cache-Control: public, max-age=86400 +GET /cdn/data/{id}.assetbundle -> 200 Cache-Control: public, max-age=31536000, immutable +GET /cdn/data/{id}.assetbundle (-r 0-1) -> 206, Content-Range: bytes 0-0/1, same directive +``` + +Tests: the two CDN routes and `getBalance` are pinned in `CdnRangeRequestTests` and +`RoomEconomyPersistenceTests` (8/8), and the full suite is 688 passed / 1 failed - the pre-existing +`AvatarCatalogSourceTests.ShippedLookupCoversTheCatalogServedToClients`. + +## Run 30 fixes (2026-10-01): the ambiguous routes, the club category rows, the carousel envelope + +One landed batch from a single player report. Each item is the client's own URL, taken from +`BepInEx/LogOutput.log` / `Player.log` in the deployed client rather than from the templates. + +### `AmbiguousMatchException` (HTTP 500) on the club member actions + +`ClubsAPI` carries `[Route("clubs/club")]` (plus the `api/clubs/v{1,2}/club` copies), so an action +with BOTH the class-relative template `{clubId}/minlevel` and an absolute +`~/clubs/club/{clubId}/minlevel` registers the same path twice. ASP.NET reports that as ambiguity, +so *every* request to it threw before any handler ran - `PUT minlevel`, `POST|DELETE members/remove`, +`PUT members/invite` and the accept/decline actions were all dead. The absolute `~/clubs/club/...` +copies are removed (the `~/club/...` aliases are a different path and stay); the same duplication in +`ClubAnnouncementsAPI` and `api/playerevents/v1/report` (EventsAPI owns v1, ContentReportAPI keeps +v2) went with them, and `PATCH admin/panel/{table}/{id}` vs `{id:long}` is now one action that +parses the id. `ServerTests/RouteAmbiguityTests.cs` reflects over every controller and asserts no +(method, path) pair has two owners, so this cannot come back silently. + +### `POST clubs/club/{id}/members/acceptinvite` -> 405 + +The string literals (and the log) show the client accepts an invitation at `.../members/acceptinvite`; +the server only ever registered `.../members/accept`, so the invite notification led nowhere. Both +verbs are registered now, with `declineinvite` and `changetype` beside them, and `invitemembers` +joins `invite`. The invite reader also accepts the field spellings the reference DTO allows +(`accountId`/`accountIds`/`playerId(s)`/`targetPlayerIds`, single or repeated) instead of only +`accountId`, which is what silently invited nobody. + +### `POST econ/api/influencerpartnerprogram/remove` -> 415 + +The economy client does not post JSON, and `[FromBody]` only accepts JSON content types. The handler +reads the fields from the query string, a urlencoded form or a JSON body (content type or not), the +same way `redeem` does. + +### `DELETE api/customAvatarItems/v1/{guid}` -> 405 and `POST .../report` -> 404 + +The item's editor addresses it by id: `PUT` (metadata edit) and `DELETE` (remove) are registered +alongside the existing `GET`, both owner-gated, and the item report route resolves the item's creator +as the reported player before funnelling into the shared `SubmitReport`. + +### `GET matchmaking/room/{roomId}/instances` -> 204 + +The instance browser is the watch menu every player opens for the room they are in. It required +`RoleType.Moderator` and answered `null` to everyone else - `null` serialises to 204 No Content, +which the client renders as "Failed to get room instance browser: Response was empty". It now +answers an array (empty when the room has no live instance) to any signed-in caller. + +### "Clubs are appearing in every single category" + +The Clubs screen asks `clubs/club/categoryTags` for its categories and then fetches each row on its +own: `GET clubs/club/search?sort=0&category=Social&count=32`, one call per tag. The server ignored +`category` and returned the whole catalogue, so the single club was drawn in every row. +`ClubStore.CategoryFilter`/`MatchesCategory` now filter on `primaryTag` or `customTags` +(case-insensitive, `#tag` tolerated) for `search`, `mostactivetoday`, `mostactivenow` and +`search/mostactivenow`; a request with no category still gets everything. + +### `Error refreshing rooms from source CarouselEndpoint` + +Still one per Play-menu open, and now explained: the client's template for the row is +`rooms/carousel/{0}&skip={1}&take={2}` (a paged read), and the client logged our answer as +`Malformed Response: '[ { "RoomId": 167, "IsDorm": false, ... } ]'` - a bare array where it reads +`{ Results, TotalResults }`, the envelope every sibling `*_rooms/{source}&skip=&take=` feed already +answers with. `rooms/carousel/{*source}` answers that envelope now. The earlier "a bare array works" +conclusion came from a run whose Play menu was fed by `lists/algorithmiclists` + `rooms/bulk` +instead, so the route had never actually been read by the client. + +Tests: `RouteAmbiguityTests`, `ClubCategoryDiscoveryTests` (9), `CarouselFeedShapeTests` (3) and +three new instance-browser cases in `MatchmakeInstanceRouteTests`; full suite 798 passed / 9 failed, +all nine the pre-existing catalog set (`AvatarCatalogSourceTests`, `DorkNetCatalogTests`, +`EquipmentInventoryTests`, `StorefrontRewardConfigTests`). + +## Run 34 (2026-10-15, live): every room's bundle answered `400`, so no room loaded its content + +### What the player saw, and what it was not + +"Error downloading room assets! Try again later or on a better connection." on every room, for every +player. In `Player.log` the exception surfaces inside `RoomLoad.RoomManager` and the client reports +`FailedToSpawnPlayer / (Code 8715)` to matchmaking; the Zenject +`InvalidOperationException: You should not try to retrieve bindings from a disposed container!` that +follows is that failed load task being torn down, not a second fault. Nothing in the client's own log +names the request, which is what made this look like a connection problem. + +### The request the client was actually making + +`BepInEx/LogOutput.log` logs every call with its status, and it has the answer: + +``` +[HttpStatus] #322 Get /rooms/unity_assets/Showdown_0_1.assetbundle/0/1 -> 400 bytes=39 reason=Bad Request +[HttpStatus] #543 Get /rooms/unity_assets/2n8bbqwnk6zs6k2blzgcdhcnc.assetbundle/0/1 -> 400 bytes=39 reason=Bad Request +``` + +Room 247 is the `MAKER_ROOM:247/313` in the error: subroom 313 advertises +`UnityAssetId: 2n8bbqwnk6zs6k2blzgcdhcnc.assetbundle`, and step two of the download is healthy - + +``` +GET /data/2n8bbqwnk6zs6k2blzgcdhcnc.assetbundle -> 200 68,673,320 b +GET /rooms/rooms/247?include=5421&unityAssetTarget=0&unityAssetVersion=1 + -> 200 SubRooms[0].UnityAssetId = 2n8bbqwnk6zs6k2blzgcdhcnc.assetbundle +``` + +- so step one, the metadata call, was the only thing broken, and it was broken for every id of that +shape on the server rather than for one room. + +### The regression, in one line + +`b33d5b3` ("Checkpoint the in-flight work before merging origin/2023") reworked `UnityAssetsCDN` for +the Studio pipeline and put this in front of the resolution: + +```csharp +if (!StudioUnityAssetRegistry.IsValidAssetId(assetId) || target < 0 || ...) return BadRequest(); +``` + +`IsValidAssetId` is `value is { Length: 40 } && value.All(char.IsAsciiLetterOrDigit)` - the shape of a +Studio manifest id, and of nothing else that is stored as a bundle. Every legacy id fails it: +`Showdown_0_1.assetbundle` (23 characters, `_` and `.`), the 24-character RecNet ids +(`2n8bbqwnk6zs6k2blzgcdhcnc`, `8aas7b5ivtaggn9swhe3wkgdj`), the 36-character dashed id the importer +binds (`32cafb11-c51b-485c-8f1f-73b73159feff`), and the built-in rooms' `*_0_1.assetbundle` names. +`ResolveLegacyFilename` was still underneath, unreachable for exactly the ids it exists for, so every +room whose subroom points at a bundle - which is every room with custom content, for every player - +answered `400 {"success":false,"error":"bad request"}`. The pre-Studio file had no such gate: +`ResolveFilename(assetId)`, the id with no extension first, then the id plus `.assetbundle`. + +### What changed + +`Server/CDN/UnityAssetsCDN.cs` - 40 alphanumerics *with a manifest behind them* is still the Studio +path, and a target/version its metadata does not name is a `404` (the file stored under a manifest id +is the manifest JSON, so it must never be handed over as a bundle). Everything else resolves as a +stored file: the name as given first, then `.assetbundle`. + +`Server/CDN/CdnStorageNames.cs` (new) holds the two read-side rules the blob routes share - one bare +file name in that one directory (no separator, no `..`), never a name ending in the admin upload's +`.uploading` suffix, and `` resolving to `.assetbundle` - and `DataCDN`, `RoomsCDN` (both the +`/cdn/room` + `/unity` blob route and the raw `{assetId}/{target}/{version}` route) and +`InventionCDN` all go through it. Two things fall out of that: `/data/.uploading` no longer +serves a half-imported bundle by name, and the bare `/unity/{id}` alias now answers for a bundle +stored only as `.assetbundle` - the hard link per bundle that Run 31 needed is no longer +required. + +### Verified + +`ServerTests/UnityAssetMetadataTests.cs` (21 cases: both spellings of three id shapes, the Studio +manifest with and without a bake, the manifest never served as a bundle, an id that only looks like a +manifest, unknown names, separators and `.uploading`, out-of-contract target/version, and the blob +routes) - 21/21. `CdnRangeRequestTests`, `StudioUploadTests`, `AdminRoomImportTests`, +`AdminRoomPartUploadTests`, `AdminBundleBindingTests` together with it: 59/59. + +Live on a local instance (Development, cwd `Server`, `http://127.0.0.1:3031`, storage +`Server/RadicadeServer-Data/CDN/Storage`), the URL from the player's log: + +``` +GET /rooms/unity_assets/Showdown_0_1.assetbundle/0/1 -> 200 + {"UnityAssetId":"Showdown_0_1.assetbundle","Target":0,"Version":1, + "Filename":"Showdown_0_1.assetbundle","RejectedDependenciesFilename":null} +GET /rooms/unity_assets/Showdown_0_1/0/1 -> 200 Filename="Showdown_0_1.assetbundle" +GET /rooms/unity_assets/Showdown_0_1.assetbundle -> 200 (the short form) +GET /rooms/unity_assets/not-on-disk.assetbundle/0/1 -> 404 {"success":false,"error":"not found"} +GET /data/Showdown_0_1.assetbundle (Range 0-15) -> 206 16 b, Accept-Ranges: bytes +GET /unity/Showdown_0_1 -> 200 76,719,774 b (was 404 without the hard link) +GET /data/Showdown.room -> 200 1,222,161 b +``` + +and the refusals, with the file present on disk so the rule is what refuses: + +``` +GET /data/probe-upload.assetbundle.uploading -> 400 (was 200, a truncated bundle) +GET /rooms/unity_assets/probe-upload.assetbundle.uploading/0/1 -> 400 +GET /rooms/unity_assets/..%2F..%2Fappsettings.Development.json/0/1 -> 400 +GET /data/../appsettings.Development.json -> 404 +``` + +The probe file was removed afterwards and the instance stopped; nothing left listening on 3030/3031. + +Full suite: 1556 passed / 12 failed. Eight of the twelve fail identically with this change stashed +out (the catalog set the Run 30 note records: `AdminGiftSkinTests`, `ConnectionRecoveryTests`, +`DorkNetCatalogTests` x2, `EquipmentInventoryTests` x2, `StorefrontRewardConfigTests`, +`WeeklyChallengeTests`), and the other four (`GameRewardFlowTests` x2, `RecNetShapeConformanceTests` +x2) pass when their classes run alone and mutate the same two statics - +`DBConfig.DefaultAvatarCatalog` / `DefaultAvatarUnlocks` - that `AvatarCatalogSourceTests`, +`AvatarWireFormatTests` and `EquipmentInventoryTests` write from parallel collections. That race is +pre-existing and is not this change's. + +Not verified: nothing was deployed, so production still answers the `400` until the server is +rebuilt and restarted, and the client has to rejoin room 247 to confirm the load. + +## Run 35 (2026-10-15, live): the join box had no sender, so the client had no name to put on it + +### The box already carried a sender - no room could set one + +RecNet's `Gift.FromPlayerId` is the field the client reads to name a box's sender, and the panel's own +Gift Player page already lands there (`AdminGiftFromTests`). A room's join gift built its `Gift` in +`RoomJoinGiftUtils.TryDeliverAsync` and left `FromPlayerId` at its default of zero, so every join box +arrived from nobody, and no room setting could change that. + +### What changed + +- `RoomData.JoinGiftFromPlayerId` (bigint, `0` = no sender), added both by + `20261015120000_AddRoomJoinGiftFromPlayerId` and by `EnsureRoomJoinGiftColumns` for a database whose + migration history is already past that point. +- `RoomJoinGiftUtils.TryDeliverAsync` puts it on the box it hands out on join. +- The admin panel's Room Details modal carries a FromPlayerId inside the Gift Box On Join block + (`assets/js/rooms.js` in `Radicade-Admin`), and `AdminPanelAPI.UpdateRoom` reads + `join_gift_from_player_id`, refusing anything that is not digits rather than dropping it quietly. + +The id travels as digits in a string, never as a JSON number: an account id is eighteen digits, a JSON +number is read back as a double, and its last digits are rounded away - so the panel would save a +different account than the one it was shown. The room row the panel reads and the value it posts are +both text, the same way the Gift Player page already posts ids. + +Only a sender that actually changes bumps `JoinGiftVersion`, so a save that leaves the field alone does +not hand every player a fresh box. + +### Verified + +`ServerTests/RoomJoinGiftFromTests.cs`, 8/8, driving the real routes and then reading the box the way a +client does (`GET api/avatar/v2/gifts`, asserted on the wire JSON): the saved sender becomes the box's +sender; an eighteen-digit id survives the panel both ways; a changed sender re-issues the box while a +save that leaves it alone does not; a blank clears it back to no sender; a payload without the field +keeps the stored sender; a non-digit value is a `400`; and a room that names no sender still hands out +its box. + +Live on a local instance (Development, cwd `Server`, `http://127.0.0.1:3031`, SQL Server +`.\SQLEXPRESS`) the migration applied on startup and the route behaved: + +``` +Applying migration '20261015120000_AddRoomJoinGiftFromPlayerId'. +GET /admin/panel/rooms -> 200 all 45 rooms carry + "join_gift_from_player_id": "0" +PATCH /admin/panel/rooms/58 {"join_gift_from_player_id":"114903603133502227"} + -> 200 {"success":true} +GET /admin/panel/rooms/58 -> 200 "join_gift_from_player_id": "114903603133502227" + "join_gift_version": 2 (was 1) +PATCH /admin/panel/rooms/58 {"join_gift_from_player_id":"not-a-player"} + -> 400 'join_gift_from_player_id' must be + an account id in digits, or blank + for no sender. (room untouched) +PATCH /admin/panel/rooms/58 {"join_gift_from_player_id":"","join_gift_version":1} + -> 200 "join_gift_from_player_id": "0" + "join_gift_version": 1 +``` + +Room 58 was left exactly as it started (no sender, version 1) and the instance was stopped; nothing +left listening on 3031. + +Not verified: the modal itself was not clicked in a browser - the panel sits behind SSO, so the served +page redirects before the room list loads. `rooms.js` was parsed as a classic script in the browser +(`new Function` over the served file, no `SyntaxError`) and its field markup and save handler read; +nothing was deployed, so production needs a rebuild and a restart, at which point the column arrives +with the rest of the startup migrations. + +Full suite: 1566 passed / 10 failed / 1 skipped, and every failure is one the Run 34 note already +records as pre-existing: `AdminGiftSkinTests` (x1), `ConnectionRecoveryTests` (x1), +`DorkNetCatalogTests` (x2), `EquipmentInventoryTests` (x2), `StorefrontRewardConfigTests` (x1), +`WeeklyChallengeTests` (x1), and the two order-dependent `RecNetShapeConformanceTests` avatar-catalog +cases that mutate `DBConfig.DefaultAvatarCatalog` / `DefaultAvatarUnlocks` from parallel collections. +The two `GameRewardFlowTests` scheduled into that same race passed this run. diff --git a/docs/2023-ONBOARDING-FORCE-NOTES.md b/docs/2023-ONBOARDING-FORCE-NOTES.md new file mode 100644 index 0000000..86bb8f0 --- /dev/null +++ b/docs/2023-ONBOARDING-FORCE-NOTES.md @@ -0,0 +1,139 @@ +# Forced new-account onboarding: what the 2026-10-12 runs proved + +The client-side force (`2023Patch/RadicadeSeptemberClient/ForcedNewAccountOnboarding.cs`) has been +removed. This file records what it measured, because the measurements are the useful part and +several of them contradict the guesses that built it. + +The conclusion they point at: **this is a server-side problem.** The 2023 client decides everything +about the title screen from data the server sends it, and every attempt to override that decision +inside the client either did nothing or produced a state the client was never designed to be in. + +## 1. Where the title screen decision is actually made + +From the Beta install's `BepInEx/LogOutput.log`, in this order, before the token request: + +``` +[Onboarding] enter KFGCEPNLFBA (BaseRouteFlowModel route-flow update) +[Onboarding] enter NHAJFLJDHBA (route-flow getter) +[Onboarding] exit NHAJFLJDHBA => HHGLIBCDIDA +[Onboarding] enter get_IsInAccountCreationState +[Onboarding] exit get_IsInAccountCreationState => false <- and then "Welcome back!" +[Net] Post /auth/connect/token [AuthGrant] grant_type=cached_login +``` + +So the question the client asks itself is "am I mid-account-creation?", answered **before** it +decides how to sign in, from the player-settings markers it holds: + +- `Recroom.AccountCreation.HasStarted` +- `Recroom.AccountCreation.HasFinished` + +and it reads those back out of `GET /playersettings/playersettings`. Answer `false`, and the client +takes the cached-account route and resumes with `grant_type=cached_login`. + +`get_IsInAccountCreationState` and `get_IsInCachedAccountState` are both `public bool` getters on +`RRUI.Data.TitleScreenFlowModel` — cheap to patch, and the first one is definitely called. Overriding +it was tried: it answers the question the *client* asks, but the client is not the only thing that +decides, so the result was a different sign-in, not a different flow (see 3). + +## 2. The flow update crashes when the cached-account list is not empty + +`RRUI.Data.TitleScreenFlowModel.OFOIOHEFGAB()` is the client's own title-screen update. In the runs +where the server returned a real cached account, it threw, immediately after that response arrived: + +``` +[Net] Get /auth/cachedlogin/forplatformid/{redacted} -> 200 bytes=179 +[Onboarding] enter OFOIOHEFGAB +Exception: Il2CppInterop.Runtime.Il2CppException: System.NullReferenceException + at RRUI.Data.TitleScreenFlowModel.OFOIOHEFGAB () +``` + +In the run where the same endpoint answered `[]` (2 bytes, no account), the same method did **not** +throw, and no `[ForceOnboard] ... OFOIOHEFGAB threw` line was recorded all run. Same client build, +same plugins; the only difference was that one response. + +That member is what moves the title screen to its next page. While it throws, no page — the +client's or a patch's — is applied, which is why "the hook installed but the screen did not move" +looked like a patching problem for so long. It is worth finding out whether the client is being +handed a cached account that is missing something the real service always sends. + +What the server sends for that endpoint today (live, `forplatformid/0/76561198624498265`): + +```json +[{ "accountId": 18, "platform": 0, "platformId": "76561198624498265", + "lastLoginTime": "2026-10-12T14:41:14.8984545", "requirePassword": false }] +``` + +## 3. Overriding the state changes the sign-in, not the flow + +With `get_IsInAccountCreationState` forced to `true`: + +``` +[ForceOnboard] forced get_IsInAccountCreationState false -> true +[HttpStatus] #6 Get /auth/cachedlogin/forplatformid/{redacted} -> 200 bytes=2 (empty) +[AuthGrant] connect/token grant_type=create_account (was: cached_login) +[HttpStatus] #8 Post /auth/connect/token -> 200 bytes=703 +... token claims carry "userId":"19" ... +[HttpStatus] #26 Post /matchmaking/player/login -> 200 +``` + +The client left the cached-account path, which is what was wanted, and then **provisioned a brand-new +account** rather than being put through the flow for the account that already existed. The creation +pages' own hooks (`SubmitAvatarCustomizationAndGoToNext`, `SubmitAccountCreationBirthdayAndGoToNext`, +`AcceptCodeOfConductAndGoToNext`, `LaunchGameAccountCreation`) never fired: the client did not walk +the pages, it just made an account and launched. + +So forcing the state gets a new account, not onboarding. The account that *needs* onboarding is +chosen by the server, from `CachedLogins` and the creation markers. + +## 4. Two server-side things found on the way + +**The standalone guard cannot fire for this client.** `Server/Auth/Cached/CachedLoginAPI.cs`: + +```csharp +if (platform == Platforms.Standalone) return Ok(new List()); +``` + +`Platforms.Standalone` is `7`, but this client identifies as platform `0` (**Steam**, with a SteamID64 +as its platform id — `"rn.plat":"0"`, `"rn.platid":"76561198624498265"` in its own token). That guard +therefore never applies to the build being tested, and the row is returned. + +**Two SteamIDs appear for the same machine.** `...24498265` (account 18, the one with the cached +row) and `...23533377` (the identity that created account 19). A platform change explains it, but it +means `CachedLogins` is keyed on the *platform identity*, not on the machine, and a device that +changes platform looks like a brand-new device to every "has this device been here before?" rule in +`AuthAPI` and `PlayerUtils`. + +## 5. Client interop facts worth keeping + +`RRUI.Data.TitleScreenFlowModel`, from `BepInEx/interop/Assembly-CSharp.dll` (metadata, not inferred): + +| Member | Signature | +|---|---| +| `HBKLANJACMF` | `void (JPDJBDGFIGP)` — the page navigator; its parameter is *named* `ENNJBOCEGDH`, which is why a named-value-type prefix was rejected by HarmonyX | +| `DBHMKMHNJOO` | `JPDJBDGFIGP ()` — the reported page | +| `get_IsInAccountCreationState` | `bool ()` | +| `get_IsInCachedAccountState` | `bool ()` | +| `OFOIOHEFGAB` | `void ()` — the title-screen update | +| `KFGCEPNLFBA` | `void ()` — the route-flow update | +| `get_LocalAccount` | `INMPLDLFHAF ()` | +| `LaunchGameCachedAccount` / `LaunchGameAccountCreation` | `void ()` | +| `SubmitAccountCreationBirthdayAndGoToNext`, `SubmitAccountCreationUsernameAndGoToNext`, `SubmitAccountCreationPasswordAndGoToNext`, `AcceptCodeOfConductAndGoToNext`, `SubmitAvatarCustomizationAndGoToNext` | `void ()` | + +Page enum `JPDJBDGFIGP`: `TITLE_SCREEN_NEW_ACCOUNT = 8`, `TITLE_SCREEN_CACHED_ACCOUNT = 9`, +`TITLE_SCREEN_INTERRUPTED_ACCOUNT_CREATION = 10`, `AVATAR_CUSTOMIZATION = 18`, `BIRTHDAY = 19`, +`USERNAME_NON_JUNIOR = 20`, `USERNAME_JUNIOR = 21`, `PASSWORD = 22`. + +`TitleScreenFlowModel` also exposes `initialBootPage`, `titleScreenNewAccountPage`, +`titleScreenCachedAccountPage`, `titleScreenInterruptedAccountCreationPage`, `avatarCustomizationPage`, +`birthdayPage`, `usernamePage`, `passwordPage`, `accountCreationCodeOfConductPage`, +`accountCreationLaunchGamePage` as `TitleScreenPage` properties, and the flow navigates by +`RRUI.Navigation.Route` through `RRUI.Data.BaseRouteFlowModel`. + +## 6. The one thing not to repeat + +The force was gated on a marker file, and an early build wrote that marker when the flow reached its +own end **regardless of whether anything had been forced**. A plain walk of the vanilla flow +therefore consumed the arming, and every later boot printed +`the marker file is present, so nothing will be forced` — which is exactly what the 01:26, 10:41 and +10:43 logs all say. Two test runs in a row were spent on a force that was switched off by its own +bookkeeping, and the gate now says so in the log rather than in a comment. diff --git a/docs/GIFT-CONTEXT-DUMP.md b/docs/GIFT-CONTEXT-DUMP.md new file mode 100644 index 0000000..6e7af3b --- /dev/null +++ b/docs/GIFT-CONTEXT-DUMP.md @@ -0,0 +1,160 @@ +# Gift context dump and implementation handoff + +Snapshot: 2026-10-08. Source: current local server checkout. +This is the complete server GiftContext enum, not an independently verified client enum dump. +Numeric gaps are intentional. Context identifies a gift's source; it does not supply its item or implement its challenge. + +## Friendotron + +- There is no enum member named Friendotron. +- POST /econ/api/freegifts/v1/sendmultiple currently returns an empty array and does not grant anything (Server/API/Compatibility/CompatibilityAPI.cs). +- GET /api/config/v1/freegiftbutton currently returns false (Server/API/Config/ConfigAPI.cs). +- The placeholder config contains Friendotron_AllowSelfGifting and Friendotron_MaxUsersPerSession (Server/DB/DBConfig.cs). Placeholder entries do not establish the live database configuration. +- Notification enum FreeGiftButtonItemsAdded = 110 exists in Server/RecNet/Enums/Messages. +- Local inspection log .tmp-gift.log identifies RecNet.MultiRecipientFreeGiftRequestDTO, DIEKLMGBLKA.RequestPurchaseForFreeGiftButtonDTO and DIEKLMGBLKA.GiftItemDTO. Their names are leads, not a verified request schema. +- Next implementation work: inspect the target client's request/response/config schemas and context choice; implement recipient validation, eligibility/cooldowns, gift selection, durable grants and retry deduplication; then enable the feature with the expected config payload. Do not assume Purchased_Gift_A is Friendotron's context merely because it is used for purchased gifts. + +## Weeklys + +- All_Weekly_Challenge_Complete = 4. +- Weekly_Challenge_Complete = 6. +- RewardType.WeeklyCompleteButHaveSkin = 4 is a DIFFERENT enum from GiftContext. +- Server/API/Progression/Challenges/V2/ChallengeV2.cs has GET getCurrent returning {} and POST updateProgress returning an empty successful response, under both /api/challenge/v2 and /econ/api/challenge/v2. +- Server/API/GameRewards/V1/GameRewardsAPI.cs maps WeeklyCompleteButHaveSkin to All_Weekly_Challenge_Complete. Its BuildReward method constructs token choices of 300, 600 and 900 for that reward type; this does not implement weekly challenge progress or a weekly skin award. +- Next implementation work: recover the target client's challenge DTOs/objectives; provide current weekly definitions, period dates and reward items; persist authenticated progress and completion; grant the configured reward once per period; verify the already-owned-skin fallback and rollover behavior. + +## Gift delivery and wire format + +- Server/RecNet/Avatar/Gift.cs stores Context as GiftContext and exposes integer GiftContext as an alias. Id and GiftPackageId alias GiftDropId; Message aliases Tooltip; GiftRarity aliases Rarity. +- Item payloads include AvatarItemDesc, ConsumableItemDesc, EquipmentPrefabName / EquipmentModificationGuid, and CurrencyType / Currency. A context alone does not specify a skin. +- POST /api/avatar/v2/gifts/generate (also /econ/api/avatar/v2/gifts/generate) accepts GiftContext or MFPELEJLJLD; AlternateGiftContext or CIEHFJEIOJJ; Message or DPAJDIEBNKM; and Xp / XP. The alternate context is parsed but is not used to select the reward in this method. +- Generation first looks for an outstanding reward, a pending box, or a recently handed-over box; supported activity contexts can resolve a room reward. Otherwise BuildGeneratedGift makes a random token box of 10, 25, 50, 100, 250 or 500. Sending context 4 or 6 alone is therefore not a weekly-skin implementation. +- Generated gifts are persisted with notify:false because the response hands the box to the client. +- Server/API/Storefront/StorefrontShared.cs provides PersistGiftBoxAsync and SendGiftPackageNotificationAsync. Reuse the existing grant/claim lifecycle rather than inventing a separate inventory path. +- Server/Utils/GameRewardRedemption.cs handles outstanding/pending/recent reward reuse; Server/Utils/RroRewardPools.cs maps activity contexts and pools. +- AvatarV2 also implements gift claim and consume routes. Exercise generation, notification, claim, consume, retries and reconnects with the target client before declaring a new reward flow complete. + +## Full GiftContext enum + +| Value | Name | +| ---: | --- | +| -1 | None | +| 0 | Default | +| 1 | First_Activity | +| 2 | Game_Drop | +| 3 | All_Daily_Challenges_Complete | +| 4 | All_Weekly_Challenge_Complete | +| 5 | Daily_Challenge_Complete | +| 6 | Weekly_Challenge_Complete | +| 10 | Unassigned_Equipment | +| 11 | Unassigned_Avatar | +| 12 | Unassigned_Consumable | +| 20 | Reacquisition | +| 21 | Membership | +| 30 | NUX_TokensAndDressUp | +| 31 | NUX_Experiment1 | +| 32 | NUX_Experiment2 | +| 33 | NUX_Experiment3 | +| 34 | NUX_Experiment4 | +| 35 | NUX_Experiment5 | +| 50 | GameRewards | +| 51 | GameRewards_Tokens | +| 100 | LevelUp | +| 500 | Purchased_Gift_A | +| 501 | Purchased_Gift_B | +| 502 | Purchased_Gift_C | +| 503 | Purchased_Gift_D | +| 1000 | Holiday | +| 1001 | Contest | +| 1002 | Promotion | +| 1003 | SubscribersOnly | +| 1100 | Deprecated | +| 1200 | RecRoyale | +| 2000 | DEPRECATED_Paintball_ClearCut | +| 2001 | DEPRECATED_Paintball_Homestead | +| 2002 | DEPRECATED_Paintball_Quarry | +| 2003 | DEPRECATED_Paintball_River | +| 2004 | DEPRECATED_Paintball_Dam | +| 2005 | DEPRECATED_Paintball_DriveIn | +| 2010 | Paintball_ClearCut | +| 2011 | Paintball_Homestead | +| 2012 | Paintball_Quarry | +| 2013 | Paintball_River | +| 2014 | Paintball_Dam | +| 2015 | Paintball_DriveIn | +| 3000 | DEPRECATED_Discgolf_Propulsion | +| 3001 | DEPRECATED_Discgolf_Lake | +| 3010 | Discgolf_Propulsion | +| 3011 | Discgolf_Lake | +| 3500 | Discgolf_Mode_CoopCatch | +| 4000 | Quest_Goblin_A | +| 4001 | Quest_Goblin_B | +| 4002 | Quest_Goblin_C | +| 4003 | Quest_Goblin_S | +| 4004 | Quest_Goblin_Consumable | +| 4010 | Quest_Cauldron_A | +| 4011 | Quest_Cauldron_B | +| 4012 | Quest_Cauldron_C | +| 4013 | Quest_Cauldron_S | +| 4014 | Quest_Cauldron_Consumable | +| 4100 | Quest_Pirate1_A | +| 4101 | Quest_Pirate1_B | +| 4102 | Quest_Pirate1_C | +| 4103 | Quest_Pirate1_S | +| 4104 | Quest_Pirate1_X | +| 4105 | Quest_Pirate1_Consumable | +| 4200 | Quest_Dracula1_A | +| 4201 | Quest_Dracula1_B | +| 4202 | Quest_Dracula1_C | +| 4203 | Quest_Dracula1_S | +| 4204 | Quest_Dracula1_X | +| 4205 | Quest_Dracula1_Consumable | +| 4206 | Quest_Dracula1_SS | +| 4500 | Quest_SciFi_A | +| 4501 | Quest_SciFi_B | +| 4502 | Quest_SciFi_C | +| 4503 | Quest_SciFi_S | +| 4504 | Quest_Scifi_Consumable | +| 5000 | DEPRECATED_Charades | +| 5001 | Charades | +| 6000 | DEPRECATED_Soccer | +| 6001 | Soccer | +| 7000 | DEPRECATED_Paddleball | +| 7001 | Paddleball | +| 8000 | DEPRECATED_Dodgeball | +| 8001 | Dodgeball | +| 9000 | DEPRECATED_Lasertag | +| 9001 | Lasertag | +| 10000 | DEPRECATED_Bowling | +| 10001 | Bowling | +| 11000 | StuntRunner_TheMainEvent_A | +| 11001 | StuntRunner_TheMainEvent_B | +| 11002 | StuntRunner_TheMainEvent_C | +| 11003 | StuntRunner_TheMainEvent_D | +| 11004 | StuntRunner_TheMainEvent_S | +| 11005 | StuntRunner_TheMainEvent_X | +| 11006 | StuntRunner_TheMainEvent_Consumable | +| 11007 | StuntRunner_TheMainEvent_SS | +| 100000 | Store_LaserTag | +| 100010 | Store_RecCenter | +| 110000 | Consumable | +| 110100 | Token | +| 110200 | Punchcard_Challenge_Complete | +| 110201 | All_Punchcard_Challenges_Complete | +| 200000 | Commerce_Purchase | + +## RewardType enum (separate numeric namespace) + +| Value | Name | +| ---: | --- | +| 0 | FirstActivityOfDay | +| 1 | DailyChallengesComplete | +| 2 | PostGameActivity | +| 3 | PostQuestActivity | +| 4 | WeeklyCompleteButHaveSkin | +| 5 | Levelup | +| 6 | ReferralReward | + +## Scope and validation + +Enum values were expanded directly from the C# source, including explicit resets and implicit increments. JSON is in docs/gift-context.json. No runtime code or live data was changed. Friendotron's exact client contract and weekly challenge payloads still require client inspection; this document does not claim those features work. diff --git a/docs/KEEPSAKES-STATUS.md b/docs/KEEPSAKES-STATUS.md new file mode 100644 index 0000000..49f5f37 --- /dev/null +++ b/docs/KEEPSAKES-STATUS.md @@ -0,0 +1,72 @@ +# Keepsakes status + +Keepsakes are enabled for all rooms, with a room limit of 10 and the catalog's +per-category limits. The September client tool defaults to enabled through +Keepsakes.EnableTool in the BepInEx configuration. RADICADE_KEEPSAKE_TOOL can still +be used to override it (skip disables hooks; off installs observation-only hooks). + +## Confirmed client compatibility fixes + +The October 4 client log showed repeated FNKNJGBLBHO Keepsake runtime null references, +a disabled client tool patch, and GET /api/keepsakes/events/1. Native inspection of +the installed 20230831 client established these contracts: + +- GGHPHLPMAAD reads room data as Instances, CollectionRecords, and + KeepsakeProgressionEventIds. The server previously omitted Instances entirely. + Instances now contains all active room instances, with the caller's collection + records separate, so collected items remain available for count/category lookup. +- EBHENPDHDLN reads event data as Instances and CollectionRecords, each containing + RoomId/KeepsakeInstanceIds groups (FHGMHPEJIGJ). GET events/{id} now serves this. +- AMAOGLDGNLL posts placement to /api/keepsakes with RoomId, SubRoomId and + KeepsakeCategory, and expects a GUID response. Collection is POST + /api/keepsakes/{guid}/collect; deletion is DELETE /api/keepsakes/{guid}. + The older room-based routes remain available. +- JNOKNEBCEMD and NIAPIJPPFCM require KeepsakeRoomLists to contain room DTO objects, + not bare numeric room IDs. Lists now include event identity and unlock settings. + AllRooms enumerates database rooms for the event definition rather than emitting + an empty list that cannot grant room eligibility. +- BAEFNEHEPIJ reads a player progression record (AccountId, Xp, RewardsCollected, + BonusRewardsCollected, GameMinutesToday, XpBoostLastPurchasedAt), not another copy + of the event definition. Event XP derives from stored collection records. A repeat + pickup returns zero additional XP; no player-level XP is awarded by these routes. + +Inspection output is under artifacts/keepsake-*.txt. The client patch now uses +explicit September type references and handles zero-argument overloads safely. +No exception suppression was added. + +## Validation and installation + +56 Keepsake/startup compatibility tests pass, including the actual client request +flow, non-null empty lists, object-shaped event room membership, per-player +collection and XP, placement limits, ownership, removal, and config overrides. +The server and September client plugin both build successfully. + +The client DLL was installed in the 2023-09-06T06-53-47Z game folder with a backup +named RadicadeSeptemberClient.dll.before-keepsake-fix-20261004-214443. The normal +local server output was rebuilt. No remote deployment or in-game verification was +performed. Start the rebuilt server and relaunch the game, then place a keepsake, +save/rejoin, collect/rejoin, and delete it. Other unrelated glitches have not been +verified fixed by this work. + +## Account authorization fix (October 5) + +The screenshot's account authorization banner is a login-token issue, not a room +CAN_INVITE permission. Native inspection of FNKNJGBLBHO.DKKIJJLCNLH shows a call +to BLJANPFJMDG slot 4 (IBDFIOPDFBD.MABOKPPEEMA), which reads the account's Keepsake +flag. DAJDMOPMEGG.NOGOHJKFDFC initializes it from the JWT role `keepsake`; its +neighboring event-edit flag comes from `livekeepsakeeventroomsaveoverride`. +AuthAPI.GenerateToken now issues both when KeepsakeFeature.Enabled is true. +It grants neither developer nor moderator access. The separate room co-owner +check remains in place. Existing tokens need a fresh login after the server update. + +Validation: 77 authentication, Keepsake, and startup compatibility tests passed, +including feature-enabled and feature-disabled JWT role checks. + +The latest available logs (October 4, 23:40) also show PrefabSpawner.Append and +PrefabSpawner.Replace null references during CompositeController.OnModelChanged, +plus Harmony/IL2CPP startup type-loading warnings. These are not established as +fixed by the token change. No generic exception suppression was added. Evidence: +artifacts/keepsake-auth-check.txt, keepsake-account-auth-real.txt, and +keepsake-auth-roles.txt; full UI stacks are in Unity's Player.log. + +The event is now disabled in the shipped configuration (EventEnabled: false), independently of Keepsake placement. Active-event lookup returns JSON null; room payloads advertise no progression event. Set EventEnabled to true to restore the event. The DynamicUiPrefabRepair client patch fills missing JNHHEBGEAMP mappings using a controller-compatible multi-select float prefab. diff --git a/docs/QUEST-20230831-TEST.md b/docs/QUEST-20230831-TEST.md new file mode 100644 index 0000000..57a9f35 --- /dev/null +++ b/docs/QUEST-20230831-TEST.md @@ -0,0 +1,30 @@ +# Quest August 31 standalone port + +Source: C:/RecRoom/RecRoom-Quest-20230831 (unmodified). +Working files and APK: artifacts/quest-20230831. +Separate package: com.radicade.questaugust, version 2368643. + +Original IL2CPP SHA256: 656e5f0e2d83859d4f0c768db601367d5a9d69f6f7902221bd3b90b1b62703cb. +Unity SHA256 matches September's guarded scoped TLS patch exactly. +Metadata header: literal table offset field 0x74, size 0x24, data offset 0xa0, size 0x3c. Eight server literals redirected. The initial candidate data field 0x98 failed validation and was not patched; 0xa0 was verified using the version literal and bounds. + +Device runtime inspector produced quest-method-map.tsv and quest-recnet-fields.tsv. +August native changes, guarded by original IL2CPP hash: +- Oculus GetAuthParams 0x3945b40 -> PlatformManager 0x36ad1f4. +- CurrentPlatform 0x394490c -> Standalone 0x4415134. +- Title error 0x3871f38 and cached-login error 0x3877758 -> GoToLogin 0x386ce14. Class method ordering matched September; cached-login error literals independently confirmed. +- Preserve Oculus initialization through SetVRDisplayMode at 0x3945124; redirect at 0x3945128 to Standalone.Initialize 0x4415188 with original frame restoration. +- Input dispatch table 0xbf18740: slot 7 from 0x3f13fc0 to Oculus slot 1, 0x3f13fd0. + +compat.c ports September's narrowly scoped image request key-ID hint, exact-placeholder image acceptance, fresh HTTP connection experiment, and path/status/quit logging. August RVAs: chunk reader 0x8f926cc; current URI 0x8f8b598; absolute path 0x9dc711c; quit 0x3899f70; send 0x715e560; image verifier 0x668b438. All four hook prologues matched exact expected bytes. Image verifier identified from its signature-error literal and call after Base64 parsing. TLS verification remains enabled; placeholder acceptance is image compatibility, not cryptographic image authentication. + +Signed RadicadeQuest20230831.apk passed apksigner verification after 16 KiB zip alignment. OBB packing passed CRC and entry-list checks; headset SHA256 matches PC: 241328edeecbfeb53321b5fee62edb9284ba4b7880abcc69e5848204d5545a47 (1,415,411,861 bytes). +September app remains separate. August login/gameplay validation pending. + +User subsequently confirmed August login and gameplay work. Remember-password request: original independent-login guard rejected cached/refresh grants and suppressed platform account discovery, so the checkbox could not resume login. + +Server update a998b5f pushed to origin/2023: remembered Standalone requests require a signed, unexpired HS256 session header bound to client recroom, platform 7, device namespace, account, token version and password-hash fingerprint. Invalid sessions fall back to password; bare IDs and synthetic legacy refresh GUIDs remain insufficient. Successful Standalone auth returns the session in X-Radicade-Quest-Session. Thirteen focused tests passed, including expired/forged/wrong-device/wrong-account/wrong-platform/revoked/password-changed rejection. Existing cached-account enumeration remains hidden. + +RadicadeQuest20230831-Remember.apk signed/verified and installed in place. remember.c includes remember.h: private mode-0600 session file (no password stored), checkbox state from ManuallyLogin's require-password field, scoped HTTPS token request header, response Receive hook saving only successful server-issued session header, rejection/logout cleanup. Hooks start after five seconds to precede startup cached login. Exact prologues guarded. Logout trampoline explicitly relocates its fourth ADRP instruction to the verified page 0x10010000. No credential values logged. Server deployment and login/restart/logout testing pending; September APK has not received remembered-login changes. + +2026-10-07 restart verification: installed APK SHA256 matches Remember.apk (02a3c9061d494c0686c028238fb982c2219285b396f66391afa2703ecd801159). All three remembered-login hooks reported active. Private session file existed with mode 0600; contents were not printed. Force-stop/relaunch of com.radicade.questaugust produced session-attached diagnostic, HTTP 200 from /auth/connect/token, renewed-session saved, HTTP 200 from /accounts/account/me and /matchmaking/player/login, then room requests. No manual-login event occurred in this startup capture. Automatic session resume verified for this restart; logout/rejection UI behavior and September port remain untested. diff --git a/docs/QUEST-20230907-TEST.md b/docs/QUEST-20230907-TEST.md new file mode 100644 index 0000000..6dfa0d5 --- /dev/null +++ b/docs/QUEST-20230907-TEST.md @@ -0,0 +1,153 @@ +# Quest 20230907 server test + +## Current investigation: post-login `doll` and saved-account fallback (2026-10-07) + +After the user supplied a valid birthday, login proceeded further. At 19:18:22 client emitted fatal quit 1550502249 after a Hile report; VPS message was `33489407,7`. Exact September disassembly locates the comma-pair formatter in HGEONHBDDLK.PLGOEDKHHBI (0x4f91ffc), gated by `AntiHile.QD`, reporting type 701. Live config verified that key=true. GameConfigAPI now overrides only that key=false; two focused tests pass for seeded true and missing-key cases, including preservation of unrelated values and stored data. Default Debug test build hit a locked DLL; isolated QuestConfigTest build passed. Commit 04826ca pushed to origin/2023 after cherry-picking the tested local 0b7894b onto remote 2a2df52 in artifacts/quest-config-integration. Primary dirty workspace preserved. VPS deployment and gameplay retest pending. Separate crm/me/config exception observed; not established as the cause of this shutdown. + +FreshHTTP test at 19:07 passed the formerly failing current-saves response, outfits, and image verification (placeholder acceptance logged 19:07:54.993). No fatal quit or hex parsing error appeared through 19:09; heartbeats continued. Unity explicitly logged `Junior: False` at 19:07:38.968 but `meta junior login error` at 19:07:50.632. User screenshot confirms false database flags, JuniorState=0, and an unset birthday (year 0001). BuildSelfAccount recalculates juniorState=PendingOverride (2) when birthday is absent/year<=1900 regardless of stored zero. Client TitleScreenFlowModel.EJKBHHGALEH -> HBGNIICMAMG -> BKJMLBBNEFL.FBOEAHDMPJF checks the self-account state at +0xa8 for 2 before falling back to its Junior boolean, then checks Meta hardware. This supports incomplete birthday onboarding as the remaining restriction, not an actual true Junior flag. No birthday, Junior flag, or restriction check has been modified. + +ImageCompat login at 19:02:38 authenticated successfully (token 200 and self-account 200), but failed before image loading. At 19:02:42.494 the custom-avatar-current-saves response began (200) without a matching chunk end. At 19:02:42.635 Unity reported `Can't parse '' as a hex number!`; manual login returned `doll` at 19:02:43.596. A later matchmaking heartbeat hit the same parsing error. No image-placeholder acceptance or fatal-quit event occurred during this attempt, so the image fix remains unverified. Filtered capture: image-compat-events.log. + +FreshHTTP.apk is a one-variable transport experiment on top of ImageCompat: HTTPManager.SendRequest sets IsKeepAlive=false via its runtime setter only for https://ns.radicade.app/ requests. This tests connection reuse as a cause; it does not establish the root cause or suppress parser errors. TLS and response validation are unchanged. Native compilation and APK signature verification passed. Device login test pending. + +ImageKey test at 18:52 confirmed the request hint reached the VPS and changed the failure from `Signature malformed` to `Signature verification failed`. Headset log `image-key-events.log` records this failure followed by fatal quit code 1550502249. User supplied matching VPS reports. The server's SignatureData is a fixed 256-byte placeholder, not a signature of the returned image. + +Built, apksigner-verified, and installed ImageCompat.apk in place, preserving data. Exact-build image verifier HFFEEGBKKFK.DGDJIHJHHBN at RVA 0x6deb934 is reached after header/key-id parsing. The hook recognizes only the exact server placeholder byte array and accepts it in this image-only path; all other signatures call the original verifier. This intentionally relaxes image authenticity checking for that placeholder, does not provide cryptographic image authentication, and does not alter TLS certificate validation or fatal-quit behavior. Source: tools/quest-http-inspect.c and tools/quest-image-placeholder.h. Prologue verified against original ELF and compilation passed -Wall -Wextra -Werror. First alignment attempt failed in sandbox; elevated alignment succeeded. Login outcome pending. No server update required for this test. + +Confirmed next trigger from user-provided VPS reports: two `Obcured` reports say `Signature malformed` for the same image URL with `sig=p1`. QuitInspect captured two fatal quits with code 1550502249 and a delegate-invocation caller; shutdown's SpeedHackDetector exception follows them. Live `/images/{name}` returned key-id `KEY:RSA:p1.rec.net`, whereas patched Quest metadata expects `.radicade.internal`. This is an image-signature key-ID mismatch, not proof of a Junior account restriction or TLS certificate-pin failure. Existing PC ImageSignatureRequest.AddKeyId already solves that header selection mismatch with `rcsig`. + +Built, verified and installed ImageKey.apk in place. Added an exact-prologue-checked HTTPManager.SendRequest hook (RVA `0x688e200`) that adds the expected rcsig only to `https://ns.radicade.app/images/` requests, preserves resize/signature query parameters, and uses the IL2CPP write barrier when replacing the URI. Existing hinted/fragment URLs are left unchanged. Original TLS and RSA verification remain active; the hook activates after the diagnostic 30-second delay. A live request with that parameter returned HTTP 200 and the correct `.radicade.internal` key ID. Headset acceptance remains pending; the server still supplies static signature bytes, so successful signature cryptographic verification is not established. Capture: `artifacts/quest-tls-diagnostic/image-key-events.log`. + +After deployment, PID 21799 received `/outfits/me` HTTP 200 at 14:21:37 and progressed to version check and additional startup requests. The user saw “Account not supported” before exit. Two `/api/PlayerReporting/v1/hile` requests preceded exit; Android recorded signal 9 after the activity stopped at 14:21:48. A public account-2 probe returned isJunior=false, which does not establish private self-account state. The client's SessionManager message table maps error 83 to the Meta Junior restriction text, but the triggering code has not yet been captured. + +At the user's request, inspected https://github.com/recflare/quest at `43efb7b9f46a335e978af5520979d8a4d65e81f2` in `artifacts/recflare-quest-reference`. It targets April 2023, not this September build. Its certificate-pinning and fatal-quit diagnostics suggest a separate post-TLS integrity check worth testing; they do not establish our cause. No repository build scripts or bypass hooks were run. Added an independently implemented exact-prologue-checked trace at this client's SessionManager.FatalApplicationQuit RVA `0x3a1c0b4`, logging code/caller/reason length only and preserving behavior. Signed/verified QuitInspect.apk installed in place. Activation is not yet confirmed because the app exited before diagnostic output; user asked to keep headset awake at the password page. No new server changes or account edits made. + +The user reported the VPS updated after `a808239`. An unauthenticated live check of `/outfits/me` now returns 401 instead of 404, consistent with the new protected route being deployed. Reopened HttpInspect-v3 on the connected Quest as PID 21799; filtered capture is `artifacts/quest-tls-diagnostic/http-after-outfit-deploy.log`. Authenticated response/client acceptance is still pending a manual login. + +Server fix committed and pushed to origin/2023 as `a808239`: authenticated `GET /outfits/me` now wraps the existing account avatar in the September current-outfit DTO. Exact-client metadata resolved the request state machine `MOEENPJECCH` returning `FMLIPOPOKDJ`; generated formatter `JOJEHOPNNIN` defines DataVersion, LegacyData, CustomizationSettings, Selections, Slot, Name, Accessibility and ThumbnailFileName. Legacy formatter `DEAAHJJLAAE` defines SelectionsV1, SelectionsV2, FaceFeatures, SkinColor and HairColor. The initial adapter uses version 0 legacy data, current slot 0 and an empty modern selections list. Client acceptance of the response remains pending VPS deployment and a headset test. All three QuestOutfitsTests passed, covering account isolation, auth rejection, JSON shape and missing-avatar initialization. + +Later capture at 14:04:24–26 reproduced empty chunk-length errors on `/datacollection/data/event` and `/matchmaking/player/heartbeat`, after the 14:02:29 login failure. The intermittent transport issue is therefore not proven fixed. Avatar descriptor warnings also remain unresolved. The field inspector now supports secondary Android users (active user 10) and records RecNet interface fields as well as runtime fields; it reads type metadata only. + +HttpInspect-v3 captured a complete manual login attempt at 14:02:21–29. Password token, account/me and matchmaking login all returned HTTP 200. Every observed chunked response completed, including the avatar catalog. `/outfits/me` returned HTTP 404 at 14:02:24; the client reported `AMIIDJOEAMH: HTTP Error 404` and manual-login `doll` at 14:02:29. This attempt did not reproduce the earlier empty chunk-length parse error. The client also logged invalid legacy avatar-item descriptors, a separate compatibility concern. The missing outfit route is the current concrete server lead; its response contract is being inspected before implementation. + +Follow-up: the user confirmed the password page but signing in exited PID 14518 with signal 11 at 13:57:06. The v2 diagnostic was unsafe: optimized call sites for `ReadChunkLength` do not preserve its unused receiver argument, so interpreting that argument as an HTTPResponse can dereference invalid memory. Do not reuse HttpInspect or HttpInspect-v2. Updated the hook to `ReadChunked` (RVA `0x6d941e0`), whose receiver is actually consumed, and verified its four copied prologue instructions against the original binary. Built and signature-verified HttpInspect-v3.apk, installed in place, and launched PID 16445. This corrects the identified diagnostic defect; the original post-login `doll` issue remains unresolved pending a captured attempt. Filtered continuous capture is saved locally in `artifacts/quest-tls-diagnostic/http-v3-events.log`. + +The user successfully interacted with the v3 password form using Touch controllers. Supplied server logs show the headset password grant accepted, followed by authenticated account and matchmaking requests. Password authentication itself succeeded; gameplay has not been reached. At 01:26:07 the client reported `doll` with BestHTTP `ReadChunkLength` failing to parse an empty string as hexadecimal. Successful server request logs do not identify which response failed in the client. + +Added `tools/quest-http-inspect.c`, an exact-prologue-checked experimental native hook that logs HTTP chunk reads using response identity, status and URL path only. It does not log passwords, authorization headers, query strings or response bodies. The diagnostic activates after 30 seconds; this timing remains a diagnostic workaround. + +A subsequent saved-account auto-login followed `TitleScreenFlowModel.MNKODPJLNDD`, a different error callback from the initial cached-login path, and showed error 3 before exit. The client patch now redirects that callback at RVA `0x39c67e0` to the existing password page (`GoToLogin`, `0x39bbd80`) as well. Server cached-login rejection remains intact. + +Built, signed, verified and installed `artifacts/quest-tls-diagnostic/HttpInspect-v2.apk` as an in-place update to `com.radicade.questtls`. No uninstall or data clearing occurred. Quest PID 14518 logged cached-login error 3 at 13:55:58, then activated the chunk diagnostic at 13:56:11 and remained running. Visual confirmation of the password fallback and a fresh manual login with the diagnostic active are pending. No additional server change or deployment was made for this diagnostic. + +## v3 controller dispatch test (2026-10-07) + +The user confirmed v2 displays the password form but physical Touch controllers do not interact, although they work in the Meta menu. Static analysis resolved `InputManager.FMJDLCGGGKP`'s virtual call at `0x460a254` as CurrentPlatform (same vtable slot used by `get_CurrentPlatformIsOculusType`). Its dispatch table at RVA `0xc35a550` maps Standalone=7 to return no VR input (`0x460a2b0`). Oculus=1 dispatches to `0x460a2c0`, which selects the existing Quest input handler for hardware type 5. + +The v3 script changes only that table entry in addition to v2 changes. It preserves password-based platform-7 server authentication and selects Oculus input using the original hardware check. Installed signed `StandaloneLogin-v3.apk` in the separate diagnostic package without uninstalling. Patched IL2CPP SHA-256 is `1a646b3c85ecf8101e7c131cd87012ab6a9764d01685addee03d2251a811243b`. Signature verification passed; controller interaction and end-to-end password login require headset testing. No additional server changes were made. + +## v2 prototype reaches password page (2026-10-07) + +Server commit `835534b` was pushed to origin/2023 and the user deployed it. A credential-free live probe returned the specific standalone password-required response, confirming the new guard is active. Those two deployment-probe cached-login 401s were not headset password attempts. + +The expanded read-only inspector saved `artifacts/quest-tls-diagnostic/quest-method-map.tsv`. Runtime method names resolve the Quest-specific original initialization call at `0x3d09f14` to `RecRoom.LocalPlayerController.SetVRDisplayMode`; v1 skipped it. The v2 patch preserves original initialization through that call, then invokes standalone account initialization with the original stack frame correctly restored. It also redirects `TitleScreenFlowModel.DALDIFKKDDA` to its existing `GoToLogin` method. This error redirect is currently broad (all title-screen errors), so this remains an experimental build. + +Installed `StandaloneLogin-v2.apk` into `com.radicade.questtls` without uninstalling. PID 15954 reported Standalone/OculusQuest, received the expected cached-login 401, and stayed running. An ADB screenshot visually confirmed the username/password page, rather than the error screen. Controller interaction and successful password authentication remain pending user testing. No password was supplied or logged. The original-package TLS-fixed APK and both OBBs remain available. Signature verification, ARM64 stub disassembly, 16 KiB alignment, and Python syntax checks passed. + +## Independent-login prototype: not playable; rolled back (2026-10-07) + +Added local server support for wire platform 7 (`Standalone`) with password-only login. It clears caller-supplied account identity before resolving the username, generates a local `radicade-quest:` device namespace, ignores bearer headers for this password grant, rejects cached-login/refresh/signup grants, and suppresses unauthenticated standalone cached-account discovery. Existing whitelist and anti-cheat checks remain active. These are experimental server changes, not deployed or pushed. Refresh and new-account onboarding are not implemented; an existing Radicade account with a password is required. With whitelist enabled, the derived local identity must currently be explicitly allowed. + +All 26 `AuthOnboardingFlowTests` passed, including five standalone cases covering correct/incorrect passwords, correct token identity, hidden cached accounts, and rejection of device-only cached/refresh/signup requests. + +`tools/patch-quest-standalone-login.py` reproduces a failed client experiment: three exact-build checked branches redirect Oculus initialization/auth parameters/platform enum to existing standalone implementations. Only 12 bytes in `libil2cpp.so` differ from the TLS-fixed clone. The signed APK `artifacts/quest-tls-diagnostic/StandaloneLogin.apk` passed v2/v3 verification and 16 KiB alignment. On Quest (PID 12596), it passed platform initialization and logged `Platform: Standalone`, `Hardware: OculusQuest`, but then attempted cached login, received HTTP 401, and displayed error code 3. The user reported no usable hands. A screenshot confirmed the error screen; the cause of missing interaction is not established. This is NOT a working Quest login implementation, and the initialization replacement must not be promoted to a release. + +Restored `RadicadeQuest-ScopedTLS-Test.apk` over the separate diagnostic package without uninstalling (ADB Success), then launched the unchanged original-package TLS-fixed app. Both OBBs and private data were retained. Before another client test, trace Quest input initialization and the title-screen transition to password login separately; do not simply replace the full initialization method again. The user reports deploying by pushing Git and gives `C:\radicade-server` as the server folder. No deployment workflow was found in this checkout; production update/restart mechanics are not yet verified. + +## Latest result: TLS fixed; Meta account initialization remains blocked (2026-10-07) + +The currently installed original-package APK is `artifacts/quest-tls-diagnostic/RecRoomQuest-ScopedTLS-Test.apk`, SHA-256 `064fbf29f4c3340abf45863664fd84af1404a74b46c64fa08d733f40654bdffa`. It retains package `com.AgainstGravity.RecRoom`, versionCode `2378787`, and the original patched metadata. The only changed APK payload is `lib/arm64-v8a/libunity.so`; signing metadata also changes. The user explicitly approved replacing the previous differently signed test app and its resulting private-data deletion. The OBB was restored and its SHA-256 again matched `ef856a56bfe1667f00435c2b07fb465766196c02bac8b220c7cc36db926998fc`. + +Native instrumentation found that platform-root X509 validation of `ns.radicade.app` succeeded (`result=0`, `raw_flags=0`, `unity_flags=0`), but the configured managed verification callback subsequently returned `0x8`. This demonstrates an additional client-side rejection; its managed implementation/pinning policy has not been identified. Changing server TLS versions or certificate issuers did not address it. + +`tools/diagnose-quest-tls.py --radicade-system-trust` routes only the exact hostname `ns.radicade.app` through Unity's existing default verifier (including platform CA chain and hostname validation), preserving the configured callback for all other hosts. It does not return unconditional verification success. The script accepts only the exact known Unity library SHA-256, uses checked empty RX-segment padding, and preserves existing native instructions outside the three hook sites and RX segment length fields. Without that option it only adds diagnostic logging. The native library SHA-256 for the scoped variant is `37c5709cca6b5dff019e16685a819d50d064d3a4c19f45d642cb5383011e23d7`. + +Both the separate diagnostic package (`com.radicade.questtls`) and the original-package scoped build passed TLS and loaded game configuration. The original-package run (PID 6353) remained running past the former exit point, but logged `GetLoggedInUser failed: The user isn't signed in or their account state wasn't in a recoverable state. (2|-1)`, followed by `Unable to load Oculus user account`. Meta's platform logs also show `OBTAIN_TOKEN_3P` failing with HTTP 400 for this package. No account proof, platform identity, entitlement result, or server authentication policy was forged or changed. Gameplay/login is not yet working. + +The original signing password was unavailable. The new local diagnostic signing key and password file are in `artifacts/quest-tls-diagnostic/diagnostic.p12` and `diagnostic.password`, excluded by that directory's `.gitignore`; retain these for non-destructive future updates. A separate diagnostic app and its OBB remain installed. Signed v2/v3 verification, 16 KiB alignment, original-package payload comparison, checked ARM64 disassembly, and on-device positive TLS tests passed. Rejection-path behavior has not yet been independently exercised with an invalid server certificate. + +The VPS currently serves ZeroSSL and has Caddy debug enabled. It is running in the user's foreground PowerShell window. The older sections below describe the investigation history, not the current installed build. + +### Platform integration investigation (2026-10-07) + +Added `tools/quest-platform-inspect.c`, a read-only JNI diagnostic which enumerates selected runtime class methods and field offsets without reading account values or invoking authentication methods. Installed it only in the separate `com.radicade.questtls` diagnostic app as `artifacts/quest-tls-diagnostic/PlatformInspect.apk`. The original-package TLS-fixed APK and both OBBs were retained. The first inspector run raced Unity initialization and aborted; delaying inspection by 30 seconds allowed the second run (PID 10871) to complete. This delay is a diagnostic workaround, not a general initialization synchronization guarantee. + +The complete successful method inventory is `artifacts/quest-tls-diagnostic/platform-methods.txt`; selected ARM64 disassembly is `platform-disassembly.txt`. Confirmed actual Quest RVAs: `OculusPlatformManager.Initialize` = `0x3d09d8c`, `OculusPlatformManager.GetAuthParams` = `0x3d0a930`, `StandalonePlatformManager.Initialize` = `0x43d6f0c`, and `PlatformManager.GetAuthParams` = `0x396ad48`. The standalone manager exists in the Android build; its platform getter returns enum value 7 and its PC-platform getter returns true. Therefore simply swapping the entire manager would change device behavior and is not yet a validated Quest integration. The server's current Platforms enum does not define value 7. No platform methods or server authentication behavior were changed by this diagnostic. + +Next implementation work must preserve Quest hardware behavior and add authenticated Radicade account credentials/session validation before enabling an independent login path. The existing server device-code implementation auto-approves a configured Studio account; it must not be reused as Quest account authorization. A successful metadata inspection does not establish gameplay compatibility or successful login. + +### Confirmed remaining Meta failure + +The user confirmed their Meta profile and other online apps work. Correlated platform logs for the original-package fixed build show `https://graph.oculus.com/authenticate_application` returning HTTP 400, OAuth error code `27`, subcode `1891543`, title `Unofficial Application`. The response explicitly identifies the app as modified and disables platform functionality. This is an app-authorization rejection, not evidence that the user's headset account needs resetting. Changing Caddy or reinstalling the same modified APK cannot supply the original publisher's signing identity. Remaining work requires a separately designed Radicade sign-in integration (for example, server-validated account login/device pairing or an appropriately registered Meta app), preserving account authentication rather than fabricating a Meta user/proof or accepting arbitrary platform IDs. + +The supplied folder is an apktool-decoded Android package plus an OBB asset tree. Its APK manifest is `com.AgainstGravity.RecRoom`, Quest/Quest 2 6DoF, ARM64, and the loaded Unity version literal is `20230907` (not the desktop `20230831` build). + +## What is ready + +`tools/patch-quest-client-metadata.py` recognizes this exact Quest metadata layout, resolves its live IL2CPP string-literal table, and redirects its bootstrap/auth/site/image URLs to `https://ns.radicade.app`. It keeps the image-signature tail `.radicade.internal`. It is dry-run by default, verifies the rebuilt table against every original literal, writes a `.orig` backup and a patch marker only with `--apply`, and refuses unknown/mismatched builds. + +From the repository root, first preview the changes: + +```powershell +python tools/patch-quest-client-metadata.py 'C:\RecRoom\RecRoom-Quest-20230907' --server https://ns.radicade.app +``` + +The image rule targets the server's signed `/images/img/{name}` route; the domain's `/images/avatar/` is not the image endpoint and returns a large generic response. The account's `GET /` nameserver advertises all other service URLs under the same server. + +## Earlier state: NSC test APK installed and launched on Quest 3 + +The Quest 3 is connected via ADB. The local test-signed build was installed after uninstalling the prior test-signed package (the original Store app had already been removed earlier). The app was launched as a VR activity on-device; Unity initialized the OpenXR/Oculus runtime and reached `Connecting to RecNet`. Its TLS request still fails with `Mono.Security.Interface.TlsException: Handshake failed - error code: UNITYTLS_INTERNAL_ERROR, verify result: 4294957312`, then the RecNet nameserver query reports `summer` and the app exits around 17 seconds after start. + +A second APK variant adds the official Let’s Encrypt Root YR cross-signed-by-ISRG-Root-X1 certificate as a raw resource and trusts it only for `ns.radicade.app`, while retaining Android system trust and disallowing cleartext. It was aligned for 16 KiB pages and signed with a separate local test key; Apktool build, `apksigner` v2/v3 verification, `zipalign`, package/version/ARM64 checks and inspection of the packaged NSC all passed. Installing it required uninstalling the previous local test package because its signing certificate differed; the user's consent was obtained. The matching OBB was restored from the PC copy and SHA-256 verified identical (`ef856a56bfe1667f00435c2b07fb465766196c02bac8b220c7cc36db926998fc`). This certificate-resource experiment did not change the TLS error or startup exit. + +The currently installed APK is `C:\\Users\\user\\Downloads\\RadicadeQuest20230907\\RecRoomQuest-radicade-nsc-test.apk` (package `com.AgainstGravity.RecRoom`, version `20230907`, versionCode `2378787`). Its narrow Android trust config is `apk/res/xml/network_sec_config.xml`, and the cross-signed CA resource is `apk/res/raw/isrg_root_yr_by_x1.pem`. This test shows that adding an Android Network Security Config trust anchor does not fix this Unity `MobileAuthenticatedStream` TLS handshake, so do not keep changing Android trust settings or weaken TLS validation; investigate the TLS chain/protocol at UnityTLS or server-side and keep certificate validation enabled. + +The headset OBB at `/sdcard/Android/obb/com.AgainstGravity.RecRoom/main.2378787.com.AgainstGravity.RecRoom.obb` is present and hash-matches the local build. Uninstalling the test app removed the on-headset OBB directory, so the local copy was pushed back and verified after install. + +## Earlier APK/OBB build notes + +The pre-existing working tree had a decoded Quest APK (`apk/`) and separately decoded OBB asset tree (`obb/`), not the original archives or publisher signing key. The untouched source under `C:\\Users\\user\\Downloads\\RecRoom-Quest-20230907` was not modified. A separate working copy under `C:\\Users\\user\\Downloads\\RadicadeQuest20230907` has the metadata patch applied; Apktool 3.0.3 built its APK, which was page-aligned and signed with local-only debug keys. Package, version, min SDK 25, target SDK 32 and ARM64 native code were validated. A new `tools/package-quest-obb.py` validated all 22,199 OBB entries and built the matching expansion-file name (1,394,848,868 bytes); the on-headset copy now has a verified matching hash. Android platform-tools 37.0.1, build-tools 36.1.0 and Apktool were downloaded into `%LOCALAPPDATA%\\Radicade\\QuestToolchain`. + +The remaining significant functional blocker is identity: this APK is built for Oculus/Meta and its IL2CPP metadata contains Oculus Platform APIs (`GetUserProof`/`GetLoggedInUser`-related flows), but the server currently only verifies Steam tickets when `platform == Steam`. Account creation also accepts only platform IDs prefixed `75` or `76`, and the existing allowlist is named/configured as a Steam ID list. The server version-check route currently allows all versions, but `20230907` is not in `DBConfig.SupportedGameVersions`; this does not prove the rest of the server protocol works. The endpoint patch does not add Meta identity verification, change server authentication policy, or port the Windows BepInEx/Harmony plugin to Android. + +The metadata patch remains reproducible with the dry-run command above. The current test APK and matching expansion file are installed and have been run on the Quest 3; the app still exits after UnityTLS handshake failure during nameserver discovery. New Meta account onboarding is not supported/verified; existing linked-user login is unknown. + +## Safe next steps + +1. Keep this test APK/OBB and current headset install available for controlled TLS investigation. Do not uninstall/reinstall again without a reason: that deletes app-private data and Android also removes the OBB directory. +2. Investigate the failure within Unity's `MobileAuthenticatedStream`/UnityTLS against the exact chain served by Caddy (leaf `ns.radicade.app` → YR1 → Root YR cross-signed by ISRG Root X1). Do not disable certificate or hostname validation; the domain-scoped Android NSC root addition had no effect. +3. If the TLS request succeeds, resolve and test Meta/Oculus identity support with the Radicade server before attempting account creation. Do not weaken authentication based on an assumed Oculus ID shape. + +## TLS 1.2-only test (2026-10-06 Pacific) + +The user identified the server as a netcup Windows VPS, running C:\Caddy\caddy.exe v2.11.4 with C:\Caddy\Caddyfile. The live Radicade site already used key_type rsa2048. The user added protocols tls1.2 tls1.2. Reload initially failed because Caddy was no longer running; the user started it in a foreground PowerShell window. + +External OpenSSL checks confirmed that TLS 1.2 succeeded with certificate verification OK and TLS 1.3 was rejected with protocol-version alert 70. A fresh launch of the existing Quest APK (PID 29072) still produced UNITYTLS_INTERNAL_ERROR, verify result 4294957312, followed by nameserver error summer and OnApplicationQuit at approximately 15.2 seconds. The process was absent afterward. TLS 1.2-only did not resolve startup; certificate-chain compatibility remains unproven. No APK reinstall or OBB change was performed. The temporary TLS restriction should be reverted on the VPS; rollback has not yet been confirmed. Caddy currently depends on the user's foreground PowerShell process remaining running. + +Rollback confirmed: the user successfully reloaded via 127.0.0.1:2019, and an external TLS 1.3 handshake subsequently succeeded with certificate verification OK. The TLS 1.2-only restriction is no longer active. + +## ZeroSSL chain test + +After the user changed the Radicade site's ACME issuer to ZeroSSL and reloaded successfully, an external OpenSSL probe confirmed the served chain changed to ns.radicade.app -> ZeroSSL RSA DV SSL CA 2 -> Sectigo Public Server Authentication Root R46 -> USERTrust RSA Certification Authority, with verification OK. The new chain was active before the Quest launch. + +The existing Quest APK was launched again (PID 30187). It reached Connecting to RecNet at 9.44 seconds, reported the identical UNITYTLS_INTERNAL_ERROR / verify result 4294957312 at 13.16 seconds, then nameserver error summer and OnApplicationQuit at 15.14 seconds. The process exited. Changing issuer did not fix startup. This weakens a Let's Encrypt-specific explanation but does not establish the exact validation failure. ZeroSSL remains configured on the VPS; no rollback has been performed. + +Read-only inspection confirmed that the test APK's live metadata literals 41405 (auth) and 41411 (nameserver) both resolve to https://ns.radicade.app. The headset reports Android 14, and both /system/etc/security/cacerts and /apex/com.android.conscrypt/cacerts contain certificate files. This does not establish that Unity successfully loads or trusts those files. Further diagnosis should correlate Caddy TLS handshake errors with a fresh Quest launch before making additional certificate or client changes. + +## Correlated Caddy debug and headset trust-store checks (2026-10-07 Pacific) + +Quest PID 31620 again failed at about 13.29 seconds and quit at 15.27 seconds. The user supplied Caddy debug lines at 07:06:07 UTC showing SNI ns.radicade.app, supported protocol versions [771,770,769] (maximum TLS 1.2), successful selection of the ZeroSSL certificate, and handshake EOF. EOF alone does not identify the client's reason for closing. + +The numeric Unity verify result 4294957312 is signed -9984 (-0x2700), matching Mbed TLS's certificate verification failed code; this correspondence suggests certificate validation failure but does not identify the precise Unity verification flag or callback. + +Pulled the headset's 134 public system CA certificates to artifacts/quest-system-cacerts and built artifacts/quest-system-ca-bundle.pem. OpenSSL TLS 1.2 verification of the live Radicade server succeeded with this CAfile, -no-CApath, -no-CAstore, -verify_return_error, and -verify_hostname ns.radicade.app. This establishes that the served chain can validate with the headset's public system roots using OpenSSL, not that Unity loads these roots or validates identically. Investigation should focus on Unity's trust loading/custom validation and chain-building behavior rather than assume a missing system root or TLS version mismatch. Caddy debug remains enabled; ZeroSSL remains active. diff --git a/docs/WEEKLY-CHALLENGES.md b/docs/WEEKLY-CHALLENGES.md new file mode 100644 index 0000000..5bc78f0 --- /dev/null +++ b/docs/WEEKLY-CHALLENGES.md @@ -0,0 +1,96 @@ +# Weekly challenges and RadAdmin + +The player routes are GET `api/challenge/v2/getCurrent` and POST +`api/challenge/v2/updateProgress`, also under `econ/`. Both require player auth. +The server stores progress in `WeeklyChallenges`, keyed by player and map ID. +The new `20261008120000_AddWeeklyChallenges` migration is applied by the existing +server startup migration step. This work has not been deployed or game-tested. + +## Add-A-Challenge + +RadAdmin's Challenges page saves library entries through the existing +`/admin/panel/challenges` CRUD API. A gameplay preset generates the client config; +names, descriptions and tooltips are editable. The initial supported presets are +single-event goals: finish an activity, tag a player, defeat an AI enemy, capture +a flag, and block with a shield. Count/streak and arbitrary legacy configs are +not yet supported by the weekly validator. + +## Publishing a weekly + +Open **Challenges**, choose the current week or one of the next three weeks, and: + +1. Enter the display title and duplicate-item token amount (1–10,000). +2. Search Equipment skins or Avatar items and select the exact catalog item. +3. Select three to five library challenges. Create any missing goals with New challenge. +4. Leave Published checked and click Publish weekly. Uncheck it and save to disable the week. + +An unscheduled week has no challenges or reward. There is no built-in lineup or reward +rotation. Existing saved schedules remain supported. Publish the current week after +upgrading if it previously used the old automatic rotation. + +GET `/admin/panel/weekly-challenges` returns four `weeks`, with map IDs, dates, +content and `Revision`. GET `/admin/panel/weekly-challenges/rewards` returns searchable +catalog choices with `key`, `kind`, `name`, and `detail`. POST the schedule: + +```json +{ + "ChallengeMapId": 100000, + "Revision": "revision from GET, or empty for a new schedule", + "Title": "Weekly Challenges", + "Enabled": true, + "ChallengeIds": [1, 2, 3], + "RewardKey": "exact key returned by the reward catalog endpoint", + "FallbackTokens": 300 +} +``` + +All admin routes use existing admin-key validation and RadAdmin's same-origin proxy. +The server resolves the reward against its loaded catalog; arbitrary gift payloads +are not accepted. A stale revision returns HTTP 409; reload before publishing again. +Schedules are stored as JSON in SQL `ServerConfig` under `WeeklyChallengeMap:{id}`. +No additional database migration is needed for schedule fields. + +## Schedule, progress and rewards + +- Reset remains Wednesday 00:00 UTC. The admin page displays local time. +- Completing any three goals awards the selected skin or avatar item once. + Three is the installed client's completion threshold, not configurable content. +- Owned or pending rewards use the configured token fallback. +- Library edits do not change a published snapshot until it is published again. +- Current-week edits preserve progress when the challenge ID and gameplay definition + match. Changed goals reset. Already-earned rewards remain earned and cannot be + awarded twice. Players may need to reopen their challenge screen after an edit. +- Disabled weeks expose no player challenges and reject progress updates. +- SQL schedule and player-state locks serialize saves/completion. Gift creation and + progress commit together; consumed boxes are not recreated. +- `WeeklyCompleteButHaveSkin` does not create a second award. + +Progress comes from the authenticated client's event system. The server validates +map/goal identity, immutable predicates and completion consistency; it does not +independently prove gameplay events. SQL concurrency still needs integration testing +against SQL Server; automated state tests use EF InMemory. + +## Client evidence + +Inspected installed Windows build 20230831, RecNet.Runtime and +RecRoom.Challenges.Runtime. Local `artifacts/weekly-*.txt` native dumps establish: + +- FEPKIMEALDP serializer: ChallengeMapId, ChallengeThemeString, Challenges, Gift, + FallbackGiftName, StartAt, EndAt, ServerTime, CompletedRequired. +- BGJILCJGENL serializer: ChallengeId, Name, Config (JSON **string**), Description, + Tooltip, Complete. +- BEODGPKOFEI::ALKHLDNCPHD posts ChallengeMapId, ChallengeId, Config, Complete. +- WeeklyChallengeManager::get_CompletedRequired counts three completions. +- Challenge types: ct=0 composite with rc required predicates; ct=6 event filter, + vs accepted values. Event IDs 2/4/5/8/10 correspond to the starter goals. + +## Verification + +37 weekly/admin scheduling and existing gift-flow/persistence tests passed. +Admin JavaScript self-test checks generated presets, selection bounds, scheduling +payload, success/error handling and matching HTML element IDs. + +Game check after deploying both projects: publish the current week, reopen the weekly +board, complete a goal, and relog to verify progress. Complete three and open the +chosen reward. Verify a second player who owns the item receives the configured +tokens. Check both avatar and skin rewards. Live client behavior has not been tested. diff --git a/docs/gift-context.json b/docs/gift-context.json new file mode 100644 index 0000000..8ac172f --- /dev/null +++ b/docs/gift-context.json @@ -0,0 +1,418 @@ +[ + { + "Name": "None", + "Value": -1 + }, + { + "Name": "Default", + "Value": 0 + }, + { + "Name": "First_Activity", + "Value": 1 + }, + { + "Name": "Game_Drop", + "Value": 2 + }, + { + "Name": "All_Daily_Challenges_Complete", + "Value": 3 + }, + { + "Name": "All_Weekly_Challenge_Complete", + "Value": 4 + }, + { + "Name": "Daily_Challenge_Complete", + "Value": 5 + }, + { + "Name": "Weekly_Challenge_Complete", + "Value": 6 + }, + { + "Name": "Unassigned_Equipment", + "Value": 10 + }, + { + "Name": "Unassigned_Avatar", + "Value": 11 + }, + { + "Name": "Unassigned_Consumable", + "Value": 12 + }, + { + "Name": "Reacquisition", + "Value": 20 + }, + { + "Name": "Membership", + "Value": 21 + }, + { + "Name": "NUX_TokensAndDressUp", + "Value": 30 + }, + { + "Name": "NUX_Experiment1", + "Value": 31 + }, + { + "Name": "NUX_Experiment2", + "Value": 32 + }, + { + "Name": "NUX_Experiment3", + "Value": 33 + }, + { + "Name": "NUX_Experiment4", + "Value": 34 + }, + { + "Name": "NUX_Experiment5", + "Value": 35 + }, + { + "Name": "GameRewards", + "Value": 50 + }, + { + "Name": "GameRewards_Tokens", + "Value": 51 + }, + { + "Name": "LevelUp", + "Value": 100 + }, + { + "Name": "Purchased_Gift_A", + "Value": 500 + }, + { + "Name": "Purchased_Gift_B", + "Value": 501 + }, + { + "Name": "Purchased_Gift_C", + "Value": 502 + }, + { + "Name": "Purchased_Gift_D", + "Value": 503 + }, + { + "Name": "Holiday", + "Value": 1000 + }, + { + "Name": "Contest", + "Value": 1001 + }, + { + "Name": "Promotion", + "Value": 1002 + }, + { + "Name": "SubscribersOnly", + "Value": 1003 + }, + { + "Name": "Deprecated", + "Value": 1100 + }, + { + "Name": "RecRoyale", + "Value": 1200 + }, + { + "Name": "DEPRECATED_Paintball_ClearCut", + "Value": 2000 + }, + { + "Name": "DEPRECATED_Paintball_Homestead", + "Value": 2001 + }, + { + "Name": "DEPRECATED_Paintball_Quarry", + "Value": 2002 + }, + { + "Name": "DEPRECATED_Paintball_River", + "Value": 2003 + }, + { + "Name": "DEPRECATED_Paintball_Dam", + "Value": 2004 + }, + { + "Name": "DEPRECATED_Paintball_DriveIn", + "Value": 2005 + }, + { + "Name": "Paintball_ClearCut", + "Value": 2010 + }, + { + "Name": "Paintball_Homestead", + "Value": 2011 + }, + { + "Name": "Paintball_Quarry", + "Value": 2012 + }, + { + "Name": "Paintball_River", + "Value": 2013 + }, + { + "Name": "Paintball_Dam", + "Value": 2014 + }, + { + "Name": "Paintball_DriveIn", + "Value": 2015 + }, + { + "Name": "DEPRECATED_Discgolf_Propulsion", + "Value": 3000 + }, + { + "Name": "DEPRECATED_Discgolf_Lake", + "Value": 3001 + }, + { + "Name": "Discgolf_Propulsion", + "Value": 3010 + }, + { + "Name": "Discgolf_Lake", + "Value": 3011 + }, + { + "Name": "Discgolf_Mode_CoopCatch", + "Value": 3500 + }, + { + "Name": "Quest_Goblin_A", + "Value": 4000 + }, + { + "Name": "Quest_Goblin_B", + "Value": 4001 + }, + { + "Name": "Quest_Goblin_C", + "Value": 4002 + }, + { + "Name": "Quest_Goblin_S", + "Value": 4003 + }, + { + "Name": "Quest_Goblin_Consumable", + "Value": 4004 + }, + { + "Name": "Quest_Cauldron_A", + "Value": 4010 + }, + { + "Name": "Quest_Cauldron_B", + "Value": 4011 + }, + { + "Name": "Quest_Cauldron_C", + "Value": 4012 + }, + { + "Name": "Quest_Cauldron_S", + "Value": 4013 + }, + { + "Name": "Quest_Cauldron_Consumable", + "Value": 4014 + }, + { + "Name": "Quest_Pirate1_A", + "Value": 4100 + }, + { + "Name": "Quest_Pirate1_B", + "Value": 4101 + }, + { + "Name": "Quest_Pirate1_C", + "Value": 4102 + }, + { + "Name": "Quest_Pirate1_S", + "Value": 4103 + }, + { + "Name": "Quest_Pirate1_X", + "Value": 4104 + }, + { + "Name": "Quest_Pirate1_Consumable", + "Value": 4105 + }, + { + "Name": "Quest_Dracula1_A", + "Value": 4200 + }, + { + "Name": "Quest_Dracula1_B", + "Value": 4201 + }, + { + "Name": "Quest_Dracula1_C", + "Value": 4202 + }, + { + "Name": "Quest_Dracula1_S", + "Value": 4203 + }, + { + "Name": "Quest_Dracula1_X", + "Value": 4204 + }, + { + "Name": "Quest_Dracula1_Consumable", + "Value": 4205 + }, + { + "Name": "Quest_Dracula1_SS", + "Value": 4206 + }, + { + "Name": "Quest_SciFi_A", + "Value": 4500 + }, + { + "Name": "Quest_SciFi_B", + "Value": 4501 + }, + { + "Name": "Quest_SciFi_C", + "Value": 4502 + }, + { + "Name": "Quest_SciFi_S", + "Value": 4503 + }, + { + "Name": "Quest_Scifi_Consumable", + "Value": 4504 + }, + { + "Name": "DEPRECATED_Charades", + "Value": 5000 + }, + { + "Name": "Charades", + "Value": 5001 + }, + { + "Name": "DEPRECATED_Soccer", + "Value": 6000 + }, + { + "Name": "Soccer", + "Value": 6001 + }, + { + "Name": "DEPRECATED_Paddleball", + "Value": 7000 + }, + { + "Name": "Paddleball", + "Value": 7001 + }, + { + "Name": "DEPRECATED_Dodgeball", + "Value": 8000 + }, + { + "Name": "Dodgeball", + "Value": 8001 + }, + { + "Name": "DEPRECATED_Lasertag", + "Value": 9000 + }, + { + "Name": "Lasertag", + "Value": 9001 + }, + { + "Name": "DEPRECATED_Bowling", + "Value": 10000 + }, + { + "Name": "Bowling", + "Value": 10001 + }, + { + "Name": "StuntRunner_TheMainEvent_A", + "Value": 11000 + }, + { + "Name": "StuntRunner_TheMainEvent_B", + "Value": 11001 + }, + { + "Name": "StuntRunner_TheMainEvent_C", + "Value": 11002 + }, + { + "Name": "StuntRunner_TheMainEvent_D", + "Value": 11003 + }, + { + "Name": "StuntRunner_TheMainEvent_S", + "Value": 11004 + }, + { + "Name": "StuntRunner_TheMainEvent_X", + "Value": 11005 + }, + { + "Name": "StuntRunner_TheMainEvent_Consumable", + "Value": 11006 + }, + { + "Name": "StuntRunner_TheMainEvent_SS", + "Value": 11007 + }, + { + "Name": "Store_LaserTag", + "Value": 100000 + }, + { + "Name": "Store_RecCenter", + "Value": 100010 + }, + { + "Name": "Consumable", + "Value": 110000 + }, + { + "Name": "Token", + "Value": 110100 + }, + { + "Name": "Punchcard_Challenge_Complete", + "Value": 110200 + }, + { + "Name": "All_Punchcard_Challenges_Complete", + "Value": 110201 + }, + { + "Name": "Commerce_Purchase", + "Value": 200000 + } +] diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..78649b9 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "obfuscar.globaltool": { + "version": "2.2.50", + "commands": [ + "obfuscar.console" + ], + "rollForward": false + } + } +} \ No newline at end of file